Guide

NestJS fundamentals explained

A junior developer copies route handlers from Stack Overflow into a single index.ts until the file hits two thousand lines and nobody dares refactor it. A senior team reaches for NestJS instead: a TypeScript framework that wraps Express or Fastify with Angular-inspired modules, dependency injection, and a predictable request pipeline of pipes, guards, interceptors, and exception filters. Nest does not replace Node.js — it organizes how you use it. Enterprises, fintech APIs, and B2B SaaS backends adopt Nest when they need structure, testability, and OpenAPI docs without surrendering the npm ecosystem. This guide covers the module graph, controllers and providers, validation and auth guards, configuration and lifecycle hooks, pairing Nest with Prisma, a Harbor Supply order microservice worked example, a framework decision table, common pitfalls, and a production checklist — alongside our TypeScript fundamentals guide and REST API design overview.

What NestJS is and how requests flow

NestJS is a progressive Node.js framework for building efficient and scalable server-side applications. Kamil Myśliwiec designed it to bring patterns familiar from Spring and Angular — inversion of control, decorators, modular boundaries — to JavaScript backends. Under the hood, Nest boots either Express (default) or Fastify as the HTTP adapter; your code rarely touches the raw req/res objects unless you opt into lower-level access via @Req().

Every HTTP request passes through a layered pipeline before your controller method runs:

  1. Middleware — Express-style functions for logging, CORS, or raw body parsing (registered in module configure()).
  2. Guards — authorization checks returning true or throwing ForbiddenException (JWT validation, role checks).
  3. Interceptors — wrap execution: transform responses, add timing headers, cache, or map entities to DTOs.
  4. Pipes — validate and transform inputs (ValidationPipe turns plain JSON into class instances and rejects invalid bodies with 400).
  5. Controller handler — your business logic, usually delegating to injected services.
  6. Exception filters — catch thrown errors and shape consistent JSON error responses.

That ordering is why Nest feels opinionated in a good way: cross-cutting concerns have named homes instead of copy-pasted middleware chains in every route file.

Core building blocks

  • Module (@Module) — a cohesive feature unit declaring controllers, providers, and imports/exports.
  • Controller (@Controller) — handles HTTP routes; thin layer mapping requests to service calls.
  • Provider / Service (@Injectable) — injectable class holding business logic, DB access, or external API clients.
  • Dependency injection — Nest’s IoC container resolves constructor parameters automatically when types are known (or via @Inject() tokens).
  • DTO (Data Transfer Object) — classes with class-validator decorators defining allowed request/response shapes.

Modules, controllers, and providers

A Nest application is a tree of modules rooted at AppModule. Feature modules keep domains isolated — OrdersModule, AuthModule, HealthModule — each exporting only what other modules need:

@Module({
  imports: [PrismaModule, ConfigModule],
  controllers: [OrdersController],
  providers: [OrdersService],
  exports: [OrdersService],
})
export class OrdersModule {}

Controllers declare routes with decorators. Path parameters, query strings, and bodies map to handler arguments:

@Controller('orders')
export class OrdersController {
  constructor(private readonly orders: OrdersService) {}

  @Post()
  @HttpCode(201)
  create(@Body() dto: CreateOrderDto) {
    return this.orders.create(dto);
  }

  @Get(':id')
  findOne(@Param('id', ParseUUIDPipe) id: string) {
    return this.orders.findById(id);
  }
}

Services hold logic the controller should not know about — transactions, idempotency keys, calling payment gateways. Keep controllers under ~20 lines per method; if a handler grows complex, extract a command object or domain service.

Module boundaries that scale

  • Shared modulesDatabaseModule, LoggerModule exported globally with @Global() sparingly (overuse hides dependencies).
  • Dynamic modulesConfigModule.forRoot(), JwtModule.registerAsync() for env-driven setup.
  • Lazy-loaded modules — rarely needed for HTTP APIs; more common in GraphQL microservice gateways.
  • Circular imports — use forwardRef(() => OtherModule) as a last resort; prefer event emitters or a shared facade service.

Validation, pipes, and DTOs

Nest pairs with class-validator and class-transformer. Enable a global validation pipe in main.ts:

app.useGlobalPipes(new ValidationPipe({
  whitelist: true,
  forbidNonWhitelisted: true,
  transform: true,
}));

whitelist: true strips unknown properties (mass-assignment protection). transform: true coerces query strings to numbers and plain objects to class instances. A DTO might look like:

export class CreateOrderDto {
  @IsUUID()
  customerId: string;

  @IsArray()
  @ValidateNested({ each: true })
  @Type(() => OrderLineDto)
  lines: OrderLineDto[];
}

export class OrderLineDto {
  @IsString()
  skuId: string;

  @IsInt()
  @Min(1)
  @Max(9999)
  quantity: number;
}

Built-in pipes include ParseIntPipe, ParseUUIDPipe, and DefaultValuePipe. Custom pipes implement PipeTransform for domain-specific normalization (trimming SKUs, uppercasing currency codes). Validation failures return 400 Bad Request with a structured array of constraint messages — easier for API clients than ad-hoc error strings.

Guards, interceptors, and exception filters

Guards answer “may this request proceed?” Implement CanActivate and attach with @UseGuards(JwtAuthGuard) at controller or method level. Passport strategies (@nestjs/passport) integrate JWT, OAuth2, and API keys cleanly. Guards run before pipes on route handlers — do not parse bodies in guards; only inspect headers and metadata.

Interceptors wrap the handler observable. Common uses:

  • ClassSerializerInterceptor — strip @Exclude() fields from entities before JSON serialization.
  • LoggingInterceptor — record method, URL, and latency.
  • CacheInterceptor — HTTP caching for idempotent GETs (pair with Redis for distributed cache).
  • TimeoutInterceptor — cancel hung downstream calls.

Exception filters map thrown HttpException subclasses (and unknown errors) to consistent response bodies. A global filter can log stack traces server-side while returning safe messages to clients — critical for security and supportability.

Configuration, lifecycle, and the HTTP adapter

@nestjs/config loads .env files and exposes ConfigService for typed access. Prefer registerAsync with validate using Joi or class-validator so the app refuses to boot with missing DATABASE_URL or JWT_SECRET. Never commit secrets; inject them via environment variables in production (Kubernetes secrets, Docker Compose env files, or your host’s secret store).

main.ts bootstraps the app:

const app = await NestFactory.create(AppModule, {
  bufferLogs: true,
});
app.enableShutdownHooks();
app.setGlobalPrefix('api/v1');
await app.listen(process.env.PORT ?? 3000);

enableShutdownHooks() lets Nest respond to SIGTERM — finish in-flight requests before exit, matching graceful shutdown discipline. For higher throughput, switch the adapter: NestFactory.create(AppModule, new FastifyAdapter()) — Fastify often wins on raw requests-per-second, while Express has the broader middleware compatibility story.

OpenAPI and versioning

@nestjs/swagger generates OpenAPI 3 specs from decorators (@ApiProperty, @ApiBearerAuth). Serve Swagger UI at /docs in staging only — disable or protect it in production. URI versioning (/api/v1, /api/v2) via @Controller({ version: '1' }) keeps breaking changes manageable; see our API versioning guide for header vs URI tradeoffs.

Database access with Prisma

Nest does not ship an ORM; teams commonly pair it with Prisma. Pattern: a PrismaService extends PrismaClient and implements OnModuleInit / OnModuleDestroy:

@Injectable()
export class PrismaService extends PrismaClient
  implements OnModuleInit, OnModuleDestroy {
  async onModuleInit() { await this.$connect(); }
  async onModuleDestroy() { await this.$disconnect(); }
}

Inject PrismaService into domain services. Use $transaction for multi-step writes (inventory decrement + order insert). In serverless or high-scale deployments, point Prisma at a connection pooler — see our connection pooling guide. Alternative stacks: TypeORM and MikroORM have first-class Nest modules; Drizzle fits teams wanting SQL-first ergonomics without a heavy generator.

Testing Nest applications

@nestjs/testing provides Test.createTestingModule() to wire modules in isolation. Override providers with mocks:

const module = await Test.createTestingModule({
  controllers: [OrdersController],
  providers: [
    OrdersService,
    { provide: PrismaService, useValue: mockPrisma },
  ],
}).compile();

Unit-test services without HTTP; use supertest against app.getHttpServer() for integration tests. E2E tests spin up the full AppModule against a disposable Postgres (Testcontainers). Run prisma migrate deploy in CI before the test suite. Nest’s DI makes swapping real payment clients for fakes straightforward — a major reason teams choose it over unstructured Express codebases.

Worked example: Harbor Supply order service

Harbor Supply splits its monolithic Next.js API into a dedicated Nest service for order fulfillment. Module layout:

  • AppModule — imports ConfigModule, PrismaModule, OrdersModule, HealthModule.
  • OrdersModuleOrdersController, OrdersService, InventoryService.
  • AuthModule — JWT strategy validating B2B partner tokens; RolesGuard restricts admin routes.

POST /api/v1/orders flow:

  1. JwtAuthGuard validates bearer token; attaches partnerId to request.
  2. ValidationPipe maps body to CreateOrderDto; rejects malformed line items.
  3. OrdersService.create runs a Prisma $transaction: lock SKUs, verify stock, insert order with price snapshots, decrement inventory.
  4. LoggingInterceptor records latency; on success returns 201 with OrderResponseDto (no internal cost fields).
  5. On duplicate Idempotency-Key header, service returns the existing order instead of double-charging — see idempotency patterns.

Health checks expose GET /health via @nestjs/terminus probing Postgres and Redis. Deploy: Docker image, node dist/main.js behind nginx reverse proxy, two replicas with rolling updates. Structured JSON logs go to stdout for aggregation; correlation IDs pass through an interceptor from incoming X-Request-Id headers.

Framework decision table

Choose NestJS when… Prefer Fastify/Express raw when… Prefer Django/FastAPI when…
TypeScript team wants DI and modular structure Microservice is <5 routes and will stay tiny Team strength is Python, not JavaScript
Multiple engineers need consistent patterns Maximum middleware ecosystem compatibility matters Django admin or Python ML stack is central
OpenAPI, guards, and testing utilities out of the box You reject decorators and framework magic Async Python data science integrations dominate
Sharing types with a React/Next front end Prototype speed beats long-term structure ASGI async I/O with minimal ceremony (FastAPI)
Enterprise audit requirements for layered architecture Edge worker bundle size must be minimal ORM and migrations owned by Django ecosystem

Common pitfalls

  • God modules — everything imported in AppModule; split by domain early.
  • Fat controllers — SQL and business rules in route handlers; move to services.
  • Request-scoped providers everywhere — performance hit; default to singleton scope.
  • Missing global ValidationPipe — unvalidated bodies reach services.
  • Swagger exposed in production — leaks schema and aids attackers.
  • New PrismaClient per request — connection exhaustion; use one injectable service.
  • Blocking the event loop — heavy sync crypto or JSON in handlers; offload to workers.
  • Ignoring shutdown hooks — in-flight orders truncated on deploy.

Production checklist

  • Global ValidationPipe with whitelist and transform enabled.
  • Config validated at bootstrap; secrets from environment, not source code.
  • Structured logging (Pino via nestjs-pino) with request correlation IDs.
  • Health and readiness endpoints for load balancer and Kubernetes probes.
  • Global exception filter returning safe client errors; full stacks logged server-side only.
  • Rate limiting at reverse proxy or @nestjs/throttler for abuse protection.
  • CI: unit tests for services, integration tests for critical routes, nest build in pipeline.
  • Graceful shutdown enabled; SIGTERM drains connections before exit.
  • OpenAPI generated in CI and published to internal docs; Swagger UI disabled in prod.
  • Dependency audit: npm audit, pinned lockfile, regular Nest minor upgrades.

Key takeaways

  • NestJS adds structure and testability on top of Express or Fastify — not a replacement for Node fundamentals.
  • Modules, controllers, and providers define clear boundaries; keep controllers thin.
  • Pipes and guards enforce validation and auth before business logic runs.
  • Prisma + Nest is a common production stack; pool connections and use transactions for inventory-style writes.
  • Invest in testing modules early — DI pays off when mocks are trivial to wire.

Related reading