Skip to content

Versioning

A long-lived API outlives its first wire shape. URI versioning lets a controller mount under a /v<N> prefix without touching its path = "…" — one attribute, one prefix, every consumer of the route table sees the same thing.

use nest_rs_http::{controller, routes};
#[controller(path = "/items", version = "1")]
pub struct ItemsV1Controller {
#[inject]
svc: Arc<ItemsService>,
}
#[routes]
impl ItemsV1Controller {
#[get("/")]
async fn list(&self) -> Json<Vec<Item>> {
Json(self.svc.list())
}
#[get("/:id")]
async fn show(&self, Path(id): Path<u64>) -> Json<Item> {
Json(self.svc.find(id))
}
}

Routes mount under /v1/items. The boot log, the OpenAPI document, and the served path all route through the same version_path helper, so they cannot drift:

Terminal window
$ nestrs run dev
2026-06-03T10:14:22Z INFO nest_rs::http: bound 2 routes on 0.0.0.0:3000
GET /v1/items (ItemsV1Controller::list)
GET /v1/items/:id (ItemsV1Controller::show)

The version string is opaque — "1", "2", "beta" all work; the prefix is built as format!("/v{version}"). Stick to integers unless you have a reason not to.

A controller per version, each module-listed in the app:

apps/api/src/module.rs
use nest_rs_core::module;
#[module(providers = [
ItemsV1Controller,
ItemsV2Controller,
])]
pub struct ItemsHttpModule;
apps/api/src/controllers/v1.rs
#[controller(path = "/items", version = "1")]
pub struct ItemsV1Controller { /* ... */ }
apps/api/src/controllers/v2.rs
#[controller(path = "/items", version = "2")]
pub struct ItemsV2Controller { /* ... */ }

Two controllers, two mount paths (/v1/items, /v2/items), two OpenAPI tags by default (the controller struct name groups routes), zero runtime overhead — the version is decided at boot, not per request.

A common pattern: V2 delegates to the V1 service when the change is purely on the wire (rename a field, add an optional field), and only the controller’s Json<T> shape differs. The service stays one.

The version isn’t a header, isn’t a query, isn’t a content type. Three consequences:

  • Clients pick the version by URL. A migration is one URL change, not a header negotiation.
  • The OpenAPI document carries one path per version. A doc consumer can list both versions side by side.
  • A cache or proxy can route on the path without inspecting headers.

If header-based versioning is a hard requirement (some media-type-driven APIs lean on Accept: application/vnd.api+json; version=2), the URI path is the wrong tool. Use a custom guard that reads the header and dispatches inside the handler.

  • Controllers & routes — back to the route table.
  • OpenAPI — the generated spec includes the version prefix on every operation.
  • ConfigurationHttpTransport::global_prefix for the orthogonal “everything under /api” case (versioning stacks on top: /api/v1/items).