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:
- Middleware — Express-style functions for logging, CORS, or raw body parsing (registered in module
configure()). - Guards — authorization checks returning
trueor throwingForbiddenException(JWT validation, role checks). - Interceptors — wrap execution: transform responses, add timing headers, cache, or map entities to DTOs.
- Pipes — validate and transform inputs (
ValidationPipeturns plain JSON into class instances and rejects invalid bodies with 400). - Controller handler — your business logic, usually delegating to injected services.
- 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-validatordecorators 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 modules —
DatabaseModule,LoggerModuleexported globally with@Global()sparingly (overuse hides dependencies). - Dynamic modules —
ConfigModule.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.OrdersModule—OrdersController,OrdersService,InventoryService.AuthModule— JWT strategy validating B2B partner tokens;RolesGuardrestricts admin routes.
POST /api/v1/orders flow:
JwtAuthGuardvalidates bearer token; attachespartnerIdto request.ValidationPipemaps body toCreateOrderDto; rejects malformed line items.OrdersService.createruns a Prisma$transaction: lock SKUs, verify stock, insert order with price snapshots, decrement inventory.LoggingInterceptorrecords latency; on success returns201withOrderResponseDto(no internal cost fields).- On duplicate
Idempotency-Keyheader, 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
ValidationPipewith 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/throttlerfor abuse protection. - CI: unit tests for services, integration tests for critical routes,
nest buildin pipeline. - Graceful shutdown enabled;
SIGTERMdrains 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
- Node.js fundamentals explained — runtime, event loop, and async I/O under Nest
- TypeScript fundamentals explained — types, decorators, and modules Nest builds on
- Prisma fundamentals explained — schema, Client, and migrations for Nest services
- REST API design explained — resource naming, status codes, and error shapes