What You'll Learn
- Why pagination, filtering, and sorting exist
- The four common pagination styles
- How to design filter and sort parameters
- What good API responses include for navigation
Why This Matters
An endpoint like GET /users could return 10 users or 10 million. Without pagination, the server tries to return all of them — slow, memory-hungry, and usually crashes the client. Without filtering, the client must download everything to find what it needs. Without sorting, results come back in arbitrary order. These three features turn a useless list endpoint into a useful one.
Pagination — The Four Styles
1. Offset / Limit pagination
The simplest style. The client specifies how many items per page and which page (or offset) to start at:
GET /users?limit=20&offset=40
# Returns items 41–60
Or equivalently with page numbers:
GET /users?limit=20&page=3
# Returns items 41–60 (page 3 of 20-per-page)
Pros: Easy to implement; lets clients jump to any page.
Cons: Slow for deep offsets — the database still scans through skipped rows. If items are inserted while paginating, pages shift.
2. Cursor pagination
The server returns a cursor — a token pointing to the last item returned. The client sends it back to get the next page:
GET /users?limit=20
# Returns first 20 users + next_cursor=abc123
GET /users?limit=20&cursor=abc123
# Returns next 20 users + next_cursor=def456
Pros: Fast even for deep pages; stable when items are inserted.
Cons: Cannot jump to an arbitrary page — only forward/back.
3. Keyset pagination
A specific kind of cursor pagination where the cursor is the ID of the last item:
GET /users?limit=20
# Returns users with IDs 1–20
GET /users?limit=20&after_id=20
# Returns users with IDs 21–40
4. Page-based (with total count)
Offset/limit plus a total count, useful for showing "Page 3 of 47":
GET /users?page=3&limit=20
{
"data": [...20 users...],
"pagination": {
"page": 3,
"limit": 20,
"total": 940,
"total_pages": 47
}
}
Which Pagination Style to Use?
| Style | When to use |
|---|---|
| Offset/Limit | Small datasets (<10k items), simple APIs |
| Cursor | Large datasets, infinite scroll, real-time feeds |
| Keyset | Ordered by ID, stable data |
| Page-based with total | UIs that show "Page X of Y" |
Filtering — Restricting What Comes Back
Filters are query parameters that match specific field values:
GET /users?role=admin → only admins
GET /users?status=active&city=Mumbai → active users in Mumbai
GET /products?min_price=100&max_price=500 → products in price range
Filter operators
For richer filtering, APIs use conventions like:
?created_at__gte=2026-01-01 → created on or after Jan 1
?name__contains=anita → name contains "anita"
?email__endswith=@example.com → email ends with this domain
The __gte, __contains convention comes from Django and is widely used.
Multiple values for one filter
?role=admin&role=editor → admins OR editors
?status=in,active → comma-separated values
Sorting — Controlling Order
Sort parameters specify the field and direction:
GET /users?sort=name → ascending by name
GET /users?sort=-name → descending (minus prefix)
GET /users?sort=last_name,first_name → multi-field sort
Conventions:
sort=name— ascendingsort=-name— descending (the minus prefix is widely used)sort=last_name,first_name— sort by last name, then first nameorder=ascororder=desc— alternative convention
Combining Pagination, Filtering, and Sorting
GET /users?role=admin&status=active&sort=-created_at&page=2&limit=20
Translation: "Give me page 2 (20 per page) of active admin users, sorted by creation date, newest first."
What Good Responses Include
A well-designed paginated response tells the client how to navigate:
{
"data": [
{"id": 21, "name": "Anita"},
{"id": 22, "name": "Ravi"}
// ... 20 items total
],
"pagination": {
"page": 2,
"limit": 20,
"total": 940,
"total_pages": 47,
"has_next": true,
"has_prev": true
},
"links": {
"self": "/users?page=2&limit=20",
"next": "/users?page=3&limit=20",
"prev": "/users?page=1&limit=20",
"first": "/users?page=1&limit=20",
"last": "/users?page=47&limit=20"
}
}
The links section is HATEOAS-lite — clients just follow URLs, no need to construct them.
Common Mistakes
- No pagination at all. Returning 100k items in one response crashes clients.
- Default limit too high. If
?limit=defaults to 1000, you might as well have no limit. Default to 20–50. - No max limit. A client sending
?limit=999999should be capped, not honored. - Inconsistent sort conventions.
sort=-namein one endpoint,sort=name&order=descin another. Pick one style. - Filter parameters that don't match field names. If the field is
created_at, the filter should be?created_at=, not?date=or?created=. - Forgetting to handle the "no results" case. Empty page should return
{"data": [], "pagination": {"total": 0}}, not 404.
Practical Exercise (5 minutes)
Call JSONPlaceholder and try these:
# Pagination via _page and _limit
curl "https://jsonplaceholder.typicode.com/posts?_page=2&_limit=5"
# Filtering via userId
curl "https://jsonplaceholder.typicode.com/posts?userId=1"
# Sorting via _sort and _order
curl "https://jsonplaceholder.typicode.com/posts?_sort=id&_order=desc"
Combine all three: "Posts by user 1, sorted by ID descending, page 1 with 3 per page."
Mini Challenge
Design the pagination, filtering, and sorting API for a "transactions" endpoint. The endpoint returns potentially millions of transactions. Decide: offset or cursor? What filters? What sort fields? What does the response look like? Write a sample request and response.
Key Takeaways
- Pagination: offset/limit for small datasets, cursor for large ones, keyset for ID-ordered data.
- Filtering: use query parameters that match field names. Use
__gte,__containsfor operators. - Sorting:
?sort=fieldascending,?sort=-fielddescending. Comma-separate for multi-field. - Always cap the limit (e.g., max 100) and provide a sensible default (e.g., 20).
- Include navigation links in paginated responses so clients can follow without constructing URLs.
Previously: Lesson 18 covered URL design.
Today: You learned how to handle large collections efficiently.
Next: Lesson 20 covers API versioning and idempotency — two concepts that keep APIs stable over time.
FAQ
What is the difference between offset and cursor pagination?
Offset pagination skips the first N items (e.g., OFFSET 40 LIMIT 20). Cursor pagination uses a token pointing to the last item returned (e.g., WHERE id > 60 LIMIT 20). Cursor is faster for deep pages and stable when data is inserted during pagination. Offset is simpler and allows jumping to any page.
How do I filter by date range?
Use two filters, one for start and one for end: ?created_at__gte=2026-01-01&created_at__lte=2026-12-31. Or use a single range parameter: ?created_at__range=2026-01-01,2026-12-31. The two-parameter form is more common and clearer.
Comments
Comments
Post a Comment