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.
Mount one controller under a version
Section titled “Mount one controller under a version”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:
$ 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.
Run multiple versions side by side
Section titled “Run multiple versions side by side”A controller per version, each module-listed in the app:
use nest_rs_core::module;
#[module(providers = [ ItemsV1Controller, ItemsV2Controller,])]pub struct ItemsHttpModule;#[controller(path = "/items", version = "1")]pub struct ItemsV1Controller { /* ... */ }#[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.
When the prefix matters elsewhere
Section titled “When the prefix matters elsewhere”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.
Going further
Section titled “Going further”- Controllers & routes — back to the route table.
- OpenAPI — the generated spec includes the version prefix on every operation.
- Configuration —
HttpTransport::global_prefixfor the orthogonal “everything under/api” case (versioning stacks on top:/api/v1/items).