What’s Changed & Migration

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against NestJS 12.1 (npm @nestjs/core 12.1.2, published 2026-09-30; the newest GitHub release page is v12.1.1, 2026-09-28), TypeScript 6 (6.0.x, what the Nest 12 templates install), Node.js 22 and 24 LTS, Express 5 and Fastify 5, and was checked on 2026-10-02. The code examples target the Nest 12 ESM template with Vitest and supertest, and the output blocks are what they produce. The nest upgrade --dry-run report comes from @nestjs/cli 12.0.8 run against a small NestJS 11 project.

NestJS moved quickly between the book’s era (NestJS 10, Node.js 16, TypeScript 4, published January 2025) and today. This page is the one place that collects what is different, with the version and date of each change, the old way next to the new way, the page of this section that teaches it, the official migration guide and the GitHub release notes. Read it once if you are following the book or an older tutorial, then use the tables as a lookup. Where a row says "pre-1.0", the package is still at a 0.x version and its API may change; the pages that teach it show the established alternative.

Release timeline

Dates are those of the GitHub release page, except 12.1.2, which has an npm publication but no GitHub release page yet. The migration guide column links the guide that was current for that major: the archived versions of the documentation keep their own migration-guide page.

Table 1. NestJS releases that matter for this section
Version Date Headline Migration guide Release notes

8.0.0

2021-07-07

HttpModule and HttpService of @nestjs/common deprecated in favour of @nestjs/axios.

v8 guide

v8.0.0

9.0.0

2022-07-08

ConfigurableModuleBuilder, the REPL and durable providers appear; HttpModule is gone from @nestjs/common.

v9 guide

v9.0.0

10.0.0

2023-06-15

The generation the book targets. CacheModule, CACHE_MANAGER, CacheInterceptor, @CacheKey and @CacheTTL are removed from @nestjs/common and move to @nestjs/cache-manager, whose 2.0 ships the same day; Reflector.createDecorator() follows in 10.2.0 (2023-08-21).

v10 guide

v10.0.0

11.0.0

2025-01-16

Express 5 becomes the default, Node.js 16 and 18 are no longer supported (@nestjs/core declares >= 20), termination hooks run in reverse order, ParseDatePipe and IntrinsicException are added, @nestjs/cache-manager 3 moves to cache-manager 6 and Keyv.

v11 guide

v11.0.0

11.1.0

2025-04-23

RabbitMQ topic exchanges and wildcards, microservice status, on() and unwrap() on clients and servers.

v11 guide

v11.1.0

11.2.0

2026-08-14

The HTTP QUERY method (@QueryMethod()) and the SSE signal.

current guide

v11.2.0

12.0.0

2026-08-27

ESM packages, Standard Schema, rebuilt CLI (nest upgrade, nest deploy), oxlint, Vitest, Rspack, @nestjs/observe, route conflict diagnostics, errorCode, structured logging params, removed legacy Terminus API.

v12 guide

v12.0.0

12.1.0

2026-09-23

Built-in cookies, CSRF protection and security headers, Fastify upload interceptors backed by @fastify/multipart, an array of global prefixes in the release notes, ParseDatePipe default fix, @Optional() inheritance fix.

v12 guide

v12.1.0

12.1.1

2026-09-28

Logger fixes (JSON output, level changes, fatal, buffered logs), many microservice reconnect fixes, circular dependency detection across more than two providers.

v12 guide

v12.1.1

12.1.2

2026-09-30 (npm)

Latest packages on npm at the time of writing; no GitHub release page.

v12 guide

npm

The 11.x line is still maintained (v11.2.6 was published on 2026-09-23), so an application that cannot move to Node.js 20.19 or to ESM packages yet is not stranded.

Node.js and TypeScript requirements

The Node.js floor depends on what you are doing. Running a Nest 12 application and using the CLI’s generators are different questions, because @nestjs/schematics inherits a higher floor from the Angular devkit it builds on.

Table 2. Runtime and language requirements, checked 2026-10-02
What you are doing Requirement Notes

Running a Nest 12 application

Node.js 20.19+, or 22.12+ on the 22.x line

@nestjs/core declares >= 20, but the packages are ESM-only and CommonJS applications load them through require(esm), which is unflagged from Node.js 20.19 and 22.12. The 21.x line never received it.

nest new, nest generate, nest upgrade

Node.js 22.22.3+, 24.15+ or 26+

The 23.x and 25.x lines and earlier 22.x and 24.x releases are excluded. @nestjs/cli itself declares >= 20.11.

Jest loading the ESM-only v12 packages

Node.js 24.9+

Older versions fail with ERR_REQUIRE_ASYNC_MODULE. Vitest, the default for ESM projects, does not have this limit.

CommonJS Nest 12 on AWS Lambda

NODE_OPTIONS=--experimental-require-module

Lambda disables require(esm) by default on its Node.js 20, 22 and 24 runtimes. Append the flag to any existing NODE_OPTIONS value.

Nest 11 (for comparison)

Node.js 20+ (@nestjs/core >= 20, CLI >= 20.11)

Node.js 16 and 18 stopped being supported in 11.0.0 (2025-01-16). Nest 10’s CLI accepted Node.js 16.14+.

TypeScript

6.0.x

@nestjs/cli 12 depends on typescript ~6.0.2 and generated projects pin ^6.0.2; nest upgrade moves a project to TypeScript 6. TypeScript 7.0 (the native compiler) was published on npm on 2026-10-02 but is not what nest new generates, so this section does not claim Nest supports it. Nest 11’s CLI used TypeScript 5.7, Nest 10’s used 5.3.

Express and Fastify

Express 5.2, Fastify 5.12

Express 5 has been the default since 11.0.0; see the routing section below.

The book’s Dockerfiles (node:16-alpine, node:18) and CI workflows (node-version: 14, actions/*@v3) are therefore all past their end of life; the deployment and CI/CD pages show current ones, and getting started and the CLI covers the project layout of a new project.

What changed, by area

Each table lists the version and date in which the change shipped, what older material (the book, tutorials written for Nest 8 to 10) does, what to do now, and where this section teaches it. "Guide" is the migration guide for that major and "notes" is the GitHub release. Dates for packages other than @nestjs/core are the npm publication dates of the first release that carries the change.

Versions, runtime and tooling

Version Date Old way New way Covered on Guide Notes

12.0.0

2026-08-27

CommonJS packages; require('@nestjs/core') is plain CommonJS.

All core packages ship as ESM. CommonJS applications keep working through require(esm); moving your own code to ESM is optional ("type": "module", "module": "nodenext", .js extensions on relative imports, import.meta.dirname instead of __dirname).

TypeScript for NestJS

guide

12.0.0

12.0.0

2026-08-27

nest new generates one template: CommonJS, Jest, ESLint.

nest new asks for CommonJS or ESM. ESM projects use Vitest, CommonJS projects use Jest 30, and every project uses oxlint instead of ESLint. Existing projects keep their tooling.

Getting started and the CLI, Testing

guide

12.0.0

12.0.0

2026-08-27

webpack for monorepos and for --webpack; webpack and webpackConfigPath in nest-cli.json.

Rspack is the default bundler for monorepos. --webpack and --webpackPath (and the matching nest-cli.json options) are deprecated in favour of --builder rspack and --rspackPath. nest upgrade rewrites nest-cli.json and the matching package.json scripts.

Project structure and workspaces

guide

12.0.0

12.0.0

2026-08-27

npm, yarn and pnpm; the angular schematic; hand-written decorators with SetMetadata.

bun is a supported package manager; the angular schematic is removed; the decorator schematic emits Reflector.createDecorator() decorators.

Getting started and the CLI, Custom decorators

guide

12.0.0

12.0.0

2026-08-27

Upgrading by hand with npm-check-updates; no command to deploy.

nest upgrade (alias update) moves every @nestjs/* package to its v12 major and applies the mechanical migrations; nest deploy forwards to Mau (@nestjs/mau, a dev dependency installed on first use). New build options: --parallel [n], --emit-declarations, --no-type-check, --silent, --rspackPath, and includeLibraryAssets in nest-cli.json.

Getting started and the CLI, Deployment; see nest upgrade below

guide

12.0.0

9.0.0

2022-07-08

Hand-written configuration for dynamic modules.

ConfigurableModuleBuilder generates the register()/forRoot() family and their async variants; the REPL (nest start --entryFile repl) and durable providers arrive in the same release.

Dynamic modules, Injection scopes, Debugging and Devtools

v9 guide

9.0.0

10.2.0

2023-08-21

SetMetadata('roles', roles) and reflector.get('roles', ctx.getHandler()) with string keys.

Reflector.createDecorator<string[]>() returns a typed decorator that is also the key (reflector.get(Roles, handler)); DiscoveryService.createDecorator() does the same for discovery.

Execution context, Reflector and discovery

v10 guide

10.2.0

11.0.0

2025-01-16

Expected, non-HTTP exceptions are logged as errors by the built-in filters; dates in query strings are parsed by hand.

Exceptions that extend IntrinsicException (as HttpException does) are never logged by the built-in filters; ParseDatePipe parses and validates dates (its default fix is in 12.1.0). Lazy-loaded modules (LazyModuleLoader, already in 8.0.0) are covered with the other late-binding tools.

Exception filters, Pipes and validation, ModuleRef and lazy loading

v11 guide

11.0.0

12.0.0 (docs)

2026-08-27

The Devtools package is called @nestjs/devtools.

The package is @nestjs/devtools-integration (12.0.0 on npm) and it requires snapshot: true in the NestFactory.create() options.

Debugging and Devtools

guide

12.0.0

Express 5 routing and HTTP

Version Date Old way New way Covered on Guide Notes

11.0.0

2025-01-16

forRoutes(''), @Get('files/'), optional parameters with ? (:id?), regular-expression characters in paths, nested query strings (?filter[a]=1 parsed into objects).

Express 5 and path-to-regexp 8: wildcards are named (splat, or {*splat} to also match the bare path), optional parts use braces ({.:ext}), regexp characters are no longer allowed, and the query parser is simple (restore the old one with app.set('query parser', 'extended')). Nest still converts an unnamed and logs a warning, so old code runs, but the docs call bare * deprecated. The Fastify adapter does not support mid-route wildcards.

Controllers and routing, Middleware, Fastify and HTTP platforms

v11 guide

11.0.0

12.0.0

2026-08-27

Routes are matched in declaration order, so @Get(':id') declared before @Get('me') silently handles /me.

Two opt-in NestFactory.create() options: routeConflictPolicy (duplicate and shadow, each 'off', 'warn' or 'error') and routeResolutionStrategy: 'specificity' (literal segments before parameters). Both default to the old behaviour.

Controllers and routing

guide

12.0.0

11.2.0

2026-08-14

Only GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD and @All().

@QueryMethod() maps to the HTTP QUERY method (a safe method with a body); it is not called @Query() because that is the parameter decorator.

Controllers and routing

guide

11.2.0

11.2.0

2026-08-14

An @Sse() handler cleans up only through RxJS finalize when the client leaves.

The SSE handler can receive an AbortSignal (@SseSignal()) that fires on disconnect.

Server-sent events

guide

11.2.0

12.0.0

2026-08-27

The Express adapter closes the server and may cut in-flight requests; adapter error mapping differs per platform.

The Express adapter drains in-flight requests on shutdown; error mapping was reworked across core, Express and Fastify.

Lifecycle events and shutdown

guide

12.0.0

12.1.0

2026-09-23

cookie-parser or @fastify/cookie, and res.cookie() or reply.setCookie(), depending on the platform.

Built-in, adapter-agnostic cookies: the cookies application option (with a secret for signing), @Cookies(), @SignedCookies(), and setCookie() / clearCookie().

Cookies and sessions

guide

12.1.0

12.1.0

2026-09-23

import * as helmet from 'helmet', csurf-style packages, express-rate-limit.

app.useSecurityHeaders() and app.enableCsrfProtection() on both adapters; Helmet and @fastify/helmet remain valid, and rate limiting is @nestjs/throttler 6.

Security headers, CORS and CSRF, Rate limiting and encryption

guide

12.1.0

12.1.0

2026-09-23

Multer interceptors on Express only; Fastify applications bring their own multipart handling.

Fastify upload interceptors backed by @fastify/multipart: MultipartModule, MultipartOptionsFactory and createMultipartOptions() replace the Multer counterparts on Fastify.

File upload and streaming

guide

12.1.0

12.1.0 (notes only)

2026-09-23

app.setGlobalPrefix('api'); exclude is an array of paths or { path, method } objects.

The release notes list "array of global prefixes" (PR 17713), but the published 12.1.2 packages still type the prefix as a string. See The global prefix: string, with an array of exclusions.

Versioning and the global prefix

guide

12.1.0

Caching, HTTP client and packages that moved

Version Date Old way New way Covered on Guide Notes

10.0.0, 11.0.0

2023-06-15, 2025-01-16

CacheModule, CACHE_MANAGER, CacheInterceptor, @CacheKey and @CacheTTL imported from @nestjs/common; cache-manager-redis-store; TTL in seconds.

Import them from @nestjs/cache-manager (removed from @nestjs/common in 10.0.0). With Nest 11, @nestjs/cache-manager 3 (2025-01-17) uses cache-manager 6 and Keyv (@keyv/redis for Redis); TTLs are in milliseconds.

Caching

v10 guide, v11 guide

10.0.0, 11.0.0

8.0.0, 9.0.0

2021-07-07, 2022-07-08

HttpModule and HttpService from @nestjs/common; .toPromise() on the returned Observable.

Deprecated in @nestjs/common in 8.0.0 (moved to @nestjs/axios) and removed from it in 9.0.0: use @nestjs/axios, and firstValueFrom() / lastValueFrom() instead of .toPromise(). The current docs teach the fetch-based @nestjs/http-client (pre-1.0) with a migration path from @nestjs/axios.

HTTP client

v8 guide

8.0.0

12.0.0

2026-08-27

@nestjs/bull with @Process() (legacy); Bull as a "circuit breaker".

@nestjs/bullmq with WorkerHost; circuit breaking, retries and timeouts belong to @nestjs/resilience (pre-1.0) or a library such as cockatiel.

Events, scheduling and queues, Resilience, idempotency and locks

guide

12.0.0

Validation, serialization and configuration

Version Date Old way New way Covered on Guide Notes

12.0.0

2026-08-27

ValidationPipe with class-validator DTO classes only.

@Body(), @Query(), @Param() and @RawBody() accept a schema option for Standard Schema libraries (Zod, Valibot, ArkType) and StandardSchemaValidationPipe validates it. class-validator stays supported with no plan to remove it. ValidationPipe also gained errorFormat ('list' or 'grouped'), and ArgumentMetadata is generic (custom pipe signatures may need type edits).

Pipes and validation

guide

12.0.0

12.0.0

2026-08-27

ClassSerializerInterceptor with @Exclude() and @Expose() only.

StandardSchemaSerializerInterceptor with @SerializeOptions({ schema }) validates and shapes responses from a schema; ClassSerializerInterceptor remains.

Serialization

guide

12.0.0

12.0.0

2026-08-27

ConfigModule.forRoot({ validationSchema: Joi.object(…​), validationOptions: { allowUnknown: false } }).

validationSchema takes any Standard Schema (Zod recommended for new code). Joi must be v18 or later and its settings move under validationOptions.libraryOptions; Joi keeps allowUnknown: true and abortEarly: false as defaults. The @nestjs/config v4 behaviour (Nest 11, 2025-01-17) already changed the precedence of get() and added getOrThrow(), validatePredefined and skipProcessEnv.

Configuration

guide

12.0.0

12.0.0

2026-08-27

Swagger DTOs described only with decorators or the CLI plugin.

The same Standard Schema schemas feed @nestjs/swagger 12 (see the OpenAPI introduction in the official docs).

OpenAPI and Swagger

guide

12.0.0

12.0.0

2026-08-27

Errors identified by their message string; custom message shapes.

HttpException options accept errorCode, serialized into the response body.

Exception filters and error handling

guide

12.0.0

Authentication, authorization and security

Version Date Old way New way Covered on Guide Notes

2026 (pre-1.0)

Aug to Oct 2026

A hand-written JWT guard with bcrypt and @nestjs/jwt; Passport strategies.

The official docs teach @nestjs/authentication and @nestjs/authorization (pre-1.0: sessions, two-factor, magic links, API keys, policies). The Passport and JWT approach moved to the recipes and remains a valid, established choice.

Authentication, Authorization

guide

12.0.0

12.1.0

2026-09-23

aes-256-ctr with Node crypto; passwords hashed without a work factor.

An authenticated mode (aes-256-gcm) for encryption, and a password hasher with a work factor (PasswordHasher from @nestjs/authentication, which uses scrypt, or argon2 or bcrypt); the security pages show both.

Rate limiting and encryption, Security best practices

guide

12.1.0

GraphQL

Version Date Old way New way Covered on Guide Notes

10.0.0 to 12.0.0

2023 to 2026

apollo-server-express and apollo-server-core (end of life), no driver, ApolloError, apollo-server-plugin-base, subscriptions never enabled in the module config.

@nestjs/apollo with @apollo/server and the mandatory driver option (@nestjs/apollo 14 needs @apollo/server 5 and graphql 16.10 or 17); GraphQL errors and plugins use the Apollo Server 4 and 5 APIs.

GraphQL code-first and schema-first, Subscriptions, security and plugins

v10 guide

10.0.0

12.0.0

2026-08-27

playground: true (GraphQL Playground); subscriptions-transport-ws.

GraphiQL is the default IDE (graphiql: { …​ } takes an options object); subscriptions-transport-ws support is removed in @nestjs/graphql 14, so use subscriptions: { 'graphql-ws': true }. The protocols are wire-incompatible: update clients and onConnect callbacks. nest upgrade renames the option and swaps the packages.

GraphQL code-first and schema-first, Subscriptions, security and plugins

guide

12.0.0

(graphql-subscriptions 3.0.0)

2024-11-11

pubSub.asyncIterator('topic').

pubSub.asyncIterableIterator('topic'); one shared PubSub provider, not one new PubSub() per resolver.

Subscriptions, security and plugins

v11 guide

11.0.0

12.0.0

2026-08-27

Federation described only conceptually.

ApolloFederationDriver and ApolloGatewayDriver for Federation 2.

GraphQL federation

guide

12.0.0

Microservices, queues and reliability

Version Date Old way New way Covered on Guide Notes

11.0.0, 11.1.0

2025-01-16, 2025-04-23

Fire-and-forget clients; transport options fixed at module registration.

Clients and servers expose a status observable, on() and unwrap(); options can come from dependency injection; RabbitMQ supports topic exchanges and wildcards (11.1.0).

Microservices fundamentals, Microservice transports

v11 guide

11.1.0

12.0.0

2026-08-27

npm i nats, StringCodec, JSONCodec.

NATS v3: npm uninstall nats && npm install @nestjs/microservices @nats-io/transport-node; direct helpers (headers()) come from @nats-io/nats-core; packets are JSON strings and custom deserializers read msg.json(). nest upgrade swaps the package and rewrites imports.

Microservice transports

guide

12.0.0

12.0.0

2026-08-27

Kafka handlers matched by exact string; gRPC errors surface as UNKNOWN; no hook before a handler runs.

@MessagePattern() and @EventPattern() accept a RegExp on Kafka; GrpcExceptionFilter and status-specific exceptions map to gRPC status codes; a pre-request hook runs before each handler.

Microservice transports, Microservices architecture and patterns

guide

12.0.0

12.0.0

2026-08-27

WebSocket gateways are singletons; handleDisconnect(client) only.

Gateways support request-scoped providers (the socket is injectable through REQUEST), and handleDisconnect can receive the disconnect reason.

WebSockets

guide

12.0.0

2026 (pre-1.0), 12.1.0

Aug to Oct 2026

A hand-rolled outbox table, idempotency keys, Redlock-style locks and sagas; EventEmitter2 across services; "sagas" in @nestjs/cqrs presented as distributed.

First-party @nestjs/outbox, @nestjs/idempotency, @nestjs/locks, @nestjs/workflows and @nestjs/resilience (all pre-1.0); durable sagas in @nestjs/cqrs 12.1; EventEmitter2 stays in-process only.

Outbox and durable workflows, Resilience, idempotency and locks, CQRS

guide

12.1.0

2026 (pre-1.0)

Aug to Oct 2026

Webhooks hand-written as controllers; i18n, mail and file storage with unrelated third-party modules.

@nestjs/webhooks, @nestjs/i18n, @nestjs/mail and @nestjs/storage (all pre-1.0), documented next to the established libraries; @nestjs/drizzle (pre-1.0) joins TypeORM, Prisma, MikroORM, Sequelize and Mongoose.

Webhooks, i18n, mail and file storage, Prisma, Drizzle, MikroORM and Sequelize

guide

12.0.0

Health checks, logging and observability

Version Date Old way New way Covered on Guide Notes

12.0.0

2026-08-27

Custom indicators extend HealthIndicator, call getStatus() and throw HealthCheckError. The timeout option of the database, microservice and gRPC indicators.

The legacy API (deprecated in @nestjs/terminus 11) is removed. Inject HealthIndicatorService, check(key) and return up() or down(), or use attempt(…​) with .withTimeout(ms); the per-indicator timeout option is deprecated.

Health checks and observability

guide

12.0.0

11.0.0, 12.0.0

2025-01-16, 2026-08-27

ConsoleLogger prints text; each extra argument is a separate record; Winston or Pino for JSON.

ConsoleLogger has JSON output (json: true, 11.0) and structured params (12.0, on by default): a plain object after the message becomes params or, with flattenParams, root fields. structuredParams: false restores the old output.

Logging

guide

12.0.0

12.0.0

2026-08-27

A generic APM agent attached to the HTTP server; OpenTelemetry wired by hand.

@nestjs/observe (pre-1.0) plugs into Nest’s lifecycle through the instrument application option (distributed tracing, error monitoring, MCP server). If you wire OpenTelemetry yourself, the API changed: resourceFromAttributes replaces Resource, ATTR_* constants replace SemanticResourceAttributes, OTLP replaces the Jaeger exporter, and the SDK must start before Nest is imported.

Health checks and observability

guide

12.0.0

Lifecycle, dependency injection and testing

Version Date Old way New way Covered on Guide Notes

11.0.0

2025-01-16

Termination hooks (onModuleDestroy, beforeApplicationShutdown, onApplicationShutdown) run in the same order as the init hooks; module resolution keyed by serialized tokens.

Termination hooks run in reverse order; middleware of global modules runs first; module resolution uses object-reference keys, which speeds up bootstrap.

Lifecycle events and shutdown, Providers and dependency injection

v11 guide

11.0.0

12.0.0

2026-08-27

Lifecycle hooks of related providers run in an order that depends on registration.

Hooks are invoked by component hierarchy level; review any ordering assumption between related providers and modules.

Lifecycle events and shutdown

guide

12.0.0

12.0.0, 12.1.0

2026-08-27, 2026-09-23

A subclass without its own constructor inherits the parent’s @Optional() markers.

The guide says the markers are no longer inherited; the 12.1.0 notes list a fix that inherits them again when the constructor is not redeclared. Declare the constructor and repeat @Optional(). See @Optional() and inheritance.

Providers and dependency injection

guide

12.1.0

12.0.0

2026-08-27

Jest only; import * as request from 'supertest'.

Vitest is the default for ESM projects (with SWC through unplugin-swc for decorator metadata; Jest uses @swc/jest); import request from 'supertest'. useMocker() auto-mocking and overriding global enhancers through useExisting are available. @nestjs/testing stays runner-agnostic.

Testing, Testing GraphQL, WebSockets and microservices

guide

12.0.0

Deployment and documentation

Version Date Old way New way Covered on Guide Notes

12.0.0

2026-08-27

Dockerfiles that skip the build step or install production dependencies before building; Google Container Registry; a version: key in Docker Compose files.

Multi-stage builds that build first, then copy dist and production dependencies; Artifact Registry; Compose files without version:; nest deploy for Mau.

Deployment, CI/CD

guide

12.0.0

(docs restructure)

2026-09

The Techniques chapter, /recipes/prisma, /recipes/mikroorm and /recipes/swagger.

Application, Data, HTTP, Security, Observability and Reliability groups with new URLs; see The September 2026 documentation restructure.

NestJS section index

guide

12.0.0

Before-and-after examples

The following examples target @nestjs/core 12.1.2, Express 5.2.1, TypeScript 6.0.3, Zod 4.6.5 and Vitest 4.1.11, with requests sent through supertest.

Standard Schema in the route decorators

@Body({ schema }) and @Param('id', { schema }) only attach the schema; StandardSchemaValidationPipe validates it. Zod is used here, but any Standard Schema library works.

standard-schema.ts
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
import { z } from 'zod';

// Any Standard Schema library works (Zod 4, Valibot, ArkType); Zod is used here.
export const createBookSchema = z.object({
  title: z.string().min(1),
  isbn: z.string().regex(/^\d{13}$/),
});
export type CreateBook = z.infer<typeof createBookSchema>;

@Controller('books')
export class BooksController {
  private readonly books: Array<CreateBook & { id: number }> = [];

  @Post()
  // The decorator only attaches the schema; StandardSchemaValidationPipe validates it.
  create(@Body({ schema: createBookSchema }) body: CreateBook) {
    const book = { id: this.books.length + 1, ...body };
    this.books.push(book);
    return book;
  }

  @Get(':id')
  findOne(@Param('id', { schema: z.coerce.number().int().positive() }) id: number) {
    return { id, typeofId: typeof id };
  }
}

Register the pipe once, for example in main.ts (app.useGlobalPipes(new StandardSchemaValidationPipe())). A valid body and an invalid one produced:

Output
{"id":1,"title":"Dune","isbn":"9780441013593"}
{"message":["title: Too small: expected string to have >=1 characters","isbn: Invalid string: must match pattern /^\\d{13}$/"],"error":"Bad Request","statusCode":400}

GET /books/42 returned {"id":42,"typeofId":"number"} (the schema coerces the string) and GET /books/-1 returned 400. Source: Migration guide, "Route decorator schemas".

Machine-readable error codes

error-code.ts
import { BadRequestException, Controller, Post } from '@nestjs/common';

@Controller('accounts')
export class AccountsController {
  @Post('password')
  changePassword() {
    // errorCode is serialized into the response body next to the message.
    throw new BadRequestException('Password is too weak', {
      errorCode: 'WEAK_PASSWORD',
    });
  }
}
Output (400)
{"message":"Password is too weak","error":"Bad Request","statusCode":400,"errorCode":"WEAK_PASSWORD"}

Route conflicts

The controller below declares the parametric route first, as in the documentation’s example:

route-conflicts.ts
import { Controller, Get, Param } from '@nestjs/common';

@Controller('members')
export class MembersController {
  // Declared first: on Express it is registered first and shadows 'me'.
  @Get(':id')
  byId(@Param('id') id: string) {
    return { handler: 'byId', id };
  }

  @Get('me')
  me() {
    return { handler: 'me' };
  }
}

With the defaults, GET /members/me returned {"handler":"byId","id":"me"}. With the two opt-in options:

const app = await NestFactory.create(MembersModule, {
  routeConflictPolicy: { duplicate: 'error', shadow: 'warn' },
  routeResolutionStrategy: 'specificity',
});

routeResolutionStrategy: 'specificity' alone made GET /members/me return {"handler":"me"}. routeConflictPolicy: { shadow: 'warn' } logged the following warning at bootstrap, and { shadow: 'error' } aborted the bootstrap with the same text under a "Conflicting HTTP routes detected" heading:

Warning (logger.warn)
Route GET /members/me (MembersController#me) is shadowed by GET /members/:id (MembersController#byId). The first-registered route will match all matching requests on order-sensitive adapters.

Combining shadow: 'error' with routeResolutionStrategy: 'specificity' started without error, because the specificity ordering removes the shadowing. Source: Controllers, "Route conflicts and resolution order".

Express 5 wildcards, optional parts and the query parser

express5-routes.ts
import { Controller, Get, MiddlewareConsumer, Module, NestModule, Param, Query } from '@nestjs/common';
import type { Request, Response, NextFunction } from 'express';

@Controller()
export class FilesController {
  // Express 4: 'files/*'. Express 5 needs a named wildcard; the value is an array of segments.
  @Get('files/*path')
  file(@Param('path') path: string[]) {
    return { path };
  }

  // '{*path}' also matches the bare 'docs' (the wildcard is optional).
  @Get('docs{/*path}')
  docs(@Param('path') path?: string[]) {
    return { path: path ?? null };
  }

  // Express 5 uses the 'simple' query parser: 'a[b]=1' stays a flat key, it is no longer nested.
  @Get('search')
  search(@Query() query: Record<string, unknown>) {
    return query;
  }

  // Express 4: 'report/:id?'. Express 5 has no '?'; optional parts use braces.
  @Get('report/:id{.:ext}')
  report(@Param('id') id: string, @Param('ext') ext?: string) {
    return { id, ext: ext ?? null };
  }
}

export function tag(req: Request, res: Response, next: NextFunction) {
  res.setHeader('x-seen', 'yes');
  next();
}

@Module({ controllers: [FilesController] })
export class FilesModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    // Express 4 / Nest 10: forRoutes('*'). Nest 11+: a named wildcard.
    consumer.apply(tag).forRoutes('{*splat}');
  }
}

export function restoreExtendedQueryParser(app: import('@nestjs/platform-express').NestExpressApplication) {
  // Express 5 defaults to the 'simple' parser; opt back in to nested objects.
  app.set('query parser', 'extended');
}
Responses
GET /files/a/b/c.txt                    {"path":["a","b","c.txt"]}
GET /docs                               {"path":null}
GET /docs/x/y                           {"path":["x","y"]}
GET /report/7                           {"id":"7","ext":null}
GET /report/7.pdf                       {"id":"7","ext":"pdf"}
GET /search?filter[genre]=sf&tag=a&tag=b  {"filter[genre]":"sf","tag":["a","b"]}
GET /search?filter[genre]=sf  (after app.set('query parser', 'extended'))  {"filter":{"genre":"sf"}}

The old spellings still run: a controller with @Get('old/') and a module that calls forRoutes('') start, answer 200 and apply the middleware, but Nest logs a warning (Unsupported route path: "/old/*" …​ Attempting to auto-convert to "/old/{*path}"…​). Treat that warning as a to-do item: the converter is a compatibility layer, not a supported spelling. Source: Controllers, "Route wildcards" and Express: Migrating to Express 5.

Structured logging params

const logger = new ConsoleLogger('Users', { json: true });
logger.log('User created', { userId: 1, plan: 'free' });
Output of the four modes (timestamps and ids shortened)
text:                   [Nest] ...  LOG [Users] User created { userId: 1, plan: 'free' }
json:                   {"level":"log","pid":...,"message":"User created","context":"Users","params":{"userId":1,"plan":"free"}}
json + flattenParams:   {"level":"log","pid":...,"message":"User created","context":"Users","userId":1,"plan":"free"}
structuredParams false: [Nest] ...  LOG [Users] User created
                        [Nest] ...  LOG [Users] Object(2) { userId: 1, plan: 'free' }

With structuredParams: false the object is a separate record, as in Nest 11. Source: Logger, "Structured logging params".

Terminus health indicators without HealthCheckError

health-indicator.ts (indicator)
@Injectable()
export class DogHealthIndicator {
  constructor(@Inject(HealthIndicatorService) private readonly indicators: HealthIndicatorService) {}

  isHealthy(key: string, badBoys: number) {
    const indicator = this.indicators.check(key);
    // v12: return down()/up(); throwing HealthCheckError no longer reports "down".
    if (badBoys > 0) return indicator.down({ badBoys });
    return indicator.up();
  }
}
Output (200, then 503 with the indicator down)
{"status":"ok","info":{"dogs":{"status":"up"}},"error":{},"details":{"dogs":{"status":"up"}}}
{"status":"error","info":{},"error":{"dogs":{"badBoys":2,"status":"down"}},"details":{"dogs":{"badBoys":2,"status":"down"}}}

Cache TTL in milliseconds

import { CACHE_MANAGER, CacheModule } from '@nestjs/cache-manager';

// cache-manager v6 + Keyv: the ttl is in MILLISECONDS (the book-era API used seconds).
await this.cache.set(key, value, 50);
// CacheModule.register({ ttl: 1000 }) is one second.

The value was readable right after set() and gone after 120 ms, so the third argument is milliseconds. Source: Caching, "Time-to-live (TTL)".

The global prefix: string, with an array of exclusions

prefix.ts
import type { INestApplication } from '@nestjs/common';

export function configurePrefix(app: INestApplication) {
  // exclude has always been an array; the prefix itself is a string in 12.1.2.
  app.setGlobalPrefix('api', { exclude: ['health', 'status'] });
}

export function arrayPrefixTypeCheck(app: INestApplication) {
  // The v12.1.0 notes list "array of global prefixes"; the published 12.1.2 typings
  // still take a string, so this line is a compile-time error.
  // @ts-expect-error prefix must be a string in the published packages
  app.setGlobalPrefix(['api', 'v1']);
}

The first function behaved as documented (/health unprefixed, /api/members/5 served, /members/5 404). The @ts-expect-error line proves the 12.1.2 signature is setGlobalPrefix(prefix: string, options?: GlobalPrefixOptions): if the array overload had shipped, the directive would itself be reported as unused. Forcing the array through a cast did not fail at bootstrap, but /api/members/5, /v1/members/5 and /api,v1/members/5 all returned 404. The v12.1.0 release notes (PR 17713) and the published 12.1.2 packages therefore disagree; use a single string prefix and exclude (which was always an array), and re-check the typings after upgrading. Source: Global prefix.

@Optional() and inheritance

The migration guide says Nest 12 reads optional markers with Reflect.getOwnMetadata, so a subclass without its own constructor keeps the parent’s parameter types but loses their optional status, and Nest throws UnknownDependenciesException. The 12.1.0 release notes list "inherit optional ctor params when the constructor is not redeclared" (PR 17838). Both were tested with a parent that has an optional parameter, a subclass that does not redeclare the constructor and a subclass that does:

optional-inheritance.ts
@Injectable()
export class BaseClient {
  constructor(@Optional() @Inject('HTTP_OPTIONS') readonly options?: HttpOptions) {}
}

// Does not redeclare the constructor: what happens to the parent's @Optional()?
@Injectable()
export class ImplicitChild extends BaseClient {}

// Recommended: redeclare the constructor and repeat the marker.
@Injectable()
export class ExplicitChild extends BaseClient {
  constructor(@Optional() @Inject('HTTP_OPTIONS') options?: HttpOptions) {
    super(options);
  }
}
Output with @nestjs/core 12.1.2 (the HTTP_OPTIONS provider missing, then present)
BaseClient, dep missing:     resolved options=undefined
ExplicitChild, dep missing:  resolved options=undefined
ImplicitChild, dep missing:  resolved options=undefined
BaseClient, dep present:     resolved options={"timeout":5}
ExplicitChild, dep present:  resolved options={"timeout":5}
ImplicitChild, dep present:  resolved options={"timeout":5}

A typed parameter without @Inject() (constructor(@Optional() readonly mailer?: Mailer)), compiled with SWC so that decorator metadata is emitted as in a real project, gives the same result: the implicit subclass resolves with mailer undefined. With 12.1.2, then, the behaviour matches the 12.1.0 fix, not the migration guide’s text. The 12.0.x behaviour is the one the guide describes. The portable rule is the one the guide gives: when a subclass takes optional dependencies, declare the constructor and repeat @Optional(). That is correct on every v12 release and does not depend on the fix. Source: Migration guide, "@Optional() is no longer inherited" and release v12.1.0.

nest upgrade

nest upgrade (alias nest update) ships with the CLI, so upgrade the CLI first, and do it globally if you use a global binary, because the command only bumps the project’s local @nestjs/cli dependency:

npm i -g @nestjs/cli@latest @nestjs/schematics@latest
npm i -D @nestjs/cli@latest @nestjs/schematics@latest   # if the CLI is a local dev dependency
nest upgrade --dry-run      # print the report, change nothing
nest upgrade                # apply the changes and run the install

It refuses to run on Node.js releases without require(esm) (older than 20.19, or 22.x older than 22.12). It moves every recognised @nestjs/* package to its v12-compatible major (and reports unrecognised ones), bumps TypeScript to v6, raises engines.node when it declares a lower floor, migrates the webpack options of nest-cli.json and package.json scripts to Rspack, renames the GraphQL playground option to graphiql and swaps the subscription packages, replaces nats with @nats-io/transport-node, moves @nestjs/config library settings under libraryOptions and bumps Joi to v18, bumps Jest to v30 where present, can install and wire @nestjs/observe, checks the tsconfig files for options TypeScript 6 or the ESM packages no longer support, and prints notes about behaviour it cannot migrate (lifecycle hook ordering, pipe signatures, structured logging params). It deliberately does not move your project to ESM, Vitest or oxlint.

Table 3. nest upgrade options
Option Description

--dry-run, -d

Report the changes that would be made without writing to the filesystem.

--skip-install, -s

Skip the package installation.

--observe, --no-observe

Set up @nestjs/observe, or skip it, without prompting (the prompt defaults to no, and Observe is not set up without a terminal).

--tag [tag], -t

Use an npm dist-tag (for example next) instead of the default version ranges.

--collection [name], -c

The schematics collection to use.

A dry run against a small NestJS 11 project (CommonJS, nest-cli.json with webpack: true, a GraphQL module with playground: true, TypeScript 5.7 and Jest 29) printed the following report (abridged) and ended with "Dry run enabled. No files written to disk.":

Output of nest upgrade --dry-run --no-observe (abridged)
Changes applied:
  ✔ Updated NestJS packages: @nestjs/common@^12.0.0, @nestjs/core@^12.0.0, ... @nestjs/graphql@^14.0.0, @nestjs/apollo@^14.0.0
  ✔ Updated "typescript" from "^5.7.3" to "^6.0.0" (required by @nestjs/cli and @nestjs/schematics v12)
  ✔ Updated "jest" from "^29.7.0" to "^30.0.0"
  ✔ nest-cli.json: compilerOptions.builder set to {"type":"rspack","options":{"configPath":"webpack.config.js"}}
  ✔ package.json: updated the "build" script to use Rspack ("nest build --builder rspack")
  ✔ /src/app.module.ts: renamed "playground: true" to "graphiql: true"
  ✔ Scheduled "npm install"
Notes:
  ℹ @nestjs/config now validates with Standard Schema ...
Please review:
  ⚠ TypeScript 6 deprecates several legacy "tsconfig.json" options ...
  ⚠ Jest: the NestJS 12 packages are ESM-only and Jest can only require() ESM modules on Node.js v24.9+ ...
Action required:
  ➜ Port the webpack configuration file(s) "webpack.config.js" to Rspack ...
  ➜ tsconfig.json uses "module": "commonjs" with a legacy module resolution, which cannot resolve the ESM-only NestJS 12 packages. ...
UPDATE package.json (629 bytes)
UPDATE nest-cli.json (262 bytes)
UPDATE src/app.module.ts (348 bytes)

The report separates what the command did, what to review and what you must do by hand (here the tsconfig.json module and moduleResolution settings and the webpack configuration file). Its note about @nestjs/config still prints the legacy /techniques/configuration URL, which the documentation redirects to /application/configuration. Running nest upgrade --dry-run in CI on a branch is a cheap way to see what a framework upgrade will touch; the CI/CD page shows where it fits in a pipeline. Source: CLI usages, "nest upgrade" and Migration guide, "Upgrading packages".

Where the book is outdated

The reference book (see the section bibliography) targets NestJS 10, Node.js 16 and TypeScript 4, and was published in January 2025, a few days before NestJS 11. Use it for the concepts and this section for the current API; the tables above give every difference. In short:

  • Runtime and tooling: its Node.js, Docker and CI versions are past their end of life, and it predates nest upgrade, ESM packages, Vitest, oxlint, Rspack, bun and Mau.

  • Express 5 (11.0.0): its forRoutes('*'), unnamed wildcards and optional ? parameters need the named forms, and nested query strings are no longer parsed by default.

  • Packages that moved: caching comes from @nestjs/cache-manager (cache-manager 6, Keyv, TTL in milliseconds), HTTP calls from @nestjs/axios or @nestjs/http-client, queues from @nestjs/bullmq, health checks from the HealthIndicatorService API, GraphQL from @nestjs/apollo with a driver, GraphiQL and graphql-ws.

  • Validation, serialization and configuration: Standard Schema is a second option beside class-validator; @nestjs/config is a first-class dependency, not an afterthought, and credentials come from it, never from source code or synchronize: true in production.

  • Security: Helmet, CSRF and cookies have built-in equivalents since 12.1, rate limiting is @nestjs/throttler 6, and encryption uses an authenticated mode.

  • Reliability: the book’s hand-rolled outbox, idempotency, locks and sagas have first-party, pre-1.0 counterparts, and @nestjs/cqrs sagas are in-process, not distributed.

  • Code errors: the book contains code that does not run (decorators that do not exist, removed TypeORM APIs, errors returned as HTTP 200, a response interceptor registered twice, GraphQL types without @ObjectType, one new PubSub() per resolver, invalid CLI commands). The pages of this section show the correct form where they touch those topics instead of repeating them.

What changed recently

  • NestJS 11.2.0 (2026-08-14): the HTTP QUERY method and the SSE signal, plus fixes for lazy-loaded modules and Fastify middleware on excluded routes.

  • NestJS 12.0.0 (2026-08-27): ESM packages, Standard Schema, the rebuilt CLI, native observability, route conflict diagnostics, errorCode, structured logging, and the breaking changes listed in the migration guide (Node.js floor, lifecycle hook order, NATS v3, graphql-ws only, GraphiQL, @nestjs/config validation, pipe signatures, removed Terminus legacy API, deprecated webpack workflows, removed angular schematic).

  • NestJS 12.1.0 (2026-09-23): built-in cookies, CSRF protection and security headers, Fastify upload interceptors, the ParseDatePipe default fix, middleware fixes, the optional constructor parameter fix and the array-prefix entry that the published packages do not contain.

  • NestJS 12.1.1 (2026-09-28) and 12.1.2 (2026-09-30): logger fixes for JSON output, level changes, fatal and buffered logs, reconnect fixes for the Redis, MQTT, TCP, NATS, Kafka and RabbitMQ clients, ParseArrayPipe item checking and circular dependency detection across more than two providers.

  • Documentation (September 2026): the restructure described below, a new Observability group, a new Reliability group, and the migration guide rewritten for 11 to 12.

  • Ecosystem: TypeORM is on 1.x (1.1.1), Prisma’s latest tag points at the 8.0.0 release candidate (8.0.0-rc.19), so the pages of this section pin prisma@7 (check npm view prisma dist-tags before choosing a version), @nestjs/graphql 14 resolves GraphQL 16, and TypeScript 7.0 is on npm while Nest 12 templates stay on TypeScript 6.

The September 2026 documentation restructure

In September 2026 the official documentation split its Techniques chapter into three groups, Application, Data and HTTP, moved the security topics into a Security group, and added Observability and Reliability groups; the left-hand navigation of docs.nestjs.com now has Introduction, Overview, Fundamentals, Application, Data, Security, HTTP, Observability, Reliability, GraphQL, WebSockets, Microservices, Deployment, CLI, OpenAPI, Recipes, FAQ and Devtools groups. Old links keep working through redirects, but most tutorials and the book still use the old addresses. Every link in this section uses the new addresses, which were checked against the sidebar of the documentation source repository on 2026-10-02 (the site is a single-page application that answers any path with HTTP 200, so the check cannot rely on the status code).

The legacy addresses below are listed as plain text on purpose: they are not links to follow, only addresses to recognise.

Table 4. Legacy URL to new URL
Legacy address (plain text) New address Covered on

/techniques/caching

/data/caching

Caching

/techniques/compression

/http/compression

MVC, static files and compression

/techniques/configuration

/application/configuration

Configuration

/techniques/cookies

/http/cookies

Cookies and sessions

/techniques/database and /techniques/sql

/data/overview (sections land on /data/typeorm and /data/sequelize)

Data overview and TypeORM

/techniques/events

/application/events

Events, scheduling and queues

/techniques/file-upload and /techniques/streaming-files

/http/file-upload

File upload and streaming

/techniques/http-module

/application/http-client

HTTP client

/techniques/logger

/application/logger

Logging

/techniques/mongodb

/data/mongodb

MongoDB with Mongoose

/techniques/mvc

/http/mvc

MVC, static files and compression

/techniques/performance

/http/performance

Performance optimization

/techniques/queues

/application/queues

Events, scheduling and queues

/techniques/serialization

/application/serialization

Serialization

/techniques/server-sent-events

/http/server-sent-events

Server-sent events

/techniques/session

/http/session

Cookies and sessions

/techniques/task-scheduling

/application/task-scheduling

Events, scheduling and queues

/techniques/validation

/application/validation

Pipes and validation

/techniques/versioning

/http/versioning

Versioning and the global prefix

/techniques/authentication

/security/authentication

Authentication

/techniques/security

/security/helmet

Security headers, CORS and CSRF

/techniques/hot-reload

/recipes/hot-reload

Recipes and tooling

/recipes/prisma

/data/prisma

Prisma, Drizzle, MikroORM and Sequelize

/recipes/mikroorm

/data/mikroorm

Prisma, Drizzle, MikroORM and Sequelize

/recipes/swagger

/openapi/introduction

OpenAPI and Swagger

/fundamentals/dependency-injection

/fundamentals/custom-providers

Providers and dependency injection

/fundamentals/async-components

/fundamentals/async-providers

Providers and dependency injection

/fundamentals/unit-testing and /fundamentals/e2e-testing

/fundamentals/testing

Testing

/components

/providers

Providers and dependency injection

/application-context

/standalone-applications

Platform agnosticism and standalone apps

Two practical consequences follow. Anchors survive the move for most pages (the redirect keeps the fragment, and the split Database page maps each old section to the page it now lives on), so an old deep link normally still lands in the right place. And the versioned archives (/v8 to /v11 on docs.nestjs.com) keep the old structure and their own migration guides, which is where to look when you maintain an older application.

Checklist for moving a book-era project to NestJS 12

  1. Upgrade Node.js to a supported release (20.19+ or 22.12+ to run, 22.22.3+ or 24.15+ to use the generators) and the CLI, then run nest upgrade --dry-run and read the report.

  2. Run nest upgrade, fix the tsconfig.json and Rspack items it lists, and run the test suite on Node.js 24.9+ if you stay on Jest.

  3. Replace unnamed wildcards and ? parameters, check any code that relied on nested query strings, and consider routeConflictPolicy in a test environment.

  4. Move cache, HTTP client, queue and health-check code to the packages and APIs in the tables above.

  5. Give every subclass with optional dependencies an explicit constructor with @Optional().

  6. Review lifecycle hook ordering, GraphQL subscriptions clients, NATS imports and custom pipe signatures, then adopt Standard Schema, the built-in security features and the new first-party packages on your own schedule.