📑 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 References

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.

PiecePackageJob
Core runtime@nestjs/coreDI container, module system, lifecycle hooks, platform abstraction
Common@nestjs/commonDecorators (@Controller, @Get, @Body…), pipes, guards, interceptors, exception filters, utilities
HTTP platform@nestjs/platform-express default / platform-fastifyAdapter that binds Nest to Express/Fastify — swap without touching app code
CLI@nestjs/cliScaffolding (nest new), generators (nest g resource), build & serve with watch mode
Current majorv11 stableReleased 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.

📦 Modules — @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.

🎯 Controllers — @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.

🧩 Providers — @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.

🔀 DI Container

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).

Minimal feature module — cats.module.ts
@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 {}
Root module — app.module.ts (bootstrap entry)
@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.

Classic constructor injection
@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:

SyntaxMeaningUse case
{ provide: X, useClass: Y }Provide class X but instantiate YSwap implementations (e.g. mock/staging)
{ provide: X, useValue: {...} }Provide a literal valueConfig objects, constants, test mocks
{ provide: X, useFactory: (deps) => ... }Build via a factory function, with injected depsAsync/conditional setup, env-driven values
{ provide: X, useExisting: Y }Alias: same instance as YRe-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.

  • 1
    Createnest new my-app scaffolds a TS project with Jest preconfigured (choose npm/pnpm/yarn).
  • 2
    Generatenest 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).
  • 3
    Runnpm run start:dev = watch mode with incremental compilation; start:debug adds a Node inspector port for Chrome DevTools.
  • 4
    Buildnpm run build emits to dist/ (tsc by default; SWC via @swc/cli for much faster builds).
  • 5
    Testnpm test (Jest unit tests in *.spec.ts), npm run test:e2e (supertest against the full app in test/).
  • Generated layout (src/)
    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)
    main.ts — typical bootstrap
    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()).

    cats.controller.ts
    @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

    DecoratorWhat 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 + validation
    • ParseUUIDPipe, ParseEnumPipe, ParseArrayPipe — format checks
    • DefaultValuePipe — supply a default before another pipe runs
    • ValidationPipe — the big one: validates DTOs with class-validator
    DTO with class-validator decorators
    export class CreateCatDto {
      @IsString() @IsNotEmpty()
      name: string;
    
      @IsInt() @Min(0) @Max(30)
      age: number;
    
      @IsOptional() @IsString()
      breed?: string;
    }
    Binding the ValidationPipe
    // 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

    OptionEffect
    whitelist: trueStrips any property not declared in the DTO (kills mass-assignment)
    forbidNonWhitelisted: true400 instead of silently stripping unknown props
    transform: trueCoerces 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: trueReports 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.

    Role guard using the Reflector + metadata
    @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)

  • 1
    LoginAuthService.login() verifies credentials (bcrypt/argon2 hash), signs a JWT with @nestjs/jwt (JwtService.sign(payload)).
  • 2
    Verify — a JwtAuthGuard (usually built on @nestjs/passport + passport-jwt, or a hand-rolled guard) reads Authorization: Bearer <token> and verifies the signature/expiry.
  • 3
    Attach user — on success the guard stores the decoded payload on req.user; a @CurrentUser() param decorator exposes it to handlers.
  • 4
    Authorize — a RolesGuard (or per-route checks) compares req.user.role against route metadata.
  • Global guard registration
    // 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 → middlewareguardsinterceptors (before)pipes → controller handler → interceptors (after) → response, with exception filters catching anything thrown anywhere along the way.

    🛡️ Middleware

    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.

    🪝 Interceptors — NestInterceptor

    Wrap handler execution with Observables (RxJS): log timing, transform responses (map), add caching (tap), retry/fallback (catchError), or run a DB transaction around the handler.

    🚨 Exception filters — @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.

    🧪 ExecutionContext

    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).

    Interceptor example — request logging + response mapping
    @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`)),
        );
      }
    }
    Custom exception filter
    @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

    Loading env vars
    // 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

    🗄️ TypeORM (@nestjs/typeorm)

    Entity classes with decorators (@Entity(), @Column()), repositories injected via @InjectRepository(Entity). Classic TypeOrmModule.forRoot() + forFeature([Entity]) per module.

    🔺 Prisma (see the Prisma study note)

    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/.

    Prisma recipe (ties into your Prisma v7 note)
    @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

    ModulePurpose
    @nestjs/swaggerOpenAPI/Swagger generation from decorators (@ApiTags, @ApiProperty, @ApiBearerAuth) — served at /api/docs via SwaggerModule.setup()
    @nestjs/cache-managerIn-memory or Redis caching via @UseInterceptors(CacheInterceptor)
    @nestjs/scheduleCron/interval jobs: @Cron('0 8 * * *'), @Interval()
    @nestjs/terminusHealth checks: /health endpoint with DB/disk probes (great for K8s readiness probes)
    @nestjs/throttler (community)Rate limiting via @Throttle() + ThrottlerGuard
    @nestjs/microservicesService-to-service transport: TCP, Redis, NATS, MQTT, RabbitMQ, Kafka, gRPC — same decorators over @MessagePattern()
    @nestjs/websockets / @nestjs/graphqlGateway-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.

    Unit test — mock the service
    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([]);
      });
    });
    e2e test — full HTTP stack
    // 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 (or tsc for type-strict builds). SWC is dramatically faster; keep tsc --noEmit in CI for type checking.
    • Editor: the Nest generator emits .vscode/launch.json debug configs; start:debug + Chrome DevTools is the standard flow.
    • Monorepos: Nest apps work well in pnpm/nx monorepos; nest g app / nest g library exist 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).

    ⚡ Express
    • 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
    🐾 NestJS
    • 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
    🚀 Fastify
    • Very fast: JSON Schema serialization
    • Plugin system (encapsulation built in)
    • No app architecture opinion
    • Great as the engine under NestJS
    • Schema-first validation mindset
    AspectExpressNestJSFastify
    ArchitectureNone (middleware chain)Modules + DI + AOP pipelinePlugin encapsulation
    TypeScriptManual setupFirst-class, scaffoldedGood, manual setup
    ValidationHand-rolledValidationPipe + class-validatorJSON Schema (fast)
    OpenAPIThird-party@nestjs/swagger built-in@fastify/swagger
    TestingsupertestJest + DI mocking out of the boxinject + supertest
    PerfGood≈ platform (Express/Fastify) + small overheadExcellent
    Learning curveShallowSteeper (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

    ✅ Great fit

    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.

    ⚠️ Think twice

    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

  • 1
    package.json — Nest version (v10/v11), key deps: typeorm vs prisma vs mongoose, passport/jwt, swagger, config, schedule.
  • 2
    src/main.ts — global prefix, CORS, global pipes/guards/filters, port, versioning.
  • 3
    app.module.ts — the full module import tree; this is your map of features.
  • 4
    One feature module — read its controller → service → DTO → entity to internalize the project's conventions.
  • 5
    Auth setup — find the guard(s) and how req.user gets populated; you'll touch this in every request flow.
  • 6
    DB layer — PrismaService / TypeORM entities; run 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