📑 Contents
Overview / TL;DR Core Architecture Dependency Injection Nest CLI & Structure Controllers & Routes Pipes & Validation Guards & Auth Interceptors / Filters / MW Config, DB & Swagger Testing & Tooling vs Express / Fastify When to Use NestJS ReferencesNestJS — Deep Study
The progressive Node.js framework: modules, dependency injection, decorators, and the full request pipeline — everything you need before jumping into a NestJS codebase.
1. Overview / TL;DR
NestJS is a progressive Node.js framework for building efficient, reliable, and
scalable server-side applications. It is written in and fully embraces TypeScript, and its
architecture is directly inspired by Angular: an opinionated module system, a
dependency-injection container, and heavy use of decorators to
describe routing, validation, guards, and everything else declaratively. Under the hood it runs on
Express by default (Fastify is a drop-in alternative via @nestjs/platform-fastify),
so the entire Express middleware ecosystem remains available.
The core idea: in NestJS, everything your application does is a provider (service) registered in a module. The framework's dependency-injection container instantiates and wires those providers together, and an HTTP request flows through a fixed, extensible pipeline — middleware → guards → interceptors → pipes → handler → interceptors → exception filters. You write business logic in services and declare behavior with decorators; NestJS handles the plumbing. That inversion of control is why NestJS projects look remarkably similar to each other, which makes them easy to onboard into.
| Piece | Package | Job |
|---|---|---|
| Core runtime | @nestjs/core | DI container, module system, lifecycle hooks, platform abstraction |
| Common | @nestjs/common | Decorators (@Controller, @Get, @Body…), pipes, guards, interceptors, exception filters, utilities |
| HTTP platform | @nestjs/platform-express default / platform-fastify | Adapter that binds Nest to Express/Fastify — swap without touching app code |
| CLI | @nestjs/cli | Scaffolding (nest new), generators (nest g resource), build & serve with watch mode |
| Current major | v11 stable | Released Jan 2025 — TypeScript 5 support, SWC build integration, Node 20+ baseline |
Why it matters for your project: NestJS is the most popular structured Node.js
framework for teams. If the project you're joining is a NestJS app, reading it is mostly a matter of
following the module tree (app.module.ts → feature modules), understanding the
providers registered in each module, and tracing how decorators on controllers map to routes.
This note walks through every concept you'll encounter, in the order you'll encounter it.
2. Core Architecture — Modules, Controllers, Providers
NestJS is organized around three building blocks. A module groups related
controllers (HTTP-facing) and providers (logic + state) into a
feature unit. The AppModule is the root that imports everything.
@Module()The organizing unit. The decorator takes imports (other modules),
controllers, providers, and exports (providers made
visible to importing modules). One module per feature keeps the app navigable.
@Controller()Thin HTTP layer. A class decorated with @Controller('cats') plus method
decorators like @Get(':id') maps to routes. Controllers parse input via parameter
decorators and delegate to services — they hold no business logic.
@Injectable()Services, repositories, adapters — anything with behavior or state. Registered in a module's
providers array, instantiated by the DI container, and injected into controllers or
other providers via constructor parameters.
The engine underneath: builds an object graph from module metadata, resolves constructor dependencies recursively, and by default gives every module's providers a singleton scope (one instance per app).
@Module({
imports: [DatabaseModule], // other modules this one needs
controllers: [CatsController], // route handlers
providers: [CatsService], // registered for DI
exports: [CatsService], // visible to modules that import CatsModule
})
export class CatsModule {}
@Module({
imports: [CatsModule, UsersModule, AuthModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Reading an existing project: start at src/main.ts (bootstrap,
global prefix, CORS, validation), then app.module.ts for the module list, then drill
into each feature module. The module graph is the architecture map.
3. Dependency Injection — the Engine Under Everything
NestJS implements its own lightweight DI container. Dependencies are declared in the constructor and resolved from the container, never instantiated by hand. This is what makes services unit-testable (you inject mocks) and what keeps the module graph explicit.
@Injectable()
export class CatsService {
constructor(
private readonly prisma: PrismaService, // resolved from CatsModule's providers (or imports)
@Inject('CONFIG') private readonly config: AppConfig, // custom token
) {}
}
Provider syntaxes (custom providers)
Besides a plain class, a provider entry can be an object literal that tells the container how to build the value:
| Syntax | Meaning | Use case |
|---|---|---|
{ provide: X, useClass: Y } | Provide class X but instantiate Y | Swap implementations (e.g. mock/staging) |
{ provide: X, useValue: {...} } | Provide a literal value | Config objects, constants, test mocks |
{ provide: X, useFactory: (deps) => ... } | Build via a factory function, with injected deps | Async/conditional setup, env-driven values |
{ provide: X, useExisting: Y } | Alias: same instance as Y | Re-export under a different token |
Scopes
- DEFAULT (singleton) — one instance per app, shared everywhere. Default and almost always correct.
- REQUEST — a fresh instance per request (adds allocation overhead; use only for per-request state).
- TRANSIENT — a fresh instance per injection site.
Scope is set via the @Injectable({ scope: Scope.REQUEST }) option or a provider's
scope field.
Circular dependencies: if module A imports B and B imports A, use
forwardRef(() => B) in the imports array and
@Inject(forwardRef(() => BService)) on the constructor parameter. Better: redesign
so the cycle disappears — a shared module or an event/queue is usually cleaner.
4. Nest CLI & Project Structure
The CLI (@nestjs/cli) scaffolds projects and generates code. 90% of NestJS apps follow
the same generated layout, so the structure below is what you'll see in the project you're joining.
nest new my-app scaffolds a TS project with Jest preconfigured (choose npm/pnpm/yarn).nest g module cats, nest g controller cats, nest g service cats — or all at once with nest g resource cats (CRUD + DTOs + tests, optional GraphQL/WebSocket variants).npm run start:dev = watch mode with incremental compilation; start:debug adds a Node inspector port for Chrome DevTools.npm run build emits to dist/ (tsc by default; SWC via @swc/cli for much faster builds).npm test (Jest unit tests in *.spec.ts), npm run test:e2e (supertest against the full app in test/).src/
├── main.ts # bootstrap: NestFactory.create + app.listen
├── app.module.ts # root module — the import tree starts here
├── app.controller.ts # root controller (usually GET / health)
├── app.service.ts
└── cats/ # feature module (nest g resource cats)
├── cats.module.ts # @Module wiring controllers + providers
├── cats.controller.ts
├── cats.service.ts
├── dto/ # create-cat.dto.ts, update-cat.dto.ts
└── entities/ # or schemas/ — data shapes
test/ # e2e tests (app.e2e-spec.ts)
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('api'); // every route becomes /api/...
app.enableCors(); // CORS for browser clients
app.useGlobalPipes(new ValidationPipe({ whitelist: true })); // global validation
await app.listen(3000);
}
bootstrap();
Global prefix & versioning: many production apps combine
setGlobalPrefix('api') with app.enableVersioning({ type: VersioningType.URI })
and @Version('1') on controllers — so routes look like /api/v1/cats.
Check main.ts first if the routes in your project don't match the controllers
literally.
5. Controllers & Request Handling
Controllers are classes with route decorators. Method decorators define the HTTP verb + path;
parameter decorators pull values out of the request. Handlers can be async and return objects/arrays
directly — Nest serializes them to JSON (or streams via @Res()).
@Controller('cats') // base path: /cats
export class CatsController {
constructor(private readonly catsService: CatsService) {}
@Get() // GET /cats
findAll(@Query('age') age?: number) { return this.catsService.findAll(age); }
@Get(':id') // GET /cats/42
findOne(@Param('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id); // pipes run before the handler
}
@Post()
@HttpCode(201) // default for POST is already 201
create(@Body() dto: CreateCatDto) { return this.catsService.create(dto); }
@Delete(':id')
remove(@Param('id', ParseIntPipe) id: number) { return this.catsService.remove(id); }
}
Parameter decorators cheat sheet
| Decorator | What it injects |
|---|---|
@Req() / @Request() | The raw Express/Fastify request object |
@Res() / @Response() | The raw response object — only for streaming/headers; using it switches the handler to library-specific mode and you lose Nest's return-value handling |
@Param('id') | Route parameter(s) — can chain a pipe: @Param('id', ParseIntPipe) |
@Query('page') | Query-string parameter(s) |
@Body() | Request body — usually a DTO class, validated by ValidationPipe |
@Headers('x-token') | Header value(s) |
@Ip(), @HostParam() | Client IP; host pattern variable (sub-domain routing) |
@Session() | Session object (needs express-session configured) |
Custom param decorators: createParamDecorator lets you build reusable
extractors (e.g. @CurrentUser() that reads the user off the request). Very common in
real apps for auth — see the guard section below.
6. Pipes & Validation
A pipe transforms input data before the handler receives it (parse, coerce, default) and/or validates
it (reject with a 400). Pipes run after guards, before the handler. The star is
ValidationPipe, which works with class-validator decorators on DTO classes.
Built-in pipes
ParseIntPipe,ParseFloatPipe,ParseBoolPipe— type coercion + validationParseUUIDPipe,ParseEnumPipe,ParseArrayPipe— format checksDefaultValuePipe— supply a default before another pipe runsValidationPipe— the big one: validates DTOs with class-validator
export class CreateCatDto {
@IsString() @IsNotEmpty()
name: string;
@IsInt() @Min(0) @Max(30)
age: number;
@IsOptional() @IsString()
breed?: string;
}
// Globally (recommended — one line in main.ts):
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
// Or per controller/handler:
@UsePipes(new ValidationPipe({ whitelist: true }))
@Post() create(@Body() dto: CreateCatDto) { ... }
Key options
| Option | Effect |
|---|---|
whitelist: true | Strips any property not declared in the DTO (kills mass-assignment) |
forbidNonWhitelisted: true | 400 instead of silently stripping unknown props |
transform: true | Coerces plain JSON to DTO class instances (enables @IsInt on strings, default values, etc.) |
transformOptions: { enableImplicitConversion: true } | Auto-converts primitives ('42' → 42) based on TS types |
stopAtFirstError: true | Reports the first error per property instead of all |
Rule of thumb: always register ValidationPipe globally with
whitelist: true + transform: true. If the project you're joining doesn't
have it, that's the single highest-value improvement to suggest — it's also a classic interview
question about NestJS security defaults.
7. Guards, Auth & the Execution Context
Guards decide whether a request may proceed (auth, roles, rate limits…). They
implement CanActivate and return a boolean — or throw an exception to deny. Guards run
before interceptors and pipes.
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(ctx: ExecutionContext): boolean {
const roles = this.reflector.get<string[]>('roles', ctx.getHandler()) ?? [];
if (roles.length === 0) return true; // no @Roles() → public
const req = ctx.switchToHttp().getRequest();
return roles.includes(req.user?.role);
}
}
// Usage:
@Roles('admin') // custom decorator: SetMetadata('roles', ['admin'])
@Post() create(@Body() dto: CreateCatDto) { ... }
How real apps do auth (JWT flow)
AuthService.login() verifies credentials (bcrypt/argon2 hash), signs a JWT with @nestjs/jwt (JwtService.sign(payload)).JwtAuthGuard (usually built on @nestjs/passport + passport-jwt, or a hand-rolled guard) reads Authorization: Bearer <token> and verifies the signature/expiry.req.user; a @CurrentUser() param decorator exposes it to handlers.RolesGuard (or per-route checks) compares req.user.role against route metadata.// In the module (recommended — works with DI):
providers: [{ provide: APP_GUARD, useClass: JwtAuthGuard }]
// or app-level (no DI):
app.useGlobalGuards(new JwtAuthGuard());
Whitelisting public routes: a global auth guard makes everything
protected — including login and health checks. Use a metadata decorator (e.g.
@Public()) and read it in the guard with the Reflector, or set
@SkipAuth()-style flags. This is a very common bug in production NestJS apps.
8. Interceptors, Exception Filters & Middleware
The remaining AOP (aspect-oriented) primitives wrap the request pipeline. Learning the order they run in is the key to reading any NestJS request path.
Full request order: incoming request → middleware → guards → interceptors (before) → pipes → controller handler → interceptors (after) → response, with exception filters catching anything thrown anywhere along the way.
Classic Express-style functions (req, res, next). Bound per-route with
consumer.apply(LoggerMiddleware).forRoutes('cats'). Use for logging, request ID
injection, CORS headers, body preprocessing.
NestInterceptorWrap handler execution with Observables (RxJS): log timing, transform responses
(map), add caching (tap), retry/fallback (catchError),
or run a DB transaction around the handler.
@Catch()Centralize error handling. Throw HttpException / BadRequestException
/ NotFoundException from anywhere; a filter formats the response shape
(statusCode, message, custom fields). ArgumentsHost gives
access to req/res.
The universal abstraction guards/interceptors/filters receive: getClass(),
getHandler(), switchToHttp() (also switchToRpc /
switchToWs — which is why the same guard works across HTTP, microservices and
WebSockets).
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(ctx: ExecutionContext, next: CallHandler): Observable<unknown> {
const started = Date.now();
return next.handle().pipe(
map((data) => ({ data, ok: true })), // wrap every response
tap(() => console.log(`took ${Date.now() - started}ms`)),
);
}
}
@Catch(NotFoundException)
export class NotFoundFilter implements ExceptionFilter {
catch(exception: NotFoundException, host: ArgumentsHost) {
const res = host.switchToHttp().getResponse();
res.status(404).json({ code: 'NOT_FOUND', message: exception.message });
}
}
Standard exceptions (BadRequestException,
UnauthorizedException, ForbiddenException,
NotFoundException, ConflictException, GatewayTimeoutException…)
map to proper HTTP statuses out of the box. Prefer throwing those over custom ones — and let the
built-in HttpExceptionFilter handle them unless you need a specific response shape.
9. Integrations — Config, Databases & Swagger
NestJS ships first-party modules for the boring-but-essential infrastructure. These follow the same
pattern: a forRoot (or forRootAsync) registration that produces a global
provider.
Configuration — @nestjs/config
// app.module.ts
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true, envFilePath: ['.env', '.env.local'] }),
],
})
export class AppModule {}
// anywhere:
constructor(private readonly config: ConfigService) {}
const port = this.config.get<number>('PORT', 3000);
Database — TypeORM vs Prisma
@nestjs/typeorm)Entity classes with decorators (@Entity(), @Column()),
repositories injected via @InjectRepository(Entity). Classic
TypeOrmModule.forRoot() + forFeature([Entity]) per module.
No entity classes — a generated client instead. The standard recipe is a
PrismaService extends PrismaClient implements OnModuleInit registered as a global
provider, then injected anywhere. If your project uses Prisma, this is exactly what you'll find
in src/prisma/.
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() { await this.$connect(); } // lifecycle hook
}
@Module({ providers: [PrismaService], exports: [PrismaService] })
export class PrismaModule {}
// cats.service.ts
constructor(private readonly prisma: PrismaService) {}
async findAll() { return this.prisma.cat.findMany(); }
More first-party modules you'll meet
| Module | Purpose |
|---|---|
@nestjs/swagger | OpenAPI/Swagger generation from decorators (@ApiTags, @ApiProperty, @ApiBearerAuth) — served at /api/docs via SwaggerModule.setup() |
@nestjs/cache-manager | In-memory or Redis caching via @UseInterceptors(CacheInterceptor) |
@nestjs/schedule | Cron/interval jobs: @Cron('0 8 * * *'), @Interval() |
@nestjs/terminus | Health checks: /health endpoint with DB/disk probes (great for K8s readiness probes) |
@nestjs/throttler (community) | Rate limiting via @Throttle() + ThrottlerGuard |
@nestjs/microservices | Service-to-service transport: TCP, Redis, NATS, MQTT, RabbitMQ, Kafka, gRPC — same decorators over @MessagePattern() |
@nestjs/websockets / @nestjs/graphql | Gateway-based WebSocket servers; GraphQL (Apollo/Mercurius) with code-first or schema-first |
10. Testing & Tooling
NestJS is designed for testability — DI means you override providers with mocks in one line. Jest comes preconfigured; the e2e harness boots the whole app with supertest.
describe('CatsController', () => {
let controller: CatsController;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
controllers: [CatsController],
providers: [{ provide: CatsService, useValue: { findAll: jest.fn().mockResolvedValue([]) } }],
}).compile();
controller = moduleRef.get(CatsController);
});
it('returns the mocked list', async () => {
await expect(controller.findAll()).resolves.toEqual([]);
});
});
// test/app.e2e-spec.ts
const app = await NestFactory.create(AppModule);
await app.init();
await request(app.getHttpServer())
.get('/api/cats')
.expect(200)
.expect((res) => expect(res.body).toBeInstanceOf(Array));
Tooling notes
- Build speed:
nest build --builder swc(ortscfor type-strict builds). SWC is dramatically faster; keeptsc --noEmitin CI for type checking. - Editor: the Nest generator emits
.vscode/launch.jsondebug configs;start:debug+ Chrome DevTools is the standard flow. - Monorepos: Nest apps work well in pnpm/nx monorepos;
nest g app/nest g libraryexist for multi-app setups. - Version pinning: check
package.json— v10 vs v11 differ subtly in Node/TS requirements (v11 baseline: Node ≥ 20, TS ≥ 5.x).
11. NestJS vs Express vs Fastify
All three run on Node and are TypeScript-friendly, but they occupy very different points on the structure axis. NestJS is not a competitor to Express in the same tier — it's an opinionated layer on top of it (or Fastify).
- Minimal, middleware-only core
- No structure — you invent the conventions
- Huge middleware ecosystem
- Great for tiny services & prototypes
- Churns at scale: routes/validation/auth all manual
- Opinionated: modules + DI + decorators
- Batteries included: validation, guards, Swagger, testing
- TypeScript-first, Angular-like mental model
- Consistent structure across teams/projects
- Runs on Express or Fastify underneath
- Very fast: JSON Schema serialization
- Plugin system (encapsulation built in)
- No app architecture opinion
- Great as the engine under NestJS
- Schema-first validation mindset
| Aspect | Express | NestJS | Fastify |
|---|---|---|---|
| Architecture | None (middleware chain) | Modules + DI + AOP pipeline | Plugin encapsulation |
| TypeScript | Manual setup | First-class, scaffolded | Good, manual setup |
| Validation | Hand-rolled | ValidationPipe + class-validator | JSON Schema (fast) |
| OpenAPI | Third-party | @nestjs/swagger built-in | @fastify/swagger |
| Testing | supertest | Jest + DI mocking out of the box | inject + supertest |
| Perf | Good | ≈ platform (Express/Fastify) + small overhead | Excellent |
| Learning curve | Shallow | Steeper (DI, decorators, RxJS) | Shallow–medium |
Other structured alternatives: AdonisJS (Laravel-style, batteries included, less DI-heavy), LoopBack (model-driven APIs), Hono (ultra-light, edge-first, no structure). NestJS's differentiator remains its Angular-like DI + module system and the huge first-party integration catalog.
12. When to Use NestJS — & Your First-Project Checklist
Team projects needing consistent structure · complex domains with many modules · long-lived services that will grow · TypeScript-first teams · apps that need auth, validation, OpenAPI, queues, or microservices from day one · anyone who already knows Angular-style DI.
Tiny one-route services (Express/Hono is less ceremony) · latency-critical hot paths (use Fastify platform or bare Fastify) · teams that strongly prefer functional-style handlers · edge/serverless functions where cold-start and bundle size dominate (NestJS works but is heavy).
Jumping into an existing NestJS project — read in this order
package.json — Nest version (v10/v11), key deps: typeorm vs prisma vs mongoose, passport/jwt, swagger, config, schedule.src/main.ts — global prefix, CORS, global pipes/guards/filters, port, versioning.app.module.ts — the full module import tree; this is your map of features.req.user gets populated; you'll touch this in every request flow.npm run start:dev and hit the Swagger UI (/api/docs) to see live routes.Mental model in one sentence: NestJS = Express/Fastify engine + an Angular-style DI container + declarative decorators + a fixed request pipeline. Once you can answer "which module provides this service, and what runs before my handler?" you understand the project.
13. References
-
1NestJS Official DocumentationThe authoritative reference — first steps, overview, and every technique chapter.
-
2nestjs/nest — GitHubSource code, releases, and roadmap. The framework is MIT-licensed and actively maintained.
-
3Nest CLI OverviewAll
nestcommands: new, generate schematics, build, start, and flags. -
4Custom ProvidersuseClass / useValue / useFactory / useExisting, injection tokens, and provider scopes.
-
5Pipes — Validation & TransformationBuilt-in pipes, ValidationPipe options, and custom pipe authoring.
-
6Guards & Execution ContextCanActivate, the ExecutionContext abstraction, and binding guards.
-
7Interceptors & Exception FiltersRxJS-based interceptors and centralized error handling with ArgumentsHost.
-
8Lifecycle EventsOnModuleInit / OnApplicationShutdown etc. — where connect/disconnect logic lives.
-
9Prisma + NestJS RecipeThe canonical PrismaService setup — pairs with the Prisma ORM study note in this collection.
-
10Authentication (JWT + Passport)The official JWT/Passport walkthrough — the base most production auth flows build on.
-
11@nestjs/core on npmPackage page with version history, peer deps (Node/TS/Express), and weekly downloads.
-
12NestJS Releases / ChangelogPrimary source for major-version changes (v9 → v10 → v11) and deprecations.