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.

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.
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.
# 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:devLệ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.
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).
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.
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ả.
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<User> {
// 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<User> {
// 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<User> {
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<void> {
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.
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.
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<User> {
// 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<User> {
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<User> {
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<void> {
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<User | undefined> {
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.
Sẵn sàng chinh phục phỏng vấn Node.js / NestJS?
Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.
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.
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.
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.
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.
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<Response>();
const request = ctx.getRequest<Request>();
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 đủ.
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<Response>();
const request = ctx.getRequest<Request>();
// 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.
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ự đă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.
# 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ệ.
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.
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();
}
}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ế.
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<Omit<User, 'password'>> {
// 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<Omit<User, 'password'>> {
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<User, 'password'>;
}
async update(id: number, updateUserDto: UpdateUserDto): Promise<Omit<User, 'password'>> {
// 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<User, 'password'>;
}
async remove(id: number): Promise<void> {
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.
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<T> {
success: boolean;
data: T;
timestamp: string;
}
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, ApiResponse<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<ApiResponse<T>> {
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.
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<any> {
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
Bắt đầu luyện tập!
Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.
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.
Thẻ
Chia sẻ
