Khi ứng dụng còn ít dữ liệu, nhiều đội phát triển thường trả toàn bộ danh sách về client rồi để frontend tự lọc, tự sắp xếp. Cách này nhanh để làm demo, nhưng khi dữ liệu tăng lên, trang bắt đầu tải chậm, API nặng, database bị quét nhiều và người dùng gặp trải nghiệm không ổn định. Phân trang, lọc và sắp xếp là những phần tưởng nhỏ, nhưng nếu thiết kế tốt từ đầu sẽ giúp hệ thống dễ mở rộng hơn rất nhiều. Một nguyên tắc cơ bản: đừng trả nhiều dữ liệu hơn mức người dùng cần xem. Với danh sách đơn hàng, bài viết, sản phẩm hoặc thông báo, API nên hỗ trợ phân trang rõ ràng. Cách phổ biến là dùng limit và offset, ví dụ: /orders?limit=20&offset=40. Cách này dễ hiểu, dễ debug, phù hợp với dữ liệu vừa phải. Tuy nhiên, khi bảng lớn hoặc dữ liệu thay đổi liên tục, offset lớn có thể chậm và có nguy cơ bị trùng hoặc bỏ sót bản ghi nếu có dữ liệu mới được thêm vào giữa lúc người dùng đang xem. Trong nhiều trường hợp, cursor-based pagination là lựa chọn tốt hơn. Thay vì hỏi “bỏ qua 40 bản ghi”, client hỏi “lấy 20 bản ghi sau mốc này”. Ví dụ: /orders?limit=20&cursor=eyJpZCI6MTAwMH0. Cursor có thể dựa trên id, created_at hoặc tổ hợp nhiều trường. Cách này thường ổn định hơn cho timeline, danh sách tin nhắn, lịch sử giao dịch hoặc notification feed. Về lọc dữ liệu, nên thống nhất tên tham số và hành vi ngay từ đầu. Ví dụ: status=paid, from_date=2026-01-01, to_date=2026-01-31, keyword=abc. Tránh để mỗi API dùng một kiểu như state, order_status, q, search_text nếu không có lý do rõ ràng. Sự nhất quán giúp frontend dễ tích hợp và giảm lỗi khi sản phẩm mở rộng. Một điểm hay bị bỏ qua là sắp xếp. Nếu cho phép sort tự do theo mọi cột, có thể vô tình tạo truy vấn rất nặng. Nên giới hạn danh sách trường được phép sort, chẳng hạn created_at, total_amount, name. Với các danh sách quan trọng, hãy đảm bảo database có index phù hợp với cách lọc và sắp xếp thực tế. Ví dụ, nếu màn hình quản trị thường lọc đơn hàng theo status và sắp xếp theo created_at giảm dần, index trên status, created_at có thể hữu ích hơn là chỉ index từng cột riêng lẻ. API cũng nên trả metadata đủ dùng. Với offset pagination, response có thể gồm items, limit, offset, total nếu việc đếm tổng không quá tốn kém. Nhưng đừng mặc định lúc nào cũng count toàn bộ nếu bảng rất lớn. Có thể cân nhắc trả has_more hoặc next_cursor thay cho total. Người dùng thường cần biết còn trang sau hay không hơn là con số tổng chính xác tuyệt đối. Ví dụ response đơn giản: { "items": [...], "next_cursor": "...", "has_more": true } Ở phía frontend, cần tránh gọi API liên tục mỗi khi người dùng gõ một ký tự vào ô tìm kiếm. Hãy dùng debounce, ví dụ chờ 300-500ms sau khi người dùng ngừng gõ rồi mới gửi request. Đồng thời nên hủy request cũ nếu request mới đã được tạo, để tránh tình trạng kết quả cũ trả về sau và ghi đè lên kết quả mới. Một vài lời khuyên có thể áp dụng ngay: - Đặt limit mặc định và limit tối đa, ví dụ mặc định 20, tối đa 100. - Không cho client yêu cầu page size quá lớn chỉ vì “tiện”. - Chuẩn hóa tên tham số lọc, sort và phân trang giữa các API. - Chỉ cho sort theo các trường đã được kiểm soát. - Kiểm tra query plan khi danh sách bắt đầu chậm, đừng chỉ tăng cấu hình máy chủ. - Với dữ liệu thay đổi liên tục, cân nhắc cursor thay vì offset. - Trả lỗi rõ ràng khi tham số không hợp lệ, ví dụ sort=unknown_field. Phân trang và lọc dữ liệu không chỉ là chuyện giao diện hiển thị bao nhiêu dòng. Đây là phần nằm giữa trải nghiệm người dùng, thiết kế API và hiệu năng database. Làm cẩn thận từ đầu sẽ giúp ứng dụng tránh được nhiều vấn đề khó chịu khi dữ liệu tăng lên: trang chậm, kết quả thiếu ổn định, API khó dùng và database chịu tải không cần thiết.