NestJS Core Concepts & Dependency Injection

NestJS là một progressive Node.js framework để build các server-side applications hiệu quả và scalable. Nó dùng TypeScript làm mặc định, embrace decorators, và ship kèm một DI container mạnh mẽ — tất cả được lấy cảm hứng từ kiến trúc của Angular.
Nếu bạn đã dùng Express.js, bạn đã biết HTTP engine phía dưới NestJS. Nếu bạn đã dùng Angular, NestJS sẽ cảm thấy quen thuộc ngay lập tức. Và nếu bạn đã đọc về Dependency Injection & IoC, bạn sẽ thấy những patterns đó được áp dụng trong thực tế xuyên suốt mọi NestJS application.
Trong bài này, chúng ta sẽ deep-dive vào các core concepts làm NestJS hoạt động — không chỉ làm thế nào để dùng chúng, mà còn tại sao chúng tồn tại và chúng khớp với nhau thế nào.
Bạn Sẽ Học Được Gì
Learning Outcomes:
✅ Hiểu Angular-NestJS connection (decorators, modules, DI)
✅ Master ba building blocks: Modules, Controllers, và Providers
✅ Deep dive vào DI container của NestJS và custom providers
✅ Trace full request lifecycle (Middleware → Guards → Interceptors → Pipes → Controller → Filters)
✅ Implement lifecycle hooks (OnModuleInit, OnModuleDestroy, v.v.)
✅ Test NestJS applications với @nestjs/testing và mocked providers
✅ Build một REST API hoàn chỉnh minh họa tất cả concepts cùng nhau
Prerequisites
Trước khi bắt đầu:
TypeScript fundamentals: Phase 1: TypeScript Fundamentals
DI theory: Dependency Injection & IoC Explained
Express basics: Getting Started with Express.js
NestJS và Angular: Sự Kết Nối
NestJS được model rõ ràng theo Angular. Nếu bạn là Angular developer chuyển sang backend, bảng này sẽ trông rất quen:
| Angular | NestJS | Mục đích |
|---|---|---|
@Component() | @Controller() | Xử lý incoming requests / render views |
@Injectable() | @Injectable() | Đánh dấu class là DI-injectable |
@NgModule() | @Module() | Tổ chức cấu trúc application |
| Services | Providers | Business logic containers |
| Pipes | Pipes | Data transformation và validation |
| Guards | Guards | Authorization / access control |
| Interceptors | Interceptors | Cross-cutting concerns (logging, caching) |
| Middleware | Middleware | Pre-route request processing |
DNA kiến trúc giống nhau:
Cài Đặt NestJS Project
Cài CLI
npm install -g @nestjs/cliTạo Project Mới
nest new my-nestjs-app
cd my-nestjs-appCLI sinh ra cấu trúc project sạch:
src/
├── app.controller.ts # Root controller
├── app.controller.spec.ts # Controller tests
├── app.module.ts # Root module
├── app.service.ts # Root service
└── main.ts # Application entry pointEntry Point: main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();NestFactory.create() nhận root module, resolve tất cả dependencies, wire up DI container, và start HTTP server (Express mặc định, hoặc Fastify nếu cấu hình).
Chạy Development Server
npm run start:devTruy cập http://localhost:3000 — bạn sẽ thấy "Hello World!" được serve bởi default controller.
Core Building Blocks
Mọi NestJS application được xây từ ba pieces cơ bản: Modules, Controllers, và Providers.
Modules
Module là class được decorate với @Module(). Nó tổ chức related controllers và providers thành một unit gắn kết.
import { Module } from '@nestjs/common';
import { TasksController } from './tasks.controller';
import { TasksService } from './tasks.service';
@Module({
controllers: [TasksController],
providers: [TasksService],
exports: [TasksService], // cho phép module khác dùng
})
export class TasksModule {}Decorator @Module() nhận object với bốn properties:
| Property | Mục đích |
|---|---|
imports | Các modules khác mà module này cần providers được export từ đó |
controllers | Controllers xử lý incoming requests |
providers | Services và các injectable classes khác |
exports | Providers cần có sẵn cho các modules import |
Root module (AppModule) import tất cả feature modules:
import { Module } from '@nestjs/common';
import { TasksModule } from './tasks/tasks.module';
import { UsersModule } from './users/users.module';
@Module({
imports: [TasksModule, UsersModule],
})
export class AppModule {}Controllers
Controllers xử lý incoming HTTP requests và trả về responses. Chúng được decorate với @Controller() và dùng method decorators cho specific routes.
import {
Controller, Get, Post, Put, Delete,
Param, Body, HttpCode, HttpStatus,
} from '@nestjs/common';
import { TasksService } from './tasks.service';
import { CreateTaskDto } from './dto/create-task.dto';
import { UpdateTaskDto } from './dto/update-task.dto';
@Controller('tasks') // route prefix: /tasks
export class TasksController {
constructor(private readonly tasksService: TasksService) {}
@Get()
findAll() {
return this.tasksService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.tasksService.findOne(id);
}
@Post()
@HttpCode(HttpStatus.CREATED)
create(@Body() createTaskDto: CreateTaskDto) {
return this.tasksService.create(createTaskDto);
}
@Put(':id')
update(@Param('id') id: string, @Body() updateTaskDto: UpdateTaskDto) {
return this.tasksService.update(id, updateTaskDto);
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
remove(@Param('id') id: string) {
return this.tasksService.remove(id);
}
}Chú ý constructor: private readonly tasksService: TasksService. Đây là constructor injection — DI container tự động tạo và inject một TasksService instance. Nếu bạn đã làm việc với @Autowired của Spring Boot, đây là cùng pattern với TypeScript syntax.
Các parameter decorators thường dùng:
| Decorator | Express Equivalent | Mục đích |
|---|---|---|
@Param(key) | req.params[key] | Route parameters |
@Body(key) | req.body[key] | Request body |
@Query(key) | req.query[key] | Query parameters |
@Headers(key) | req.headers[key] | Request headers |
@Req() | req | Full request object |
@Res() | res | Full response object |
Providers (Services)
Providers là sức mạnh của NestJS. Bất kỳ class nào được decorate với @Injectable() đều có thể được quản lý bởi DI container.
Loại provider phổ biến nhất là service — class đóng gói business logic:
import { Injectable, NotFoundException } from '@nestjs/common';
import { CreateTaskDto } from './dto/create-task.dto';
import { UpdateTaskDto } from './dto/update-task.dto';
export interface Task {
id: string;
title: string;
description: string;
completed: boolean;
}
@Injectable()
export class TasksService {
private tasks: Task[] = [];
findAll(): Task[] {
return this.tasks;
}
findOne(id: string): Task {
const task = this.tasks.find((t) => t.id === id);
if (!task) {
throw new NotFoundException(`Task với ID "${id}" không tìm thấy`);
}
return task;
}
create(createTaskDto: CreateTaskDto): Task {
const task: Task = {
id: Date.now().toString(),
...createTaskDto,
completed: false,
};
this.tasks.push(task);
return task;
}
update(id: string, updateTaskDto: UpdateTaskDto): Task {
const task = this.findOne(id);
Object.assign(task, updateTaskDto);
return task;
}
remove(id: string): void {
const index = this.tasks.findIndex((t) => t.id === id);
if (index === -1) {
throw new NotFoundException(`Task với ID "${id}" không tìm thấy`);
}
this.tasks.splice(index, 1);
}
}Decorator @Injectable() nói với NestJS: "class này có thể tham gia vào DI system." Không có nó, container sẽ không quản lý class.
Dependency Injection Deep Dive
Nếu bạn đã đọc Dependency Injection & IoC Explained, bạn biết theory — Martin Fowler's assembler, constructor injection, và Hollywood Principle. Bây giờ hãy xem NestJS implement nó như thế nào.
DI Container Hoạt Động Thế Nào
Khi NestJS bootstrap application với NestFactory.create(AppModule), đây là những gì xảy ra bên dưới:
Container dùng TypeScript's metadata reflection để discover những gì mỗi class cần. Khi bạn viết:
@Controller('tasks')
export class TasksController {
constructor(private readonly tasksService: TasksService) {}
}TypeScript (với emitDecoratorMetadata: true trong tsconfig.json) emit metadata về constructor parameter types. Container đọc metadata này và biết: "TasksController cần một TasksService — để tôi tìm hoặc tạo một cái."
Đây là lý do tại sao @Injectable() cần thiết trên providers: nó trigger TypeScript emit metadata khiến automatic injection có thể thực hiện được.
Custom Providers
Cách chuẩn để register một provider là shorthand:
@Module({
providers: [TasksService], // shorthand cho { provide: TasksService, useClass: TasksService }
})Nhưng NestJS hỗ trợ bốn loại custom providers để kiểm soát chi tiết hơn:
1. Class Providers (useClass)
Thay thế một implementation bằng implementation khác:
@Module({
providers: [
{
provide: TasksService,
useClass:
process.env.NODE_ENV === 'test'
? MockTasksService
: TasksService,
},
],
})
export class TasksModule {}2. Value Providers (useValue)
Inject constants, configuration objects, hoặc mock implementations:
@Module({
providers: [
{
provide: 'API_KEY',
useValue: process.env.API_KEY,
},
{
provide: 'CONFIG',
useValue: {
database: 'postgres',
port: 5432,
},
},
],
})
export class AppModule {}Inject dùng @Inject() với token:
@Injectable()
export class ApiService {
constructor(@Inject('API_KEY') private readonly apiKey: string) {}
}3. Factory Providers (useFactory)
Tạo providers dynamically với access đến các injected dependencies khác:
@Module({
providers: [
ConfigService,
{
provide: 'DATABASE_CONNECTION',
useFactory: async (configService: ConfigService) => {
const options = configService.getDatabaseConfig();
return createConnection(options);
},
inject: [ConfigService], // dependencies cho factory
},
],
})
export class DatabaseModule {}4. Alias Providers (useExisting)
Tạo alias trỏ đến existing provider:
@Module({
providers: [
TasksService,
{
provide: 'AliasedTasksService',
useExisting: TasksService, // cùng instance như TasksService
},
],
})
export class TasksModule {}Quick reference:
| Type | Dùng khi | Ví dụ |
|---|---|---|
useClass | Swap implementations (testing, feature flags) | MockService trong test, RealService trong prod |
useValue | Inject constants hoặc config objects | API keys, feature flags, mock objects |
useFactory | Dynamic creation với async logic | Database connections, conditional setup |
useExisting | Alias một provider sang provider khác | Backward compatibility, interface tokens |
Injection Scopes
Mặc định, NestJS providers là singletons — một instance được chia sẻ xuyên suốt application. Nhưng bạn có thể thay đổi điều này:
import { Injectable, Scope } from '@nestjs/common';
@Injectable({ scope: Scope.REQUEST })
export class RequestScopedService {
// Instance mới được tạo cho mỗi incoming request
}
@Injectable({ scope: Scope.TRANSIENT })
export class TransientService {
// Instance mới cho mỗi consumer inject nó
}| Scope | Behavior | Use Case |
|---|---|---|
DEFAULT | Single instance chia sẻ toàn app | Hầu hết services, stateless logic |
REQUEST | Instance mới cho mỗi HTTP request | Request-specific data (user context, tenant) |
TRANSIENT | Instance mới cho mỗi injection | Stateful helpers, per-consumer counters |
Performance note: Scope REQUEST và TRANSIENT disable một số optimizations và lan truyền qua injection chain. Dùng chúng tiết kiệm.
Request Lifecycle
Mọi HTTP request trong NestJS đi qua một pipeline được định nghĩa rõ ràng. Hiểu pipeline này rất quan trọng để biết đặt logic ở đâu.
Middleware
Middleware chạy đầu tiên, trước bất kỳ NestJS-specific logic nào. Nó có access đến raw request và response objects — giống như Express middleware.
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log(`[${req.method}] ${req.url}`);
next();
}
}Register middleware trong module:
import { Module, MiddlewareConsumer, NestModule } from '@nestjs/common';
@Module({ /* ... */ })
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes('*');
}
}Guards
Guards quyết định request có nên tiếp tục không. Chúng trả về true (cho phép) hoặc false (từ chối). Hoàn hảo cho authentication và authorization.
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const token = request.headers.authorization;
return this.validateToken(token);
}
private validateToken(token: string): boolean {
// validate JWT hoặc API key
return !!token;
}
}Áp dụng với @UseGuards():
@Controller('tasks')
@UseGuards(AuthGuard)
export class TasksController { /* ... */ }Interceptors
Interceptors wrap route handler, cho bạn kiểm soát cả request (trước) và response (sau). Chúng dùng RxJS observables.
import {
Injectable, NestInterceptor,
ExecutionContext, CallHandler,
} from '@nestjs/common';
import { Observable, tap } from 'rxjs';
@Injectable()
export class TimingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const start = Date.now();
return next
.handle()
.pipe(
tap(() => {
const duration = Date.now() - start;
console.log(`Request mất ${duration}ms`);
}),
);
}
}Pipes
Pipes transform hoặc validate incoming data trước khi nó đến route handler. NestJS ship kèm một số built-in pipes:
import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common';
@Controller('tasks')
export class TasksController {
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
// id được đảm bảo là number
return this.tasksService.findOne(id);
}
}Để validate request body, dùng ValidationPipe với class-validator:
import { IsString, IsNotEmpty, IsOptional, IsBoolean } from 'class-validator';
export class CreateTaskDto {
@IsString()
@IsNotEmpty()
title: string;
@IsString()
@IsOptional()
description?: string;
}
export class UpdateTaskDto {
@IsString()
@IsOptional()
title?: string;
@IsBoolean()
@IsOptional()
completed?: boolean;
}Enable globally trong main.ts:
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // bỏ các properties không biết
forbidNonWhitelisted: true, // throw khi có unknown properties
transform: true, // auto-transform sang DTO types
}));
await app.listen(3000);
}Exception Filters
Exception filters catch errors được throw trong request processing và transform chúng thành user-friendly responses.
import {
ExceptionFilter, Catch,
ArgumentsHost, HttpException,
} from '@nestjs/common';
import { Response } from 'express';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const status = exception.getStatus();
response.status(status).json({
statusCode: status,
message: exception.message,
timestamp: new Date().toISOString(),
});
}
}Nơi đặt từng loại logic:
| Layer | Mục đích | Ví dụ |
|---|---|---|
| Middleware | Raw request processing, logging | Request logger, CORS, body parser |
| Guards | Access control (quyết định yes/no) | Auth check, role-based access |
| Interceptors | Transform request/response, timing | Caching, response mapping, timing |
| Pipes | Validate/transform input data | DTO validation, type coercion |
| Exception Filters | Format error responses | Custom error format, error logging |
Lifecycle Hooks
NestJS providers có thể implement lifecycle hooks — các methods được gọi tại các thời điểm cụ thể trong quá trình startup và shutdown của application.
| Hook | Interface | Khi nào được gọi |
|---|---|---|
onModuleInit() | OnModuleInit | Sau khi module's dependencies được resolve |
onApplicationBootstrap() | OnApplicationBootstrap | Sau khi tất cả modules initialized, trước khi listen |
onModuleDestroy() | OnModuleDestroy | Khi module đang bị destroy (shutdown signal) |
beforeApplicationShutdown() | BeforeApplicationShutdown | Sau onModuleDestroy(), trước khi connections đóng |
onApplicationShutdown() | OnApplicationShutdown | Sau khi tất cả connections đóng |
Thứ Tự Thực Thi
Ví Dụ Thực Tế: Database Connection
import {
Injectable,
OnModuleInit,
OnModuleDestroy,
Logger,
} from '@nestjs/common';
@Injectable()
export class DatabaseService implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger(DatabaseService.name);
private connection: any;
async onModuleInit() {
this.logger.log('Đang kết nối đến database...');
this.connection = await this.createConnection();
this.logger.log('Database đã kết nối');
}
async onModuleDestroy() {
this.logger.log('Đang đóng database connection...');
await this.connection?.close();
this.logger.log('Database connection đã đóng');
}
private async createConnection() {
return { close: async () => {} };
}
}Quan trọng: Để nhận shutdown signals (SIGTERM, SIGINT), bạn phải enable shutdown hooks:
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableShutdownHooks(); // cần thiết cho OnModuleDestroy và shutdown hooks
await app.listen(3000);
}Ghép Tất Cả Lại
Hãy build một Tasks API hoàn chỉnh minh họa modules, controllers, providers, DI, validation pipes, và lifecycle hooks hoạt động cùng nhau.
Cấu Trúc Project
src/
├── main.ts
├── app.module.ts
├── tasks/
│ ├── tasks.module.ts
│ ├── tasks.controller.ts
│ ├── tasks.service.ts
│ ├── dto/
│ │ ├── create-task.dto.ts
│ │ └── update-task.dto.ts
│ └── interfaces/
│ └── task.interface.tstask.interface.ts
export interface Task {
id: string;
title: string;
description: string;
completed: boolean;
createdAt: Date;
}DTOs với Validation
// create-task.dto.ts
import { IsString, IsNotEmpty, IsOptional } from 'class-validator';
export class CreateTaskDto {
@IsString()
@IsNotEmpty()
title: string;
@IsString()
@IsOptional()
description?: string;
}
// update-task.dto.ts
import { IsString, IsOptional, IsBoolean } from 'class-validator';
export class UpdateTaskDto {
@IsString()
@IsOptional()
title?: string;
@IsString()
@IsOptional()
description?: string;
@IsBoolean()
@IsOptional()
completed?: boolean;
}TasksService (Provider)
import { Injectable, NotFoundException, OnModuleInit, Logger } from '@nestjs/common';
import { Task } from './interfaces/task.interface';
import { CreateTaskDto } from './dto/create-task.dto';
import { UpdateTaskDto } from './dto/update-task.dto';
@Injectable()
export class TasksService implements OnModuleInit {
private readonly logger = new Logger(TasksService.name);
private tasks: Task[] = [];
onModuleInit() {
this.logger.log('TasksService initialized — sẵn sàng quản lý tasks');
}
findAll(): Task[] {
return this.tasks;
}
findOne(id: string): Task {
const task = this.tasks.find((t) => t.id === id);
if (!task) {
throw new NotFoundException(`Task "${id}" không tìm thấy`);
}
return task;
}
create(dto: CreateTaskDto): Task {
const task: Task = {
id: Date.now().toString(),
title: dto.title,
description: dto.description ?? '',
completed: false,
createdAt: new Date(),
};
this.tasks.push(task);
return task;
}
update(id: string, dto: UpdateTaskDto): Task {
const task = this.findOne(id);
Object.assign(task, dto);
return task;
}
remove(id: string): void {
const index = this.tasks.findIndex((t) => t.id === id);
if (index === -1) {
throw new NotFoundException(`Task "${id}" không tìm thấy`);
}
this.tasks.splice(index, 1);
}
}TasksController (với DI)
import {
Controller, Get, Post, Put, Delete,
Param, Body, HttpCode, HttpStatus,
} from '@nestjs/common';
import { TasksService } from './tasks.service';
import { CreateTaskDto } from './dto/create-task.dto';
import { UpdateTaskDto } from './dto/update-task.dto';
import { Task } from './interfaces/task.interface';
@Controller('tasks')
export class TasksController {
// DI: NestJS tự động inject TasksService
constructor(private readonly tasksService: TasksService) {}
@Get()
findAll(): Task[] {
return this.tasksService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string): Task {
return this.tasksService.findOne(id);
}
@Post()
@HttpCode(HttpStatus.CREATED)
create(@Body() dto: CreateTaskDto): Task {
return this.tasksService.create(dto);
}
@Put(':id')
update(@Param('id') id: string, @Body() dto: UpdateTaskDto): Task {
return this.tasksService.update(id, dto);
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
remove(@Param('id') id: string): void {
this.tasksService.remove(id);
}
}TasksModule (Wiring)
import { Module } from '@nestjs/common';
import { TasksController } from './tasks.controller';
import { TasksService } from './tasks.service';
@Module({
controllers: [TasksController],
providers: [TasksService],
exports: [TasksService],
})
export class TasksModule {}main.ts (Bootstrap)
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}));
app.enableShutdownHooks();
await app.listen(3000);
console.log('Application đang chạy tại http://localhost:3000');
}
bootstrap();Test với curl
# Tạo task
curl -X POST http://localhost:3000/tasks \
-H "Content-Type: application/json" \
-d '{"title": "Học NestJS", "description": "Master core concepts"}'
# Liệt kê tất cả tasks
curl http://localhost:3000/tasks
# Lấy một task cụ thể
curl http://localhost:3000/tasks/<task-id>
# Cập nhật task
curl -X PUT http://localhost:3000/tasks/<task-id> \
-H "Content-Type: application/json" \
-d '{"completed": true}'
# Xóa task
curl -X DELETE http://localhost:3000/tasks/<task-id>
# Validation error (thiếu title)
curl -X POST http://localhost:3000/tasks \
-H "Content-Type: application/json" \
-d '{"description": "không có title"}'
# → 400 Bad Request: "title should not be empty"NestJS vs Express vs Spring Boot
| Feature | Express | NestJS | Spring Boot |
|---|---|---|---|
| Architecture | Minimal, flexible | Opinionated, modular | Opinionated, modular |
| Language | JavaScript/TypeScript | TypeScript (first-class) | Java/Kotlin |
| DI | Manual hoặc third-party | Built-in container | Built-in container |
| Decorators | Không có | Phong phú | Annotations |
| Validation | Manual hoặc middleware | Built-in pipes + class-validator | Bean Validation (JSR-380) |
| ORM | Sequelize, Prisma, v.v. | TypeORM, Prisma, MikroORM | JPA/Hibernate |
| Testing | Mocha/Jest + manual DI | Jest + @nestjs/testing | JUnit + Spring Test |
| Learning Curve | Thấp | Trung bình | Trung bình-Cao |
| Performance | Cao | Tốt (Express bên dưới) | Tốt |
| Best For | Simple APIs, prototypes | Enterprise Node.js | Enterprise Java |
Express cho bạn tự do. NestJS cho bạn cấu trúc. Spring Boot cho bạn Java ecosystem. Chọn dựa trên sở thích ngôn ngữ của team và mức độ architectural guidance bạn muốn.
Testing NestJS Applications
Testing là nơi kiến trúc DI của NestJS thực sự được đền đáp. Package @nestjs/testing cho bạn spin up một lightweight testing module thay thế real providers bằng mocks — không cần server đang chạy.
Unit Testing Service
// tasks.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { TasksService } from './tasks.service';
import { NotFoundException } from '@nestjs/common';
describe('TasksService', () => {
let service: TasksService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [TasksService],
}).compile();
service = module.get<TasksService>(TasksService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
it('should create a task', () => {
const task = service.create({ title: 'Mua sữa', description: 'Sữa tươi' });
expect(task.id).toBeDefined();
expect(task.title).toBe('Mua sữa');
expect(task.completed).toBe(false);
});
it('should throw NotFoundException khi task không tồn tại', () => {
expect(() => service.findOne('id-không-tồn-tại')).toThrow(NotFoundException);
});
it('should delete a task', () => {
const task = service.create({ title: 'Mua sữa', description: '' });
service.remove(task.id);
expect(() => service.findOne(task.id)).toThrow(NotFoundException);
});
});Unit Testing Controller (với Mocked Service)
// tasks.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { TasksController } from './tasks.controller';
import { TasksService } from './tasks.service';
const mockTasksService = {
findAll: jest.fn(),
findOne: jest.fn(),
create: jest.fn(),
update: jest.fn(),
remove: jest.fn(),
};
describe('TasksController', () => {
let controller: TasksController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [TasksController],
providers: [
{
provide: TasksService,
useValue: mockTasksService, // Inject mock thay vì real service
},
],
}).compile();
controller = module.get<TasksController>(TasksController);
});
it('should gọi findAll khi GET /', async () => {
const tasks = [{ id: '1', title: 'Test', completed: false }];
mockTasksService.findAll.mockReturnValue(tasks);
const result = controller.findAll();
expect(result).toEqual(tasks);
expect(mockTasksService.findAll).toHaveBeenCalledTimes(1);
});
it('should gọi create với đúng DTO khi POST /', () => {
const dto = { title: 'Task mới', description: 'chi tiết' };
const created = { id: '2', ...dto, completed: false };
mockTasksService.create.mockReturnValue(created);
const result = controller.create(dto);
expect(result).toEqual(created);
expect(mockTasksService.create).toHaveBeenCalledWith(dto);
});
});Integration Testing với Supertest
Để end-to-end tests spin up full HTTP layer:
// tasks.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
describe('Tasks API (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
app.useGlobalPipes(new ValidationPipe());
await app.init();
});
afterAll(async () => {
await app.close();
});
it('POST /tasks — tạo task', () => {
return request(app.getHttpServer())
.post('/tasks')
.send({ title: 'Integration test task', description: 'Testing full stack' })
.expect(201)
.expect((res) => {
expect(res.body.id).toBeDefined();
expect(res.body.title).toBe('Integration test task');
expect(res.body.completed).toBe(false);
});
});
it('POST /tasks — trả 400 khi thiếu title', () => {
return request(app.getHttpServer())
.post('/tasks')
.send({ description: 'Không có title' })
.expect(400);
});
it('GET /tasks — trả array of tasks', () => {
return request(app.getHttpServer())
.get('/tasks')
.expect(200)
.expect((res) => {
expect(Array.isArray(res.body)).toBe(true);
});
});
it('GET /tasks/:id — trả 404 cho id không biết', () => {
return request(app.getHttpServer())
.get('/tasks/không-tồn-tại')
.expect(404);
});
});Testing Guards và Interceptors
// auth.guard.spec.ts
describe('AuthGuard', () => {
let guard: AuthGuard;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [AuthGuard],
}).compile();
guard = module.get<AuthGuard>(AuthGuard);
});
const createMockContext = (token?: string): ExecutionContext => ({
switchToHttp: () => ({
getRequest: () => ({
headers: { authorization: token },
}),
}),
} as unknown as ExecutionContext);
it('should cho phép request với valid token', () => {
expect(guard.canActivate(createMockContext('Bearer valid-token'))).toBe(true);
});
it('should từ chối request không có token', () => {
expect(guard.canActivate(createMockContext())).toBe(false);
});
});Testing philosophy trong NestJS: Unit test services (pure business logic), integration test controllers (HTTP layer + DI wiring), và e2e test critical user flows. DI container làm cho việc swap real implementations bằng mocks trở nên trivial.
Bài Tập Thực Hành
Bài Tập 1: Thêm Users Module
Build UsersModule từ đầu:
Userentity vớiid,email,name,rolefieldsUsersServicevới findAll, findByEmail, create, update, deleteUsersControllervới full CRUD routes- DTOs với
class-validatordecorators - Wire tất cả vào
AppModule
Bài Tập 2: JWT Authentication Guard
Tạo hệ thống JWT authentication thực:
- Cài
@nestjs/jwtvà@nestjs/passport AuthModulevớiJwtModule.register({ secret, expiresIn })JwtAuthGuardvalidate Bearer tokens/auth/loginendpoint trả về JWT đã signed- Áp dụng
@UseGuards(JwtAuthGuard)cho tất cả task routes
Bài Tập 3: Caching Interceptor
Build CacheInterceptor:
- Lưu
GETresponses trongMap<string, any>keyed theo URL - Trả cached data nếu có (skip route handler)
- Invalidate cache entries khi
POST/PUT/DELETErequest hit cùng base path - Có configurable TTL qua custom decorator
@CacheTTL(seconds)
Bài Tập 4: Global Configuration Module
Cài đặt configuration management đúng cách:
- Cài
@nestjs/config - Tạo file
.envvớiDB_HOST,DB_PORT,JWT_SECRET,PORT - Register
ConfigModule.forRoot({ isGlobal: true })trongAppModule - Inject
ConfigServicevàoTasksServicevà đọcDB_HOST - Thêm config validation schema dùng
joihoặcclass-validator
Bài Tập 5: Full Test Suite
Viết test suite hoàn chỉnh cho tasks feature:
- Unit tests cho
TasksServicecovering tất cả methods và edge cases - Controller tests dùng mocked
TasksService - E2E tests cho full CRUD flow với
supertest - Guard test verify authentication logic
Tóm Tắt và Key Takeaways
| Concept | Key Insight |
|---|---|
| Angular–NestJS connection | Cùng decorators, modules, DI — Angular architecture trên server |
| Modules | Tổ chức providers + controllers thành cohesive features; dùng imports/exports để share providers |
| Controllers | Xử lý HTTP routes với method decorators; nhận injected services qua constructor |
| Providers | Bất kỳ class @Injectable() nào — services, repositories, factories, guards |
| DI Container | Resolve dependency graphs tự động; hỗ trợ useClass, useValue, useFactory, useExisting |
| Request Lifecycle | Middleware → Guards → Interceptors → Pipes → Handler → Interceptors post → Filters |
| Lifecycle Hooks | OnModuleInit để warmup, OnModuleDestroy để cleanup, BeforeApplicationShutdown cho graceful shutdown |
| Testing | Test.createTestingModule() tạo isolated module; swap real providers bằng useValue mocks |
Bài Viết Liên Quan
- Dependency Injection & IoC Explained — theory đằng sau DI container của NestJS
- Getting Started with Express.js — HTTP engine bên dưới NestJS
- Express Middleware Deep Dive — middleware concepts mang vào NestJS
- SOLID Principles Explained — design principles mà NestJS thể hiện
- Getting Started with Spring Boot — so sánh NestJS với Java enterprise framework
- Layered Architecture Guide — architectural pattern mà module system của NestJS theo
Tiếp theo: NestJS Database Integration — TypeORM, Prisma, và repository patterns
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.