# NestJS: Xây dựng REST API hoàn chỉnh từ đầu > Hướng dẫn đầy đủ xây dựng REST API chuyên nghiệp với NestJS. Controller, Service, Module, xác thực dữ liệu với class-validator và xử lý lỗi được giải thích chi tiết. - Published: 2026-01-13 - Updated: 2026-04-07 - Author: SharpSkill - Tags: nestjs, nodejs, typescript, rest api, backend - Reading time: 15 min --- NestJS đã khẳng định vị thế là framework Node.js hàng đầu để xây dựng ứng dụng server-side có khả năng mở rộng và dễ bảo trì. Lấy cảm hứng từ Angular, NestJS mang đến kiến trúc module, dependency injection và hỗ trợ TypeScript native. Việc xây dựng REST API chuyên nghiệp trở nên có cấu trúc và có thể dự đoán được. > **Tại sao chọn NestJS trong năm 2026** > > NestJS 11 giới thiệu những cải tiến hiệu suất đáng kể, hỗ trợ native signal cho graceful shutdown và tích hợp đơn giản hơn với các ORM hiện đại như Prisma và Drizzle. Framework vẫn đảm bảo tương thích ngược với các phiên bản trước. ## Cài đặt và Cấu hình Dự án NestJS CLI tạo ra một dự án hoàn chỉnh với cấu trúc sẵn sàng cho production. TypeScript, ESLint và unit test được cấu hình tự động. ```bash # terminal # Global installation of the NestJS CLI npm install -g @nestjs/cli # Create a new project nest new my-api # Navigate to the project cd my-api # Start in development mode with hot-reload npm run start:dev ``` Lệnh này tạo dự án với cấu trúc file có tổ chức và các dependency thiết yếu đã được cài sẵn. ```typescript // src/main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ValidationPipe } from '@nestjs/common'; async function bootstrap() { // Create the NestJS application const app = await NestFactory.create(AppModule); // Enable global validation app.useGlobalPipes(new ValidationPipe({ whitelist: true, // Removes non-decorated properties forbidNonWhitelisted: true, // Rejects requests with unknown properties transform: true, // Transforms payloads to DTO instances })); // Configure global prefix for all routes app.setGlobalPrefix('api'); await app.listen(3000); } bootstrap(); ``` Cấu hình này kích hoạt xác thực request tự động và thêm tiền tố `/api` cho tất cả các route. ## Tìm hiểu Kiến trúc Module NestJS tổ chức code thành các module, mỗi module đóng gói một domain chức năng cụ thể. Module khai báo controller (điểm cuối HTTP), provider (service xử lý nghiệp vụ) và import (dependency từ module khác). ```typescript // src/app.module.ts import { Module } from '@nestjs/common'; import { UsersModule } from './users/users.module'; import { ProductsModule } from './products/products.module'; import { AuthModule } from './auth/auth.module'; // The root module imports all application modules @Module({ imports: [ UsersModule, // User management ProductsModule, // Product catalog AuthModule, // Authentication ], }) export class AppModule {} ``` Mỗi module nghiệp vụ đều tuân theo cùng một cấu trúc: file module, controller và service. ```typescript // src/users/users.module.ts import { Module } from '@nestjs/common'; import { UsersController } from './users.controller'; import { UsersService } from './users.service'; @Module({ // Controllers handle HTTP requests controllers: [UsersController], // Providers are injectable throughout the module providers: [UsersService], // Exports make providers available to other modules exports: [UsersService], }) export class UsersModule {} ``` Khi export `UsersService`, các module khác có thể import `UsersModule` và sử dụng service này thông qua dependency injection. ## Xây dựng Controller CRUD Hoàn chỉnh Controller định nghĩa các route HTTP và ủy quyền logic nghiệp vụ cho service. Các decorator của NestJS giúp code mang tính biểu đạt và tự mô tả. ```typescript // src/users/users.controller.ts import { Controller, Get, Post, Put, Delete, Body, Param, Query, HttpCode, HttpStatus, ParseIntPipe, } from '@nestjs/common'; import { UsersService } from './users.service'; import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; import { User } from './entities/user.entity'; // Route prefix: /api/users @Controller('users') export class UsersController { // Inject service via constructor constructor(private readonly usersService: UsersService) {} // POST /api/users - Create a user @Post() @HttpCode(HttpStatus.CREATED) async create(@Body() createUserDto: CreateUserDto): Promise { // The DTO is automatically validated before reaching here return this.usersService.create(createUserDto); } // GET /api/users - List with pagination @Get() async findAll( @Query('page', new ParseIntPipe({ optional: true })) page: number = 1, @Query('limit', new ParseIntPipe({ optional: true })) limit: number = 10, ): Promise<{ data: User[]; total: number }> { return this.usersService.findAll(page, limit); } // GET /api/users/:id - Retrieve by ID @Get(':id') async findOne(@Param('id', ParseIntPipe) id: number): Promise { // ParseIntPipe automatically converts and validates the parameter return this.usersService.findOne(id); } // PUT /api/users/:id - Full update @Put(':id') async update( @Param('id', ParseIntPipe) id: number, @Body() updateUserDto: UpdateUserDto, ): Promise { return this.usersService.update(id, updateUserDto); } // DELETE /api/users/:id - Deletion @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) async remove(@Param('id', ParseIntPipe) id: number): Promise { await this.usersService.remove(id); } } ``` Các pipe như `ParseIntPipe` đảm bảo chuyển đổi và xác thực tham số tự động. Khi thất bại, lỗi 400 được trả về mà không cần xử lý thủ công. > **Các Pipe Xác thực Tích hợp** > > NestJS cung cấp nhiều pipe tích hợp sẵn: `ParseIntPipe`, `ParseBoolPipe`, `ParseArrayPipe`, `ParseUUIDPipe`. Mỗi pipe đều xác thực và chuyển đổi dữ liệu đầu vào trước khi handler được thực thi. ## Triển khai Business Service Service đóng gói logic nghiệp vụ và truy cập dữ liệu. Được đánh dấu bằng `@Injectable()`, service được quản lý bởi dependency injection container của NestJS. ```typescript // src/users/users.service.ts import { Injectable, NotFoundException } from '@nestjs/common'; import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; import { User } from './entities/user.entity'; @Injectable() export class UsersService { // In-memory database simulation private users: User[] = []; private idCounter = 1; async create(createUserDto: CreateUserDto): Promise { // Create the user entity const user: User = { id: this.idCounter++, ...createUserDto, createdAt: new Date(), updatedAt: new Date(), }; this.users.push(user); return user; } async findAll(page: number, limit: number): Promise<{ data: User[]; total: number }> { // Calculate pagination const start = (page - 1) * limit; const end = start + limit; return { data: this.users.slice(start, end), total: this.users.length, }; } async findOne(id: number): Promise { const user = this.users.find(u => u.id === id); // Throw exception if user doesn't exist if (!user) { throw new NotFoundException(`User with ID ${id} not found`); } return user; } async update(id: number, updateUserDto: UpdateUserDto): Promise { const user = await this.findOne(id); // Merge existing data with updates Object.assign(user, updateUserDto, { updatedAt: new Date() }); return user; } async remove(id: number): Promise { const index = this.users.findIndex(u => u.id === id); if (index === -1) { throw new NotFoundException(`User with ID ${id} not found`); } this.users.splice(index, 1); } // Utility method for other services async findByEmail(email: string): Promise { return this.users.find(u => u.email === email); } } ``` Exception `NotFoundException` tự động tạo response HTTP 404 với thông báo lỗi đã được định dạng. ## Xác thực Dữ liệu với class-validator DTO (Data Transfer Object) định nghĩa cấu trúc dữ liệu đầu vào. Các decorator từ `class-validator` chỉ định các quy tắc xác thực được áp dụng tự động bởi `ValidationPipe`. ```typescript // src/users/dto/create-user.dto.ts import { IsEmail, IsNotEmpty, IsString, MinLength, MaxLength, IsOptional, Matches, } from 'class-validator'; export class CreateUserDto { @IsNotEmpty({ message: 'Email is required' }) @IsEmail({}, { message: 'Invalid email format' }) email: string; @IsNotEmpty({ message: 'Password is required' }) @MinLength(8, { message: 'Password must be at least 8 characters' }) @MaxLength(50, { message: 'Password cannot exceed 50 characters' }) @Matches( /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, { message: 'Password must contain at least one uppercase, one lowercase, and one number' } ) password: string; @IsNotEmpty({ message: 'First name is required' }) @IsString() @MinLength(2) @MaxLength(50) firstName: string; @IsNotEmpty({ message: 'Last name is required' }) @IsString() @MinLength(2) @MaxLength(50) lastName: string; @IsOptional() @IsString() @MaxLength(20) phone?: string; } ``` Đối với cập nhật từng phần, `PartialType` biến tất cả các trường thành optional trong khi vẫn giữ nguyên các quy tắc xác thực. ```typescript // src/users/dto/update-user.dto.ts import { PartialType, OmitType } from '@nestjs/mapped-types'; import { CreateUserDto } from './create-user.dto'; // All fields from CreateUserDto become optional // Password is excluded from standard updates export class UpdateUserDto extends PartialType( OmitType(CreateUserDto, ['password'] as const) ) {} ``` Entity đại diện cho cấu trúc dữ liệu được lưu trữ trong database. ```typescript // src/users/entities/user.entity.ts export class User { id: number; email: string; password: string; firstName: string; lastName: string; phone?: string; createdAt: Date; updatedAt: Date; } ``` ## Xử lý Lỗi Tập trung NestJS cung cấp các HTTP exception tích hợp sẵn. Global exception filter cho phép tùy chỉnh định dạng response lỗi một cách nhất quán trên toàn bộ ứng dụng. ```typescript // src/common/filters/http-exception.filter.ts import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, } from '@nestjs/common'; import { Request, Response } from 'express'; // Catches all HttpExceptions @Catch(HttpException) export class HttpExceptionFilter implements ExceptionFilter { catch(exception: HttpException, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse(); const request = ctx.getRequest(); const status = exception.getStatus(); // Retrieve error message (can be string or object) const exceptionResponse = exception.getResponse(); const message = typeof exceptionResponse === 'string' ? exceptionResponse : (exceptionResponse as any).message; // Standardized response format response.status(status).json({ success: false, statusCode: status, timestamp: new Date().toISOString(), path: request.url, method: request.method, message: message, }); } } ``` Để bắt các lỗi không phải HTTP (lỗi hệ thống, exception chưa được xử lý), filter thứ hai đảm bảo bao phủ đầy đủ. ```typescript // src/common/filters/all-exceptions.filter.ts import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, } from '@nestjs/common'; import { Request, Response } from 'express'; // Catches ALL exceptions (including system errors) @Catch() export class AllExceptionsFilter implements ExceptionFilter { catch(exception: unknown, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse(); const request = ctx.getRequest(); // Determine HTTP code and message const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR; const message = exception instanceof HttpException ? exception.message : 'Internal server error'; // Log error for debugging console.error('Exception caught:', exception); response.status(status).json({ success: false, statusCode: status, timestamp: new Date().toISOString(), path: request.url, message: message, }); } } ``` Đăng ký filter toàn cục trong file bootstrap của ứng dụng. ```typescript // src/main.ts (updated) import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ValidationPipe } from '@nestjs/common'; import { HttpExceptionFilter } from './common/filters/http-exception.filter'; import { AllExceptionsFilter } from './common/filters/all-exceptions.filter'; async function bootstrap() { const app = await NestFactory.create(AppModule); // Global validation app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true, })); // Global exception filters app.useGlobalFilters( new AllExceptionsFilter(), new HttpExceptionFilter(), ); app.setGlobalPrefix('api'); await app.listen(3000); } bootstrap(); ``` > **Thứ tự Filter Rất Quan trọng** > > Thứ tự đăng ký filter quyết định thứ tự thực thi. Filter đăng ký đầu tiên sẽ được thực thi cuối cùng. `AllExceptionsFilter` phải được đăng ký trước `HttpExceptionFilter` để đóng vai trò fallback cho các exception chưa được xử lý. ## Tích hợp với Prisma ORM Prisma đơn giản hóa tương tác database thông qua typed client được tạo tự động từ schema. Đây là cách tích hợp đầy đủ với NestJS. ```bash # terminal # Install Prisma npm install prisma @prisma/client # Initialize Prisma with PostgreSQL npx prisma init --datasource-provider postgresql ``` Định nghĩa data schema xác định các model và quan hệ. ```prisma // prisma/schema.prisma generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model User { id Int @id @default(autoincrement()) email String @unique password String firstName String @map("first_name") lastName String @map("last_name") phone String? createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // Relations posts Post[] @@map("users") } model Post { id Int @id @default(autoincrement()) title String content String published Boolean @default(false) authorId Int @map("author_id") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // Relation to User author User @relation(fields: [authorId], references: [id]) @@map("posts") } ``` Tạo module Prisma có thể tái sử dụng trên toàn bộ ứng dụng. ```typescript // src/prisma/prisma.service.ts import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; @Injectable() export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { // Automatic connection on module startup async onModuleInit() { await this.$connect(); } // Clean disconnection on application shutdown async onModuleDestroy() { await this.$disconnect(); } } ``` ```typescript // src/prisma/prisma.module.ts import { Global, Module } from '@nestjs/common'; import { PrismaService } from './prisma.service'; // @Global makes the service available throughout the application @Global() @Module({ providers: [PrismaService], exports: [PrismaService], }) export class PrismaModule {} ``` Service Users được cập nhật sử dụng Prisma để truy cập database thực tế. ```typescript // src/users/users.service.ts (Prisma version) import { Injectable, NotFoundException, ConflictException } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; import { User } from '@prisma/client'; import * as bcrypt from 'bcrypt'; @Injectable() export class UsersService { constructor(private readonly prisma: PrismaService) {} async create(createUserDto: CreateUserDto): Promise> { // Check email uniqueness const existingUser = await this.prisma.user.findUnique({ where: { email: createUserDto.email }, }); if (existingUser) { throw new ConflictException('This email is already in use'); } // Hash the password const hashedPassword = await bcrypt.hash(createUserDto.password, 10); // Create the user const user = await this.prisma.user.create({ data: { ...createUserDto, password: hashedPassword, }, }); // Exclude password from response const { password, ...result } = user; return result; } async findAll(page: number, limit: number): Promise<{ data: User[]; total: number }> { // Parallel execution of count and paginated query const [data, total] = await Promise.all([ this.prisma.user.findMany({ skip: (page - 1) * limit, take: limit, orderBy: { createdAt: 'desc' }, select: { id: true, email: true, firstName: true, lastName: true, phone: true, createdAt: true, updatedAt: true, }, }), this.prisma.user.count(), ]); return { data: data as User[], total }; } async findOne(id: number): Promise> { const user = await this.prisma.user.findUnique({ where: { id }, select: { id: true, email: true, firstName: true, lastName: true, phone: true, createdAt: true, updatedAt: true, }, }); if (!user) { throw new NotFoundException(`User with ID ${id} not found`); } return user as Omit; } async update(id: number, updateUserDto: UpdateUserDto): Promise> { // Check existence await this.findOne(id); const user = await this.prisma.user.update({ where: { id }, data: updateUserDto, select: { id: true, email: true, firstName: true, lastName: true, phone: true, createdAt: true, updatedAt: true, }, }); return user as Omit; } async remove(id: number): Promise { await this.findOne(id); await this.prisma.user.delete({ where: { id } }); } } ``` ## Interceptor cho Chuyển đổi Response Interceptor cho phép chuyển đổi response một cách đồng nhất trên toàn bộ API. Interceptor chuyển đổi chuẩn hóa định dạng tất cả response. ```typescript // src/common/interceptors/transform.interceptor.ts import { Injectable, NestInterceptor, ExecutionContext, CallHandler, } from '@nestjs/common'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; // Interface for standardized response format export interface ApiResponse { success: boolean; data: T; timestamp: string; } @Injectable() export class TransformInterceptor implements NestInterceptor> { intercept(context: ExecutionContext, next: CallHandler): Observable> { return next.handle().pipe( map(data => ({ success: true, data, timestamp: new Date().toISOString(), })), ); } } ``` Interceptor logging theo dõi request và thời gian thực thi để giám sát hiệu suất. ```typescript // src/common/interceptors/logging.interceptor.ts import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger, } from '@nestjs/common'; import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; @Injectable() export class LoggingInterceptor implements NestInterceptor { private readonly logger = new Logger(LoggingInterceptor.name); intercept(context: ExecutionContext, next: CallHandler): Observable { const request = context.switchToHttp().getRequest(); const { method, url } = request; const now = Date.now(); return next.handle().pipe( tap(() => { const response = context.switchToHttp().getResponse(); const { statusCode } = response; const duration = Date.now() - now; // Structured log format this.logger.log( `${method} ${url} ${statusCode} - ${duration}ms` ); }), ); } } ``` ## Kết luận NestJS cung cấp kiến trúc vững chắc và có khả năng mở rộng để xây dựng REST API chuyên nghiệp. Sự kết hợp của TypeScript, dependency injection và các decorator mang tính biểu đạt cho phép xây dựng ứng dụng dễ bảo trì và dễ kiểm thử. ### Checklist cho API NestJS Chất lượng - Cấu trúc module với phân tách trách nhiệm rõ ràng - DTO với xác thực class-validator cho tất cả đầu vào - Service riêng biệt cho logic nghiệp vụ - Xử lý lỗi tập trung với exception filter - Interceptor cho chuyển đổi và logging - Tích hợp Prisma cho truy cập database có kiểu dữ liệu - ValidationPipe toàn cục với whitelist được bật - Tiền tố API nhất quán trên tất cả route Sức mạnh của NestJS nằm ở cấu trúc có quan điểm rõ ràng, tự nhiên hướng dẫn đến các best practice. Các pattern đã được chứng minh như dependency injection và phân tách layer tạo ra code có thể kiểm thử và phát triển, sẵn sàng cho ứng dụng enterprise. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/vi/blog/node-nestjs/nestjs-building-complete-rest-api