Skip to main content

NestJS

A progressive Node.js framework for building efficient, scalable server-side applications, built with and fully supporting TypeScript. Heavily inspired by Angular's architecture (modules, decorators, dependency injection).

Why NestJS?​

  • Brings structure and convention to Node.js backends (unlike the "bring your own architecture" feel of Express).
  • TypeScript-first, though plain JavaScript is supported.
  • Built on top of Express by default (can swap to Fastify for better performance).
  • Encourages SOLID principles through modules, dependency injection, and separation of concerns.
  • Great for teams coming from Angular, Spring, or .NET — the mental model (DI, decorators, modules) will feel familiar.

Core Building Blocks​

1. Modules​

Every Nest app has at least one root module (AppModule). Modules organize the app into cohesive blocks of functionality.

import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
imports: [], // other modules this module depends on
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService], // makes UsersService available to modules that import this one
})
export class UsersModule {}

Key idea: modules are singletons by default — imported once, shared everywhere it's imported (unless registered as scoped, see below).

2. Controllers​

Handle incoming requests and return responses. Route handling lives here — logic should not.

import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';

@Controller('users') // prefix: /users
export class UsersController {
constructor(private readonly usersService: UsersService) {}

@Get()
findAll() {
return this.usersService.findAll();
}

@Get(':id')
findOne(@Param('id') id: string) {
return this.usersService.findOne(id);
}

@Post()
create(@Body() dto: CreateUserDto) {
return this.usersService.create(dto);
}
}

3. Providers / Services​

Where business logic lives. Anything decorated with @Injectable() can be injected via Nest's DI container.

import { Injectable, NotFoundException } from '@nestjs/common';

@Injectable()
export class UsersService {
private users = [];

findAll() {
return this.users;
}

findOne(id: string) {
const user = this.users.find((u) => u.id === id);
if (!user) throw new NotFoundException(`User ${id} not found`);
return user;
}

create(dto: CreateUserDto) {
const user = { id: crypto.randomUUID(), ...dto };
this.users.push(user);
return user;
}
}

Dependency Injection (DI)​

Nest's DI container resolves providers automatically based on constructor parameter types.

constructor(private readonly usersService: UsersService) {}
  • providers array in a module tells Nest "this class can be injected."
  • exports makes a provider available to modules that import this module.
  • Custom providers can use useValue, useClass, useFactory, or useExisting for more control:
{
provide: 'CONFIG_OPTIONS',
useFactory: () => ({ apiKey: process.env.API_KEY }),
}

Provider Scopes​

ScopeBehavior
DEFAULTSingleton, shared across the whole app (default, most performant)
REQUESTNew instance per incoming request
TRANSIENTNew instance every time it's injected
@Injectable({ scope: Scope.REQUEST })
export class RequestScopedService {}

⚠️ Request-scoped providers bubble up — anything injecting them also becomes request-scoped, which can hurt performance if overused.

DTOs & Validation​

DTOs (Data Transfer Objects) define the shape of incoming data. Pair with class-validator and class-transformer for automatic validation.

npm install class-validator class-transformer
import { IsEmail, IsString, MinLength } from 'class-validator';

export class CreateUserDto {
@IsString()
@MinLength(2)
name: string;

@IsEmail()
email: string;
}

Enable globally in main.ts:

import { ValidationPipe } from '@nestjs/common';

app.useGlobalPipes(new ValidationPipe({
whitelist: true, // strips properties not in the DTO
forbidNonWhitelisted: true, // throws if extra properties are sent
transform: true, // auto-transforms payloads into DTO instances
}));

Pipes​

Transform or validate input data before it hits the route handler.

  • Built-in: ValidationPipe, ParseIntPipe, ParseUUIDPipe, DefaultValuePipe.
  • Can be applied at parameter, handler, controller, or global level.
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.usersService.findOne(id);
}

Guards​

Handle authorization — decide whether a request should proceed. Run before route handlers.

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';

@Injectable()
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
return !!request.headers.authorization;
}
}
@UseGuards(AuthGuard)
@Get('profile')
getProfile() { ... }

Common real-world pattern: JwtAuthGuard + RolesGuard combo, with a custom @Roles() decorator and metadata via Reflector.

Interceptors​

Wrap around request/response handling — great for logging, response transformation, caching, timeout handling. Powered by RxJS.

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const now = Date.now();
return next.handle().pipe(
tap(() => console.log(`Request took ${Date.now() - now}ms`)),
);
}
}

Exception Filters​

Centralize error handling and shape error responses consistently.

import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const status = exception.getStatus();

response.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
message: exception.message,
});
}
}

Request Lifecycle (order matters!)​

Incoming Request
→ Middleware
→ Guards
→ Interceptors (pre-controller)
→ Pipes
→ Route Handler (Controller)
→ Interceptors (post-controller)
→ Exception Filters (if error thrown)
→ Response

This ordering trips people up a lot — e.g. Guards run before Pipes, so you can't rely on a validated/transformed DTO inside a Guard.

Middleware​

Closer to raw Express middleware — runs before guards, has access to req/res/next. No access to the ExecutionContext.

import { Injectable, NestMiddleware } from '@nestjs/common';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log(`${req.method} ${req.originalUrl}`);
next();
}
}

Applied in the module via configure():

export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LoggerMiddleware).forRoutes('*');
}
}

Custom Decorators​

Useful for pulling data out of the request cleanly.

import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const CurrentUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},
);
@Get('me')
getMe(@CurrentUser() user: User) {
return user;
}

Database Integration​

Most common combos:

  • TypeORM (@nestjs/typeorm) — mature, decorator-based, works well with Nest's DI.
  • Prisma — great DX and type safety, but integrates as a service rather than natively like TypeORM.
  • Mongoose (@nestjs/mongoose) — for MongoDB.

Example with TypeORM:

@Entity()
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column()
email: string;
}
@Module({
imports: [TypeOrmModule.forFeature([User])],
...
})
export class UsersModule {}
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
) {}

Configuration​

Use @nestjs/config for environment variables with validation.

npm install @nestjs/config
ConfigModule.forRoot({
isGlobal: true,
envFilePath: '.env',
validationSchema: Joi.object({
DATABASE_URL: Joi.string().required(),
PORT: Joi.number().default(3000),
}),
});

Testing​

Nest ships with Jest by default and a testing utility module for building isolated test modules.

import { Test } from '@nestjs/testing';

describe('UsersService', () => {
let service: UsersService;

beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [UsersService],
}).compile();

service = module.get(UsersService);
});

it('should create a user', () => {
const user = service.create({ name: 'Alice', email: 'a@b.com' });
expect(user).toHaveProperty('id');
});
});
  • Unit tests: mock dependencies with useValue in the test module's providers.
  • e2e tests: use supertest against a full compiled Nest app instance (app.getHttpServer()).

Microservices & Other Transports​

Nest isn't just HTTP — it has first-class support for:

  • Microservices: TCP, Redis, NATS, Kafka, RabbitMQ, gRPC (@nestjs/microservices).
  • WebSockets: @nestjs/websockets with Gateways (@WebSocketGateway()).
  • GraphQL: @nestjs/graphql, supports both code-first (decorators) and schema-first approaches.
  • CRON / scheduled tasks: @nestjs/schedule with @Cron(), @Interval(), @Timeout().

CLI Cheatsheet​

# install CLI
npm i -g @nestjs/cli

# new project
nest new project-name

# generate resources (CRUD scaffold w/ controller, service, module, DTOs)
nest g resource users

# generate individual pieces
nest g module orders
nest g controller orders
nest g service orders
nest g guard auth
nest g interceptor logging
nest g pipe validation
nest g filter http-exception

Project Structure (typical)​

src/
├── main.ts # entry point, bootstraps the app
├── app.module.ts # root module
├── common/ # shared guards, pipes, interceptors, decorators
├── config/ # configuration module & env validation
└── modules/
└── users/
├── users.module.ts
├── users.controller.ts
├── users.service.ts
├── dto/
│ ├── create-user.dto.ts
│ └── update-user.dto.ts
└── entities/
└── user.entity.ts

Gotchas / Things I Learned the Hard Way​

  • Circular dependencies between modules/services: use forwardRef(() => OtherModule) — but treat frequent circular deps as a sign of a design smell, not just a bug to patch around.

  • Global pipes/guards/interceptors set in main.ts (app.useGlobalPipes(...)) won't have DI access to other providers the same way module-level ones do — if you need DI inside a global pipe/guard, register it as a provider in AppModule using the APP_PIPE / APP_GUARD / APP_INTERCEPTOR tokens instead:

    providers: [
    { provide: APP_GUARD, useClass: AuthGuard },
    ]
  • Request-scoped providers hurt performance if injected widely — a new instance gets created per request for the whole injection chain.

  • exports vs providers: forgetting to export a provider from its module is the #1 cause of "Nest can't resolve dependencies" errors when another module tries to use it.

  • Default HTTP adapter is Express; swap to Fastify (@nestjs/platform-fastify) for meaningfully better raw throughput, but double check third-party middleware compatibility first.

  • class-transformer's @Exclude() / @Expose() + a global ClassSerializerInterceptor is the clean way to strip sensitive fields (like password) from responses instead of manually deleting keys.