VersioningTechniques · NestJS

Ship version 2 of the cats API without breaking version 1: URI versioning with a default for the controllers that existed before, a version 2 controller on the same path, a route shared by both versions, and a health check outside them.

What you will learn

Read the theory for Versioning

All Techniques lessons

All NestJS courses

loading types…

What you'll learn

  • Enable URI versioning and explain what defaultVersion does and does not do
  • Serve two versions of the same path from two controllers, and one route under several versions with an array
  • Keep a route outside versions with VERSION_NEUTRAL

Versioning

The first clients of the cats API were built against GET /cats returning a bare array of { id, name, age }. Then the shelter wanted the full cat, with adoptable, and a list wrapped in { items, total } so a page could show a count. Both are reasonable; the second breaks every existing client. An API that has shipped cannot change its shape under people's feet, so it gains a version: the old shape keeps answering under version 1, the new one arrives as version 2, and clients move when they are ready. Nest supports four ways of saying which version a request wants, and this lesson uses the one you have seen in every public API's URL.

Four ways to name a version

app.enableVersioning() in main.ts switches versioning on and chooses how a request names its version:

app.enableVersioning({ type: VersioningType.URI });

URI versioning puts it in the path, https://example.com/v1/route, the v prefix added by Nest and configurable with prefix. Header versioning reads a custom header, type: VersioningType.HEADER, header: 'Custom-Header'. Media type versioning reads the Accept header, Accept: application/json;v=2, with key: 'v='. Custom versioning takes an extractor function that receives the request and returns the version, or an array of versions best first. Whichever you pick, the rest of the code is identical: a controller or a route says which versions it serves, and Nest routes each request to the matching one.

Saying which version a route serves

A whole controller can carry a version:

@Controller({ path: 'cats', version: '1' })
export class CatsControllerV1 {
  @Get()
  findAll() {
    return 'This action returns all cats for version 1';
  }
}

With URI versioning that controller answers GET /v1/cats. A second controller with version: '2' and the same path answers GET /v2/cats, and the two do not collide because the version is part of what the router matches. Inside one controller, @Version('2') on a handler does the same for a single route, so two handlers can share a path and differ by version.

A route that does not change between versions need not be copied. An array serves several: @Version(['1', '2']), or version: ['1', '2'] on the controller. And a route that lives outside versions altogether, a health check, is version neutral: version: VERSION_NEUTRAL answers at its plain path, /health, and nowhere else.

The default version

Once versioning is on, a controller that names no version serves nothing, and every request must name one: GET /cats is a 404, only GET /v1/cats exists. defaultVersion is about the first half of that, not the second:

app.enableVersioning({ type: VersioningType.URI, defaultVersion: '1' });

It is the version given to controllers and routes that declare none, so an API that adds versioning after shipping can leave its existing controllers untouched and they become version 1. It does not make a version-less request match: GET /cats stays a 404, and a route that must answer without a version says so with VERSION_NEUTRAL. defaultVersion may also be an array, or VERSION_NEUTRAL itself.

Middleware can be versioned too: consumer.apply(LoggerMiddleware).forRoutes({ path: 'cats', method: RequestMethod.GET, version: '2' }) runs for version 2 only.

Your task

Version 2 of the cats API arrives without breaking version 1.

  1. In main.ts, enable URI versioning with version 1 as the default, so the existing CatsController, which names no version, becomes version 1 untouched.
  2. CatsV2Controller is version 2 of the same path, and CatsModule registers it.
  3. Creating a cat is identical in both versions: serve POST under both from the version 1 controller.
  4. HealthController is version neutral: it answers at /health.

Run after step 1 alone and request GET /cats, then GET /v1/cats: the first is gone and the second exists. Versioning moved every route, and the old path no longer means anything.

When it fails

  • Every route is 404 after enabling versioning: no defaultVersion, so a controller without a version serves nothing; and a request without a version matches nothing whatever you set, only /v1/... exists.
  • GET /v2/cats answers with the version 1 array: the second controller names no version and received the default, so both are version 1 and the first registered wins.
  • POST /v2/cats is 404: the create route serves version 1 only; give it both versions.
  • GET /health is 404 while GET /v1/health works: the controller received the default version; a neutral route needs version: VERSION_NEUTRAL.

Remember

  • app.enableVersioning({ type }): URI, header, media type or custom; the controllers are written the same way for all four.
  • @Controller({ path, version }) or @Version() on a handler says what a route serves; an array serves several.
  • defaultVersion is for routes that name none, not for requests; a route outside versions is VERSION_NEUTRAL.
  • A version is part of what the router matches, so two controllers may share a path.
Stuck? Show a hint

main.ts: app.enableVersioning({ type: VersioningType.URI, defaultVersion: '1' }). cats-v2.controller.ts: @Controller({ path: 'cats', version: '2' }). cats.controller.ts: @Version(['1', '2']) on create. health.controller.ts: @Controller({ path: 'health', version: VERSION_NEUTRAL }). Register CatsV2Controller in CatsModule.