NestJS Reference: Modules, Controllers, Services, Guards, TypeORM & Testing
NestJS is a Node.js framework built on top of Express (or Fastify) with a strong opinion about structure. It uses TypeScript and decorators to enforce a module/controller/service architecture similar to Angular. The key concepts: Modules group related features, Controllers handle HTTP routing, Services contain business logic and are injected via Dependency Injection. NestJS is the dominant choice when teams want Angular-style structure for their API layer.
1. Modules, Controllers & Services
@Module, @Controller, @Injectable, and the basic request lifecycle
// npm install @nestjs/core @nestjs/common reflect-metadata rxjs
// npm install -g @nestjs/cli
// nest new my-api
// users.module.ts — groups all user-related components:
import { Module } from "@nestjs/common";
import { UsersController } from "./users.controller";
import { UsersService } from "./users.service";
@Module({
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService], // expose to other modules that import UsersModule
})
export class UsersModule {}
// users.controller.ts — handles HTTP routing:
import { Controller, Get, Post, Body, Param, Delete, HttpCode, HttpStatus } from "@nestjs/common";
import { UsersService } from "./users.service";
import { CreateUserDto } from "./dto/create-user.dto";
@Controller("users") // route 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()
@HttpCode(HttpStatus.CREATED)
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
@Delete(":id")
@HttpCode(HttpStatus.NO_CONTENT)
remove(@Param("id") id: string) {
return this.usersService.remove(+id);
}
}
// users.service.ts — business logic:
import { Injectable, NotFoundException } from "@nestjs/common";
@Injectable()
export class UsersService {
private users = [];
findAll() { return this.users; }
findOne(id: number) {
const user = this.users.find(u => u.id === id);
if (!user) throw new NotFoundException(`User #${id} not found`);
return user;
}
create(dto: CreateUserDto) { /* ... */ }
remove(id: number) { /* ... */ }
}
2. Pipes, DTOs & Validation
class-validator DTOs, ValidationPipe, ParseIntPipe, and transformation
// npm install class-validator class-transformer
// create-user.dto.ts — defines and validates request shape:
import { IsEmail, IsString, MinLength, IsOptional, IsEnum } from "class-validator";
import { Transform } from "class-transformer";
export class CreateUserDto {
@IsString()
@MinLength(2)
name: string;
@IsEmail()
@Transform(({ value }) => value.toLowerCase()) // normalize on intake
email: string;
@IsOptional()
@IsEnum(["admin", "user", "moderator"])
role?: string;
}
// Enable globally in main.ts:
import { ValidationPipe } from "@nestjs/common";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // strip unknown properties (never trust client)
forbidNonWhitelisted: true, // throw 400 if unknown fields present
transform: true, // auto-transform payload to DTO class instances
transformOptions: { enableImplicitConversion: true },
}));
await app.listen(3000);
}
// ParseIntPipe — transform route param to number:
@Get(":id")
findOne(@Param("id", ParseIntPipe) id: number) {
return this.usersService.findOne(id);
}
// Other built-in pipes:
// ParseUUIDPipe — validates UUID format
// ParseBoolPipe — "true"/"false" strings → boolean
// DefaultValuePipe — provide fallback for optional params
// Partial update DTO — all fields optional:
import { PartialType } from "@nestjs/mapped-types";
export class UpdateUserDto extends PartialType(CreateUserDto) {}
3. Guards, Interceptors & Middleware
JwtAuthGuard, RolesGuard, response transformation interceptors, and logging middleware
// Guard — controls access (runs before route handler):
import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(private jwtService: JwtService) {}
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const token = request.headers.authorization?.split(" ")[1];
if (!token) return false;
try {
request.user = this.jwtService.verify(token, { secret: process.env.JWT_SECRET });
return true;
} catch { return false; }
}
}
// Apply guard to route or controller:
@UseGuards(JwtAuthGuard)
@Get("profile")
getProfile(@Request() req) { return req.user; }
// Custom decorator — extract current user:
export const CurrentUser = createParamDecorator(
(_, ctx: ExecutionContext) => ctx.switchToHttp().getRequest().user
);
@Get("profile")
getProfile(@CurrentUser() user: User) { return user; }
// Interceptor — transform response, add logging:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from "@nestjs/common";
import { Observable } from "rxjs";
import { map, tap } from "rxjs/operators";
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map(data => ({ data, timestamp: new Date().toISOString() }))
);
}
}
// Middleware — runs before guards (like Express middleware):
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log(`${req.method} ${req.url}`);
next();
}
}
// Apply in module:
configure(consumer: MiddlewareConsumer) {
consumer.apply(LoggerMiddleware).forRoutes("*");
}
4. Database with TypeORM or Prisma
@nestjs/typeorm entity setup, repository pattern, and @nestjs/config for env vars
// npm install @nestjs/typeorm typeorm pg @nestjs/config
// app.module.ts — wire up database + config:
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { ConfigModule, ConfigService } from "@nestjs/config";
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }), // process.env accessible everywhere
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
type: "postgres",
host: config.get("DB_HOST"),
port: config.get<number>("DB_PORT"),
username: config.get("DB_USER"),
password: config.get("DB_PASS"),
database: config.get("DB_NAME"),
entities: [__dirname + "/**/*.entity{.ts,.js}"],
synchronize: config.get("NODE_ENV") !== "production", // NEVER in prod
autoLoadEntities: true,
}),
}),
],
})
export class AppModule {}
// user.entity.ts:
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from "typeorm";
@Entity("users")
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
email: string;
@Column()
name: string;
@Column({ default: "user" })
role: string;
@CreateDateColumn()
createdAt: Date;
}
// users.module.ts — register entity:
@Module({ imports: [TypeOrmModule.forFeature([User])], ... })
export class UsersModule {}
// users.service.ts — use Repository:
@Injectable()
export class UsersService {
constructor(@InjectRepository(User) private repo: Repository<User>) {}
findAll() { return this.repo.find(); }
findOne(id: number) { return this.repo.findOneBy({ id }); }
create(dto: CreateUserDto) { return this.repo.save(this.repo.create(dto)); }
async update(id: number, dto: UpdateUserDto) {
await this.repo.update(id, dto);
return this.findOne(id);
}
remove(id: number) { return this.repo.delete(id); }
}
5. Exception Filters, Testing & CLI
Custom exception filters, unit testing services, e2e testing, and nest CLI commands
// Global exception filter — consistent error response shape:
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();
const message = exception.getResponse();
response.status(status).json({
statusCode: status,
message: typeof message === "string" ? message : (message as any).message,
timestamp: new Date().toISOString(),
});
}
}
// main.ts: app.useGlobalFilters(new HttpExceptionFilter());
// Unit test a service (no HTTP layer):
describe("UsersService", () => {
let service: UsersService;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
UsersService,
{ provide: getRepositoryToken(User), useValue: {
find: jest.fn(), findOneBy: jest.fn(), save: jest.fn(), delete: jest.fn(),
}},
],
}).compile();
service = module.get<UsersService>(UsersService);
});
it("should throw NotFoundException for missing user", async () => {
jest.spyOn(service, "findOne").mockResolvedValue(null);
await expect(service.findOne(999)).rejects.toThrow(NotFoundException);
});
});
// nest CLI:
nest new my-api # scaffold project
nest g module users # generate module
nest g controller users --no-spec # controller without spec file
nest g service users # injectable service
nest g guard auth/jwt-auth # guard
nest g filter http-exception # exception filter
nest g pipe validation # custom pipe
nest build # compile to dist/
nest start --watch # dev mode (ts-node-dev)
Track Node.js and framework releases at ReleaseRun. Related: Express Reference | FastAPI Reference | TypeScript Reference | Nodejs EOL Tracker
🔍 Free tool: npm Package Health Checker — check NestJS packages — @nestjs/core, @nestjs/typeorm, passport — for known CVEs and active maintenance.
Founded
2023 in London, UK
Contact
hello@releaserun.com