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.

Hướng dẫn NestJS xây dựng REST API hoàn chỉnh

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.

src/main.tstypescript
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).

src/app.module.tstypescript
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.

src/users/users.module.tstypescript
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ả.

src/users/users.controller.tstypescript
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.

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.

src/users/users.service.tstypescript
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.

src/users/dto/create-user.dto.tstypescript
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.

src/users/dto/update-user.dto.tstypescript
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.

src/users/entities/user.entity.tstypescript
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.

src/common/filters/http-exception.filter.tstypescript
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 đủ.

src/common/filters/all-exceptions.filter.tstypescript
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.

src/main.ts (updated)typescript
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/schema.prismaprisma
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.

src/prisma/prisma.service.tstypescript
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();
  }
}
src/prisma/prisma.module.tstypescript
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ế.

src/users/users.service.ts (Prisma version)typescript
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.

src/common/interceptors/transform.interceptor.tstypescript
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.

src/common/interceptors/logging.interceptor.tstypescript
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ẻ

#nestjs
#nodejs
#typescript
#rest api
#backend

Chia sẻ

Bài viết liên quan