REST API Filtering, Sorting, and Pagination
A real-world API never returns a single item at a time. It deals with lists — sometimes millions of records. Without tools to filter, sort, and paginate those lists, clients receive overwhelming amounts of data or have no way to find what they need. This topic shows you how to design these three essential features.
Why These Features Matter
Scenario: An e-commerce database has 50,000 products. ❌ Without filtering, sorting, pagination: GET /products → returns all 50,000 products in one response → Slow network, crashed app, poor user experience ✅ With filtering, sorting, pagination: GET /products?category=phones&sort=price&page=1&limit=20 → Returns 20 phones sorted by price — fast and usable
Filtering
Filtering lets the client ask for a subset of resources that meet certain conditions. All filtering happens through query parameters.
Basic filter examples: /orders?status=shipped /users?role=admin /products?inStock=true Multiple filters (AND logic): /products?category=electronics&inStock=true&maxPrice=5000 Range filters: /orders?createdAfter=2024-01-01&createdBefore=2024-06-30 /products?minPrice=500&maxPrice=2000
Designing Filter Parameters
Use descriptive, readable names: ✅ Clear filter names: ?status=active ?minPrice=100&maxPrice=500 ?createdAfter=2024-01-01 ?inStock=true ❌ Vague or cryptic: ?f1=active ?p=100-500 ?d=20240101
Searching
Searching is different from filtering. A filter checks exact or range matches. A search checks whether a text string appears in a field — like a search bar.
Filtering (exact match): /products?category=laptops → Returns products where category IS "laptops" Searching (text match): /products?search=wireless → Returns products where name or description CONTAINS "wireless" Combined: /products?category=electronics&search=wireless → Products in electronics that mention "wireless"
Use the parameter name search or q for full-text search queries.
Sorting
Sorting controls the order in which results are returned. Use a sort parameter for the field name and an order parameter for direction.
Single field sort:
/products?sort=price → sorted by price ascending (default)
/products?sort=price&order=desc → sorted by price descending
/products?sort=name → sorted alphabetically by name
Multi-field sort (some APIs support this):
/products?sort=category,price → sort by category first, then price
/products?sort=-price,name → descending price, then ascending name
─┬
└── minus sign = descending (a common shorthand)
Sort Parameter Design Options
Option A – separate sort and order params (clearest): /products?sort=price&order=desc Option B – minus sign prefix for descending (compact): /products?sort=-price Option C – comma-separated for multiple sorts: /products?sort=category,-price Pick one style and use it consistently across your whole API.
Pagination
Pagination splits a large collection into smaller pages. The client requests one page at a time instead of the whole collection.
Style 1: Page-Based Pagination
Parameters: page + limit (or pageSize)
Request: GET /products?page=2&limit=10
Server returns items 11-20 out of 143 total.
Response:
{
"data": [ ...10 products... ],
"page": 2,
"pageSize": 10,
"totalPages": 15,
"totalItems": 143
}
Diagram:
Page 1: [1–10] Page 2: [11–20] Page 3: [21–30] ...
Client requests one page at a time using ?page=N
Style 2: Cursor-Based Pagination
Parameters: cursor (a token pointing to a position in the dataset)
Better for large, real-time datasets where new items are added frequently.
First request:
GET /posts?limit=10
Response:
{
"data": [ ...10 posts... ],
"nextCursor": "eyJpZCI6MjB9" ← opaque token
}
Next page:
GET /posts?limit=10&cursor=eyJpZCI6MjB9
Response:
{
"data": [ ...next 10 posts... ],
"nextCursor": "eyJpZCI6MzB9"
}
No nextCursor in response = end of list.
Advantage over page-based:
If new posts are added at the top, page-based pagination shifts results
and shows the same post twice or skips posts. Cursor-based does not.
Page-Based vs Cursor-Based
┌──────────────────────┬────────────────────┬──────────────────────────┐ │ │ Page-Based │ Cursor-Based │ ├──────────────────────┼────────────────────┼──────────────────────────┤ │ Ease of use │ Simple │ More complex │ │ Jumping to page N │ Yes │ No │ │ Real-time data safe │ No │ Yes │ │ Best for │ Admin lists, │ Social feeds, timelines │ │ │ reports │ │ └──────────────────────┴────────────────────┴──────────────────────────┘
Combining All Three Features
Real-world API call: GET /products ?category=electronics ← filter &inStock=true ← filter &search=wireless ← search &sort=price ← sort by price &order=asc ← ascending &page=1 ← first page &limit=20 ← 20 items per page Full URL: /products?category=electronics&inStock=true&search=wireless&sort=price&order=asc&page=1&limit=20
Always Return Pagination Metadata
Response metadata the client needs:
{
"data": [ ...results... ],
"page": 1,
"pageSize": 20,
"totalItems": 143,
"totalPages": 8,
"hasNext": true,
"hasPrev": false
}
Without metadata, the client cannot build a pagination UI or know when it has reached the last page.
Key Points
- Filtering narrows results by exact match or range:
?status=active - Searching does text matching:
?search=wireless - Sorting controls order:
?sort=price&order=desc - Page-based pagination is simple and common; cursor-based is better for real-time data
- Always include total count, page info, and next/previous indicators in paginated responses
