Khi dữ liệu còn ít, việc trả về toàn bộ danh sách có vẻ đơn giản: một API lấy tất cả sản phẩm, tất cả bài viết hoặc tất cả giao dịch. Nhưng khi dữ liệu tăng lên, cách làm này nhanh chóng gây chậm giao diện, nặng server và khó sử dụng. Phân trang không chỉ là chuyện “chia mỗi trang 20 dòng”, mà là một phần quan trọng trong trải nghiệm và thiết kế API. Một ví dụ thực tế: màn hình quản lý đơn hàng. Nếu mỗi lần mở trang mà hệ thống tải 10.000 đơn gần nhất, người dùng phải chờ lâu, trình duyệt tốn bộ nhớ, còn server phải truy vấn và truyền dữ liệu không cần thiết. Trong phần lớn trường hợp, người dùng chỉ xem vài chục đơn đầu tiên, rồi lọc theo trạng thái, ngày tạo hoặc mã đơn. Có ba kiểu phổ biến: - Phân trang theo số trang: ví dụ page=2&pageSize=20. Dễ hiểu, phù hợp với danh sách ổn định như bài viết, sản phẩm trong trang quản trị. Nhược điểm là khi dữ liệu thay đổi liên tục, bản ghi có thể bị trùng hoặc bị bỏ sót giữa các trang. - Phân trang theo offset: ví dụ offset=40&limit=20. Linh hoạt, nhưng với bảng dữ liệu lớn, offset quá cao có thể khiến truy vấn chậm nếu không tối ưu. - Phân trang theo cursor: ví dụ lấy các bản ghi sau createdAt/id cuối cùng của trang trước. Cách này phù hợp với feed, thông báo, lịch sử giao dịch, nơi dữ liệu thường xuyên phát sinh mới. Một số lời khuyên có thể áp dụng ngay: 1. Luôn đặt giới hạn pageSize/limit tối đa. Đừng để client truyền limit=100000 rồi làm nghẽn hệ thống. Ví dụ mặc định 20, tối đa 100 là một lựa chọn thường gặp, tùy loại dữ liệu. 2. Sắp xếp phải ổn định. Nếu sort theo createdAt, nên có thêm id làm tiêu chí phụ để tránh trường hợp nhiều bản ghi cùng thời điểm gây lặp hoặc thiếu dữ liệu. Ví dụ: ORDER BY created_at DESC, id DESC. 3. Trả về đủ thông tin cho client. Với phân trang theo số trang, có thể trả total, page, pageSize, totalPages nếu việc đếm tổng không quá tốn. Với cursor, có thể trả nextCursor và hasMore. Đừng bắt frontend tự đoán bằng cách kiểm tra “có đúng 20 item hay không” trong mọi tình huống. 4. Kết hợp phân trang với lọc và tìm kiếm. Người dùng hiếm khi muốn lật từng trang đến trang 80. Họ cần bộ lọc theo thời gian, trạng thái, từ khóa, người tạo hoặc danh mục. Phân trang tốt mà thiếu lọc vẫn là trải nghiệm kém. 5. Cân nhắc infinite scroll cẩn thận. Infinite scroll hợp với mạng xã hội, tin tức, danh sách khám phá. Nhưng với màn hình quản trị, bảng dữ liệu hoặc nơi người dùng cần quay lại một vị trí cụ thể, phân trang rõ ràng thường dễ dùng hơn. Nếu dùng infinite scroll, nên giữ trạng thái cuộn khi người dùng mở chi tiết rồi quay lại. 6. Đừng quên trạng thái tải và rỗng. Khi chuyển trang hoặc tải thêm, giao diện nên có loading nhẹ, không làm nhảy layout quá nhiều. Nếu không có dữ liệu, hãy nói rõ: “Không có đơn hàng nào trong khoảng thời gian này” thay vì chỉ để bảng trống. Một thiết kế API đơn giản có thể như sau: GET /orders?status=paid&from=2026-01-01&limit=20&cursor=abc. Phản hồi gồm items, nextCursor và hasMore. Với trang quản trị cần biết tổng số đơn sau khi lọc, có thể bổ sung total, nhưng nên kiểm tra hiệu năng nếu dữ liệu lớn. Tóm lại, phân trang là cách giúp hệ thống tải đúng lượng dữ liệu cần thiết, giúp giao diện phản hồi nhanh hơn và giúp người dùng định hướng tốt hơn. Đừng chờ đến khi danh sách chậm mới sửa; hãy thiết kế phân trang ngay từ đầu, nhất là với dữ liệu có khả năng tăng theo thời gian.