typescript 47 lines · 9 steps

How API versioning works in NestJS

Two controllers share the /users path but serve different logic per API version, while a neutral controller opts out entirely.

Explained by highlit
1import { Controller, Get, Param, Post, Body, Version, VERSION_NEUTRAL } from '@nestjs/common';
2import { UsersService } from './users.service';
3import { CreateUserDto } from './dto/create-user.dto';
4 
5@Controller({ path: 'users', version: '1' })
6export class UsersV1Controller {
7 constructor(private readonly usersService: UsersService) {}
8 
9 @Get()
10 findAll() {
11 return this.usersService.findAll();
12 }
13 
14 @Get(':id')
15 findOne(@Param('id') id: string) {
16 return this.usersService.findOne(id);
17 }
18}
19 
20@Controller({ path: 'users', version: '2' })
21export class UsersV2Controller {
22 constructor(private readonly usersService: UsersService) {}
23 
24 @Get()
25 findAll() {
26 return this.usersService.findAllPaginated();
27 }
28 
29 @Version(['2', '3'])
30 @Get(':id')
31 findOne(@Param('id') id: string) {
32 return this.usersService.findOneWithProfile(id);
33 }
34 
35 @Post()
36 create(@Body() dto: CreateUserDto) {
37 return this.usersService.create(dto);
38 }
39}
40 
41@Controller({ path: 'health', version: VERSION_NEUTRAL })
42export class HealthController {
43 @Get()
44 check() {
45 return { status: 'ok' };
46 }
47}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Version metadata lets two controllers claim the same path without colliding, so each version evolves independently.
  2. 2Per-route @Version overrides let a single handler answer for multiple API versions when behavior hasn't changed.
  3. 3VERSION_NEUTRAL exempts a route from versioning entirely, ideal for cross-cutting endpoints like health checks.

Related explainers

python
from flask import Blueprint, jsonify, request, abort
 
v1 = Blueprint("users_v1", __name__)
v2 = Blueprint("users_v2", __name__)

Versioning a Flask API with Blueprints

api versioning blueprints rest
Intermediate 7 steps
typescript
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';

How a JWT strategy authenticates in NestJS

authentication jwt passport
Intermediate 7 steps
typescript
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http';
import { BehaviorSubject, Observable, throwError } from 'rxjs';
import { catchError, filter, take, switchMap } from 'rxjs/operators';

Refreshing auth tokens in an Angular interceptor

http-interceptor token-refresh rxjs
Advanced 8 steps
typescript
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { Reflector } from '@nestjs/core';
import { JwtService } from '@nestjs/jwt';

How a GraphQL auth guard works in NestJS

authentication authorization jwt
Intermediate 7 steps
python
import os
from datetime import timedelta
 
 

Class-based config in a Flask app factory

configuration app-factory inheritance
Intermediate 7 steps
typescript
type ResizeCallback = (size: { width: number; height: number }) => void;
 
export function observeResize(callback: ResizeCallback, delay = 150) {
  let timeoutId: ReturnType<typeof setTimeout> | undefined;

Debouncing window resize in TypeScript

debounce closures event-listeners
Intermediate 6 steps

Share this explainer

Here's the card — post it anywhere.

How API versioning works in NestJS — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code