Pagination and Filtering
When an API endpoint could return thousands of records, you can't send them all at once. Pagination breaks large collections into manageable chunks. Filtering lets clients request only what they need. Together, they make APIs practical for real-world data volumes.
Offset-Based Pagination
The simplest approach uses offset and limit parameters:
GET /users?offset=20&limit=10
This requests 10 users starting from position 20. The response includes pagination metadata:
{
"data": [...],
"pagination": {
"total": 150,
"offset": 20,
"limit": 10
}
}
Offset pagination is intuitive and allows jumping to any page. However, it has problems with large datasets — counting total records is expensive, and results can shift if data changes between requests.
Cursor-Based Pagination
For large or frequently changing datasets, cursor-based pagination works better:
GET /users?cursor=abc123&limit=10
The cursor is an opaque token pointing to a specific position. The response provides the next cursor:
{
"data": [...],
"pagination": {
"nextCursor": "def456",
"hasMore": true
}
}
Cursors are stable even when data changes — you won't see duplicates or miss records. The tradeoff is you can't jump to arbitrary pages.
Filtering
Let clients request specific subsets of data:
GET /users?status=active&role=admin
Design filters to be combinable. Multiple filters typically use AND logic — the above returns users who are both active AND admins.
For more complex filtering, consider a query parameter syntax:
GET /products?price[gte]=100&price[lte]=500
GET /orders?createdAt[after]=2024-01-01
Sorting
Allow clients to control result order:
GET /users?sort=createdAt # Ascending (oldest first)
GET /users?sort=-createdAt # Descending (newest first)
GET /users?sort=lastName,firstName # Multiple fields
The minus prefix for descending order is a common convention. Document your sorting syntax clearly.
Combining Everything
A real request might combine all three:
GET /orders?status=pending&sort=-createdAt&cursor=abc123&limit=20
This fetches pending orders, newest first, continuing from a previous cursor, 20 at a time.
Default Limits
Always set reasonable defaults and maximums. Don't let clients request unlimited results:
limit = min(request.limit or 20, 100) # Default 20, max 100