Recipes & Tooling

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), @nestjs/cli 12.0.8, TypeScript 6 (6.0.x, what the Nest 12 templates install), Node.js 24 LTS (Node.js 20.19+ or 22.12+ run Nest 12), Express 5 and Fastify 5, and was checked on 2026-10-02. The tooling versions are @swc/core 1.16, @swc/cli 0.8, unplugin-swc 2, @swc/jest 0.2, webpack 5.111, @rspack/core 2.2, Vitest 4.1 (on Vite 8), Jest 30.5, nestjs-cls 7.0 and @nestjs/observe 0.3.6 (pre-1.0). The request-context examples apply to an ESM project (Vitest, supertest, Express and Fastify); the SWC and hot-reload examples each need their own build configuration, and the page says whether a given one targets an ESM or a CommonJS project. In the output blocks, process ids and timestamps are shortened.

The Nest documentation has a Recipes chapter and an FAQ chapter that sit beside the core guides. Most of their pages are the subject of a page elsewhere in this section; this one covers the four topics that have no better home (the SWC builder, hot-module replacement, AsyncLocalStorage and the samples repository), points to the courses and community resources, and ends with an index that says where every other recipe and FAQ page is treated. The running scenario is still Bookshelf.

SWC in depth

SWC is a compiler written in Rust. In a Nest project it replaces the part of nest build that turns TypeScript into JavaScript, which makes the compile step itself much faster. The price is that it only strips types: it does not check them. The basics (installing @swc/cli and @swc/core, nest start -b swc, the comparison with tsc and Rspack) are in Getting Started & CLI; this section covers the options, the output, and the places where SWC behaves differently from tsc.

Options at a glance

Where Option Effect

nest-cli.json compilerOptions

builder

"swc", or an object { "type": "swc", "options": { …​ } }. The options object takes swcrcPath (the .swcrc file to use, shown below) and extensions (the file extensions to compile; the recipe’s example adds .tsx and .jsx, which Nest applications do not normally need, so it was not run).

nest-cli.json compilerOptions

typeCheck

true runs tsc in noEmit mode next to SWC and fails the build on type errors. Default false.

nest-cli.json compilerOptions

emitDeclarations

true also writes .d.ts files. Default false.

nest build and nest start

-b swc, --type-check, --no-type-check, --emit-declarations

The same switches on the command line. Both options are ignored (the CLI prints an Info line) when the builder is not swc.

.swcrc in the project root, or the file named by swcrcPath

SWC’s own configuration

Parser, decorator and module settings. The builder is preconfigured for Nest, so a .swcrc is only needed to change something.

Type checking and declaration files

A build with both options on looks like this:

nest-cli.json
{
  "$schema": "https://json.schemastore.org/nest-cli",
  "collection": "@nestjs/schematics",
  "sourceRoot": "src",
  "compilerOptions": {
    "deleteOutDir": true,
    "builder": "swc",
    "typeCheck": true,
    "emitDeclarations": true
  }
}
npx nest build
Output
-  TSC  Initializing type checker...
✔  TSC  Initializing type checker...
>  TSC  Found 0 issues.
>  SWC  Running...
Successfully compiled: 2 files with swc (21.09ms)
>  SWC  Emitting declaration files...

dist then held app.module.js, app.module.js.map, main.js, their .d.ts files and a tsconfig.build.tsbuildinfo. The declaration for a small module is what tsc would have written:

dist/app.module.d.ts
export declare class AppController {
    hello(): string;
}
export declare class AppModule {
}

Declaration files matter when the project is a library that other packages import (see Project Structure & Workspaces); an application does not need them, so leave emitDeclarations off. Type checking matters always: without it a type error compiles and ships (the demonstration is in the CLI page). In watch mode the check runs asynchronously, so the process restarts quickly and the errors appear next to it. A CI job should build with typeCheck on, or run tsc --noEmit as a separate step.

On speed: the documentation claims SWC is about 20 times faster than the TypeScript compiler on its recipe page, and about 10 times on the CLI overview page. On a six-file project the difference is not visible: nest build -b tsc takes about 0.72 s and nest build -b swc --no-type-check about 0.76 s wall time, although SWC itself reports some 22 ms for the compile, because starting the CLI dominates a build that small. The factor depends on the project size and on whether type checking is on (it is the slow part), so measure your own project before promising a number.

The .swcrc file and the module format

Without a .swcrc the builder emits the module format that tsc would: ES modules when package.json has "type": "module" (the default of new projects) and CommonJS otherwise. A .swcrc that sets module overrides that choice, which is a common way to break an ESM project. This .swcrc in the root of an ESM project makes nest build succeed and the application crash at start-up:

Do not copy: a CommonJS module setting in an ESM project
{ "module": { "type": "commonjs" } }
Output of node dist/main.js
ReferenceError: Cannot determine intended module format because both 'exports' and top-level await are present. If the code is intended to be CommonJS, wrap await in an async function. If the code is intended to be an ES module, use export instead of module.exports/exports.

The fix is to leave module out of .swcrc in ESM projects. In a CommonJS project, the setting "module": { "type": "commonjs" } is what Jest needs (below).

To keep the SWC configuration somewhere other than the root, point the builder at it:

nest-cli.json
{
  "$schema": "https://json.schemastore.org/nest-cli",
  "collection": "@nestjs/schematics",
  "sourceRoot": "src",
  "compilerOptions": {
    "deleteOutDir": true,
    "builder": { "type": "swc", "options": { "swcrcPath": "infrastructure/.swcrc" } },
    "typeCheck": true
  }
}
infrastructure/.swcrc
{
  "$schema": "https://swc.rs/schema.json",
  "sourceMaps": false,
  "jsc": {
    "parser": { "syntax": "typescript", "decorators": true, "dynamicImport": true },
    "transform": { "legacyDecorator": true, "decoratorMetadata": true },
    "baseUrl": "./"
  },
  "minify": false
}

The build succeeded, wrote no .js.map files (the file asked for none), and dist/main.js was still an ES module. The settings in the file are merged over the builder’s Nest defaults rather than replacing them: a second run with an infrastructure/.swcrc that contained only { "sourceMaps": false } still emitted the decorator and metadata helpers (_ts_decorate, _ts_metadata) and the application started and served requests. That is different from Jest with @swc/jest (below), which does not use the builder’s defaults and needs the decorator settings spelled out in its own .swcrc.

Decorator metadata and circular imports

Nest resolves constructor parameters from the metadata that emitDecoratorMetadata writes, and SWC writes it too (decoratorMetadata in .swcrc, preconfigured by the builder). The metadata of a decorated property refers to the property’s class by name at class-definition time. With two entities that import each other, that reference is read while the other module is still being initialised, and in ES modules the result is a ReferenceError. The recipe calls this out for SWC, but it is not specific to it: the same pair compiled with tsc in the ESM project failed identically.

src/entities/user.entity.ts and profile.entity.ts (each imports the other)
// user.entity.ts
import { Column, Entity, OneToOne, PrimaryGeneratedColumn } from 'typeorm';
import { Profile } from './profile.entity.js';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id!: number;

  @Column()
  name!: string;

  @OneToOne(() => Profile, (profile) => profile.user)
  profile!: Profile; // design:type metadata references Profile at definition time
}

// profile.entity.ts has the mirror-image `user!: User` property with @JoinColumn().
Output of node dist/check.js
ReferenceError: Cannot access 'Profile' before initialization

The fix is the wrapper type from the recipe: Relation<T> from TypeORM, which is just T to the type checker but a type that has no runtime value, so the metadata falls back to Object. One detail the recipe omits matters with the current templates: because isolatedModules and emitDecoratorMetadata are both on, TypeScript 6 rejects a plain import of Relation in a decorated signature with error TS1272, and it must be imported with import type:

src/entities/user.entity.ts after the fix
import { Column, Entity, OneToOne, PrimaryGeneratedColumn } from 'typeorm';
import type { Relation } from 'typeorm';
import { Profile } from './profile.entity.js';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id!: number;

  @Column()
  name!: string;

  @OneToOne(() => Profile, (profile) => profile.user)
  profile!: Relation<Profile>;
}

With both entities changed, nest build reports Found 0 issues, and a DataSource over better-sqlite3 saves a user and a profile and loads the profile with its user as usual. For ORMs without such a type, declare your own type WrapperType<T> = T and use it the same way, including for @Inject(forwardRef) ⇒ OtherService constructor parameters (see ModuleRef, Circular Dependencies & Lazy Loading).

CLI plugins, monorepos and Rspack

The Swagger and GraphQL CLI plugins are TypeScript compiler transformers, so a builder that does not run tsc does not run them. With the SWC builder, --type-check (or typeCheck: true) is what runs the plugins and writes a serialised metadata file that the application loads at start-up. The exact setup, with PluginMetadataGenerator, is on the OpenAPI page.

Monorepos compile with Rspack by default since NestJS 12, and the recipe says Rspack already compiles TypeScript with SWC through its built-in loader, so the swc-loader and webpack.config.js setup that the recipe still shows applies only to the deprecated webpack builder. Those projects get a single bundled file rather than one file per source file (see Project Structure & Workspaces).

Tests: Vitest and Jest with SWC

Tests do not go through nest build, so the test runner needs its own SWC integration to emit decorator metadata. For Vitest, unplugin-swc is a plugin in vitest.config.e2e.ts:

vitest.config.e2e.ts
import swc from 'unplugin-swc';
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    include: ['**/*.e2e-spec.ts'],
    globals: true,
    root: './',
  },
  plugins: [
    // Builds the test files with SWC, which emits the decorator metadata Nest needs.
    swc.vite({ module: { type: 'es6' } }),
  ],
});

For Jest (CommonJS projects), the transform is @swc/jest, with the decorator settings in the project’s .swcrc:

jest.config.js
module.exports = {
  rootDir: '.',
  testRegex: '.*\\.spec\\.ts$',
  testEnvironment: 'node',
  transform: {
    '^.+\\.(t|j)s?$': ['@swc/jest'],
  },
};
A .swcrc that works with @swc/jest (CommonJS)
{
  "$schema": "https://swc.rs/schema.json",
  "sourceMaps": true,
  "jsc": {
    "parser": { "syntax": "typescript", "decorators": true, "dynamicImport": true },
    "transform": { "legacyDecorator": true, "decoratorMetadata": true },
    "baseUrl": "./"
  },
  "module": { "type": "commonjs" },
  "minify": false
}

A test that injects a service by type and calls an endpoint with supertest (a BooksController that takes a BooksService) passes with the Vitest configuration above, and, in a CommonJS project, with Jest started as node --experimental-vm-modules ./node_modules/jest/bin/jest.js, which is how the scaffolded test script runs it. The same Vitest test also passes with a configuration that has no SWC plugin at all: Vitest 4.1 runs on Vite 8, which transforms TypeScript with Oxc and emits the metadata from tsconfig.json. That is why the book-era advice "Vitest needs SWC for decorators" is no longer universally true; the plugin remains the recipe’s recommendation because it keeps the behaviour independent of the Vite version and compiles tests the way production is compiled. The details, including the failure on older Vitest, are on Testing. Remember that supertest 7 needs the default import (import request from 'supertest'), and that SWC does not type-check the tests either.

Hot reload

There are two different things called hot reload in Nest, and it helps to keep them apart:

  • Watch mode (nest start --watch, with tsc or SWC) recompiles on change and restarts the process. It works with every builder and with ESM and CommonJS projects. See Getting Started & CLI.

  • Hot-module replacement (HMR) keeps the process running, swaps only the changed modules and re-creates the application inside the same process. It is the technique of the documentation’s hot-reload recipe. It saves the start-up time of the process, which for large applications is dominated by TypeScript compilation and module loading.

flowchart LR A["Edit a .ts file"] --> B{"Which loop?"} B -- "watch mode" --> C["Recompile the changed files"] C --> D["Kill the process
start a new one"] B -- "HMR" --> E["Bundler rebuilds the
changed modules only"] E --> F["Same process:
app.close() then new application"] D --> G["New code serves requests"] F --> G

HMR with webpack (the documented recipe)

The recipe targets CommonJS projects and the Nest CLI’s webpack builder. In NestJS 12 that builder is deprecated (the CLI prints a deprecation notice) and the recipe’s own warning says new projects should prefer Rspack, but it still works in a CommonJS project with @nestjs/cli 12.0.8. Install the packages the CLI asks for (it names them when one is missing, including fork-ts-checker-webpack-plugin):

npm i --save-dev webpack webpack-node-externals run-script-webpack-plugin ts-loader fork-ts-checker-webpack-plugin tsconfig-paths-webpack-plugin

Add webpack-hmr.config.js to the project root. The CLI calls the function with its default webpack options and the webpack module, and the function returns the extended configuration:

webpack-hmr.config.js
const nodeExternals = require('webpack-node-externals');
const { RunScriptWebpackPlugin } = require('run-script-webpack-plugin');

module.exports = function (options, webpack) {
  return {
    ...options,
    entry: ['webpack/hot/poll?100', options.entry],
    externals: [
      nodeExternals({
        allowlist: ['webpack/hot/poll?100'],
      }),
    ],
    plugins: [
      ...options.plugins,
      new webpack.HotModuleReplacementPlugin(),
      new webpack.WatchIgnorePlugin({
        paths: [/\.js$/, /\.d\.ts$/],
      }),
      new RunScriptWebpackPlugin({ name: options.output.filename, autoRestart: false }),
    ],
  };
};

webpack/hot/poll?100 is the HMR runtime that polls for updates every 100 ms, nodeExternals keeps node_modules out of the bundle (except that runtime), and RunScriptWebpackPlugin starts the bundle once and lets HMR do the rest, hence autoRestart: false. Then the entry file registers the hot-module callbacks:

src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

declare const module: any;

async function bootstrap() {
  if (module.hot?.data?.closePromise) {
    // wait for the previous application instance to fully shut down
    await module.hot.data.closePromise;
  }

  const app = await NestFactory.create(AppModule, {
    forceCloseConnections: !!module.hot,
  });
  await app.listen(process.env.PORT ?? 3000);

  if (module.hot) {
    module.hot.accept();
    module.hot.dispose((data: any) => {
      data.closePromise = app.close();
    });
  }
}
bootstrap();

The two details that look unnecessary are what prevent EADDRINUSE on reload: app.close() is asynchronous but webpack does not await the dispose callback, so the promise is stashed on module.hot.data and awaited by the next instance, and forceCloseConnections makes the old server drop keep-alive connections. Run it with the CLI’s webpack flags:

nest build --webpack --webpackPath webpack-hmr.config.js --watch

With a controller that returned { message: 'v1', pid: process.pid }, changing the string to v2 while the watcher ran produced this, and the next curl returned {"message":"v2","pid":26162}, the same process id as before the edit:

Output
Entrypoint main 45.9 KiB = main.js 44 KiB 0.4ca1a00cdb5cd7964f9b.hot-update.js 1.87 KiB
webpack 5.111.1 compiled successfully in 34 ms
[HMR] Updated modules:
[HMR]  - 5
[HMR]  - 3
[HMR] Update applied.
[Nest] 26162  - 10/02/2026, 7:46:11 PM     LOG [NestFactory] Starting Nest application...

HMR with Rspack

The Rspack builder is the supported direction, and its configuration function has the same shape, except that the second argument is the @rspack/core module. The recipe has no Rspack variant; this one is adapted from the webpack recipe and targets the same kind of CommonJS project. Two differences: Rspack’s own HMR runtime lives at @rspack/core/hot/poll, and Rspack has no WatchIgnorePlugin (new rspack.WatchIgnorePlugin(…​) throws "is not a constructor"), so the output directory is excluded through watchOptions.ignored instead.

rspack-hmr.config.js
const nodeExternals = require('webpack-node-externals');
const { RunScriptWebpackPlugin } = require('run-script-webpack-plugin');

// The CLI calls this function with its default Rspack options and the `@rspack/core` module.
module.exports = function (options, rspack) {
  return {
    ...options,
    entry: ['@rspack/core/hot/poll?100', options.entry],
    externals: [
      nodeExternals({
        allowlist: ['@rspack/core/hot/poll?100'],
      }),
    ],
    // Rspack has no WatchIgnorePlugin; keep the emitted output out of the watcher instead.
    watchOptions: { ignored: ['**/dist/**', '**/node_modules/**'] },
    plugins: [
      ...(options.plugins ?? []),
      new rspack.HotModuleReplacementPlugin(),
      new RunScriptWebpackPlugin({ name: options.output.filename, autoRestart: false }),
    ],
  };
};

The main.ts is the same as above. Select the builder and the configuration on the command line, or in nest-cli.json with the object form of builder (the CLI documentation names builder.options.configPath as the replacement for the deprecated webpackConfigPath):

nest build --builder rspack --rspackPath rspack-hmr.config.js --watch
nest-cli.json
{
  "$schema": "https://json.schemastore.org/nest-cli",
  "collection": "@nestjs/schematics",
  "sourceRoot": "src",
  "compilerOptions": {
    "deleteOutDir": true,
    "builder": { "type": "rspack", "options": { "configPath": "rspack-hmr.config.js" } }
  }
}

Both forms behave the same way. After the same v1 to v2 edit the process keeps its id, the response changes, and the log shows Rspack 2.2.8 compiled successfully in 27 ms followed by [HMR] Update applied.

ESM projects, limits and pitfalls

New projects are ESM, and this is where HMR is awkward. In an ESM project ("type": "module"), the webpack variant does not start at all; the CLI refuses with The webpack compiler does not support ESM projects ("type": "module" in package.json). Please use rspack instead by setting "builder": "rspack" in your nest-cli.json compilerOptions, or use --builder rspack on the command line. The Nest repository contains an ESM hot-reload sample (sample/34-hmr-esm, see the samples table below) whose script uses the same webpack flags and a .cjs configuration; check it against your CLI version before relying on it.

Rspack HMR in an ESM project (an import.meta.webpackHot check in main.ts, the HMR runtime kept out of the externals, output.publicPath set to ./) starts the application and compiles an update in about 31 ms, but the update itself fails in Node.js with [HMR] Update failed: Error [ERR_MODULE_NOT_FOUND]: Cannot find module '…​/dist/main.<hash>.hot-update.json.mjs' and the old code keeps serving. That setup is not a recommendation: for ESM projects use watch mode with the SWC builder, (its restart loop is shown on the CLI page), and use HMR where a CommonJS project with webpack or Rspack already exists.

The recipe’s own warnings apply too. A bundler does not copy assets (such as .graphql files) to dist, and a bundle does not support static glob paths such as the entities property of TypeOrmModule, so list classes explicitly. Application state (caches, in-memory sessions) is lost on every HMR cycle, as with a restart. And a configuration that works for nest build --watch is a development tool only: build production artefacts without the HMR entries.

AsyncLocalStorage and nestjs-cls

A recurring need in a web application is request context: the authenticated user, the tenant, a correlation id, a database transaction. Nest’s default answer, request-scoped providers (see Injection Scopes), makes every provider that depends on one request-scoped too and costs performance. The alternative that the documentation now prefers for the common case is Node’s AsyncLocalStorage: wrap the handling of a request in als.run(store, …​), and any code that runs inside that call, however many await`s deep, can read the same store with `als.getStore(). It is the equivalent of thread-local storage in other languages, and providers stay singletons.

flowchart TD Q["Need per-request state
in singleton providers?"] --> A{"Entry points?"} A -- "HTTP only,
one or two values" --> M["Plain AsyncLocalStorage
in a middleware"] A -- "HTTP plus jobs,
messages, GraphQL" --> C["nestjs-cls
ClsModule and ClsService"] A -- "Already using
NestJS Observe" --> O["TracerService
setAttribute / getAttribute"] Q --> R{"Needs a different
instance per request?"} R -- "yes" --> S["Request-scoped provider"]

The same mechanism is used for log correlation on the Logging page; this section is the general treatment.

A custom implementation

Nest has no built-in abstraction for it, so the recipe builds one with three parts: a module that provides one AsyncLocalStorage instance, a middleware that opens the context, and any service that reads it. Middleware is the first thing a request reaches, so the store is visible in guards, pipes, interceptors, controllers and exception filters (see Middleware and Request Lifecycle):

als.module.ts
import { AsyncLocalStorage } from 'node:async_hooks';
import { Controller, Get, Injectable, MiddlewareConsumer, Module, NestModule } from '@nestjs/common';

export interface RequestStore {
  userId: string;
}

@Module({
  providers: [{ provide: AsyncLocalStorage, useValue: new AsyncLocalStorage<RequestStore>() }],
  exports: [AsyncLocalStorage],
})
export class AlsModule {}

@Injectable()
export class ShelfService {
  constructor(private readonly als: AsyncLocalStorage<RequestStore>) {}

  // A plain singleton that reads per-request state without a request-scoped provider.
  async shelfFor(): Promise<{ userId: string; books: string[] }> {
    await new Promise((resolve) => setTimeout(resolve, Math.random() * 20)); // let requests interleave
    const userId = this.als.getStore()?.userId ?? 'anonymous';
    return { userId, books: [`Dune (shelved by ${userId})`] };
  }
}

@Controller('shelf')
export class ShelfController {
  constructor(private readonly shelf: ShelfService) {}

  @Get()
  find() {
    return this.shelf.shelfFor();
  }
}

@Module({
  imports: [AlsModule],
  providers: [ShelfService],
  controllers: [ShelfController],
})
export class ShelfModule implements NestModule {
  constructor(private readonly als: AsyncLocalStorage<RequestStore>) {}

  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply((req: { headers: Record<string, string | undefined> }, _res: unknown, next: () => void) => {
        const store: RequestStore = { userId: req.headers['x-user-id'] ?? 'anonymous' };
        // Everything the rest of the request does runs inside this callback's async context.
        this.als.run(store, () => next());
      })
      .forRoutes('{*splat}');
  }
}

When a test sends 20 concurrent requests, each with its own x-user-id, through the module, the artificial random delay makes them interleave, and every response carries the id of its own request:

Output
{"userId":"u3","books":["Dune (shelved by u3)"]}
{"userId":"anonymous","books":["Dune (shelved by anonymous)"]}

The first line is the response to the request with x-user-id: u3, the second the response to a request without the header. On the Fastify adapter the same module returned {"userId":"fast-1","books":["Dune (shelved by fast-1)"]} for x-user-id: fast-1; the middleware receives the underlying request object on both platforms, so only the types in its signature differ. The pattern was introduced in Injection Scopes and the request-id variant appears in Logging.

The limits are the ones the documentation warns about. It hides the data flow (a service now depends on something the constructor does not show), so keep the store small and avoid a catch-all object. It covers only the entry points you wrap: a cron job, a queue consumer or a message handler runs outside the request’s run() call and sees undefined. And outside any context als.getStore() returns undefined, so code that can run both ways must handle that (the example above falls back to 'anonymous').

nestjs-cls

nestjs-cls (a third-party package maintained outside the Nest core team; "CLS" stands for continuation-local storage) removes the boilerplate: a ClsModule that opens the context for you, an injectable and typed ClsService, request ids, and initialisers for transports other than HTTP. The package version here is 7.0.1, which supports NestJS 10 to 12. The first example uses four of its features at once: a typed store, a context opened by an interceptor instead of a middleware, a proxy provider, and a request id.

cls.module.ts (the module and the controller)
import { randomUUID } from 'node:crypto';
import { CanActivate, Controller, ExecutionContext, Get, Injectable, Module, UseGuards } from '@nestjs/common';
import { ClsModule, ClsService, ClsStore, InjectableProxy } from 'nestjs-cls';

// A typed store: keys and value types are checked by ClsService<BookshelfStore>.
export interface BookshelfStore extends ClsStore {
  userId: string;
  tenant: string;
}

// A guard that authenticates; the interceptor-mounted setup runs after it, so it can see request.user.
@Injectable()
export class DemoAuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const req = context.switchToHttp().getRequest();
    req.user = { id: req.headers['x-user-id'] ?? 'anonymous' };
    return true;
  }
}

@Injectable()
export class AuditService {
  constructor(private readonly cls: ClsService<BookshelfStore>) {}

  describe() {
    return { id: this.cls.getId(), userId: this.cls.get('userId'), tenant: this.cls.get('tenant') };
  }
}

// A proxy provider: a request-aware object that is still injected like a singleton.
@InjectableProxy()
@Injectable()
export class CurrentUser {
  constructor(public readonly cls: ClsService<BookshelfStore>) {}
  get id(): string {
    return this.cls.get('userId');
  }
}

@Controller('audit')
@UseGuards(DemoAuthGuard)
export class AuditController {
  constructor(
    private readonly audit: AuditService,
    private readonly user: CurrentUser,
  ) {}

  @Get()
  find() {
    return { ...this.audit.describe(), proxyUserId: this.user.id };
  }
}

@Module({
  imports: [
    ClsModule.forRoot({
      global: true,
      // Mount an interceptor instead of the middleware so setup() runs after the guards.
      interceptor: {
        mount: true,
        generateId: true,
        idGenerator: () => randomUUID(),
        setup: (cls, context) => {
          const req = context.switchToHttp().getRequest();
          cls.set('userId', req.user?.id ?? 'anonymous');
          cls.set('tenant', req.headers['x-tenant'] ?? 'public');
        },
      },
    }),
    ClsModule.forFeature(CurrentUser),
  ],
  controllers: [AuditController],
  providers: [AuditService],
})
export class AuditModule {}

A request with x-user-id: ann and x-tenant: acme returned the following (with a fresh UUID each time), on both Express and Fastify:

Output
{"id":"698c86cc-0834-4bf3-addf-f779158b3faf","userId":"ann","tenant":"acme","proxyUserId":"ann"}

The choices in this module are worth explaining:

  • Where the context opens. ClsModule.forRoot() can mount a middleware (the recipe’s choice, it is the earliest point), a guard or an interceptor (each with mount, setup, generateId and idGenerator). Middleware runs before guards, so it cannot see what a guard attaches to the request (such as request.user); a guard or interceptor opens the context later but with the full execution context, which also works for GraphQL and other transports. Setting middleware.setup, as the official recipe does, is the simplest form when nothing from the guards is needed.

  • Typing. ClsService<BookshelfStore> makes get('userId') return string and rejects unknown keys, and ClsStore is the base interface (a plain interface extending it works here, unlike the typing of the Observe store below).

  • Proxy providers. A class decorated with @InjectableProxy() and registered with ClsModule.forFeature(CurrentUser) is injected like a singleton, but its properties and methods are resolved against the current context on each access, so business classes can depend on CurrentUser without knowing about CLS. Factory proxies (forFeatureAsync()) are the variant for values that need other providers to construct.

  • Ids. generateId: true makes cls.getId() return an id per context, and the idGenerator option replaces the generator. The package can also keep the request itself in the store (the saveReq option and the CLS_REQ key, per the recipe); check the package documentation for the defaults before relying on them.

Beyond HTTP, a background job or a message handler can open its own context with the @UseCls() decorator on an async method:

The job (in cls.module.ts)
import { randomUUID } from 'node:crypto';
import { Injectable } from '@nestjs/common';
import { ClsService, UseCls } from 'nestjs-cls';
import { BookshelfStore } from './cls.module.js';

@Injectable()
export class ReindexJob {
  constructor(private readonly cls: ClsService<BookshelfStore>) {}

  @UseCls<[string]>({
    generateId: true,
    idGenerator: () => `job-${randomUUID()}`,
    setup: (cls, tenant) => cls.set('tenant', tenant),
  })
  async run(tenant: string): Promise<{ id: string; tenant: string }> {
    await Promise.resolve();
    return { id: this.cls.getId(), tenant: this.cls.get('tenant') };
  }
}

Calling run('acme') returned {"id":"job-f3585543-2d67-45cd-8265-513314256281","tenant":"acme"}. The decorator needs the module to be initialised (in a test, await moduleRef.init() after compile()); without that, the first call failed with this.beforeSetupHooks is not iterable, because the plugin hooks are registered during module initialisation.

In unit tests the ClsService is an ordinary provider, so it can be mocked. To use the real one, import the static ClsModule (which provides only the service) and wrap the call under test in runWith(), passing a hand-made store:

Unit test with a hand-made store
const mod = await Test.createTestingModule({ imports: [ClsModule], providers: [AuditService] }).compile();
const cls = mod.get<ClsService<BookshelfStore>>(ClsService);
const audit = mod.get(AuditService);

const out = cls.runWith({ userId: 'u42', tenant: 't1' }, () => audit.describe());
// out: { id: undefined, userId: 'u42', tenant: 't1' }

Calling ClsService outside any context did not throw: cls.isActive() was false and get('tenant') and getId() returned undefined.

NestJS Observe

If the application is instrumented with NestJS Observe (the pre-1.0 @nestjs/observe package, set up on the Health Checks & Observability page), it already maintains an AsyncLocalStorage store for every request, job and message it traces, and TracerService exposes it through setAttribute() and getAttribute(). There is no module or middleware to write, and the store exists wherever Observe traces an operation, not only on HTTP:

observe.module.ts
import { Controller, Get, Injectable, Module, Req } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { createObserveModule, TracerService } from '@nestjs/observe';

export const { ObserveModule, ObserveInstrument } = createObserveModule();

// A type alias (not an interface) satisfies the store constraint.
type RequestStore = { userId: string };

@Injectable()
export class CatalogService {
  constructor(private readonly tracer: TracerService<RequestStore>) {}

  forCurrentUser() {
    // Written by the controller, read here, with no module or middleware of our own.
    return { userId: this.tracer.getAttribute('userId'), traceId: this.tracer.currentTraceId() };
  }
}

@Controller('catalog')
export class CatalogController {
  constructor(
    private readonly tracer: TracerService<RequestStore>,
    private readonly catalog: CatalogService,
  ) {}

  @Get()
  find(@Req() req: { headers: Record<string, string> }) {
    this.tracer.setAttribute('userId', req.headers['x-user-id'] ?? 'anonymous');
    return this.catalog.forCurrentUser();
  }
}

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    ObserveModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        appKey: config.getOrThrow<string>('OBSERVE_APP_KEY'),
        appSecret: config.getOrThrow<string>('OBSERVE_APP_SECRET'),
        serviceId: 'bookshelf-api',
      }),
    }),
  ],
  controllers: [CatalogController],
  providers: [CatalogService],
})
export class CatalogModule {}

The application was created with NestFactory.create(CatalogModule, { instrument: ObserveInstrument }). Two concurrent requests with different x-user-id headers returned {"userId":"ann","traceId":"01a0fdb6-ed38-7b21-a7c4-acfa9344bff6"} and {"userId":"bob","traceId":"01a0fdb6-ed3c-740a-a24f-9cf87050cd3b"}: each saw its own value and its own trace id (a stand-in HTTP server received the telemetry; the hosted dashboard was not used). The API key pair comes from configuration, never from source. getAttribute() and setAttribute() throw outside a traced context, while currentTraceId() returns null; and as noted on the observability page, TracerService<Store> needs the store type to be a type alias, because an interface has no index signature and TypeScript 6 rejects it.

Choosing

Approach Use it when Watch out for

Plain AsyncLocalStorage in a middleware

HTTP only, one or two values, no extra dependency.

You write the module, the types and the non-HTTP entry points yourself.

nestjs-cls

HTTP plus GraphQL, queue consumers, scheduled jobs or messages; typed store; proxy providers; testing helpers.

A third-party dependency with its own release cycle; open the context in a guard or interceptor if setup needs authentication results.

TracerService of @nestjs/observe

The application already uses the hosted observability service.

Pre-1.0 package; throws outside a traced context.

Request-scoped providers

A provider genuinely needs a new instance per request.

The scope bubbles up the dependency graph and costs performance (see Injection Scopes).

The samples repository

The Nest repository contains a sample directory with small, runnable applications, each one the companion of a documentation topic. They are maintained alongside the framework (the documentation links to them from several pages), which makes them the quickest way to see a feature’s wiring in a complete project, and to compare your setup with the maintainers'. Listed on 2026-10-02 there were 37, numbered 01 to 37 (the topics in the table are taken from the directory names; open a sample to see what it actually wires):

Sample Shows Covered on

01-cats-app

The basic application: controllers, providers, modules.

Introduction & Architecture

02-gateways, 16-gateways-ws

WebSocket gateways with Socket.IO and with ws.

WebSockets

03-microservices, 04-grpc

TCP microservices and gRPC.

Microservice Transports

05-sql-typeorm, 13-mongo-typeorm, 06-mongoose, 14-mongoose-base, 07-sequelize

SQL and MongoDB data access with TypeORM, Mongoose and Sequelize.

Data Overview & TypeORM, MongoDB with Mongoose

08-webpack, 34-hmr-esm

Bundling with webpack, and hot-module replacement in an ESM project.

This page, Hot reload

09-babel-example

Compiling a Nest application with Babel (babel src -d dist) instead of tsc.

Not covered; compare with SWC.

10-fastify

The Fastify adapter.

Fastify & HTTP Platforms

11-swagger

OpenAPI documents.

OpenAPI & Swagger

12-graphql-schema-first, 23-graphql-code-first, 22-graphql-prisma, 33-graphql-mercurius

GraphQL, schema first and code first, with Prisma and with Mercurius.

GraphQL: Code First & Schema First

31-graphql-federation-code-first, 32-graphql-federation-schema-first

Federation.

GraphQL Federation

15-mvc, 17-mvc-fastify, 24-serve-static

Server-side rendering and static files.

MVC, Static Files & Compression

18-context

Standalone applications and execution context.

Platform Agnosticism & Standalone Apps

19-auth-jwt

JWT authentication.

Authentication

20-cache

Caching.

Caching

21-serializer, 36-valibot-serializer

Serialization.

Serialization

25-dynamic-modules

Dynamic modules.

Dynamic Modules

26-queues, 27-scheduling, 30-event-emitter, 37-outbox

Queues, scheduling, events and the transactional outbox.

Events, Scheduling & Queues, Outbox & Durable Workflows

28-sse

Server-sent events.

Server-Sent Events

29-file-upload

File uploads.

File Upload & Streaming

35-zod-validation

Validation with Zod through Standard Schema.

Pipes & Validation

Three cautions apply when using them. They track the master branch, so a sample may use an API from a newer release than the one your project has; check the sample’s package.json for the versions it pins (the HMR sample pins @nestjs/core 12.1.2 and @nestjs/cli 12.0.8). They are written for demonstration and are deliberately plain (no authentication in the data samples, in-memory storage in several). And the set changes as topics are added, so look at the directory listing rather than at an old tutorial’s link. The typescript-starter repository is the minimal starter project.

Courses and community resources

  • Official courses. courses.nestjs.com is the official courses site, linked from the documentation’s own navigation; see the site for the current course list and terms. Courses complement this section when a topic benefits from being watched rather than read. As with any course, check the course’s Nest version against the current release: the documentation changed a lot in 2026 (see the changes below).

  • The documentation and its source. docs.nestjs.com is the primary reference; its Markdown source is public at nestjs/docs.nestjs.com, and issues or corrections can be sent there. The documentation site is a single-page application, so a link to a removed page still returns HTTP 200; validate a link against the sidebar rather than the status code.

  • Release notes and issues. GitHub releases list every change with its pull request, and the repository is where bugs are reported; the @nestjs/* packages on npm are listed under the nestjs organisation. Since the pre-1.0 packages move quickly, the release notes are the reliable source for their changes.

  • Community. The community Discord server is linked from the documentation site (invite link as published on docs.nestjs.com) and is a place to ask questions. awesome-nestjs is a curated list of libraries, tools and articles, and the Trilon blog publishes Nest articles (the documentation’s Vitest recipe links to a Trilon repository). Questions and answers on Stack Overflow under the nestjs tag are plentiful, but check their dates against the Nest version they describe.

  • Third-party packages used in this section. nestjs-cls (repository), nestjs-pino, nest-winston and the others named on their pages are community projects; read their changelogs when upgrading Nest, because their peer-dependency ranges decide when they support a new major.

  • Examples of use. The documentation lists companies that use Nest (Who is using Nest?), which is useful when a decision needs a reference.

Where the other recipes and FAQ pages are treated

The recipes and FAQ pages of the documentation sidebar that are not the subject of this page each have a home in this section:

Documentation page Topic Covered on

Recipes: REPL

Interactive shell for a running application.

Debugging & Devtools

Recipes: CRUD generator

nest g resource.

Getting Started & CLI

Recipes: Passport

Passport strategies.

Authentication

Recipes: Router module

RouterModule path trees.

Modules, Versioning & Global Prefix

Recipes: Health checks

Terminus.

Health Checks & Observability

Recipes: CQRS

Commands, queries, events, sagas.

CQRS

Recipes: Serve static

ServeStaticModule.

MVC, Static Files & Compression

Recipes: Commander

Command-line applications with nest-commander.

Platform Agnosticism & Standalone Apps

FAQ: Serverless

Running Nest on AWS Lambda and similar.

Deployment, Platform Agnosticism & Standalone Apps

FAQ: HTTP adapter

HttpAdapterHost.

Fastify & HTTP Platforms

FAQ: Keep-alive connections

Connection draining on shutdown.

Lifecycle Events & Shutdown

FAQ: Global path prefix

setGlobalPrefix() and exclude.

Versioning & Global Prefix

FAQ: Raw body

rawBody: true.

Webhooks, File Upload & Streaming

FAQ: Hybrid application

One process serving HTTP and microservices.

Microservices Fundamentals

FAQ: HTTPS and multiple servers

httpsOptions, one app on several ports.

Fastify & HTTP Platforms, Deployment

FAQ: Request lifecycle

The order of middleware, guards, interceptors, pipes and filters.

Request Lifecycle

FAQ: Common errors

Nest can’t resolve dependencies and circular dependency messages.

Debugging & Devtools, Providers & Dependency Injection

Where the book is outdated

The reference book (see the section bibliography) was published in January 2025, before NestJS 12, so it cannot describe the tooling of this page as it is today:

  • SWC is now a first-class builder with typeCheck and emitDeclarations options, Rspack replaced webpack as the bundler (webpack is deprecated, and CLI 12.0.8 refuses it in ESM projects), and new projects are ESM, which changes how decorator metadata, circular imports and HMR behave.

  • New projects test with Vitest on ESM (Jest remains for CommonJS), and the SWC plugin for Vitest is optional on current Vitest, as shown above.

  • The documentation now presents AsyncLocalStorage (or the store of NestJS Observe) as the default for request context and request scope as the exception, so older material that reaches for request-scoped providers first predates that guidance.

  • Links to documentation pages from before the September 2026 restructure (the /techniques/ URLs, for instance) no longer match the current sidebar.

What changed recently

  • NestJS 12 (2026-08-27): Rspack became the default builder for monorepos, --webpack and --webpackPath were deprecated in favour of --builder rspack and --rspackPath, and nest build gained --emit-declarations, --no-type-check and --silent. The hot-reload and SWC recipes carry warnings to that effect, and CLI 12.0.8 refused the webpack builder in an ESM project. See the migration guide.

  • The async-local-storage recipe gained the NestJS Observe section (@nestjs/observe 0.x, pre-1.0), and the observability chapter is new in the September 2026 documentation restructure.

  • nestjs-cls is at 7.0 with support for NestJS 10 to 12; its ClsModule options (middleware, guard, interceptor, plugins, proxyProviders) are unchanged in spirit from the versions older tutorials describe.

  • The samples repository lists 37 applications on 2026-10-02; the highest numbers (34-hmr-esm, 35-zod-validation, 36-valibot-serializer, 37-outbox) match features the 12.x line added or emphasises (ESM, Standard Schema, the outbox).

  • Vite 8 (Oxc) made decorator metadata work in Vitest without SWC; unplugin-swc 2 remains supported.