Skip to content
Kumar Chandrachooda
.NET

Versioning at the End of the Route

FastEndpoints suffixes versions by default - /api/orders/v2 - and its release-group model versions endpoints, not APIs. Reading the route-building code explains the philosophy, and the AspVersioning bridge covers everyone it doesn't fit. Part 9 of FastEndpoints in Depth.

By Kumar Chandrachooda 03 Apr 2026 5 min read
The version rides at the end of the route - each endpoint carries its own

Ask three API teams how to version and you will get four answers and one argument. URL segment or header? Version the whole API or individual operations? FastEndpoints has a built-in answer that looks wrong at first glance — the version goes at the end of the route, /api/orders/v2 — and turns out to encode a philosophy worth understanding before you reach for options to disable it.

The mechanics first

Versioning activates when you configure it and endpoints declare versions:

app.UseFastEndpoints(c =>
{
    c.Versioning.Prefix = "v";
    c.Versioning.DefaultVersion = 1;
});

public override void Configure()
{
    Post("/orders");
    Version(2);
}

The route building happens in MainExtensions.BuildRoute (the same startup walk from Part 2), and the source comments document the two layouts directly:

// Src/Library/Main/MainExtensions.cs (FastEndpoints, MIT)
// {rPrfix}/{p}{ver}/{route}
// mobile/v1/customer/retrieve
if (Cfg.VerOpts.PrependToRoute is true)
    AppendVersion(builder, epVersion, trailingSlash: true);
...
// {rPrfix}/{route}/{p}{ver}
// mobile/customer/retrieve/v1
if (Cfg.VerOpts.PrependToRoute is not true)
    AppendVersion(builder, epVersion, trailingSlash: false);

Suffix is the default; PrependToRoute = true gives you the conventional /v1/... prefix; and a third mode, RouteTemplate, lets you place a {version} token anywhere in each route (/api/{version}/orders), with startup throwing if a versioned endpoint's route forgets the token. So the layout argument is configuration, not destiny. The interesting part is why suffix is the default.

Endpoints version; APIs release

The conventional /v2/... prefix implies the whole API has a version 2 — which, in every codebase I have seen, is a lie. What actually happens: two endpoints changed, and the other ninety-eight got copied, re-registered, or proxied into the new version namespace to keep the illusion of a uniform v2. Whole folders of V2 controllers that differ from V1 by nothing.

FastEndpoints' model — the docs call it release group versioning — inverts this. Each endpoint carries its own version history. /orders/v1 and /orders/v2 coexist as two endpoint classes; /customers/v1 never grew a v2 and never pretends to. The suffix placement reflects the unit of versioning: the version belongs to the operation, so it sits with the operation's name, not in front of the whole API's namespace.

The duplicate-route detection from Part 2 quietly enforces coherence here: registering Version(2) twice for the same route+verb is a startup crash. And a deprecation marker (Version(1).DeprecateAt(3)) lets an old endpoint advertise its sunset — which flows into Swagger grouping, below.

Where this model pays off is maintenance arithmetic. When POST /orders changes incompatibly, you add one class:

public class CreateOrderV2 : Endpoint<CreateOrderRequestV2, CreateOrderResponseV2>
{
    public override void Configure()
    {
        Post("/orders");
        Version(2);
    }
    ...
}

Ninety-eight endpoints stay untouched. The cost is conceptual: there is no single “API v2” clients can point at — a client of “the v2 API” actually consumes a mix of /orders/v2 and /customers/v1. That mix needs documenting, which is exactly what the Swagger integration's release grouping is for: each swagger doc declares a MaxEndpointVersion, and the generator includes, for every route, the highest version at or below that ceiling. Doc “release 2” therefore contains /orders/v2 alongside /customers/v1 — a correct snapshot of what a release-2 client should call. The versioning model and the documentation model are two halves of one design; adopting the first without the second is where teams get lost (Part 10 goes deep on the doc side).

What the built-in scheme refuses to do

The built-in versioning is URL-segment only. No header versioning, no media-type versioning, no query-string versioning — and reading the route-builder code makes clear this isn't an oversight: versions are baked into route registration at startup, one physical route per version. There is no request-time version resolution at all; ASP.NET Core routing does the selection by literal path match. Zero per-request overhead, zero ambiguity — and zero flexibility about where the version lives.

If your organization mandates header versioning (or you are migrating an API that already does it), the FastEndpoints.AspVersioning package bridges to Asp.Versioning (the continuation of the Microsoft API-versioning libraries). With it, endpoints join version sets and the full Asp.Versioning feature set applies — readers for headers, media types, query strings; version negotiation; the lot:

Get("/weather");
Options(x => x.WithVersionSet(">>Weather<<").MapToApiVersion(2.0));

Internally, enabling the bridge flips an IsUsingAspVersioning flag that — one of those small source details that answers a real question — disables FastEndpoints' own duplicate-route detection, because with header-based versioning multiple endpoints legitimately share one literal route. If you ever wondered why duplicate detection stopped firing after adding AspVersioning: that's why, and it means typo-duplicated routes now surface as Asp.Versioning ambiguity errors at request time instead of startup crashes. A trade to be aware of.

Choosing, honestly

My decision tree after shipping both schemes:

  • Greenfield, you own the clients (SPA + mobile talking to your backend): built-in release groups, suffix default, one swagger doc per release. The maintenance economics are unbeatable and nobody external is offended by /orders/v2.
  • Public API with REST-convention expectations: built-in scheme with PrependToRoute or a RouteTemplate — you keep per-endpoint versioning but present conventional URLs.
  • Header/media-type versioning mandated, or existing Asp.Versioning estate: the bridge package. You give up startup duplicate detection and take a runtime-negotiation dependency; you gain policy compliance.

The mistake to avoid is the hybrid nobody chose: adopting release groups but hand-copying unchanged endpoints into new versions anyway “for symmetry”. That recreates the whole-API-version lie inside a system designed to avoid it, and you pay both costs. If you find yourself writing a V2 endpoint whose handler calls the V1 handler, stop — the model is telling you that endpoint didn't change.

One more versioning surface exists that this post deliberately skipped: the version-aware client generation (Kiota and NSwag-based) that turns each release doc into a typed client package. It belongs with the Swagger machinery, which is where the series goes next — how a thousand-line OperationProcessor translates everything the library knows about your endpoints into an OpenAPI document, and how FluentValidation rules become schema constraints: Part 10, Teaching NSwag What Your Endpoints Mean.