Keyboard Shortcuts N Next post
P Previous post
S Save / unsave
R Read aloud
T Toggle theme
/ Focus search
Esc Close panels
🔥
Ready to read...
API design APIs APIs From Zero to Real-World API Testing & Automation Module 3 — REST APIs REST API

Pagination, Filtering, and Sorting in REST APIs

Reviewed & accurate
AI Summary

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?

StyleWhen to use
Offset/LimitSmall datasets (<10k items), simple APIs
CursorLarge datasets, infinite scroll, real-time feeds
KeysetOrdered by ID, stable data
Page-based with totalUIs 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 — ascending
  • sort=-name — descending (the minus prefix is widely used)
  • sort=last_name,first_name — sort by last name, then first name
  • order=asc or order=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

  1. No pagination at all. Returning 100k items in one response crashes clients.
  2. Default limit too high. If ?limit= defaults to 1000, you might as well have no limit. Default to 20–50.
  3. No max limit. A client sending ?limit=999999 should be capped, not honored.
  4. Inconsistent sort conventions. sort=-name in one endpoint, sort=name&order=desc in another. Pick one style.
  5. Filter parameters that don't match field names. If the field is created_at, the filter should be ?created_at=, not ?date= or ?created=.
  6. 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, __contains for operators.
  • Sorting: ?sort=field ascending, ?sort=-field descending. 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.
Course continuity
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.

Test Your Knowledge
How did you find this?

Comments

Join the discussion! Sign in with your Google or Blogger account, or comment as Anonymous - no account needed. For quick questions, also reach me on Telegram @cytestch.

Comments