Bootstrapping the NestJS roadmap
Resolving 93 modules across 13 phases
0%
NestJS roadmapBackend Engineering Guides
0 of 93 done
Focus mode on
NestJS 1193 modules13 phasesSnippet Embedded

NestJS,from the first module to full mastery

A complete backend roadmap built on a real codebase. Start with why modules exist, learn how the injector thinks, walk a request through every stage, then wire PostgreSQL, Redis, queues, sockets, auth, storage and tests until you can ship the whole thing.

Phase 00, module 00

Before you start

The NestJS dictionary

Every word this roadmap uses, explained three ways: in plain words, in technical terms, and with an everyday comparison.

Module 00 of 92, phase 00

A word for everything

The NestJS dictionary

A catalog of every keyword in this roadmap. Each entry says what the word means in plain language, what it means technically, and what it is like in everyday life, then links to the module that teaches it.

In detail

Use this module as a reference rather than a lesson. Filter by kind to see only decorators or only concepts, or type a word to find it. Words such as @Injectable, Guard or Pipe are highlighted in every code example on this page and link back to the module that explains them, so you can always jump from code to meaning.

Module

Concept
In plain words
A folder of code that belongs together, with a clear front door.
Technically
A class decorated with @Module that declares imports, providers, controllers and exports, forming one node in the dependency graph.
Learn it in module 05Why modules exist

@Module()

Decorator
In plain words
The label that turns a class into a module.
Technically
A class decorator storing metadata that Nest reads at startup to build the module graph.
Learn it in module 05Why modules exist

Provider

Concept
In plain words
Anything Nest can build for you and hand to another class.
Technically
A class, value or factory registered under a token in a module's providers array and resolved by the injector.
Learn it in module 10Providers and @Injectable

@Injectable()

Decorator
In plain words
Marks a class so Nest is allowed to create it and give it its needs.
Technically
A class decorator that registers the class for dependency injection and enables reading its constructor parameter types.
Learn it in module 10Providers and @Injectable

Dependency injection

Concept
In plain words
Classes ask for what they need instead of building it themselves.
Technically
An inversion of control pattern where an injector constructs dependencies and passes them through constructors.
Learn it in module 10Providers and @Injectable

Injector

Concept
In plain words
The part of Nest that builds objects in the right order.
Technically
The container that resolves tokens, instantiates providers, caches singletons and reports missing dependencies.
Learn it in module 18DI resolution and common errors

Injection token

Concept
In plain words
The name under which something is stored for injection.
Technically
A class, string, symbol or InjectionToken used as the lookup key in the injector; interfaces need one because they vanish at runtime.
Learn it in module 11Injectors, tokens and custom providers

@Inject()

Decorator
In plain words
Says exactly which token to fetch for a constructor argument.
Technically
A parameter decorator that overrides the inferred type with an explicit token.
Learn it in module 11Injectors, tokens and custom providers

Controller

Concept
In plain words
The class that receives web requests and sends replies.
Technically
A class decorated with @Controller whose methods are bound to HTTP verbs and paths.
Learn it in module 20Controllers and route handlers

@Get() and friends

Decorator
In plain words
Labels that say which web address and method a function answers.
Technically
Method decorators (@Get, @Post, @Patch, @Delete) that register a route handler with a path and HTTP method.
Learn it in module 20Controllers and route handlers

Service

Concept
In plain words
Where the real decisions and work happen.
Technically
An injectable provider that holds business logic and coordinates repositories, caches and queues.
Learn it in module 21Services and the business layer

Middleware

Concept
In plain words
Code that looks at every request before anything else does.
Technically
A function or NestMiddleware class with access to the raw request, response and next(), registered through MiddlewareConsumer.
Learn it in module 26Middleware

Guard

Concept
In plain words
Decides whether a request is allowed in.
Technically
A CanActivate class that runs after middleware with the ExecutionContext and returns true or throws.
Learn it in module 28Guards

Pipe

Concept
In plain words
Checks and tidies input before your function sees it.
Technically
A PipeTransform that validates or converts a single handler argument and throws on bad input.
Learn it in module 29Pipes

Interceptor

Concept
In plain words
Wraps a handler to do something before and after it.
Technically
A NestInterceptor that receives the call handler as an RxJS Observable and can transform, time, cache or replace the result.
Learn it in module 32Interceptors

Exception filter

Concept
In plain words
Turns errors into tidy, consistent replies.
Technically
An ExceptionFilter decorated with @Catch that writes the response for thrown exceptions.
Learn it in module 34Exception filters

Decorator

Concept
In plain words
A label with an @ sign that adds meaning to a class or function.
Technically
A TypeScript function applied at class definition that attaches metadata or wraps behaviour.
Learn it in module 09Decorators and reflect-metadata

Metadata

Concept
In plain words
Extra information stored about code, not inside it.
Technically
Key value data attached via Reflect metadata, read at runtime by Nest or by Reflector.
Learn it in module 09Decorators and reflect-metadata

Reflector

Class
In plain words
A helper that reads labels placed by decorators.
Technically
An injectable that reads metadata from handlers and classes, with get and getAllAndOverride.
Learn it in module 09Decorators and reflect-metadata

ExecutionContext

Class
In plain words
Everything Nest knows about the call happening right now.
Technically
An object exposing the handler, the class and the transport (HTTP, WebSocket or RPC) to guards and interceptors.
Learn it in module 28Guards

DTO

Concept
In plain words
The shape of data that comes in or goes out.
Technically
A data transfer object type describing request or response payloads, often inferred from a schema.
Learn it in module 39DTOs and the contracts file

Schema

Concept
In plain words
Rules that check whether data is correct.
Technically
A runtime validator, such as a Zod object, that parses unknown input into typed data or reports issues.
Learn it in module 30Request body validation with Zod

Interface

Concept
In plain words
A description of shape that only the compiler sees.
Technically
A TypeScript type that is erased at runtime, used for ports and internal records.
Learn it in module 38Interfaces in NestJS

Contract

Concept
In plain words
The promise of what a feature accepts and returns.
Technically
The collection of DTO types, event maps and response shapes in a feature's contracts file.
Learn it in module 39DTOs and the contracts file

Barrel file

File
In plain words
One file that re-exports a folder's public parts.
Technically
An index.ts that exposes a module's public surface so other code imports from one path.
Learn it in module 07Barrel files and index.ts

Dynamic module

Concept
In plain words
A module you can configure when you import it.
Technically
A module returned from a static method such as forRoot or registerAsync, carrying options and providers.
Learn it in module 15Dynamic modules and registerAsync

forwardRef()

Function
In plain words
A way to let two things that need each other both exist.
Technically
A function that defers resolution of a module or provider reference to break a circular dependency.
Learn it in module 17Circular dependencies and forwardRef

Scope

Concept
In plain words
How long a created object lives and who shares it.
Technically
Provider lifetime: DEFAULT singleton, REQUEST per request, or TRANSIENT per consumer.
Learn it in module 12Provider scopes

Lifecycle hook

Concept
In plain words
Code that runs at start up or shut down.
Technically
Methods such as onModuleInit and onApplicationShutdown called by Nest at fixed stages.
Learn it in module 19Global modules, ModuleRef and lifecycle hooks

Gateway

Concept
In plain words
The part that keeps a live two way connection with clients.
Technically
A @WebSocketGateway provider handling Socket.io events with @SubscribeMessage and broadcasting via the server.
Learn it in module 60WebSocket gateways with Socket.io

Event emitter

Concept
In plain words
A way for one part to announce news and others to react.
Technically
An in process publish subscribe bus; emit sends a named event and @OnEvent listeners receive it.
Learn it in module 53In process events with EventEmitter

Queue

Concept
In plain words
A waiting line for work that can happen later.
Technically
A persistent list of jobs stored in Redis by BullMQ, consumed by workers with retries and backoff.
Learn it in module 55BullMQ queues and processors

Processor

Concept
In plain words
The worker that takes jobs from a queue and does them.
Technically
A WorkerHost class decorated with @Processor that processes jobs with set concurrency.
Learn it in module 55BullMQ queues and processors

Cron job

Concept
In plain words
Code that runs on a schedule.
Technically
A method decorated with @Cron or @Interval, run by the scheduler at matching times.
Learn it in module 54Cron jobs and scheduling

Message broker

Concept
In plain words
A post office for messages between services.
Technically
A server such as RabbitMQ that routes messages from producers to queues through exchanges.
Learn it in module 56RabbitMQ messaging

Cache

Concept
In plain words
A quick copy of an answer so you do not compute it again.
Technically
Stored results in Redis or memory, keyed and given a time to live, read before the slower source.
Learn it in module 52Redis backed caching

TTL

Concept
In plain words
How long something stays before it expires.
Technically
Time to live: a duration after which Redis deletes a key automatically.
Learn it in module 51Redis module and service

Entity

Concept
In plain words
A class that matches a database table.
Technically
A TypeORM class decorated with @Entity whose properties map to columns.
Learn it in module 46Entities and forFeature registration

Repository

Concept
In plain words
The object you use to read and write one table.
Technically
A TypeORM Repository bound to an entity, injected with @InjectRepository.
Learn it in module 48Repositories and the query builder

Migration

Concept
In plain words
A recorded step that changes the database structure.
Technically
A versioned class with up and down methods run by TypeORM to evolve the schema.
Learn it in module 50Migrations

Transaction

Concept
In plain words
A group of changes that all happen or none do.
Technically
A database unit of work committed or rolled back atomically, often with row locks.
Learn it in module 49Transactions

JWT

Concept
In plain words
A signed pass that proves who you are for a short time.
Technically
A JSON Web Token with a header, payload and signature, verified with a secret on each request.
Learn it in module 64JWT authentication

Refresh token

Concept
In plain words
A longer lasting key used only to get a new pass.
Technically
A rotating, server stored secret exchanged for a new access token; reuse signals theft.
Learn it in module 65Refresh tokens and Redis sessions

OAuth 2.0

Concept
In plain words
Signing in with another account, such as Google.
Technically
An authorization framework where the provider issues codes and tokens after the user consents.
Learn it in module 66OAuth 2.0 sign in with Passport

Throttler

Concept
In plain words
Limits how often someone can call you.
Technically
A guard counting requests per tracker in a time window and returning 429 when over.
Learn it in module 37Rate limiting with Throttler

OpenAPI

Concept
In plain words
A written map of your web API.
Technically
A machine readable specification of REST endpoints, generated by @nestjs/swagger and shown in Swagger UI.
Learn it in module 70OpenAPI documentation for REST

AsyncAPI

Concept
In plain words
A written map of your live events.
Technically
A specification of channels and message payloads for event driven APIs such as Socket.io.
Learn it in module 63AsyncAPI documentation for Socket.io

Unit test

Testing
In plain words
A test of one piece on its own.
Technically
A test of a single class with its dependencies replaced by fakes in a testing module.
Learn it in module 76Unit testing with Jest

Integration test

Testing
In plain words
A test of pieces working together with real tools.
Technically
A test running modules against real PostgreSQL and Redis, often in Testcontainers.
Learn it in module 80Integration tests with Testcontainers

End to end test

Testing
In plain words
A test that uses the app the way a client would.
Technically
A test that boots the full application and sends real HTTP or socket traffic.
Learn it in module 81End to end tests with Supertest

Health check

Concept
In plain words
A quick question: are you alive and ready.
Technically
Endpoints built with Terminus that check the process and its dependencies for orchestrators.
Learn it in module 84Health checks with Terminus

Microservice

Concept
In plain words
A small app that does one job and talks to others.
Technically
A Nest application using a transport such as Redis, RabbitMQ or gRPC with message and event patterns.
Learn it in module 89Microservices and hybrid apps

CQRS

Concept
In plain words
Keeping asking and changing separate.
Technically
Command Query Responsibility Segregation: commands change state, queries read it, events record what happened.
Learn it in module 90CQRS with @nestjs/cqrs

Phase 01, modules 01 to 08

Ground floor

Foundations

Why NestJS exists, how a project is laid out, and what every file in a module is for.

Module 01 of 92, phase 01

The architecture Express never gave you

Why NestJS exists

NestJS is a framework that sits on top of Express or Fastify and gives a Node.js server the structure that large teams need: modules, dependency injection, and a fixed request pipeline.

In detail

Express hands you a router and leaves the rest to you, which works until ten people are editing the same app. NestJS borrows the ideas Angular proved on the frontend and applies them to the server. Every feature lives in a module, every class gets its dependencies from an injector instead of building them, and every request walks through the same ordered chain of middleware, guards, interceptors and pipes. The result is code you can test in isolation and reason about when it grows.

hello.controller.ts
TypeScript
@Controller("hello")export class HelloController {	@Get()	greet(): string {		return "Hello from NestJS";	}}
Why it matters A controller, a module and a factory call are the whole framework in miniature. Everything later in this roadmap adds to one of these three.

Module 02 of 92, phase 01

Scaffold before you sculpt

Nest CLI and project setup

The Nest CLI creates a project, generates files with the right names, and builds with tsc or SWC. Learn the three config files it writes and you understand the whole toolchain.

In detail

Run nest new once and you get main.ts, an AppModule, a test folder, nest-cli.json, tsconfig.json and tsconfig.build.json. nest-cli.json tells the CLI where source lives and which compiler to use. tsconfig must keep emitDecoratorMetadata and experimentalDecorators on, because dependency injection reads constructor types from that emitted metadata. Switching the builder to SWC makes rebuilds roughly an order of magnitude faster on large apps.

3 examples
terminal
Shell
npm i -g @nestjs/clinest new lexicon-api --package-manager pnpm --strictcd lexicon-api && pnpm start:dev
Why it matters --strict turns on strictNullChecks and noImplicitAny from day one, which is far cheaper than retrofitting them later.

Module 03 of 92, phase 01

The first breath of the app

main.ts bootstrap anatomy

main.ts creates the application from the root module, then configures everything that applies globally: prefix, versioning, CORS, pipes, shutdown hooks and documentation.

In detail

NestFactory.create walks the module graph, instantiates every provider in dependency order, and returns an application object. Anything configured on that object before listen() applies to every route. Keep main.ts short by moving each concern into a small setup function, and enable shutdown hooks so database pools and queues close cleanly when the process is told to stop.

main.ts
TypeScript
const app = await NestFactory.create(AppModule, { bufferLogs: true });app.setGlobalPrefix("api");app.enableShutdownHooks();await app.listen(config.get("PORT", 3000));
Why it matters bufferLogs holds early log lines until a custom logger is attached, so nothing printed during module init is lost.

Module 04 of 92, phase 01

An atlas of every file

Every file type in a module

A NestJS feature is a folder of small files, each with one job and a suffix that names that job. Read the suffix and you know what the file exports before opening it.

In detail

The suffix convention is not enforced by the compiler, but the CLI, editor icon themes and every experienced reader rely on it. Keep one class per file, put shared shapes in contracts or interfaces files, and keep tests beside the file they cover. The table below lists each suffix with what it holds and when to create one.

File suffixes in a NestJS feature module
FileWhat it holdsCreate one when
*.module.tsThe @Module class: imports, providers, controllers, exports, and middleware configure.One per feature, always.
*.controller.tsHTTP routes mapped to methods. Thin translation into service calls.When the feature exposes REST endpoints.
*.service.tsInjectable business logic: rules, orchestration, repository and cache calls.Whenever logic has dependencies or state.
*.gateway.tsSocket.io handlers and broadcasts for the feature's realtime channel.When clients need pushed updates.
*.contracts.tsDTO types for requests, responses and socket events, mostly inferred from schemas.As soon as data crosses a boundary.
*.schema.tsZod schemas that validate bodies, queries, headers and socket payloads.For every input the feature accepts.
*.interfaces.tsPorts and internal shapes such as repository interfaces and records.When layers talk through an abstraction.
*.constants.tsInjection tokens, data source names, limits and TTLs.When a value or token is shared inside the feature.
*.events.tsEvent names and their payload map for EventEmitter or sockets.When the feature announces things to others.
*.entity.tsTypeORM entity mapped to a PostgreSQL table.One per table the feature owns.
*.guard.tsCanActivate: authentication, roles, keys, feature flags.When access depends on the caller.
*.pipe.tsPipeTransform: validate or convert one argument.When input needs shaping before the handler.
*.interceptor.tsWraps handlers: envelopes, timing, caching, timeouts.When you change results or add cross cutting behaviour.
*.middleware.tsRaw request work before routing: ids, signatures, raw bodies.When you need the request before Nest picks a handler.
*.filter.tsExceptionFilter that turns errors into responses.When errors need a consistent shape.
*.decorator.tsCustom class, method or parameter decorators.When the same metadata or extraction repeats.
*.processor.tsBullMQ WorkerHost that processes queue jobs.When work should run in the background.
*.strategy.tsPassport strategy for JWT, Google or other providers.When adding a sign in method.
*.spec.ts / *.test.tsUnit tests beside the file they cover.For every service, guard, pipe and schema.
*.e2e-spec.tsEnd to end tests over HTTP or sockets, usually in test/.For wiring that crosses modules.
index.tsThe barrel: the feature's public surface.Once per feature root.

Module 05 of 92, phase 01

Walls with doors

Why modules exist

A module is a boundary. It owns a set of providers, decides which of them other modules may use, and declares which modules it depends on.

In detail

Without modules, every class could reach every other class and the dependency graph turns into a web. A module makes that graph explicit: imports says what this feature needs, providers says what it builds, exports says what it shares, and controllers says which routes it serves. Nest instantiates each module once and caches it, so a provider exported from SharedModule is the same instance wherever it is imported. That boundary is also what lets you load a feature lazily, replace it in tests, or lift it into its own service later.

bootstrap.module.ts
TypeScript
@Module({	imports: [TypeOrmModule.forFeature([Word]), RedisModule],	controllers: [BootstrapController],	providers: [BootstrapService, BootstrapGateway],	exports: [BootstrapService],})export default class BootstrapModule {}
Why it matters imports, providers, exports and controllers are the four doors. Anything not exported stays private to this module.

Module 06 of 92, phase 01

A place for everything

Best module and project structure

Group code by feature first and by kind second. A feature module keeps its controller, service, gateway, schema and tests together, while cross cutting code lives in a small number of top level folders.

In detail

The layout below comes from a real production codebase. src/modules holds features, src/routing holds the controller decorators that define URL planes, src/redis and src/database hold infrastructure modules, and src/common holds filters, pipes and interceptors that any feature may use. Inside a feature, every file carries the feature name as a prefix so a search for bootstrap. lists the whole feature. Large features split into sub folders such as auth, services, schema and public once a single folder passes about fifteen files.

Project root

  • src
  • commonfilters, pipes, interceptors, decorators
  • configenv schema and config module
  • databasedata sources and migrations
  • graphify-outgenerated graph output
  • middlewaresignature, request id
  • modulesfeature modules
  • posthoganalytics module
  • redisclient, service, cache keys
  • routingdecorators and plane constants
  • socketsadapters and socket guards
  • typesbranded and global types
  • utilspure helpers
  • main.ts
  • app.module.ts

One feature, flat and prefixed

  • modules/bootstrap
  • bootstrap.constants.ts83 B
  • bootstrap.contracts.ts16.2 KB
  • bootstrap.controller.ts24.3 KB
  • bootstrap.events.test.ts669 B
  • bootstrap.events.ts305 B
  • bootstrap.gateway.ts10.0 KB
  • bootstrap.interfaces.ts4.4 KB
  • bootstrap.module.ts4.5 KB
  • bootstrap.schema.test.ts2.4 KB
  • bootstrap.schema.ts3.4 KB
  • bootstrap.service.ts26.3 KB
  • index.tsbarrel

The service and controller are the large files; constants and events stay tiny and rarely change.

Routing planes

  • routing
  • constantsroute types, versions, metadata keys
  • decorators
  • api-controller.decorator.ts
  • auth-controller.decorator.ts
  • game-controller.decorator.ts
  • dashboard-controller.decorator.ts
  • internal-controller.decorator.ts
  • utilities-controller.decorator.ts
  • index.ts
  • index.tsoption interfaces and exports

Module 07 of 92, phase 01

One door per room

Barrel files and index.ts

An index.ts at the root of a module re-exports its public surface, so the rest of the app imports from one path and never reaches into private files.

In detail

A barrel turns a folder into a package with a front door. Other modules import BootstrapModule and its contracts from @modules/bootstrap and nothing else, which means you can rename or split internal files freely. Keep barrels shallow: export the module, its public service, its contracts and its tokens. Do not re-export everything with export star from every file, because deep barrels hide circular imports and slow down test startup. When two barrels import each other, Nest receives undefined at decoration time and reports a circular dependency that is really an import cycle.

2 examples
modules/bootstrap/index.ts
TypeScript
export { default as BootstrapModule } from "./bootstrap.module";export { BootstrapService } from "./bootstrap.service";export * from "./bootstrap.contracts";
Why it matters export type keeps type only exports out of the emitted JavaScript, which avoids loading a file at runtime just for its types.

Module 08 of 92, phase 01

Let the CLI draw the lines

Generators and schematics

nest generate creates modules, controllers, services, guards and whole CRUD resources, and wires each one into the nearest module for you.

In detail

Generators save typing, but their real value is consistency: every file gets the same suffix, the same spec beside it, and the right registration. nest g resource asks whether you want REST, GraphQL or WebSockets and produces a module, controller, service, DTOs and entity in one go. Use --flat to skip the extra folder, --no-spec when a test makes no sense, and --dry-run to preview what would change.

terminal
Shell
nest g resource modules/bootstrap --no-specnest g service modules/bootstrap --flat
Why it matters --flat writes into the given folder instead of creating a new one, which keeps feature folders from nesting twice.

Phase 02, modules 09 to 19

The injector's mind

Dependency injection

Providers, tokens, scopes and the resolution rules that decide what gets built and when.

Module 09 of 92, phase 02

Labels the runtime can read

Decorators and reflect-metadata

A decorator is a function that runs when a class is defined and attaches metadata to it. NestJS reads that metadata at startup to learn routes, dependencies and rules.

In detail

TypeScript supports class, method, property and parameter decorators. @Controller stores a path, @Get stores a method and path, and @Injectable marks a class for the injector. With emitDecoratorMetadata on, the compiler also writes design:paramtypes, the list of constructor argument types, which is exactly how Nest knows that BootstrapService needs a Repository. SetMetadata and Reflector let you add and read your own keys, and applyDecorators composes several decorators into one.

2 examples
public.decorator.ts
TypeScript
export const IS_PUBLIC_KEY = "isPublic";export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
Why it matters getAllAndOverride checks the method first and the class second, so a handler level @Public wins over a class level rule.

Module 10 of 92, phase 02

Things the injector can build

Providers and @Injectable

A provider is anything Nest can create and hand to another class: services, repositories, factories, helpers and plain values. @Injectable marks a class as one.

In detail

Listing a class in providers registers it under its own class as the token. When another class asks for that type in its constructor, the injector looks up the token in the current module, then in imported modules' exports, builds the instance if needed, and caches it. By default each provider is a singleton for the whole app. @Injectable is only required when the class itself has constructor dependencies, but adding it everywhere keeps intent clear.

bootstrap.service.ts
TypeScript
@Injectable()export class BootstrapService {	constructor(private readonly redis: RedisService) {}}
Why it matters The constructor lists needs, never builds them. Swapping RedisService for a fake in a test needs no change here.

Module 11 of 92, phase 02

Tokens are the real names

Injectors, tokens and custom providers

Behind every injection is a token. A class can be its own token, but strings, symbols and InjectionToken values let you inject values, factories, aliases and implementations of interfaces.

In detail

useClass maps a token to a class, which is how you bind an interface to an implementation. useValue injects a constant or a mock. useFactory runs a function with injected arguments and can be async, which is the right place to open a connection. useExisting creates an alias so two tokens resolve to one instance. Prefer symbols or exported constants over bare strings so a typo fails at compile time.

4 examples
useClass
TypeScript
{ provide: BOOTSTRAP_REPOSITORY, useClass: TypeormBootstrapRepository }
Why it matters The interface vanishes at runtime, so the symbol carries its identity. Tests bind the same symbol to an in memory repository.

Module 12 of 92, phase 02

One, many, or one per request

Provider scopes

Providers are singletons by default. REQUEST scope builds a fresh instance per incoming request, and TRANSIENT builds a fresh instance for every consumer.

In detail

Scope bubbles up: if a singleton depends on a request scoped provider, the singleton becomes request scoped too, and every request pays the construction cost. Reach for REQUEST scope only when a provider truly needs per request state, such as a tenant id, and prefer AsyncLocalStorage through nestjs-cls for request context in hot paths. TRANSIENT suits stateful helpers such as a logger that stores its own context name.

tenant.context.ts
TypeScript
@Injectable({ scope: Scope.REQUEST })export class TenantContext {	constructor(@Inject(REQUEST) private readonly req: Request) {}}
Why it matters Every class that injects TenantContext becomes request scoped. Keep the chain short or move to AsyncLocalStorage.

Module 13 of 92, phase 02

Inject it or import it

When a class should be injectable

Make a class an injectable provider when it has dependencies, holds a connection or state, or needs to be replaced in tests. Keep pure functions as plain imports.

In detail

A formatter that turns a date into a string needs nothing and changes nothing, so a plain exported function is simpler and faster to test. A class that talks to Redis, reads config, or sends email must be a provider, because the injector controls its lifetime and tests swap it for a fake. A useful rule: if you would ever write jest.mock for it, it should be injectable instead. Static utility classes that read process.env directly are a smell, since they bypass validated config.

decision.ts
TypeScript
// pure: plain functionexport const toSlug = (text: string) => slugify(text);// stateful or has deps: provider@Injectable() export class OtpService { constructor(private redis: RedisService) {} }
Why it matters Plain functions are imported, providers are injected. Mixing the two roles is the most common cause of hard to test code.

Module 14 of 92, phase 02

Lend a tool or lend the workshop

Exporting providers vs importing modules

To use a provider from another feature, import the module that owns it and make sure that module exports the provider. Never list another module's service in your own providers array.

In detail

Copying RedisService into two providers arrays creates two instances with two connection pools, which is a quiet bug. The owning module should build it once and export it. A module can also re-export an imported module, so CoreModule can import and export RedisModule and ConfigModule together and features import CoreModule alone. Export only what consumers need, because every export is a promise you have to keep.

core.module.ts
TypeScript
@Module({ imports: [RedisModule, MailModule], exports: [RedisModule, MailModule] })export class CoreModule {}
Why it matters Importing a module gives you its exports, not its internals. That is what keeps one instance per app.

Module 15 of 92, phase 02

Modules that take arguments

Dynamic modules and registerAsync

A dynamic module is a module returned from a static method such as forRoot, register or registerAsync, so the caller can pass options. ConfigurableModuleBuilder writes most of that code for you.

In detail

forRoot conventionally configures something once for the whole app, register configures it per importing module, and the Async variants accept a factory that can inject ConfigService. AuthJwtModule.registerAsync() takes no arguments because it reads its secrets from config inside its own factory, which keeps every importing feature identical.

2 examples
auth-jwt.module.ts
TypeScript
AuthJwtModule.registerAsync()
Why it matters Returning a DynamicModule object lets one class produce differently configured modules without subclassing.

Module 16 of 92, phase 02

Assembly lines for objects

Factory and builder patterns

Factories decide which implementation to build at runtime, and builders assemble complex objects step by step. Both fit naturally into NestJS providers.

In detail

A factory provider can pick a mail transport from config: SMTP in production, a JSON transport in tests. A builder is a plain class that accumulates options and returns an immutable result, which is useful for query objects, email messages and API responses. Inject factories, keep builders as plain classes created per use, since they hold per call state.

2 examples
mail-transport.factory.ts
TypeScript
useFactory: (c: ConfigService) => c.get("NODE_ENV") === "test" ? jsonTransport() : smtpTransport(c)
Why it matters The factory is the only place that knows about environments. MailService stays identical everywhere.

Module 17 of 92, phase 02

Two doors facing each other

Circular dependencies and forwardRef

A circular dependency happens when A needs B and B needs A, either between modules or between providers. forwardRef delays resolution so both can be built.

In detail

Between modules, wrap the import in forwardRef(() => QuizModule) on both sides. Between providers, use @Inject(forwardRef(() => QuizService)) on both constructors. forwardRef treats the symptom: it usually means two features share logic that belongs in a third module, or one side should communicate through events instead. Also check for barrel import cycles, which produce the same error even when the DI graph is fine.

2 examples
bootstrap.module.ts
TypeScript
// * // Importing Quiz Module //forwardRef(() => QuizModule),
Why it matters Both sides need forwardRef. Instances are built in an order Nest chooses, so never call the other side from a constructor.

Module 18 of 92, phase 02

Reading the injector's complaints

DI resolution and common errors

Nest can't resolve dependencies of X is the error every NestJS developer meets. The message tells you which argument failed and in which module, which is enough to fix it every time.

In detail

The question mark in the printed list marks the argument that failed. Check four things in order: is the provider listed in this module's providers, or exported by a module listed in imports; is the import a class, not a type that erased to Object; is there an import cycle that made the class undefined at decoration time; and is the token you inject the exact token that was provided. A provider that appears in two providers arrays is not an error but creates two instances.

error.log
TypeScript
Nest can't resolve dependencies of the BootstrapService (WordRepository, ?).Please make sure that the argument RedisService at index [1] is availablein the BootstrapModule context.
Why it matters Index [1] means the second constructor argument. Count from zero and you know exactly which parameter to fix.

Module 19 of 92, phase 02

Reaching across the graph

Global modules, ModuleRef and lifecycle hooks

@Global makes a module's exports available everywhere without importing it. ModuleRef resolves providers at runtime, and lifecycle hooks run code when modules start and stop.

In detail

Use @Global sparingly, for true infrastructure such as config, logging and Redis, because global exports hide where dependencies come from. ModuleRef.get fetches a singleton by token, resolve builds scoped providers, and create instantiates a class that was never registered. Lifecycle hooks run in a fixed order: onModuleInit, onApplicationBootstrap, then on shutdown onModuleDestroy, beforeApplicationShutdown and onApplicationShutdown.

redis.module.ts
TypeScript
@Global()@Module({ providers: [RedisService], exports: [RedisService] })export class RedisModule {}
Why it matters strict: false lets ModuleRef look outside the current module. Use it for plugin style registries, not everyday injection.

Phase 03, modules 20 to 37

The request's journey

HTTP layer

Controllers, routing planes, and every stage a request passes through before your handler runs.

Module 20 of 92, phase 03

The front desk

Controllers and route handlers

A controller maps HTTP verbs and paths to methods. It reads input, calls a service, and returns a value that Nest serializes to JSON.

In detail

Keep controllers thin: no database calls, no business rules, just translation between HTTP and your service layer. Route decorators such as @Get, @Post and @Patch take a path, and parameter decorators such as @Param, @Query, @Body and @Headers pull parts of the request in. Return plain values or promises and let Nest set status codes; use @HttpCode or @Header when you need to change them. Reach for @Res only when you stream or redirect, because it switches off the standard response handling.

bootstrap.controller.ts
TypeScript
@Get("words/:id")findOne(@Param("id", ParseIntPipe) id: number) {	return this.bootstrap.findOne(id);}
Why it matters Static paths such as words/stats are declared before words/:id, otherwise the parameter route would capture them.

Module 21 of 92, phase 03

Where the decisions live

Services and the business layer

Services hold business rules. They coordinate repositories, caches, queues and other services, and they never know about HTTP or sockets.

In detail

If a method needs a Request object, it belongs in a controller or guard. A service throws domain errors or Nest's HTTP exceptions, returns typed results, and can be called from a controller, a gateway, a cron job or a queue processor alike. Split a service when it passes about four hundred lines or starts mixing concerns: BootstrapService coordinates, WordQueryService reads, WordWriteService writes.

bootstrap.service.ts
TypeScript
async create(input: CreateWordDto): Promise<Word> {	const word = await this.words.save(this.words.create(input));	this.events.emit(BootstrapEvents.WordCreated, { id: word.id });	return word;}
Why it matters The service invalidates cache and emits an event after the write, so every caller gets the same side effects.

Module 22 of 92, phase 03

Small sharp tools

Utilities and helpers

Utilities are pure functions with no dependencies: string transforms, date math, key builders, type guards. They live in src/utils and are imported, not injected.

In detail

Group utilities by subject in small files and export them through src/utils/index.ts. Each function should be deterministic and easy to unit test without a Nest testing module. When a helper starts needing config or a client, promote it to a provider in the right module rather than passing process.env around.

utils/chunk.ts
TypeScript
export const chunk = <T>(items: T[], size: number): T[][] =>	Array.from({ length: Math.ceil(items.length / size) }, (_, i) => items.slice(i * size, i * size + size));
Why it matters shardFor decides which Words_N table a row lives in. Keeping it pure means the controller, worker and tests agree on the rule.

Module 23 of 92, phase 03

Name your planes

Custom routing decorators

Composed controller decorators replace @Controller with intent: @ApiController, @AuthController, @DashboardController. Each one sets a URL plane, a version and metadata that guards and docs read later.

In detail

applyDecorators bundles SetMetadata, Controller, ApiTags and any guard into one call. The route type metadata lets a single global guard decide policy by plane: internal routes need a service key, dashboard routes need a session, API routes need a JWT. The routing folder keeps the enums, metadata keys and option interfaces next to the decorators, and an index.ts exposes them as one import.

8 examples
api-controller.decorator.ts
TypeScript
export function ApiController(options: ApiControllerOptions): ClassDecorator {	const version = options.version ?? RouteVersion.V1;	return applyDecorators(SetMetadata(ROUTE_TYPE_KEY, RouteType.API), Controller(`${version}/${options.path}`));}
Why it matters The plane is metadata, not just a path prefix, so guards and docs can act on it without string matching URLs.

Module 24 of 92, phase 03

Paths with a past

Versioning, prefixes and RouterModule

URI versioning keeps v1 and v2 of a route alive side by side. RouterModule mounts whole modules under a path so the URL tree mirrors the module tree.

In detail

Enable versioning once in main.ts, then mark controllers or single routes with version. A global prefix such as api can exclude health checks and dashboard pages. RouterModule.register maps a module to a path and its children to sub paths, which is useful when one module owns a whole admin area. The custom decorators from the previous module are an alternative: they bake the plane and version into the controller path directly.

app.routes.ts
TypeScript
RouterModule.register([{ path: "wordlab", module: WordLabModule, children: [{ path: "dashboard", module: WordLabDashboardModule }] }])
Why it matters @Version on a method overrides the controller version, so one class can serve both shapes during a migration.

Module 25 of 92, phase 03

The journey of a single request

Request lifecycle

Every request passes through the same stages in the same order: middleware, guards, interceptors before, pipes, the handler, interceptors after, and exception filters if anything throws.

In detail

Within each stage, global runs first, then controller level, then route level. Interceptors wrap the handler, so their after logic runs in reverse order. Exception filters run from the most specific outward. Knowing this order answers most questions about where code belongs: authentication in a guard, input shape in a pipe, response shape in an interceptor, and raw request work such as signature checks in middleware.

Module 26 of 92, phase 03

The doorman

Middleware

Middleware runs before routing decisions are final and sees the raw request and response. It is the place for signature checks, request ids, raw body capture and logging.

In detail

Class middleware implements NestMiddleware and can inject providers. Functional middleware is a plain function and is slightly faster when it needs nothing. Middleware does not know which handler will run and cannot read decorator metadata, which is why authorization belongs in guards. Register it in a module's configure method with MiddlewareConsumer, or globally with app.use for third party middleware such as helmet.

request-id.middleware.ts
TypeScript
@Injectable()export class RequestIdMiddleware implements NestMiddleware {	use(req: Request, res: Response, next: NextFunction) {		req.headers["x-request-id"] ??= randomUUID();		next();	}}
Why it matters Listening to finish measures the full request, including time spent in guards, pipes and the handler.

Module 27 of 92, phase 03

Who walks past the doorman

Middleware inclusion and exclusion

MiddlewareConsumer applies middleware to whole controllers or path patterns, and exclude carves out paths and specific methods. That is how a signature check covers a controller while login stays open.

In detail

forRoutes accepts controller classes, strings with wildcards, or objects with path and method. exclude takes the same shapes and is matched before forRoutes. Paths are written without the global prefix. When you exclude an object with a method, only that verb on that path is skipped, so PATCH bootstrap/words/:id can stay open to an admin tool while GET on the same path is still checked. Chain several apply calls to give different controllers different middleware.

bootstrap.module.ts
TypeScript
consumer.apply(SignatureSecretMiddleware).exclude("bootstrap/admin/login", "bootstrap/words/paginated", "bootstrap/words/stats", "bootstrap/words/bulk", { path: "bootstrap/words/:id", method: RequestMethod.PATCH }).forRoutes(GameBootstrapController);
Why it matters Exclude whole paths as strings and single verbs as objects. Express 5 wildcards use a named form such as *path.

Module 28 of 92, phase 03

The bouncer with a list

Guards

A guard answers one question before the handler runs: may this request continue. It has the execution context, so it can read route metadata, the user and the handler.

In detail

Return true to continue, false to send 403, or throw your own exception. Register guards globally with APP_GUARD so they can inject providers, then opt routes out with metadata such as @Public. Order matters when several guards apply: authentication first, then roles, then feature flags. Guards also run for WebSocket and microservice handlers, where you switch on context.getType().

internal-key.guard.ts
TypeScript
canActivate(ctx: ExecutionContext): boolean {	const req = ctx.switchToHttp().getRequest<Request>();	return safeEqual(req.header("x-internal-key"), this.key);}
Why it matters timingSafeEqual compares in constant time, so response timing does not leak how much of the key was right.

Module 29 of 92, phase 03

Shape it or stop it

Pipes

Pipes transform and validate arguments before the handler sees them. A pipe either returns a value, possibly converted, or throws to reject the request.

In detail

Built in pipes cover common cases: ParseIntPipe, ParseUUIDPipe, ParseBoolPipe, ParseEnumPipe, ParseArrayPipe, DefaultValuePipe and ParseFilePipe. Apply a pipe to one parameter, a method, a controller, or globally. A custom pipe implements PipeTransform and receives the value plus metadata describing where it came from.

word-script.pipe.ts
TypeScript
@Get("words")list(@Query("level", new DefaultValuePipe(1), ParseIntPipe) level: number) {}
Why it matters Pipes run left to right, so DefaultValuePipe fills a missing value before ParseIntPipe converts it.

Module 30 of 92, phase 03

Trust nothing at the edge

Request body validation with Zod

A Zod schema validates the body and produces a typed value in one step. A small ZodValidationPipe connects it to NestJS and turns issues into a clean 400 response.

In detail

Define the schema in bootstrap.schema.ts and infer the DTO type from it in bootstrap.contracts.ts, so validation and types never drift. The pipe uses safeParse and maps issues to field paths. Wrap it in a @ZodBody decorator for readable controllers. Zod also handles coercion for query strings, defaults, transforms and refinements across fields.

2 examples
bootstrap.schema.ts
TypeScript
export const CreateWordSchema = z.object({	text: z.string().min(1).max(32),	gloss: z.string().regex(/^[a-z]+$/),	level: z.coerce.number().int().min(1).max(5),});
Why it matters .strict() rejects unknown keys, which stops clients from sending fields that silently get ignored.

Module 31 of 92, phase 03

Read the envelope too

Header validation

Headers carry client version, signatures, idempotency keys and locale. Validate them with the same Zod approach through a custom parameter decorator.

In detail

@Headers() returns every header as lowercase keys, which Zod can parse like any object. Use passthrough so unrelated headers do not fail validation, and reject only what you declare. Typical checks: an x-client-version that matches semver, an idempotency-key that is a UUID on POST, and accept-language limited to supported locales.

client-headers.decorator.ts
TypeScript
@Post("sessions")start(@ValidHeaders(ClientHeadersSchema) headers: ClientHeaders) {}
Why it matters Node lowercases header names, so schemas always use lowercase keys.

Module 32 of 92, phase 03

Before and after, in one place

Interceptors

An interceptor wraps the handler. It can run code before, transform the result after, map errors, add timeouts, or skip the handler entirely by returning a cached value.

In detail

Interceptors work with RxJS: next.handle() returns an Observable of the handler's result, and operators such as map, tap, timeout and catchError reshape it. Because they see the execution context and the result, they suit response envelopes, serialization, timing and caching. Register globally with APP_INTERCEPTOR to allow injection.

envelope.interceptor.ts
TypeScript
intercept(ctx: ExecutionContext, next: CallHandler) {	return next.handle().pipe(map((data) => ({ ok: true, data })));}
Why it matters The type check skips sockets and microservices, where a response envelope would break the client contract.

Module 33 of 92, phase 03

Doorman or butler

Interceptor vs middleware

Middleware sees the raw request before Nest knows the handler. Interceptors run after routing, know the handler and its metadata, and can change the result.

In detail

Choose middleware for work on the raw stream: body signatures, request ids, cookies, CORS and anything third party that speaks Express. Choose an interceptor when you need the handler, its decorators, or its return value: caching, envelopes, serialization and timing per route. Middleware cannot see exceptions thrown by the handler as values; interceptors can map them. Only interceptors, guards and pipes run for WebSocket gateways.

Middleware compared with interceptors
QuestionMiddlewareInterceptor
RunsBefore routing is finalAfter a handler is selected
Knows the handler and its metadataNoYes, through ExecutionContext and Reflector
Sees the return valueNoYes, through the Observable from next.handle()
Can map thrown errorsOnly by wrapping next() itselfYes, with catchError
Works for WebSockets and microservicesNo, HTTP onlyYes
Dependency injectionClass middleware onlyYes
Typical jobsRequest ids, signatures, raw body, CORS, helmet, cookiesEnvelopes, caching, timing, serialization, timeouts
Registered withMiddlewareConsumer or app.use@UseInterceptors or APP_INTERCEPTOR

Module 34 of 92, phase 03

Every failure, one shape

Exception filters

Exception filters catch thrown errors and write the response. A global filter gives every error the same JSON shape, with request ids and no stack traces in production.

In detail

HttpException and its subclasses carry a status and a body. Unknown errors become 500. @Catch() with no argument catches everything; @Catch(QueryFailedError) handles database errors, for example turning a unique violation into 409. Use HttpAdapterHost so the filter works on Express and Fastify alike.

all-exceptions.filter.ts
TypeScript
@Catch()export class AllExceptionsFilter implements ExceptionFilter {	catch(exception: unknown, host: ArgumentsHost) {}}
Why it matters PostgreSQL code 23505 is a unique violation. Mapping it here means services do not need a pre check for every insert.

Module 35 of 92, phase 03

Pull exactly what you need

Custom parameter decorators

createParamDecorator builds decorators like @CurrentUser or @ClientIp that extract a value from the request. Handlers stay clean and testable.

In detail

The factory receives an optional argument and the execution context. Return any value, and combine with pipes by passing them after the argument. A decorator that works for both HTTP and sockets switches on context.getType(). Keep them tiny: extraction only, no database calls.

current-user.decorator.ts
TypeScript
export const CurrentUser = createParamDecorator((key: keyof JwtUser | undefined, ctx: ExecutionContext) => {	const user = ctx.switchToHttp().getRequest().user;	return key ? user?.[key] : user;});
Why it matters Passing a key returns one field, which keeps handler signatures short when only the id is needed.

Module 36 of 92, phase 03

Bytes at the door

File interceptors and uploads

FileInterceptor, FilesInterceptor and FileFieldsInterceptor parse multipart form data with Multer. ParseFilePipe validates size and type before your handler runs.

In detail

Use memory storage when the next step is S3, so files never touch local disk. Set limits on file size and count in the interceptor options as a first line of defence, then validate again with ParseFilePipeBuilder. Check the magic bytes, not only the mimetype header, for anything security sensitive, because clients choose the header.

2 examples
single
TypeScript
@Post("avatar")@UseInterceptors(FileInterceptor("file"))upload(@UploadedFile(avatarPipe) file: Express.Multer.File) {}
Why it matters limits stops Multer reading past 2 MB, while the pipe gives a clear 422 with the reason.

Module 37 of 92, phase 03

Slow down, politely

Rate limiting with Throttler

@nestjs/throttler limits how many requests a client may make in a window. Back it with Redis so limits hold across every instance of the app.

In detail

Define named throttlers such as short and long, register ThrottlerGuard globally, and override per route with @Throttle or skip with @SkipThrottle. The default tracker is the IP; override getTracker to limit by user id or API key. Clients that exceed a limit get 429 with a Retry-After header. OTP and login routes deserve their own stricter limits.

throttle.module.ts
TypeScript
ThrottlerModule.forRoot([{ name: "short", ttl: 1000, limit: 5 }, { name: "long", ttl: 60000, limit: 120 }])
Why it matters Tracking by user id after authentication stops one office behind a single IP from sharing a limit.

Phase 04, modules 38 to 43

Shapes and promises

Types and contracts

Interfaces, DTOs, schemas and contracts, and a type driven way to keep them in step.

Module 38 of 92, phase 04

Promises that vanish at runtime

Interfaces in NestJS

TypeScript interfaces describe shapes for the compiler and disappear in the emitted JavaScript. In NestJS that means an interface can type a dependency but can never be its injection token.

In detail

Use interfaces in bootstrap.interfaces.ts for ports such as repositories and gateways, for service method results, and for options objects. Pair each injectable interface with a symbol token in bootstrap.constants.ts. Interfaces cannot validate input either, since nothing exists at runtime to check against, which is why request bodies need a schema.

bootstrap.interfaces.ts
TypeScript
export interface IWordRepository {	findById(id: number): Promise<WordRecord | null>;}
Why it matters readonly on records stops a consumer from mutating cached objects that other requests share.

Module 39 of 92, phase 04

What crosses the wire

DTOs and the contracts file

A DTO describes data that crosses a boundary: a request body, a query, a response or a socket payload. The contracts file collects every DTO type of a feature in one place.

In detail

In a Zod based codebase, contracts are inferred from schemas with z.infer, so bootstrap.contracts.ts imports schemas and exports types. Response contracts that have no schema are written as plain types. Other features, the frontend and the socket client import from contracts and nowhere else, which makes it the file to review whenever an API changes.

bootstrap.contracts.ts
TypeScript
export type CreateWordDto = z.infer<typeof CreateWordSchema>;export type UpdateWordDto = z.infer<typeof UpdateWordSchema>;
Why it matters Socket event maps live here too, so the gateway and the client agree on names and payloads.

Module 40 of 92, phase 04

The type and the guard

DTO vs schema

A schema is a runtime value that validates data. A DTO is a compile time type that describes it. You need both, and the cleanest setup derives the DTO from the schema.

In detail

The class-validator style puts both in one class: decorators validate and the class is the type. The Zod style keeps the schema as a value and infers the type, which gives richer validation, transforms and no reflection. Either works; mixing them in one feature does not. Pick one per codebase and document it in the module structure.

Schema, DTO and interface side by side
QuestionSchemaDTOInterface
Exists at runtimeYes, a Zod valueNo, a typeNo, a type
Validates inputYesNoNo
DescribesRules for incoming dataShape of data crossing a boundaryShape of data inside the app
Lives in*.schema.ts*.contracts.ts*.interfaces.ts
Can be an injection tokenNoNoNo, pair it with a symbol
Visible to clientsThrough OpenAPIYes, it is the public contractNo
2 examples
zod style
TypeScript
export const CreateWordSchema = z.object({ text: z.string() });export type CreateWordDto = z.infer<typeof CreateWordSchema>;
Why it matters One source of truth. Change the schema and the type follows.

Module 41 of 92, phase 04

Inside the walls and at the gate

Interface vs DTO

Interfaces describe how parts of your code talk to each other. DTOs describe what outsiders send and receive. Keep them apart so internal changes do not break clients.

In detail

An entity has database columns, relations and timestamps. A WordRecord interface is what services pass around. A response DTO is the public shape, often fewer fields and stable names. Mapping between them in one function per direction is a small cost that buys freedom to rename columns, split tables or change shards without touching the API.

word.mapper.ts
TypeScript
export const toWordResponse = (w: Word): WordResponseDto => ({ id: w.id, text: w.text, level: w.level });
Why it matters shard never reaches the client. The mapper is the one place that decides what is public.

Module 42 of 92, phase 04

Let the compiler carry the load

Type driven architecture

Design types first and let them drive the code: schemas infer DTOs, event maps type emitters, branded ids stop mixups, and discriminated unions make impossible states impossible.

In detail

Branded types such as UserId and RoomId are strings at runtime but distinct to the compiler, so passing a room id where a user id is expected fails to build. Discriminated unions model results and socket messages with a kind field, and an exhaustive switch with assertNever turns every new case into a compile error until it is handled. satisfies checks an object against a type without widening it.

types/brand.ts
TypeScript
export type Brand<T, B extends string> = T & { readonly __brand: B };export type UserId = Brand<string, "UserId">;
Why it matters Adding a new kind to MoveResult breaks the build at the never line until every switch handles it.

Module 43 of 92, phase 04

Names that never drift

Constants, enums and events.ts

bootstrap.constants.ts holds tokens and fixed values, and bootstrap.events.ts holds event names with their payload types. Strings typed once are strings never mistyped.

In detail

An events file exports a const object of names and an interface mapping each name to its payload. Emitters and listeners import both, so a renamed event or changed payload fails to compile everywhere it is used. Small files like these change rarely, which is why they barely move in the listing while services grow. Test them lightly: a test that asserts names are unique and namespaced catches copy paste mistakes.

2 examples
bootstrap.events.ts
TypeScript
export const BootstrapEvents = { WordCreated: "bootstrap.word.created" } as const;
Why it matters Payloads keyed by the constant mean emit and listen share one definition.

Phase 05, modules 44 to 52

Memory and state

Config, PostgreSQL and Redis

Validated environment, TypeORM with PostgreSQL, sharded data sources, and Redis for state and cache.

Module 44 of 92, phase 05

Fail fast on a missing secret

Env loader and typed config

ConfigModule loads .env files, validates them at startup, and exposes values through an injectable ConfigService. A missing or malformed variable should stop the app before it serves a request.

In detail

Validate with a Zod schema in the validate option so types are coerced and defaults applied. Group related values with registerAs namespaces and inject them as typed objects with ConfigType. Mark ConfigModule global and cache it. Never read process.env in services; inject config so tests can supply values.

3 examples
env.schema.ts
TypeScript
export const EnvSchema = z.object({	NODE_ENV: z.enum(["development", "test", "production"]),	PORT: z.coerce.number().default(3000),	DATABASE_URL: z.string().url(),});
Why it matters The app refuses to boot with a short JWT secret, which is far better than discovering it in production.

Module 45 of 92, phase 05

Plugging in PostgreSQL

TypeORM with PostgreSQL

TypeOrmModule.forRootAsync connects to PostgreSQL with settings from ConfigService. Feature modules then register the entities they use with forFeature.

In detail

Keep synchronize off outside throwaway local databases and use migrations instead. autoLoadEntities picks up every entity registered with forFeature, so the root config does not list them. Tune the pool with extra.max, set statement timeouts, and use the snake case naming strategy so TypeScript properties map to idiomatic PostgreSQL columns.

database.module.ts
TypeScript
TypeOrmModule.forRootAsync({ inject: [ConfigService], useFactory: (c) => ({ type: "postgres", url: c.get("DATABASE_URL"), autoLoadEntities: true }) })
Why it matters statement_timeout makes PostgreSQL cancel a runaway query instead of letting it hold a pool slot forever.

Module 46 of 92, phase 05

Rows with a class

Entities and forFeature registration

An entity class maps to a table. Registering it with TypeOrmModule.forFeature inside a module makes its Repository injectable there and nowhere else.

In detail

forFeature takes an array of entities and an optional data source name. Without a name it uses the default connection. The module that registers an entity owns its repository; other modules use the owning service rather than registering the entity again. Indexes, unique constraints and relations are declared on the entity and created by migrations.

word.entity.ts
TypeScript
@Entity("words")export class Word {	@PrimaryGeneratedColumn() id!: number;	@Column({ unique: true }) text!: string;}
Why it matters timestamptz stores an absolute instant, so servers in different zones read the same time.

Module 47 of 92, phase 05

Four tables, one shape

Multiple data sources and shard entities

A second named data source connects to another database. Shard entities share columns but map to different tables, and forFeature registers them under that data source name.

In detail

Name the connection in forRootAsync, then pass the same name to forFeature and to @InjectRepository. A base class holds the columns and each shard subclass only sets the table name. A small resolver picks the repository from the shard number, so services never hard code Words_2. Keep transactions inside one data source; spanning two needs an outbox or saga.

3 examples
shard registration
TypeScript
TypeOrmModule.forFeature([Words_0, Words_1, Words_2, Words_3], "WordShardsSource")
Why it matters The name string must match exactly in forRootAsync, forFeature and @InjectRepository. Keep it in a constant.

Module 48 of 92, phase 05

Asking the database well

Repositories and the query builder

Repository methods cover simple reads and writes. The query builder handles joins, aggregates, locking and anything the find options cannot express.

In detail

Wrap repeated queries in a custom repository class that injects the TypeORM repository, so services read like intent. Select only the columns you need, paginate with keyset for large tables, and add explicit indexes for every filter you ship. getRawMany returns aggregates without entity hydration, which is faster for stats.

word.repository.ts
TypeScript
this.words.createQueryBuilder("w").select("w.level", "level").addSelect("COUNT(*)", "count").groupBy("w.level").getRawMany();
Why it matters Parameters such as :term are bound by the driver, never interpolated, which is what prevents SQL injection.

Module 49 of 92, phase 05

All or nothing

Transactions

A transaction groups writes so they all commit or all roll back. In TypeORM use DataSource.transaction for most cases and a QueryRunner when you need manual control.

In detail

Inside the callback, use the provided EntityManager for every query; anything using an injected repository runs outside the transaction. Lock rows you read and then update with pessimistic_write to avoid lost updates. Emit events and enqueue jobs after commit, not inside, or a rollback can leave a job referencing data that never existed.

progress.service.ts
TypeScript
await this.dataSource.transaction(async (em) => {	await em.increment(Player, { id }, "xp", 10);	await em.insert(XpLog, { playerId: id, amount: 10 });});
Why it matters The row lock stops two concurrent awards from both reading the old xp and one of them being lost.

Module 50 of 92, phase 05

Change the schema on purpose

Migrations

Migrations are versioned scripts that change the database schema. Generate them from entity changes, review the SQL, commit them, and run them on deploy.

In detail

A separate data-source.ts file lets the TypeORM CLI find entities and migrations outside Nest. Generate with migration:generate, which diffs entities against the live schema. Read every generated file; renames often appear as drop and add, which loses data. Make down methods real so a bad deploy can be reversed.

2 examples
data-source.ts
TypeScript
export default new DataSource({ type: "postgres", url: process.env.DATABASE_URL, entities: ["src/**/*.entity.ts"], migrations: ["src/database/migrations/*.ts"] });
Why it matters This file exists only for the CLI. The app itself uses DatabaseModule and ConfigService.

Module 51 of 92, phase 05

A fast memory beside the database

Redis module and service

A global RedisModule creates one ioredis client from config and exposes a small typed RedisService. Every feature borrows that one connection.

In detail

Wrap the raw client so features use getJson, setJson and del instead of remembering to stringify. Prefix keys per app and build them in one CacheKeys helper, so you can find and expire them later. Close the client on shutdown. For BullMQ and pub sub, create separate connections, since a subscribed client cannot run normal commands.

redis.service.ts
TypeScript
async getJson<T>(key: string): Promise<T | null> {	const raw = await this.client.get(key);	return raw ? (JSON.parse(raw) as T) : null;}
Why it matters enableAutoPipelining batches commands issued in the same tick into one round trip without changing your code.

Module 52 of 92, phase 05

Answer before you ask the database

Redis backed caching

CacheModule with a Redis store gives you CacheInterceptor for whole responses and an injectable cache manager for fine grained cache aside logic.

In detail

Response caching suits public GET routes with the same answer for everyone. For anything user specific or partial, use cache aside in the service: read the key, fall back to the database, write with a TTL, and delete the key on writes. Add jitter to TTLs so many keys do not expire at once, and use a short lock to stop a stampede when a hot key expires.

2 examples
cache.module.ts
TypeScript
CacheModule.registerAsync({ isGlobal: true, useFactory: (c) => ({ stores: [new KeyvRedis(c.get("REDIS_URL"))], ttl: 60_000 }) })
Why it matters cache-manager v6 takes TTLs in milliseconds. Mixing seconds here is the most common caching bug.

Phase 06, modules 53 to 59

Work that waits

Events, schedules and queues

Event emitters, cron jobs, BullMQ, RabbitMQ, email and one time codes.

Module 53 of 92, phase 06

Tell, don't call

In process events with EventEmitter

@nestjs/event-emitter lets one feature announce that something happened and others react, without importing each other. It is the first tool against circular dependencies.

In detail

Emit after state changes are committed. Listeners decorated with @OnEvent run in the same process; set async: true and wrap work in try and catch, because a throwing async listener would otherwise surface as an unhandled rejection. Wildcards such as bootstrap.* subscribe to a family of events. For work that must survive a restart, enqueue a job instead.

bootstrap.listeners.ts
TypeScript
@OnEvent(BootstrapEvents.WordCreated, { async: true })async onWordCreated(payload: BootstrapEventPayloads[typeof BootstrapEvents.WordCreated]) {}
Why it matters The audit listener uses a wildcard, which needs wildcard: true in forRoot.

Module 54 of 92, phase 06

Clockwork you can trust

Cron jobs and scheduling

@nestjs/schedule runs methods on cron expressions, intervals or timeouts. In a cluster, guard each job with a Redis lock so only one instance runs it.

In detail

Name every job so SchedulerRegistry can pause or inspect it. Set a timeZone for anything tied to a calendar day. Keep cron handlers short: they should enqueue work, not do it, so a long task does not overlap its own next run. A SET NX lock with a TTL slightly shorter than the interval is enough for most leader election needs.

daily-reset.cron.ts
TypeScript
@Cron("0 0 * * *", { name: "daily-streak-reset", timeZone: "Asia/Kolkata" })async resetStreaks() {}
Why it matters The cron only enqueues. The worker does the heavy lifting with retries and its own concurrency.

Module 55 of 92, phase 06

A line that never forgets

BullMQ queues and processors

BullMQ stores jobs in Redis and processes them in workers with retries, backoff, delays, priorities and concurrency. @nestjs/bullmq wires queues and processors into modules.

In detail

Register the connection once with forRootAsync and each queue with registerQueue. A processor extends WorkerHost and switches on job.name. Make processors idempotent, since a job can run twice after a crash. Give jobs stable ids when duplicates must be ignored, cap retained completed jobs, and listen to failed events for alerting. Run workers in a separate process when jobs are CPU heavy.

2 examples
queues.module.ts
TypeScript
BullModule.registerQueue({ name: "mail" }, { name: "ingest" })
Why it matters Default job options apply to every queue, so retries and cleanup are consistent without repeating them.

Module 56 of 92, phase 06

Messages between services

RabbitMQ messaging

RabbitMQ routes messages between services through exchanges and queues. Use it when other services, not just this app, need to produce or consume events.

In detail

BullMQ is a job queue for one app; RabbitMQ is a broker for many. With @golevelup/nestjs-rabbitmq you publish to a topic exchange and subscribe with @RabbitSubscribe on a queue bound by routing key. Acknowledge after the work succeeds, send poison messages to a dead letter exchange, and keep consumers idempotent. The built in Nest microservice transport also supports RabbitMQ for request reply patterns.

progress.messaging.ts
TypeScript
@RabbitSubscribe({ exchange: "game.events", routingKey: "player.level.up", queue: "analytics.level-up" })handle(msg: LevelUp) {}
Why it matters A topic exchange with player.level.* lets new consumers subscribe to a family of events without touching the publisher.

Module 57 of 92, phase 06

Letters that leave on time

Emailer service with templates and a queue

Send email from a queue worker, never from the request. A MailService renders a template, and a processor delivers it with retries.

In detail

The controller or OTP service adds a mail job and returns immediately. The processor renders the template with EJS or Handlebars and sends through the injected transport from the factory module. Retries with exponential backoff absorb SMTP hiccups, and a stable job id prevents duplicate sends when a client retries.

2 examples
mail.service.ts
TypeScript
await this.queue.add("send", message, { jobId: `otp:${message.to}:${window}` });
Why it matters The limiter keeps sending under 20 messages per second, which most SMTP providers require.

Module 58 of 92, phase 06

Codes that expire on cue

OTP service backed by Redis

A one time password lives in Redis with a TTL, a hashed value and an attempt counter. Redis handles expiry and atomic counters, so the service stays small.

In detail

Store a hash of the code, never the code. Limit sends per target with a cooldown key, limit verify attempts with INCR, and delete the code after a successful check so it cannot be reused. Compare in constant time. Use a dedicated throttle on the send route as well, since every send costs money and an inbox.

otp.service.ts
TypeScript
await this.redis.client.multi().set(key, hash, "EX", 300).set(`${key}:cooldown`, "1", "EX", 60).exec();
Why it matters MULTI sends the code, attempt reset and cooldown as one atomic block, so a crash cannot leave a code without its cooldown.

Module 59 of 92, phase 06

Conducting the orchestra

Integrating multiple services

A use case often touches several services: create a player, award a starter pack, send a welcome email, and track the signup. An orchestrating service calls them in order and handles partial failure.

In detail

Keep each service focused and let one application service coordinate. Run independent steps in parallel with Promise.allSettled, run dependent ones in sequence, and push side effects that can wait into queues. Decide per step whether failure aborts the whole use case or is logged and retried later. This keeps the controller to one line and every rule in one readable place.

onboarding.service.ts
TypeScript
const player = await this.players.create(input);await Promise.allSettled([this.packs.grantStarter(player.id), this.mail.enqueue(welcome), this.analytics.capture(...)]);
Why it matters allSettled never rejects, so one failing follow up cannot hide the result of the others.

Phase 07, modules 60 to 63

Live wires

WebSockets and Socket.io

Gateways, rooms, socket guards, scaling with Redis, and AsyncAPI documentation.

Module 60 of 92, phase 07

A room that never hangs up

WebSocket gateways with Socket.io

A gateway is a provider decorated with @WebSocketGateway. @SubscribeMessage handlers receive client events, and the server instance broadcasts to rooms.

In detail

Type the server with your ServerToClientEvents and ClientToServerEvents maps from the contracts file, so emits are checked at compile time. Return a value from a handler to send an acknowledgement. Use namespaces to separate products and rooms to separate games. Validate socket payloads with the same Zod schemas you use for HTTP, through a pipe.

bootstrap.gateway.ts
TypeScript
@SubscribeMessage("room:join")async join(@ConnectedSocket() client: Socket, @MessageBody() { roomId }: { roomId: string }) {	await client.join(roomId);	return { ok: true };}
Why it matters Typing the namespace with the event maps means emit('word:created', wrongShape) fails to compile.

Module 61 of 92, phase 07

Checking tickets at the socket

Socket authentication, guards and rooms

Authenticate once in the handshake and store the user on socket.data. A WS guard then checks it on every message, and rooms scope who hears what.

In detail

A Socket.io middleware registered in afterInit verifies the JWT from handshake.auth and rejects bad tokens before the connection opens. Guards on handlers then read client.data.user, which avoids verifying the token on every message. Join each user to a personal room such as user:id so services can message them without tracking socket ids.

ws-jwt.guard.ts
TypeScript
canActivate(ctx: ExecutionContext) {	return Boolean(ctx.switchToWs().getClient<Socket>().data.user);}
Why it matters A personal room per user lets any service reach every tab and device of that user with one emit.

Module 62 of 92, phase 07

Many servers, one conversation

Scaling gateways with the Redis adapter

With more than one instance, a broadcast from one server must reach clients on the others. The Socket.io Redis adapter relays emits through Redis pub sub.

In detail

Create a custom IoAdapter that builds two Redis clients, one to publish and one to subscribe, and attaches the adapter. Register it in main.ts with app.useWebSocketAdapter. Your load balancer still needs sticky sessions if clients can fall back to long polling; forcing the websocket transport avoids that requirement.

redis-io.adapter.ts
TypeScript
const pub = new Redis(url); const sub = pub.duplicate();this.adapterConstructor = createAdapter(pub, sub);
Why it matters duplicate() gives the subscriber its own connection, since a subscribed Redis client cannot publish.

Module 63 of 92, phase 07

A map for every event

AsyncAPI documentation for Socket.io

OpenAPI describes request and response APIs. AsyncAPI describes event driven ones: channels, the messages clients send, and the messages the server emits.

In detail

nestjs-asyncapi reads decorators on gateways and payload classes and serves an AsyncAPI document and HTML viewer, much like Swagger. Describe each event as a channel with a publish or subscribe operation and a payload schema. If you prefer schemas from Zod, generate JSON Schema with zod-to-json-schema and write the YAML document by hand in docs/asyncapi.yaml.

2 examples
asyncapi.setup.ts
TypeScript
const doc = AsyncApiModule.createDocument(app, new AsyncApiDocumentBuilder().setTitle("Game realtime").build());await AsyncApiModule.setup("docs/async", app, doc);
Why it matters Pub is what clients publish to the server, Sub is what clients subscribe to. The naming is from the client's point of view.

Phase 08, modules 64 to 68

Keys and gates

Authentication and authorization

JWT with Redis sessions, OAuth 2.0, roles and signed requests.

Module 64 of 92, phase 08

Short lived passes

JWT authentication

Access tokens are short lived JWTs signed with a secret. A global guard verifies them on every request and attaches the user, and @Public opts routes out.

In detail

Use @nestjs/jwt directly for full control, or passport-jwt if you also use other Passport strategies. Keep access tokens short, around fifteen minutes, put only ids and roles in the payload, and set issuer and audience so tokens from another service are rejected. Read the token from the Authorization header with the Bearer scheme.

jwt-auth.guard.ts
TypeScript
const payload = await this.jwt.verifyAsync<JwtUser>(token);request.user = payload;
Why it matters Checking the session id in Redis lets you revoke a token before it expires, at the cost of one fast lookup.

Module 65 of 92, phase 08

Renew without asking again

Refresh tokens and Redis sessions

A long lived refresh token, stored hashed in Redis and rotated on every use, issues new access tokens. Reuse of an old refresh token revokes the whole session.

In detail

Give each login a session id. Store the hashed refresh token under session:user:sid with the refresh TTL. On refresh, compare, rotate to a new token, and keep the session id. If an already rotated token arrives, someone copied it, so delete the session. Send the refresh token as an httpOnly, secure, sameSite cookie scoped to the refresh path.

auth-tokens.service.ts
TypeScript
const { accessToken, refreshToken } = await this.tokens.issue(user);res.cookie("rt", refreshToken, { httpOnly: true, secure: true, sameSite: "strict", path: "/auth/refresh" });
Why it matters Rotation means each refresh token works once. A second use is proof of theft, so the session ends for both parties.

Module 66 of 92, phase 08

Borrowed identity

OAuth 2.0 sign in with Passport

OAuth 2.0 with OpenID Connect lets users sign in with Google or GitHub. Passport strategies handle the redirect dance, and your callback issues your own JWT and session.

In detail

Use the authorization code flow with PKCE and a state parameter, which passport-google-oauth20 handles when state is enabled. Never trust the provider profile blindly: match on provider plus provider id, not email alone. After the callback, issue your tokens and redirect to the frontend with a one time code stored briefly in Redis, rather than placing tokens in the URL.

google.strategy.ts
TypeScript
@Injectable()export class GoogleStrategy extends PassportStrategy(Strategy, "google") {}
Why it matters The frontend swaps the one minute code for tokens with a POST, so tokens never sit in browser history or server logs.

Module 67 of 92, phase 08

Who may do what

Roles and policy based authorization

Authentication says who you are; authorization says what you may do. A roles guard reads required roles from metadata and compares them with the user's roles.

In detail

Start with roles and a typed @Roles decorator. When rules depend on the resource, for example editing only your own deck, move to policies: a function that receives the user and the loaded resource and returns a boolean. CASL is a popular library for that. Run authorization after authentication so the user is always present.

roles.guard.ts
TypeScript
const required = this.reflector.getAllAndOverride(Roles, [ctx.getHandler(), ctx.getClass()]);return !required || required.some((r) => user.roles.includes(r));
Why it matters Global guards run in the order they are provided, so the JWT guard sets req.user before the roles guard reads it.

Module 68 of 92, phase 08

Proof the message was not touched

Signed requests with HMAC middleware

The client signs each request with a shared secret: an HMAC over the method, path, timestamp and body. Middleware recomputes it and rejects anything altered or replayed.

In detail

This is what SignatureSecretMiddleware does for the game client. Include a timestamp and reject requests older than a few minutes; add a nonce stored in Redis to stop exact replays inside that window. Hash the raw body, which needs rawBody: true in NestFactory.create, since re-serialised JSON may differ by whitespace or key order.

signature-secret.middleware.ts
TypeScript
const expected = createHmac("sha256", secret).update(`${ts}.${req.method}.${req.originalUrl}.${raw}`).digest("hex");
Why it matters The nonce lives exactly as long as the timestamp window, so Redis never holds more than five minutes of nonces.

Phase 09, modules 69 to 74

Files, docs and dashboards

Storage, documentation and server rendered pages

S3 uploads as a dedicated module, OpenAPI docs, and admin dashboards served by NestJS itself.

Module 69 of 92, phase 09

A drawer with infinite room

S3 upload service as a dedicated module

Wrap S3 in its own StorageModule with a client provider, an upload service and presigned URL helpers. Features import the module and never touch the AWS SDK directly.

In detail

Small files can stream through the API with putObject. Large ones should go straight from the browser to S3 with a presigned PUT URL, which saves bandwidth and memory. Generate keys yourself from a folder, a date and a random id, never from the original filename. Keep buckets private and serve reads through presigned GET URLs or a CDN with origin access control.

2 examples
storage.module.ts
TypeScript
{ provide: S3_CLIENT, inject: [ConfigService], useFactory: (c) => new S3Client({ region: c.get("AWS_REGION") }) }
Why it matters Only the service is exported. The raw client stays private so every upload goes through the same rules.

Module 70 of 92, phase 09

A contract anyone can read

OpenAPI documentation for REST

@nestjs/swagger builds an OpenAPI document from controllers and decorators and serves Swagger UI. With Zod, nestjs-zod or zod-to-openapi turns schemas into OpenAPI schemas.

In detail

Configure a DocumentBuilder with title, version, servers and security schemes, then call SwaggerModule.setup. Tag controllers, describe responses, and mark security per controller with the custom routing decorators. Exclude dashboard and internal routes. Export the JSON in CI so client SDKs can be generated and breaking changes caught in review.

2 examples
setup-openapi.ts
TypeScript
const document = SwaggerModule.createDocument(app, new DocumentBuilder().setTitle("Lexicon API").addBearerAuth().build());SwaggerModule.setup("docs", app, document);
Why it matters operationIdFactory gives every endpoint a stable name, which generated client SDKs use as method names.

Module 71 of 92, phase 09

A control room in the same server

Serving an HTML dashboard

A small admin dashboard can live inside the API: one HTML file and some JavaScript in a public folder, served by an assets controller and protected by the dashboard session guard.

In detail

ServeStaticModule is the quickest way to serve a folder. A dedicated assets controller gives more control: it can require a session, set cache headers, and stream files with StreamableFile. Remember to copy the public folder into dist with the assets entry in nest-cli.json, and exclude the dashboard paths from the global API prefix.

wordlab-dashboard-assets.controller.ts
TypeScript
@Get()page(@Res({ passthrough: true }) res: Response) {	return new StreamableFile(createReadStream(join(PUBLIC_DIR, "wordlab-dashboard.html")));}
Why it matters normalize plus stripping leading .. segments stops a request like js/..%2F..%2F.env from escaping the folder.

Module 72 of 92, phase 09

Pages drawn on the server

EJS templates with MVC rendering

NestJS can render server side templates. Set EJS as the view engine on the Express adapter and decorate handlers with @Render to return a context object.

In detail

Server rendered pages suit admin screens, email previews and simple status pages where a frontend build is overkill. Keep views beside their feature or in a views folder, copy them with nest-cli assets, and escape everything by default with <%= rather than <%-. For a page that needs the template name at runtime, inject the response and call res.render yourself.

2 examples
main.ts
TypeScript
app.setBaseViewsDir(join(__dirname, "views"));app.setViewEngine("ejs");
Why it matters setBaseViewsDir accepts an array, so each feature can keep its own views folder.

Module 73 of 92, phase 09

A feature with many rooms

A dedicated dashboard module

When a dashboard grows, give it its own module with sub folders for auth, constants, context, interfaces, schema, services and public assets, and split controllers by screen.

In detail

The WordLab dashboard keeps one controller per area: catalog, geography, image assets, lookup, preview and the page itself. Each controller stays small and maps to one screen of the HTML app. A single spec file covers the controllers with mocked services, and the module file wires the dashboard session guard, its services and its static assets. The tree below is the real layout.

WordLab dashboard

  • modules/wordlab/dashboard
  • authsession guard and login service
  • constants
  • contextrequest scoped dashboard context
  • interfaces
  • public
  • jsscreen scripts
  • wordlab-dashboard.htmlsingle page shell
  • schemaZod schemas per screen
  • servicescatalog, geography, lookup
  • wordlab-dashboard-assets.controller.tsserves HTML and JS
  • wordlab-dashboard-catalog.controller.ts
  • wordlab-dashboard-controllers.spec.tsone spec for all controllers
  • wordlab-dashboard-geography.controller.ts
  • wordlab-dashboard-image-assets.controller.tsS3 image management
  • wordlab-dashboard-lookup.controller.ts
  • wordlab-dashboard-preview.controller.tsEJS preview pages
  • wordlab-dashboard.controller.tspage and session endpoints
  • wordlab-dashboard.module.ts
wordlab-dashboard.module.ts
TypeScript
@Module({ controllers: [WordLabDashboardController, ...areaControllers], providers: [DashboardCatalogService, DashboardSessionGuard] })
Why it matters Controllers per screen keep each file under a few hundred lines and make route ownership obvious.

Module 74 of 92, phase 09

Knowing what players do

Product analytics with a PostHog module

A small src/posthog module wraps the PostHog Node client as a provider, so features capture events through an injectable service and the client flushes on shutdown.

In detail

Capture events from services after the action succeeds, with the user id as the distinct id and only non sensitive properties. Call shutdown in onApplicationShutdown so batched events are not lost. Disable the client in tests by providing a no op implementation under the same token.

analytics.service.ts
TypeScript
this.posthog.capture({ distinctId: userId, event, properties });
Why it matters Returning null when no key is set keeps local development and tests silent without any extra flags.

Phase 10, modules 75 to 82

Proof

Testing

Unit tests with Jest and Vitest, integration tests against real containers, and end to end tests over HTTP and sockets.

Module 75 of 92, phase 10

How much proof is enough

Testing strategy

Unit tests check one class with fakes, integration tests check a module against real PostgreSQL and Redis, and end to end tests check the running app over HTTP and sockets.

In detail

Most tests should be fast unit tests of services, guards, pipes and schemas. Integration tests catch what mocks hide: SQL errors, missing indexes, wrong TTLs. A smaller set of end to end tests proves wiring: middleware exclusions, global guards and serialization. Name files by kind, .spec.ts or .test.ts for units and .e2e-spec.ts for end to end, so each runner picks its own.

package.json
JSON
"test": "vitest run", "test:int": "vitest run -c vitest.int.config.ts", "test:e2e": "jest -c test/jest-e2e.json"
Why it matters Separate scripts let CI run fast unit tests on every push and slower container tests on pull requests.

Module 76 of 92, phase 10

Fakes that tell the truth

Unit testing with Jest

Test.createTestingModule builds a tiny Nest module with the class under test and fakes for its dependencies. Jest mocks record calls and return controlled values.

In detail

Provide fakes by token with useValue, then get the real class from the compiled module. Use jest.Mocked to keep fakes typed. Test behaviour, not implementation: assert on results and on calls that matter, such as cache invalidation. getRepositoryToken(Word) gives the token TypeORM uses so you can replace a repository.

2 examples
bootstrap.service.spec.ts
TypeScript
const moduleRef = await Test.createTestingModule({ providers: [BootstrapService, { provide: getRepositoryToken(Word), useValue: repo }] }).compile();
Why it matters Pick narrows each fake to the methods the service uses, so a typo in a mock fails type checking.

Module 77 of 92, phase 10

The same tests, much faster

Unit testing with Vitest

Vitest runs Jest style tests on Vite with native ESM and fast watch mode. NestJS needs one extra piece: SWC through unplugin-swc, because esbuild does not emit decorator metadata.

In detail

Without decorator metadata, Nest cannot read constructor types and every injection in a testing module fails. unplugin-swc fixes that. Use vi.fn and vi.mocked instead of jest.fn, keep globals on if you want describe and it without imports, and mirror tsconfig paths with vite-tsconfig-paths.

2 examples
vitest.config.ts
TypeScript
export default defineConfig({ plugins: [swc.vite({ module: { type: "es6" } }), tsconfigPaths()] });
Why it matters restoreMocks resets every vi.fn between tests, which removes a whole class of order dependent failures.

Module 78 of 92, phase 10

Guarding the guards

Testing schemas and events

Schemas and event maps are small, but a wrong regex or a duplicated event name breaks a feature silently. Table driven tests cover them in a few lines.

In detail

bootstrap.schema.test.ts feeds valid and invalid payloads through each schema and asserts on the result and the issue path. bootstrap.events.test.ts checks that every event name is unique and namespaced. These tests need no Nest testing module at all, so they run in milliseconds.

2 examples
bootstrap.schema.test.ts
TypeScript
it.each(invalid)("rejects %s", (_, input, path) => {	const r = CreateWordSchema.safeParse(input);	expect(r.success).toBe(false);});
Why it matters it.each turns a list of bad inputs into named tests, so a failure names the exact case.

Module 79 of 92, phase 10

Swap the parts, keep the machine

Overriding providers, guards and modules

When you compile a real module in tests, overrideProvider, overrideGuard, overrideInterceptor and overrideModule replace selected pieces while keeping the rest of the wiring.

In detail

This is the bridge between unit and end to end tests: you import the real BootstrapModule but swap the S3 client, the mail transport or the JWT guard. useMocker fills any dependency you did not list with an automatic fake, which is handy for large constructors.

overrides.ts
TypeScript
Test.createTestingModule({ imports: [AppModule] }).overrideGuard(JwtAuthGuard).useValue({ canActivate: () => true })
Why it matters The fake guard sets req.user, so handlers that read @CurrentUser still work without real tokens.

Module 80 of 92, phase 10

Real databases, disposable

Integration tests with Testcontainers

Testcontainers starts real PostgreSQL and Redis in Docker for the duration of a test file. Your module runs its real queries, migrations and TTLs against them.

In detail

Start containers in beforeAll, pass their URLs to the app through config, run migrations, and stop everything in afterAll. Truncate tables between tests rather than restarting containers. These tests catch the bugs mocks cannot: a wrong column type, a missing unique index, a Redis key without expiry.

word.repository.int.test.ts
TypeScript
const pg = await new PostgreSqlContainer("postgres:17-alpine").start();
Why it matters runMigrations builds the schema exactly as production does, so the test also proves the migrations work.

Module 81 of 92, phase 10

From the outside in

End to end tests with Supertest

End to end tests start the full Nest application and send real HTTP requests with Supertest. They prove global pipes, guards, middleware exclusions and filters work together.

In detail

Build the app with the same setup function main.ts uses, so prefixes, versioning and pipes match production. Then assert on status codes, headers and bodies. Test the edges that unit tests miss: an excluded path that skips the signature middleware, a 429 after too many OTP requests, a 400 shape from the Zod pipe.

bootstrap.e2e-spec.ts
TypeScript
await request(app.getHttpServer()).get("/api/bootstrap/words/stats").expect(200);
Why it matters configureApp is shared with main.ts. Tests that skip it pass locally and fail on the real prefix.

Module 82 of 92, phase 10

Talking to the socket in tests

Testing WebSocket gateways

Start the app on a random port and connect with socket.io-client. Emit an event with an acknowledgement and assert on the reply and on broadcasts.

In detail

Listen on port 0 so tests never collide, read the address back, and connect to the namespace. Wrap emits with emitWithAck for request reply checks. For broadcasts, connect two clients, act with one, and wait for the event on the other with a small promise helper and a timeout.

bootstrap.gateway.e2e-spec.ts
TypeScript
const reply = await client.emitWithAck("room:join", { roomId: "room-abc123" });expect(reply).toEqual({ ok: true, count: 1 });
Why it matters Registering the broadcast listener before the second join avoids a race where the event arrives first.

Phase 11, modules 83 to 90

Ship it

Production readiness

Logging, health, shutdown, security, performance, containers and service boundaries.

Module 83 of 92, phase 11

Lines you can search at 3 a.m.

Logging with the built in Logger and Pino

Nest's Logger is fine for development. In production use structured JSON logs with Pino through nestjs-pino, with request ids and redacted secrets.

In detail

nestjs-pino replaces the app logger and adds automatic request logging. Each log line is JSON with level, time, request id and context, which log platforms index directly. Redact authorization headers, cookies and passwords at the logger level so nobody can log them by accident. Keep using new Logger(Class.name) in your code; the calls route through Pino.

logging.module.ts
TypeScript
LoggerModule.forRoot({ pinoHttp: { redact: ["req.headers.authorization"], genReqId: (req) => req.headers["x-request-id"] } })
Why it matters Redaction happens before serialization, so a secret never exists in the output stream at all.

Module 84 of 92, phase 11

Proof of life

Health checks with Terminus

@nestjs/terminus exposes health endpoints that check the database, Redis, disk and memory. Orchestrators use them to decide when to send traffic and when to restart.

In detail

Expose two endpoints. Liveness answers whether the process is responsive and should check nothing external, or a slow database would restart healthy pods. Readiness checks dependencies and tells the load balancer whether to route traffic. Exclude both from the global prefix, auth and throttling.

health.controller.ts
TypeScript
@Get("ready") @HealthCheck()ready() { return this.health.check([() => this.db.pingCheck("postgres")]); }
Why it matters Liveness checks only memory. A database outage makes the app unready, not dead, so it is not restarted for nothing.

Module 85 of 92, phase 11

Leave the room tidy

Graceful shutdown

When the platform sends SIGTERM, stop accepting new requests, let in flight work finish, close workers, queues and connections, then exit.

In detail

enableShutdownHooks makes Nest run onModuleDestroy, beforeApplicationShutdown and onApplicationShutdown on signals. Close BullMQ workers so active jobs finish or return to the queue, disconnect sockets with a reason, then close Redis and the database. Set the platform's termination grace period longer than your slowest request.

shutdown.service.ts
TypeScript
async beforeApplicationShutdown(signal?: string) { this.ready = false; await sleep(5_000); }
Why it matters Failing readiness before closing gives the load balancer a moment to stop sending traffic to this instance.

Module 86 of 92, phase 11

Locks on every window

Security hardening

Helmet sets protective headers, CORS limits which origins may call you, validation rejects unknown input, and secrets come from validated config. Add CSRF protection when you use cookies for auth.

In detail

Start from the OWASP API Security Top 10. Validate everything with strict schemas, authorize every object access, rate limit authentication and OTP routes, and never return stack traces. Set trust proxy correctly so IP based limits see real clients. Keep dependencies current and run npm audit in CI.

configure-security.ts
TypeScript
app.use(helmet());app.enableCors({ origin: allowed, credentials: true });
Why it matters allowedHeaders must list the custom signature headers, or browsers block them during the CORS preflight.

Module 87 of 92, phase 11

A faster engine under the same hood

Switching to the Fastify adapter

NestJS is platform agnostic. Swap @nestjs/platform-express for @nestjs/platform-fastify and most controllers, guards and pipes keep working, with higher throughput.

In detail

Express specific code is what breaks: middleware that touches res directly, Multer based file interceptors, and @Res handlers using Express methods. Use @fastify/multipart for uploads and Fastify plugins such as @fastify/helmet. Benchmark your real endpoints; the gain is largest for small JSON responses.

main.fastify.ts
TypeScript
const app = await NestFactory.create<NestFastifyApplication>(AppModule, new FastifyAdapter({ trustProxy: true }));
Why it matters Fastify listens on 127.0.0.1 by default. Pass 0.0.0.0 or the app is unreachable inside a container.

Module 88 of 92, phase 11

Same box everywhere

Docker build and local stack

A multi stage Dockerfile builds with dev dependencies and ships a small runtime image with only production dependencies. Compose runs PostgreSQL, Redis and RabbitMQ beside it for local work.

In detail

Copy the lockfile first so dependency layers cache, build, prune dev dependencies, then copy dist into a slim image that runs as a non root user. Add a HEALTHCHECK that hits the liveness endpoint. In Compose, wait for dependency health before starting the API.

2 examples
Dockerfile
Dockerfile
FROM node:22-alpine AS buildRUN pnpm build && pnpm prune --prodFROM node:22-alpineUSER node
Why it matters USER node drops root inside the container, which limits damage if the app is ever compromised.

Module 89 of 92, phase 11

Splitting along the seams

Microservices and hybrid apps

@nestjs/microservices lets a Nest app talk over Redis, RabbitMQ, Kafka, NATS, gRPC or TCP with @MessagePattern for request reply and @EventPattern for fire and forget.

In detail

A hybrid app serves HTTP and listens on a transport at the same time with connectMicroservice. Split a service only when a module has its own scaling, release or ownership needs; a well bounded module in a modular monolith is cheaper to run. Clients inject ClientProxy and use send for replies and emit for events.

hybrid main.ts
TypeScript
app.connectMicroservice<MicroserviceOptions>({ transport: Transport.RMQ, options: { urls: [url], queue: "words" } });await app.startAllMicroservices();
Why it matters firstValueFrom turns the Observable reply into a promise, which fits ordinary async service code.

Module 90 of 92, phase 11

Separate the asking from the telling

CQRS with @nestjs/cqrs

CQRS splits writes into commands and reads into queries, each with its own handler. Events record what happened and sagas react to them.

In detail

Use it where reads and writes truly differ, for example a game round with many writes and a leaderboard read model. Commands are classes carrying intent, handlers do the work, and the EventBus publishes domain events after commit. For simple CRUD, plain services remain clearer.

submit-move.handler.ts
TypeScript
@CommandHandler(SubmitMoveCommand)export class SubmitMoveHandler implements ICommandHandler<SubmitMoveCommand> {}
Why it matters Commands, events and queries are plain classes, so they are easy to log, serialize and test.

Phase 12, modules 91 to 92

Mastery

Scale the codebase, prove the skills

Monorepos, shared libraries and a capstone that touches every module in this roadmap.

Module 91 of 92, phase 12

Many apps, one roof

Monorepos and shared libraries

nest generate app and nest generate library turn a project into a monorepo with several apps sharing libraries such as auth, redis and contracts.

In detail

Libraries get path aliases like @app/redis, so the API, the worker and an admin app import the same code. Run the queue worker as its own app that imports the same feature modules but no controllers. For larger setups, Nx or Turborepo add caching and affected only builds on top.

terminal
Shell
nest generate app workernest generate library redis
Why it matters The worker app imports the same modules as the API but starts with createApplicationContext, so it opens no port.

Module 92 of 92, phase 12

Put every piece on the board

Capstone: a realtime word game backend

Build a small realtime word bingo game that uses every phase of this roadmap. If you can ship it, test it and explain each file, you have mastered NestJS for real work.

In detail

Scope: players sign in with OTP or Google, start a bingo room over Socket.io, draw words from sharded PostgreSQL tables, and earn XP. Cache the word pool in Redis, send welcome email through BullMQ, publish level ups to RabbitMQ, upload avatars to S3, document REST with OpenAPI and sockets with AsyncAPI, and serve a small admin dashboard. Prove it with Vitest units, a Testcontainers integration suite and Supertest plus socket e2e tests, then ship it in Docker with health checks and graceful shutdown.

app.module.ts
TypeScript
@Module({ imports: [CoreModule, BootstrapModule, GameModule, AuthModule, StorageModule, WordLabDashboardModule] })export class AppModule {}
Why it matters Read this file top to bottom and you can name the module in this roadmap behind every line.

Questions people ask

Short answers to the questions that come up most while learning NestJS, each linked to the module that covers it in depth.

Do I need to know Express before NestJS?

It helps but is not required. NestJS uses Express underneath by default, so knowing how requests, middleware and responses work in Express makes the request lifecycle easier to follow. You can learn both side by side.

Module 25: Request lifecycle
Should I use Zod or class-validator?

Either works well. class-validator integrates with Swagger out of the box through DTO classes. Zod gives one source of truth for validation and types, richer transforms, and the same schemas on the frontend. Pick one per codebase and stay consistent.

Module 40: DTO vs schema
Why does Nest say it can't resolve dependencies?

The provider is not visible in that module. It is either missing from providers, not exported by the module you import, typed as an interface without a token, or undefined because of an import cycle. The index in the message tells you which constructor argument failed.

Module 18: DI resolution and common errors
When should I use forwardRef?

Only when a cycle is real and short term. Most cycles disappear when shared logic moves into a third module or one side communicates through events.

Module 17: Circular dependencies and forwardRef
Middleware, guard or interceptor: how do I choose?

Use middleware for raw request work that does not need the handler, guards for allow or deny decisions, pipes for input shape, and interceptors for anything that wraps or changes the result.

Module 33: Interceptor vs middleware
Is TypeORM still a good choice?

Yes for teams that want decorators and entities that match Nest's style. Prisma, Drizzle and MikroORM are solid alternatives; the module, provider and testing patterns in this roadmap apply to all of them.

Module 45: TypeORM with PostgreSQL
BullMQ or RabbitMQ?

BullMQ for background jobs inside one application, with retries and scheduling backed by Redis. RabbitMQ when several services publish and consume messages and need routing between them.

Module 56: RabbitMQ messaging
Jest or Vitest for a NestJS project?

Both work. Vitest is faster and ESM native but needs unplugin-swc for decorator metadata. Jest is the default the CLI generates and has the largest set of examples. The testing module API is identical in both.

Module 77: Unit testing with Vitest
How do I scale Socket.io across several servers?

Use the Redis adapter so broadcasts reach clients on every instance, and either force the websocket transport or enable sticky sessions on the load balancer.

Module 62: Scaling gateways with the Redis adapter
Where should configuration be read?

Through ConfigService or typed config namespaces, never process.env inside services. Validate once at startup so a missing secret stops the app before it serves traffic.

Module 44: Env loader and typed config
How many modules should a feature have?

Usually one. Split when a part has a different owner, different dependencies, or would be reused elsewhere, such as a dashboard inside a larger product area.

Module 05: Why modules exist
Do I need microservices to use NestJS well?

No. A modular monolith with clear module boundaries gets most of the benefit. The same boundaries make it straightforward to extract a service later if scaling or ownership demands it.

Module 89: Microservices and hybrid apps
How is progress saved on this page?

Progress is saved in this browser only, in local storage. Clearing site data resets it, and nothing is sent anywhere.

Module 92: Capstone: a realtime word game backend
What keyboard shortcuts does this roadmap support?

Press T to switch between light and dark themes, slash to search modules, and J or K to move to the next or previous module.

Module 01: Why NestJS exists

Official documentation and sources

Every source linked from the modules above, grouped by the phase that uses it and then by where it lives. 208 links in total, all opening in a new tab.

Foundations

Phase 01, Ground floor

16

Dependency injection

Phase 02, The injector's mind

19

HTTP layer

Phase 03, The request's journey

37

Config, PostgreSQL and Redis

Phase 05, Memory and state

24

Events, schedules and queues

Phase 06, Work that waits

16

Authentication and authorization

Phase 08, Keys and gates

15

Storage, documentation and server rendered pages

Phase 09, Files, docs and dashboards

15

Testing

Phase 10, Proof

16

Production readiness

Phase 11, Ship it

22

Credits

The technologies this roadmap teaches and the tools used to build the page. The people behind it are listed in the footer.

Keyboard shortcuts

Modules

←→
Previous, next module in focus mode
JK
Next, previous module
O
Focus on the current module
I
Show or hide the details
C
Collapse or expand the module
D
Mark the module as done

Page

M
Module menu
/
Search modules
T
Light or dark theme
F
Full screen
G
Back to the top

Help

?
Open this list
Esc
Close a dialog, the menu or focus mode

Shortcuts pause while you type in a search box.