Back to blog

NestJS Core Concepts & Dependency Injection

nestjstypescriptbackendnodejsrest-api
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:

AngularNestJSMụ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
ServicesProvidersBusiness logic containers
PipesPipesData transformation và validation
GuardsGuardsAuthorization / access control
InterceptorsInterceptorsCross-cutting concerns (logging, caching)
MiddlewareMiddlewarePre-route request processing

DNA kiến trúc giống nhau:

Cài Đặt NestJS Project

Cài CLI

npm install -g @nestjs/cli

Tạo Project Mới

nest new my-nestjs-app
cd my-nestjs-app

CLI 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 point

Entry 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:dev

Truy 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:

PropertyMục đích
importsCác modules khác mà module này cần providers được export từ đó
controllersControllers xử lý incoming requests
providersServices và các injectable classes khác
exportsProviders 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:

DecoratorExpress EquivalentMụ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()reqFull request object
@Res()resFull 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:

TypeDùng khiVí dụ
useClassSwap implementations (testing, feature flags)MockService trong test, RealService trong prod
useValueInject constants hoặc config objectsAPI keys, feature flags, mock objects
useFactoryDynamic creation với async logicDatabase connections, conditional setup
useExistingAlias một provider sang provider khácBackward 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ó
}
ScopeBehaviorUse Case
DEFAULTSingle instance chia sẻ toàn appHầu hết services, stateless logic
REQUESTInstance mới cho mỗi HTTP requestRequest-specific data (user context, tenant)
TRANSIENTInstance mới cho mỗi injectionStateful 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:

LayerMục đíchVí dụ
MiddlewareRaw request processing, loggingRequest logger, CORS, body parser
GuardsAccess control (quyết định yes/no)Auth check, role-based access
InterceptorsTransform request/response, timingCaching, response mapping, timing
PipesValidate/transform input dataDTO validation, type coercion
Exception FiltersFormat error responsesCustom 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.

HookInterfaceKhi nào được gọi
onModuleInit()OnModuleInitSau khi module's dependencies được resolve
onApplicationBootstrap()OnApplicationBootstrapSau khi tất cả modules initialized, trước khi listen
onModuleDestroy()OnModuleDestroyKhi module đang bị destroy (shutdown signal)
beforeApplicationShutdown()BeforeApplicationShutdownSau onModuleDestroy(), trước khi connections đóng
onApplicationShutdown()OnApplicationShutdownSau 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.ts

task.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

FeatureExpressNestJSSpring Boot
ArchitectureMinimal, flexibleOpinionated, modularOpinionated, modular
LanguageJavaScript/TypeScriptTypeScript (first-class)Java/Kotlin
DIManual hoặc third-partyBuilt-in containerBuilt-in container
DecoratorsKhông cóPhong phúAnnotations
ValidationManual hoặc middlewareBuilt-in pipes + class-validatorBean Validation (JSR-380)
ORMSequelize, Prisma, v.v.TypeORM, Prisma, MikroORMJPA/Hibernate
TestingMocha/Jest + manual DIJest + @nestjs/testingJUnit + Spring Test
Learning CurveThấpTrung bìnhTrung bình-Cao
PerformanceCaoTốt (Express bên dưới)Tốt
Best ForSimple APIs, prototypesEnterprise Node.jsEnterprise 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:

  • User entity với id, email, name, role fields
  • UsersService với findAll, findByEmail, create, update, delete
  • UsersController với full CRUD routes
  • DTOs với class-validator decorators
  • 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/jwt và @nestjs/passport
  • AuthModule với JwtModule.register({ secret, expiresIn })
  • JwtAuthGuard validate Bearer tokens
  • /auth/login endpoint 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 GET responses trong Map<string, any> keyed theo URL
  • Trả cached data nếu có (skip route handler)
  • Invalidate cache entries khi POST/PUT/DELETE request 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 .env với DB_HOST, DB_PORT, JWT_SECRET, PORT
  • Register ConfigModule.forRoot({ isGlobal: true }) trong AppModule
  • Inject ConfigService vào TasksService và đọc DB_HOST
  • Thêm config validation schema dùng joi hoặc class-validator

Bài Tập 5: Full Test Suite

Viết test suite hoàn chỉnh cho tasks feature:

  • Unit tests cho TasksService covering 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

ConceptKey Insight
Angular–NestJS connectionCùng decorators, modules, DI — Angular architecture trên server
ModulesTổ chức providers + controllers thành cohesive features; dùng imports/exports để share providers
ControllersXử lý HTTP routes với method decorators; nhận injected services qua constructor
ProvidersBất kỳ class @Injectable() nào — services, repositories, factories, guards
DI ContainerResolve dependency graphs tự động; hỗ trợ useClass, useValue, useFactory, useExisting
Request LifecycleMiddleware → Guards → Interceptors → Pipes → Handler → Interceptors post → Filters
Lifecycle HooksOnModuleInit để warmup, OnModuleDestroy để cleanup, BeforeApplicationShutdown cho graceful shutdown
TestingTest.createTestingModule() tạo isolated module; swap real providers bằng useValue mocks

Bài Viết Liên Quan


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.