Xây dựng Logic Nghiệp vụ với NestJS Service

Trong mô hình kiến trúc Layered Architecture (Kiến trúc phân tầng) của NestJS, Service (hoặc Provider) đóng vai trò là “trái tim” của ứng dụng. Đây là nơi chứa toàn bộ Business Logic (Nghiệp vụ) của hệ thống. Tài liệu này tổng hợp cấu trúc của một NestJS Service, liệt kê các phương thức thường xuyên xuất hiện và hướng dẫn bạn cách viết prompt để Claude Code sinh ra các Service hoạt động ổn định, an toàn và chuyên nghiệp nhất.

Vai trò của tầng Service

Service đứng ở giữa để phối hợp hoạt động giữa đầu nhận HTTP Request (Controller) và tầng lưu trữ dữ liệu (Repository):
Quy tắc vàng: Controller tuyệt đối không được truy cập trực tiếp DB hoặc chứa logic nghiệp vụ phức tạp. Nó chỉ nhận dữ liệu, chuyển tiếp cho Service và trả lại kết quả. Toàn bộ logic kiểm tra điều kiện, tính toán, mã hóa mật khẩu… phải nằm ở Service.

Các phương thức cốt lõi thường dùng trong Service

Một Service quản lý nghiệp vụ chuẩn thường chứa các nhóm phương thức sau:

1. Nhóm Nghiệp vụ CRUD tiêu chuẩn

Đây là các phương thức căn bản tương ứng với các tác vụ RESTful API:
  • create(createDto): Tiếp nhận dữ liệu, kiểm tra các ràng buộc nghiệp vụ (ví dụ: email đã tồn tại chưa), mã hóa dữ liệu nhạy cảm, và lưu thông qua Repository.
  • findAll(queryDto): Trả về danh sách dữ liệu, thường tích hợp logic phân trang (pagination), tìm kiếm (search) và lọc dữ liệu (filter).
  • findOne(id): Lấy chi tiết bản ghi, kiểm tra sự tồn tại và tự động ném ra lỗi NotFoundException nếu không tìm thấy.
  • update(id, updateDto): Lấy dữ liệu cũ, xử lý cập nhật các thuộc tính và lưu lại.
  • remove(id): Kiểm tra các ràng buộc trước khi xóa (ví dụ: danh mục này có chứa sản phẩm nào không) rồi tiến hành xóa.

2. Nhóm Nghiệp vụ Đặc thù (Domain Logic)

Bên cạnh CRUD, Service chứa các logic luồng công việc phức tạp hơn:
  • register(registerDto): Đăng ký tài khoản mới (hash password bằng bcrypt, kiểm tra trùng lặp email, gửi email chào mừng).
  • validateUser(email, password): Kiểm tra tài khoản và mật khẩu có khớp nhau hay không để phục vụ đăng nhập.
  • verifyEmailToken(token): Xác thực tài khoản người dùng thông qua mã token gửi qua email.

Ví dụ cấu trúc Service Toàn diện

Dưới đây là mã nguồn của một UsersService tiêu chuẩn, phối hợp xử lý lỗi bằng các Exception được NestJS cung cấp sẵn (ConflictException, NotFoundException):
src/users/users.service.ts
import { Injectable, ConflictException, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
import { RegisterDto } from './dto/register.dto';
import * as bcrypt from 'bcrypt';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private readonly userRepository: Repository<User>,
  ) {}

  // 1. Phương thức Đăng ký có chứa Business Logic phức tạp
  async register(registerDto: RegisterDto): Promise<User> {
    const { email, password, fullName } = registerDto;

    // Ràng buộc nghiệp vụ: Email không được trùng lặp
    const existingUser = await this.userRepository.findOneBy({ email });
    if (existingUser) {
      throw new ConflictException('Email này đã được sử dụng trên hệ thống.');
    }

    // Nghiệp vụ bảo mật: Mã hóa mật khẩu trước khi lưu
    const salt = await bcrypt.genSalt();
    const hashedPassword = await bcrypt.hash(password, salt);

    // Lưu vào database
    const newUser = this.userRepository.create({
      email,
      password: hashedPassword,
      fullName,
    });
    
    return await this.userRepository.save(newUser);
  }

  // 2. Phương thức Tìm một người dùng kèm xử lý lỗi NotFound chuẩn xác
  async findOneById(id: string): Promise<User> {
    const user = await this.userRepository.findOneBy({ id });
    if (!user) {
      throw new NotFoundException(`Không tìm thấy người dùng có ID #${id}`);
    }
    return user;
  }

  // 3. Phương thức Lấy toàn bộ người dùng kèm lọc bảo mật thông tin nhạy cảm
  async findAll(): Promise<User[]> {
    // Chỉ lấy các trường an toàn, loại bỏ password ra khỏi kết quả trả về
    return await this.userRepository.find({
      select: ['id', 'email', 'fullName', 'createdAt'],
    });
  }
}

Hướng dẫn viết Prompt để Claude Code thiết kế Service hoàn hảo

Để Claude Code tự động viết một Service có cấu trúc chặt chẽ và bảo mật cao, bạn nên cung cấp đầy đủ các quy tắc nghiệp vụ trong Prompt.

Prompt mẫu chuẩn thiết kế Service:

Hãy viết cho tôi một Service 'UsersService' trong file 'src/users/users.service.ts' sử dụng TypeORM:
1. Inject 'userRepository' thông qua Constructor.
2. Viết hàm 'register' nhận vào 'RegisterDto':
   - Hãy sử dụng thư viện 'bcrypt' để băm (hash) mật khẩu trước khi lưu.
   - Kiểm tra email xem đã tồn tại chưa qua hàm findOneBy. Nếu đã tồn tại, hãy ném ra lỗi 'ConflictException' kèm thông điệp tiếng Việt: "Email đã tồn tại".
   - Lưu người dùng và trả về kết quả.
3. Viết hàm 'findOne' nhận vào 'id' dạng chuỗi (UUID):
   - Truy vấn người dùng bằng ID.
   - Nếu không tìm thấy, ném ra 'NotFoundException' kèm thông báo "Không tìm thấy người dùng".
   - Nếu tìm thấy, trả về đối tượng người dùng.
4. Hãy sử dụng cấu trúc xử lý lỗi tường minh bằng async/await.
Hãy luôn yêu cầu Claude Code sử dụng đúng các HTTP Exceptions có sẵn của @nestjs/common (như BadRequestException, ForbiddenException, ConflictException, NotFoundException) để lỗi trả về cho Client luôn đi kèm đúng HTTP Status Code tiêu chuẩn.