Giới thiệu

Phân trang là một trong những yêu cầu cơ bản khi xây dựng API trả về danh sách dữ liệu lớn. Nếu không tối ưu, việc truy vấn toàn bộ bảng sẽ gây tốn tài nguyên và làm chậm phản hồi. Bài viết này sẽ hướng dẫn chi tiết cách triển khai phân trang trong NestJS kết hợp TypeORM, đồng thời giới thiệu một số mẹo tối ưu truy vấn.

Cấu trúc tham số phân trang

Thông thường, client sẽ gửi hai tham số: page (số trang) và limit (số bản ghi mỗi trang). Để tránh lỗi khi truyền giá trị không hợp lệ, chúng ta nên thiết lập giá trị mặc định và giới hạn tối đa.

DTO cho phân trang

export class PaginationDto {
  page?: number;
  limit?: number;
}

Triển khai trong Service

Trong Service, chúng ta sử dụng phương thức findAndCount của Repository để lấy dữ liệu và tổng số bản ghi trong một lần truy vấn. Điều này giúp giảm số lần round‑trip tới database.

Mã nguồn Service

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
import { PaginationDto } from './dto/pagination.dto';

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

  async paginate(paginationDto: PaginationDto) {
    const page = paginationDto.page && paginationDto.page > 0 ? paginationDto.page : 1;
    const limit = paginationDto.limit && paginationDto.limit > 0 ? Math.min(paginationDto.limit, 100) : 10;
    const [data, total] = await this.userRepo.findAndCount({
      skip: (page - 1) * limit,
      take: limit,
      order: { createdAt: 'DESC' },
    });
    return {
      data,
      meta: {
        totalItems: total,
        itemCount: data.length,
        itemsPerPage: limit,
        totalPages: Math.ceil(total / limit),
        currentPage: page,
      },
    };
  }
}

Ở đây, skiptake là các tùy chọn của TypeORM tương đương với OFFSETLIMIT trong SQL. Khi kết hợp order, chúng ta đảm bảo kết quả luôn có thứ tự ổn định.

Tối ưu truy vấn với chỉ mục

Để phân trang nhanh hơn, hãy tạo chỉ mục trên cột được sắp xếp, ví dụ createdAt. Trong file migration, chúng ta có thể thêm:

CREATE INDEX IDX_user_created_at ON user (created_at);

Chỉ mục giúp database xác định nhanh vị trí bắt đầu của mỗi trang mà không cần quét toàn bộ bảng.

Xử lý phân trang cho quan hệ

Khi truy vấn các thực thể có quan hệ OneToMany hoặc ManyToOne, việc eager load toàn bộ quan hệ có thể gây “N+1 query”. Để tránh, chúng ta sử dụng leftJoinAndSelect kết hợp với take/skip hoặc áp dụng QueryBuilder để chỉ lấy các trường cần thiết.

Ví dụ QueryBuilder

const query = this.userRepo.createQueryBuilder('user')
  .leftJoinAndSelect('user.profile', 'profile')
  .orderBy('user.createdAt', 'DESC')
  .skip((page - 1) * limit)
  .take(limit);
const [data, total] = await query.getManyAndCount();

Kết luận

Việc áp dụng phân trang đúng cách không chỉ giảm tải cho server mà còn cải thiện trải nghiệm người dùng. Kết hợp các kỹ thuật như findAndCount, chỉ mục và QueryBuilder sẽ giúp API NestJS của bạn hoạt động ổn định ngay cả khi dữ liệu tăng mạnh.

Để nắm vững toàn bộ quy trình xây dựng RESTful API chuyên nghiệp, Tham khảo khóa học "RESTful API với NestJS & TypeORM" tại đây.