How to Implement API Versioning in NestJS: URI, Header, and Custom Strategies

NestJS provides built-in API versioning through enableVersioning() and the @Version() decorator, supporting URI paths, headers, media types, and custom extractors to route requests to version-specific handlers.

NestJS offers a flexible, adapter-agnostic API versioning system that works with both Express and Fastify. According to the nestjs/nest source code, the framework implements this through metadata-driven route resolution and strategy-specific filtering. This guide demonstrates how to implement API versioning in NestJS using the four built-in versioning types defined in the core library.

Understanding the Four Versioning Strategies

The VersioningType enum in packages/common/enums/version-type.enum.ts defines four distinct strategies for supplying version information:

  • URI: Version is part of the URL path (e.g., /v1/users)
  • HEADER: Version is supplied via a custom request header (e.g., X-API-Version: 1)
  • MEDIA_TYPE: Version is extracted from the Accept header parameter (e.g., application/json;v=1)
  • CUSTOM: Version is determined by a user-defined extractor function

Each strategy uses different resolution mechanisms within the framework. URI-based versioning is handled by path manipulation in packages/core/router/route-path-factory.ts, while the other three strategies rely on version filters injected by the HTTP adapters.

Enabling Versioning in Your Application

To activate API versioning, call enableVersioning() on your application instance during bootstrap. This method is defined in packages/core/nest-application.ts and stores a VersioningOptions object in ApplicationConfig (packages/core/application-config.ts).

import { NestFactory } from '@nestjs/core';
import { VersioningType } from '@nestjs/common';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  app.enableVersioning({
    type: VersioningType.URI,
    prefix: 'api/v',       // Optional: results in /api/v1/...
    defaultVersion: '1',   // Fallback when no version is supplied
  });
  
  await app.listen(3000);
}
bootstrap();

The VersioningOptions interface (packages/common/interfaces/version-options.interface.ts) accepts different parameters depending on the strategy type. For HEADER versioning, specify the header name. For CUSTOM versioning, provide an extractor function.

Defining Versioned Controllers and Routes

Apply the @Version() decorator to individual route handlers to attach version metadata. The decorator writes to VERSION_METADATA using the reflection metadata API, as implemented in packages/common/decorators/core/version.decorator.ts.

import { Controller, Get, Version } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Get()
  findAll() {
    return 'All cats (any version)';
  }

  @Version('1')
  @Get('profile')
  getProfileV1() {
    return 'Profile v1';
  }

  @Version(['2', '3'])  // Multiple versions supported
  @Get('profile')
  getProfileV2orV3() {
    return 'Profile v2 or v3';
  }
}

Routes without the @Version() decorator are version-neutral (VERSION_NEUTRAL), meaning they respond to requests regardless of the version specified.

How NestJS Resolves Versioned Requests

The framework resolves versioned routes differently depending on the strategy:

URI Strategy: The RoutePathFactory class in packages/core/router/route-path-factory.ts constructs the final URL path by prefixing the version string to the route. When you configure prefix: 'api/v' with version '1', the factory generates /api/v1/ prefixes.

Header, Media Type, and Custom Strategies: Each HTTP adapter implements a version filter that extracts the version from the incoming request and determines handler eligibility:

When a request lacks version information, the system falls back to the configured default version. If no default is set, the request matches only version-neutral routes.

Complete Implementation Examples

URI-Based Versioning

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

Test with curl:

curl http://localhost:3000/v1/cats/profile    # → "Profile v1"

curl http://localhost:3000/v2/cats/profile    # → "Profile v2 or v3"

curl http://localhost:3000/cats               # → "All cats (neutral route)"

Header-Based Versioning

app.enableVersioning({
  type: VersioningType.HEADER,
  header: 'X-API-Version',
});

The Express adapter checks the X-API-Version header against the @Version() metadata to route the request.

Custom Extractor (Query Parameter)

app.enableVersioning({
  type: VersioningType.CUSTOM,
  extractor: (request: Request) => request.query.v as string,
});

This delegates version extraction to your custom function, which the adapter invokes during request processing.

Summary

  • Enable versioning globally using app.enableVersioning() in your bootstrap function, which stores configuration in ApplicationConfig.
  • Choose from four strategies: URI (path prefix), HEADER (custom header), MEDIA_TYPE (Accept header), or CUSTOM (extractor function).
  • Decorate routes with @Version() to attach version metadata; omit it for version-neutral routes that match any request.
  • Route resolution occurs in RoutePathFactory for URI strategy, or via adapter-specific filters in Express and Fastify adapters for other strategies.
  • Configure a default version to handle requests that don't specify a version explicitly.

Frequently Asked Questions

How do I set a default version for unversioned requests?

Pass defaultVersion to enableVersioning(). When a request arrives without version information (no URI prefix, header, or extracted value), NestJS treats it as the default version. Without this configuration, unversioned requests match only version-neutral routes.

Can a single route handler support multiple API versions?

Yes. The @Version() decorator accepts an array of strings: @Version(['2', '3']). The metadata stored by packages/common/decorators/core/version.decorator.ts supports multiple values, and the adapter's version filter checks for inclusion in that array.

What happens if a request version doesn't match any route?

If the requested version (extracted via URI, header, or custom logic) doesn't match any registered @Version() metadata and no version-neutral route exists for that path, NestJS returns a 404 Not Found. The version filters in the Express and Fastify adapters explicitly reject non-matching requests before controller execution.

Does API versioning work with Fastify?

Yes. The implementation in packages/platform-fastify/adapters/fastify-adapter.ts supports all four versioning types using Fastify's route constraint system. The versionConstraint and deriveConstraint functions handle version extraction and matching identically to the Express implementation, ensuring consistent behavior across HTTP adapters.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →