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 = "/posts", version = "1")]pub struct PostsV1Controller { #[inject] svc: Arc<PostsService>,}
#[routes]impl PostsV1Controller { #[get("/")] #[authorize(Read, posts::Entity)] async fn list(&self) -> Result<Json<Vec<Post>>> { Ok(Json(self.svc.feed().await?)) }}Routes mount under /v1/posts. 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 apiINFO nest_rs::routes: mounted route controller=PostsV1Controller method=GET path=/v1/posts handler=listThe 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, both provided by the feature’s HTTP module:
use nest_rs_core::module;
use super::controller::{PostsV1Controller, PostsV2Controller};use crate::authz::AuthzHttpModule;use crate::posts::PostsModule;
#[module( imports = [PostsModule, AuthzHttpModule], providers = [PostsV1Controller, PostsV2Controller],)]pub struct PostsHttpModule;Both controllers live in the feature’s http/controller.rs — a version is a
wire concern, not a second feature, and the app crate holds no controllers at
all.
#[controller(path = "/posts", version = "1")]pub struct PostsV1Controller { /* … */ }
#[controller(path = "/posts", version = "2")]pub struct PostsV2Controller { /* … */ }Two controllers, two mount paths (/v1/posts, /v2/posts), 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 same PostsService 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/posts).
Built by YV17labs