Thứ Bảy, 29 tháng 8, 2026

API Pagination là gì? (Vì sao API không bao giờ nên trả về 100.000 dòng dữ liệu một lúc?)

Nếu muốn tìm số điện thoại của người bạn tên Tuấn trong danh bạ, bạn sẽ làm thế nào? Sẽ tìm nơi có vần "T", sau đó tìm tên Tuấn, hay lò mò tìm từ vần A, B,C...?

Ở bài trước.

Chúng ta đã nói về API Authentication.

Server hỏi:

"Anh là ai?"

😎

Bạn đưa Token.

Server:

"OK. Cho vào."

Nhưng bây giờ lại xuất hiện một vấn đề khác.

Bạn đã được phép vào.

Bạn gọi:

GET /api/products

Server mở Database.

Có:

100.000 sản phẩm

😱

Và Server quyết định:

"Được rồi. Tôi gửi cả 100.000 sản phẩm cho anh."

...

Server:

🔥

Database:

🔥🔥

RAM:

💀

Internet:

🐌🐌🐌

Điện thoại người dùng:

"Anh đang đùa tôi à?" 😭

Đây chính là lý do chúng ta cần:

API Pagination.


API Pagination là gì?

API Pagination là kỹ thuật chia một tập dữ liệu lớn thành nhiều phần nhỏ (pages), thay vì trả về toàn bộ dữ liệu trong một API response.

Ví dụ thay vì trả:

100.000 sản phẩm

API có thể chỉ trả:

20 sản phẩm / lần

Người dùng xem trang tiếp theo thì Frontend gọi API tiếp.

Ví dụ:

GET /api/products?page=1&limit=20

Sau đó:

GET /api/products?page=2&limit=20

và tiếp tục.

Pagination giúp giảm lượng dữ liệu truyền qua mạng, giảm tải cho Server/Database và giúp giao diện phản hồi nhanh hơn.



Pagination thực ra không có gì đáng sợ 😄

Bạn đã gặp Pagination từ trước rồi.

Nhớ bài Pagination không?

Một Database có:

100.000 dòng

Nhưng bạn chỉ muốn xem:

10 dòng

thì không cần lấy cả 100.000 dòng.

Bạn có thể dùng:

SELECT *
FROM products
LIMIT 10;

Database chỉ trả về 10 dòng.

Đó chính là tư duy nền tảng của Pagination.


Ví dụ đời thường nhất 📚

Hãy tưởng tượng bạn có một quyển danh bạ điện thoại dày:

5.000 trang.

Bạn hỏi:

"Cho tôi xem toàn bộ."

Người bán sách:

"Đây."

💥

Quăng cả quyển sách vào mặt bạn.

🤣

Không ai làm vậy.

Thay vào đó:

"Trang 1 đến 20."

Bạn xem.

Xong.

"Cho tôi 20 trang tiếp."

Trang 21 đến 40.

Tiếp tục.

Đó chính là:

Pagination.


"Chua thêm" 😆

Pagination giống như đi ăn buffet.

🍜

Bạn không bê toàn bộ nhà bếp ra bàn.

Bạn lấy:

Một đĩa trước.

Ăn xong.

Lấy đĩa tiếp.

Ăn xong.

Lấy tiếp.

🤣

API cũng vậy.

Đừng bắt Server bê:

100.000 món

ra một lần.

Hãy nói:

"Cho tôi 20 món trước."


Tại sao API không nên trả 100.000 dòng?

Đây là câu hỏi rất quan trọng.

Không phải vì:

"100.000 là con số bị cấm."

😄

Mà vì dữ liệu lớn gây ra rất nhiều vấn đề.


1. Response quá lớn 📦

Nếu mỗi sản phẩm có:

id
name
price
description
image
category
stock
created_at
...

thì 100.000 sản phẩm có thể tạo ra một response rất lớn.

Server phải:

Query → xử lý → serialize → truyền đi.

Client phải:

Download → parse JSON → xử lý → render.

Không vui chút nào.


2. Database phải làm nhiều việc hơn 🐌

Bạn gọi:

SELECT *
FROM products;

Database phải lấy toàn bộ dữ liệu phù hợp.

Trong khi người dùng có thể chỉ nhìn thấy:

20 sản phẩm đầu tiên

😅

Bạn bắt Database làm:

"Anh làm 100.000 việc nhé."

Trong khi khách hàng chỉ cần:

"20 việc thôi."


3. Server phải dùng nhiều tài nguyên hơn

Dữ liệu càng lớn.

Server càng phải xử lý nhiều.

Có thể ảnh hưởng đến:

  • CPU
  • RAM
  • Database
  • Network
  • Response time

Nếu chỉ có một người dùng.

Có thể bạn chưa nhận ra.

Nhưng nếu có:

10 users
100 users
1.000 users
10.000 users

thì câu chuyện hoàn toàn khác.


4. Người dùng cũng chẳng cần 100.000 dòng 😅

Hãy nhìn một trang Shopee.

Bạn vào:

Điện thoại.

Bạn thấy một số sản phẩm.

Bạn kéo xuống.

Có thêm.

Kéo tiếp.

Có thêm.

Bạn không thấy:

"Đang tải 100.000 sản phẩm..."

🤣

Website chỉ tải dữ liệu cần thiết theo từng phần.


Pagination hoạt động thế nào?

Một cách rất phổ biến:

GET /api/products?page=1&limit=20

Trong đó:

page=1

→ Trang 1.

limit=20

→ Mỗi trang lấy tối đa 20 sản phẩm.


Trang tiếp:

GET /api/products?page=2&limit=20

Trang 3:

GET /api/products?page=3&limit=20

...


Page và Limit là gì?

Hai khái niệm này rất quan trọng.

page

Cho biết:

"Tôi đang xem trang thứ mấy?"

limit

Cho biết:

"Mỗi trang lấy bao nhiêu dòng?"

Ví dụ:

page = 3
limit = 20

Có nghĩa:

"Cho tôi 20 sản phẩm ở trang thứ 3."


LIMIT và OFFSET xuất hiện 😎

Đây là lúc bài LIMIT cũ quay trở lại.

Ví dụ:

SELECT *
FROM products
LIMIT 20 OFFSET 40;

Có nghĩa:

Bỏ qua 40 dòng đầu.

Lấy tiếp 20 dòng.

Tức là:

1 → 20
21 → 40
41 → 60

Bạn đang xem:

Trang 3.


Công thức rất dễ nhớ 🧮

Nếu:

page = 3
limit = 20

thì:

OFFSET = (page - 1) × limit

Tính:

(3 - 1) × 20
= 40

Vậy:

LIMIT 20 OFFSET 40

Ví dụ PHP

Giả sử API nhận:

?page=3&limit=20

PHP có thể lấy:

$page = $_GET['page'] ?? 1;
$limit = $_GET['limit'] ?? 20;

Sau đó:

$offset = ($page - 1) * $limit;

Và tạo query:

$sql = "SELECT *
        FROM products
        LIMIT $limit
        OFFSET $offset";

Về mặt ý tưởng.

Rất đơn giản:

page
 ↓
tính offset
 ↓
LIMIT + OFFSET
 ↓
Database
 ↓
JSON
 ↓
Frontend

Nhưng khoan! ⚠️

Đừng copy đoạn code trên rồi chạy thẳng vào Production.

😅

Tại sao?

Vì:

$_GET['limit']

là dữ liệu người dùng gửi lên.

Người dùng có thể gửi:

?limit=100000000

🤣

Nếu bạn không giới hạn.

Người dùng có thể bắt API:

"Cho tôi một triệu dòng nhé."

Server:

"Ủa?"

💀

Vì vậy API nên:

  • Validate page
  • Validate limit
  • Đặt giới hạn tối đa
  • Xử lý giá trị không hợp lệ

Ví dụ:

limit tối đa = 100

Người dùng gửi:

limit=1000000

Server có thể giới hạn xuống:

100

API Response nên trả gì?

Một API Pagination tốt thường không chỉ trả:

[
  {...},
  {...},
  {...}
]

Nó thường trả thêm thông tin Pagination.

Ví dụ:

{
  "data": [
    {
      "id": 1,
      "name": "iPhone"
    },
    {
      "id": 2,
      "name": "Samsung"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 100000,
    "total_pages": 5000
  }
}

Frontend lúc này biết:

Đang ở trang 1
Mỗi trang 20 dòng
Tổng cộng 100.000 dòng
Có 5.000 trang

😄


Frontend dùng thông tin đó thế nào?

Frontend có thể hiển thị:

← Trước

1  2  3  4  5

Sau →

Hoặc:

← Previous

Page 3 / 5000

Next →

Khi người dùng bấm:

Next

Frontend gọi:

GET /api/products?page=4&limit=20

Server trả dữ liệu.

Frontend cập nhật giao diện.

Xong.


Pagination không nhất thiết phải có nút "Trang 1, 2, 3"

Đây là điểm thú vị.

Có nhiều kiểu Pagination.


Kiểu 1: Page Number

Ví dụ:

1 2 3 4 5 6

Người dùng bấm từng trang.

Đây là kiểu rất quen thuộc.


Kiểu 2: Previous / Next

← Previous
Next →

Đơn giản hơn.


Kiểu 3: Load More

Bạn kéo xuống.

Có nút:

Load More

Bấm.

Thêm dữ liệu.

Bấm tiếp.

Thêm tiếp.


Kiểu 4: Infinite Scroll ♾️

Bạn kéo xuống.

Không thấy nút.

Kéo tới gần cuối.

Website tự gọi API.

page=2

Kéo tiếp.

page=3

Kéo tiếp.

page=4

😄

Đây là kiểu rất phổ biến trên các mạng xã hội.


Pagination và Infinite Scroll có khác nhau không?

Có.

Pagination

→ Chia dữ liệu thành các trang rõ ràng.

Infinite Scroll

→ Vẫn thường lấy dữ liệu theo từng batch/page nhưng giao diện tự động tải thêm khi người dùng cuộn.

Nói cách khác:

Infinite Scroll không có nghĩa là Server gửi vô hạn dữ liệu một lần.

🤣

Nó vẫn thường phải:

lấy từng phần.


Một vấn đề với OFFSET 😵

Đến đây.

Có một chuyện hơi nâng cao.

Bạn có:

10 triệu dòng

và gọi:

LIMIT 20 OFFSET 9000000

Database phải xử lý việc bỏ qua một lượng rất lớn dữ liệu trước khi lấy 20 dòng.

Có thể trở nên chậm tùy Database, query và index.

Vì vậy với dữ liệu cực lớn.

Chúng ta có thể gặp một kỹ thuật khác:

Cursor Pagination


Cursor Pagination là gì?

Thay vì nói:

"Cho tôi trang số 450.000."

Bạn nói:

"Cho tôi 20 dòng tiếp theo sau dòng có ID này."

Ví dụ:

GET /api/products?limit=20&after_id=5000

Server có thể xử lý theo kiểu:

SELECT *
FROM products
WHERE id > 5000
ORDER BY id
LIMIT 20;

Ý tưởng:

"Từ vị trí này, lấy tiếp 20 dòng."


Vì sao Cursor Pagination hữu ích?

Với dữ liệu rất lớn.

Cursor Pagination có thể phù hợp hơn OFFSET trong nhiều trường hợp.

Ví dụ:

Facebook feed
Twitter/X feed
Activity log
Messages
Large datasets

Bạn không nhất thiết phải biết:

"Đây là trang 58.321."

Bạn chỉ cần:

"Cho tôi phần tiếp theo."

😄


OFFSET vs Cursor

Có thể nhớ đơn giản:

OFFSET PaginationCursor Pagination
Dùng page/offsetDùng cursor
Dễ hiểuPhức tạp hơn
Phù hợp nhiều websiteHữu ích với dữ liệu lớn/luồng dữ liệu
Có thể gặp vấn đề khi OFFSET rất lớnThường tránh việc bỏ qua lượng lớn dòng
Dễ làm UI số trangHợp với Load More/Infinite Scroll

Không có kiểu nào:

"Luôn luôn tốt hơn."

Chọn cách nào phụ thuộc vào dữ liệu và nhu cầu ứng dụng.


Một vấn đề rất thú vị: dữ liệu thay đổi 😵

Giả sử bạn đang xem:

Page 1

Có:

A
B
C
D
E

Trong lúc bạn đang xem.

Một sản phẩm mới:

X

được thêm vào đầu danh sách.

Bạn bấm:

Page 2.

Có thể dữ liệu đã dịch chuyển.

😅

Bạn có thể:

  • Thấy dữ liệu trùng.
  • Bỏ sót dữ liệu.
  • Thứ tự thay đổi.

Đây là một trong những lý do Pagination thực tế cần có:

ORDER BY ổn định.


Đừng Pagination mà quên ORDER BY 😭

Ví dụ:

SELECT *
FROM products
LIMIT 20 OFFSET 20;

Bạn chưa nói rõ:

"Sắp xếp theo cái gì?"

Database không nên được yêu cầu trả một thứ tự mà ứng dụng lại kỳ vọng là ổn định nếu query không xác định thứ tự.

Thường bạn sẽ muốn:

SELECT *
FROM products
ORDER BY id DESC
LIMIT 20 OFFSET 20;

Ví dụ:

ID mới nhất lên trước.

Điều này giúp Pagination dễ dự đoán hơn.


Pagination + API + JSON

Bây giờ hãy ghép tất cả lại.

Frontend:

GET /api/products?page=2&limit=20

REST API.

Backend đọc:

page = 2
limit = 20

Tính:

offset = 20

Database:

SELECT *
FROM products
ORDER BY id DESC
LIMIT 20 OFFSET 20;

Backend nhận dữ liệu.

Trả JSON:

{
  "data": [
    {
      "id": 21,
      "name": "Product A"
    }
  ],
  "pagination": {
    "page": 2,
    "limit": 20
  }
}

Frontend hiển thị.

🎉

Đây chính là một API Pagination cơ bản.


"Chua thêm" lần 2 🤣

Hãy tưởng tượng bạn có:

100.000 cuốn sách.

Bạn gọi nhân viên thư viện:

"Anh mang cả 100.000 cuốn ra đây."

Nhân viên:

😐

"Anh đọc hết thật à?"

Bạn:

"Không. Tôi chỉ xem 20 cuốn."

Nhân viên:

💀

"..."

Đó chính là API không có Pagination.

🤣

Còn API có Pagination:

"Cho tôi 20 cuốn đầu."

Nhân viên:

"OK."

Bạn:

"Cho 20 cuốn tiếp."

Nhân viên:

"OK."

Đỡ khổ cho tất cả mọi người.


API Pagination trong Laravel

Khi bạn học Laravel.

Bạn sẽ thấy Pagination được hỗ trợ rất thuận tiện.

Ví dụ ý tưởng:

$products = Product::paginate(20);

Laravel có thể giúp tạo dữ liệu Pagination.

Response API có thể chứa:

data
current_page
last_page
per_page
total
next_page_url
prev_page_url

Tùy cách bạn xây dựng API Resource/response.

Điều này giúp bạn không phải tự viết toàn bộ logic Pagination từ đầu.

😎

Đây cũng là một trong những lý do Framework như Laravel rất được ưa chuộng.


Pagination và SEO có liên quan không? 🔎

Có.

Nhưng cần phân biệt:

API Pagination

Web Page Pagination

không hoàn toàn giống nhau.

Ví dụ website blog có:

/blog?page=1
/blog?page=2
/blog?page=3

đây là Pagination của trang web.

Còn:

/api/posts?page=2&limit=20

là Pagination của API.

Nếu website sử dụng API để tải nội dung.

Bạn cần thiết kế URL, rendering và crawling phù hợp để Search Engine có thể hiểu nội dung quan trọng.

Đặc biệt với SEO.

Đừng nghĩ:

"Có API là Google tự động hiểu hết."

😅

API phục vụ dữ liệu.

SEO còn phụ thuộc vào cách website của bạn render và liên kết nội dung.


Một lỗi newbie rất hay gặp 🤦

Bạn làm API:

/api/products

và mặc định:

limit = 10000

Bạn nghĩ:

"Cho nhiều một chút cho tiện."

🤣

Sau vài tháng.

Database:

1.000.000 products

Request:

GET /api/products

Server:

"Cho tôi xin nghỉ phép."

💀

Tốt hơn là có:

default limit = 20
max limit = 100

và bắt Client phải Pagination.


Một lỗi khác: cho Client tự do limit

Người dùng gửi:

?limit=999999999

Nếu Backend không kiểm soát.

Có thể xảy ra:

Performance problem.

Vì vậy:

Client request
      ↓
Validate
      ↓
Clamp limit
      ↓
Database

Ví dụ:

limit < 1
→ dùng 20

limit > 100
→ dùng 100

Debug API Pagination kiểu dev thật 🔍

API của bạn trả dữ liệu sai.

Ví dụ bạn yêu cầu:

page=2
limit=20

nhưng lại thấy sản phẩm của page 1.

Đừng đoán.

Kiểm tra từng bước.

1. Query Parameter

Có thực sự nhận:

page=2
limit=20

không?


2. Kiểu dữ liệu

page có phải số không?

limit có phải số không?


3. Công thức OFFSET

Có đúng:

(page - 1) × limit

không?


4. SQL

Kiểm tra:

LIMIT
OFFSET
ORDER BY

5. Response

API có trả:

page
limit
total

đúng không?


6. Frontend

Frontend có thực sự gọi:

page=2

hay vẫn gọi:

page=1

🤣


Một mẹo cực hay 💡

Khi debug Pagination.

Hãy thử dữ liệu rất nhỏ.

Ví dụ Database có:

1
2
3
4
5
6
7
8
9
10

Đặt:

limit = 3

Bạn sẽ dễ kiểm tra:

Page 1 → 1 2 3
Page 2 → 4 5 6
Page 3 → 7 8 9
Page 4 → 10

Nếu làm đúng với 10 dòng.

Sau đó mới thử:

1000
10.000
100.000

Đây là cách debug rất hiệu quả.


Checklist chuẩn không cần chỉnh 😎

  • Pagination = chia dữ liệu lớn thành nhiều phần nhỏ.

  • Không nên trả hàng trăm nghìn dòng trong một API response nếu không cần thiết.

  • page = trang hiện tại.

  • limit = số dòng mỗi trang.

  • OFFSET = (page - 1) × limit.

  • LIMIT + OFFSET là cách Pagination phổ biến.

  • Luôn validate pagelimit.

  • Nên đặt maximum limit.

  • Nên có ORDER BY ổn định.

  • API response nên trả thông tin Pagination phù hợp.

  • Infinite Scroll vẫn thường tải dữ liệu theo từng phần.

  • Cursor Pagination phù hợp với một số hệ thống dữ liệu lớn.

  • Đừng để Client yêu cầu một limit khổng lồ.

  • Debug bằng DevTools → Network.

  • Test với 10 dòng trước khi test 100.000 dòng.


FAQ nhanh

API có bắt buộc phải Pagination không?

→ Không. Nếu dữ liệu rất nhỏ thì có thể không cần. Nhưng với danh sách lớn, Pagination thường là lựa chọn rất hợp lý.

limitpage khác nhau thế nào?

limit cho biết mỗi lần lấy bao nhiêu dòng. page cho biết đang lấy phần dữ liệu thứ mấy.

Pagination có làm Database nhanh hơn không?

→ Không phải lúc nào cũng vậy, nhưng nó giúp giảm lượng dữ liệu cần lấy và truyền trong mỗi request. Hiệu năng thực tế còn phụ thuộc query, index, Database và cách Pagination được triển khai.

LIMIT có phải Pagination không?

LIMIT chỉ là một công cụ SQL. Pagination là cả cơ chế chia dữ liệu thành nhiều phần; LIMIT thường được sử dụng như một phần của cơ chế đó.

Infinite Scroll có phải Pagination không?

→ Về mặt giao diện thì khác Pagination truyền thống. Nhưng phía Backend thường vẫn cần cơ chế chia dữ liệu thành từng batch/page/cursor.

Cursor Pagination có tốt hơn OFFSET không?

→ Không phải lúc nào cũng tốt hơn. Cursor có thể phù hợp hơn với dữ liệu rất lớn hoặc các feed liên tục thay đổi, trong khi OFFSET dễ hiểu và phù hợp với nhiều trường hợp thông thường.


Bạn có thể cũng đang gặp 😭

👉 LIMIT trong MySQL là gì? 

👉 Pagination là gì? 

👉 REST API là gì? 

👉 API là gì? (Giải thích như gọi đồ ăn.)

👉 JSON là gì? (Tại sao API gần như lúc nào cũng trả về JSON?)

👉 API Authentication là gì? 

👉 CRUD là gì? 

👉 JOIN là gì? 

👉 Database chạy chậm dần vì sao?

👉 Backup Database đúng cách trước khi "toang"

👉 var_dump() là "vũ khí bí mật" của newbie

👉 echo debug và nghệ thuật "soi từng bước"

👉 Vì sao bug lúc có lúc không?


Tổng kết

API Pagination nghe có vẻ là một thuật ngữ khá "Backend".

Nhưng thực chất ý tưởng rất đơn giản:

Đừng bắt Server bê cả thư viện ra bàn khi khách chỉ muốn đọc 20 cuốn sách. 🤣

Thay vì:

GET /api/products

→ 100.000 sản phẩm

hãy chia thành:

GET /api/products?page=1&limit=20

20 sản phẩm.

Tiếp:

GET /api/products?page=2&limit=20

20 sản phẩm tiếp theo.

Và cứ thế.

Toàn bộ chuỗi mà chúng ta đã học bắt đầu nối lại:

Frontend
   ↓
REST API
   ↓
Authentication
   ↓
Pagination
   ↓
Backend
   ↓
SQL
   ↓
Database
   ↓
JSON
   ↓
Frontend

🎯

Đây chính là tư duy Backend rất quan trọng:

Không chỉ làm cho API "chạy được". Phải làm sao cho API chạy hợp lý khi dữ liệu ngày càng lớn.

Một website có 10 sản phẩm chạy tốt không có nghĩa nó sẽ chạy tốt với:

100.000 sản phẩm.

Và đó chính là lúc tư duy của một người mới bắt đầu chuyển dần sang tư duy của một developer thực tế. 😎