API Filtering, Sorting, Pagination, and Field Selection

Build collection endpoints that let clients narrow, order, page through, and shape results without slow queries or unstable response behavior.

published: reading time: 7 min read author: GeekWorkBench
Quick Summary

Collection endpoints need predictable rules for filters, sorting, pagination, and field selection. This guide compares offset and cursor pagination, shows how to keep ordering stable and page sizes bounded, and explains how allowlists and tenant checks limit query cost and data exposure. It also covers reused cursors, expensive counts, and unindexed filters, with practical controls for keeping collection APIs safe to operate.

API Filtering, Sorting, Pagination, and Field Selection

Introduction

Collection endpoints often need more than a single fixed response: clients may need to filter records, choose a stable order, move through pages, or request only a few fields. Each option changes the API contract and the database work required to serve it.

This guide compares offset and cursor pagination, shows how to validate filters and field masks, and explains why authorization scope and bounded query cost matter. It also covers failure modes and monitoring for collection APIs.

Pagination choices

Offset pagination is easy to understand: limit=25&offset=50. It works well for small datasets, but deep offsets can require scanning many rows, and insertions between page requests can shift results. Cursor pagination instead continues after a stable position. The cursor should be opaque to clients and encode or reference the last sort values, including the unique tie-breaker. Cursors are not automatically snapshots; if a client needs a consistent export while data changes, use a snapshot or export job.

GET /api/orders?status=shipped&limit=25&sort=-createdAt HTTP/1.1
Accept: application/json

HTTP/1.1 200 OK
Content-Type: application/json

{"items":[{"id":"ord_92","status":"shipped"}],"nextCursor":"cD0yMDI2LTA5LTMw...","hasMore":true}

Query and response flow

flowchart LR
    A[Parse query parameters] --> B[Validate allowlisted fields]
    B --> C[Apply tenant and permission scope]
    C --> D[Add deterministic ordering]
    D --> E[Fetch bounded page]
    E --> F[Return items and continuation cursor]

Authorization scope must be applied before filters and pagination. Otherwise the API may leak counts or return cursors over records the caller cannot access.

When to use and when not to use

Use filters, sorting, and pagination for collections that may grow or are searched in user interfaces. Offer field selection when representations are large and client needs differ. Keep a narrow fixed response if the object is small; dynamic field masks can complicate caching, schemas, and authorization. Do not promise arbitrary filtering or sorting unless you can support its query cost and index behavior.

Trade-off Analysis

Feature Strength Cost
Offset pagination Simple page numbers and links Deep scans and shifting pages
Cursor pagination Efficient continuation on indexed order Less natural page jumps; cursor lifecycle
Field selection Smaller payloads More response variants and validation
Flexible filters Serves varied clients Query planning, indexing, and abuse risk

Implementation snippet

Parse a small allowlist, clamp limits, enforce scope, and use a stable order:

const SORTS = { createdAt: "created_at", id: "id" } as const;
function parseListQuery(url: URL) {
  const requestedLimit = Number(url.searchParams.get("limit") ?? 25);
  if (!Number.isSafeInteger(requestedLimit) || requestedLimit < 1) {
    throw new HttpError(400, "invalid_limit");
  }
  const limit = Math.min(requestedLimit, 100);
  const sort = url.searchParams.get("sort") ?? "-createdAt";
  const field = sort.startsWith("-") ? sort.slice(1) : sort;
  if (!Object.hasOwn(SORTS, field)) throw new HttpError(400, "invalid_sort");
  return {
    limit,
    sortColumn: SORTS[field as keyof typeof SORTS],
    descending: sort.startsWith("-"),
  };
}

The repository query should include tenant scope, then the validated filters, then an index-backed order and limit + 1 to determine whether another page exists.

Production failure scenarios and mitigations

A client requests limit=1000000, exhausting memory; clamp limits and reject unreasonable values. A sort parameter interpolates raw SQL and enables injection; map known names to trusted columns. Offset pagination skips or repeats rows during frequent inserts; use cursor pagination over a deterministic order. A cursor is reused after permissions change; reapply authorization on every page and treat cursors as navigation hints, not access grants. An unindexed filter drives database CPU high; monitor query plans and restrict supported combinations.

Observability checklist

  • Track requested and effective page sizes, response bytes, and query latency.
  • Measure slow query rates by normalized filter and sort shape, not raw sensitive values.
  • Watch invalid query rates and cursor expiration or decode failures.
  • Alert on high-cardinality filter patterns that correlate with database saturation.

Security notes and pitfalls

Treat all query values as untrusted. Allowlist fields and operators, parameterize values, enforce per-tenant scope, and never expose fields that authorization forbids. Opaque cursors may be signed to detect tampering, but still reauthorize each request. Common mistakes include unstable sorting, unlimited page size, leaking total counts, interpreting empty filters inconsistently, and assuming a cursor itself grants permission.

Quick Recap Checklist

  • Allowlist filter and sort fields, and validate requested values.
  • Use a deterministic sort order with a unique tie-breaker for cursor pagination.
  • Set a server-side maximum page size and define behavior for invalid limits.
  • Return only authorized fields and avoid exposing sensitive data through field selection.
  • Apply tenant permissions before pagination and measure query costs in production.

Interview Questions

1. Why does cursor pagination need a deterministic sort order?

The server needs a stable position from which to continue. A unique tie-breaker prevents records with equal primary sort values from moving unpredictably between pages.

2. When is offset pagination still a reasonable choice?

It works for small, relatively stable result sets where page-number navigation matters and offsets remain shallow. For large or frequently changing collections, cursor pagination is usually more robust.

3. Why should an API cap page size if the client requested a larger one?

A server-side cap protects memory, response bandwidth, and database capacity. The API can return the effective page size or reject invalid values according to its documented contract.

4. Why apply tenant authorization before paginating a collection?

Scope the query to records the caller may access before calculating pages or counts. Filtering unauthorized records afterward can expose their presence through result sizes, page gaps, or continuation tokens.

5. How is a cursor different from a snapshot of a collection?

A cursor identifies where to continue in an ordering, but later pages may still reflect live data changes. A snapshot fixes the dataset for the duration of an export or traversal and may require separate server-side state.

6. Why might an API avoid returning an exact total count on every collection request?

Counting can require an expensive query, especially with complex filters or large datasets. Counts can also reveal information about records a caller should not infer, so return them only when the cost and access rules are clear.

7. What should an API consider before offering field selection?

Allowlist selectable fields and enforce authorization for each one. Field selection can reduce payloads, but it creates more response variants to validate, document, cache, and test.

8. How should an API handle unknown filter or sort parameters?

Choose a consistent policy and document it. Rejecting unknown names catches typos early; ignoring them can support forward compatibility, but may silently return broader results than the caller intended.

9. Why should the server reauthorize a request that uses a previously issued cursor?

Permissions can change after the first page, and a cursor is only a navigation hint. Rechecking access on each request prevents an old token from bypassing current tenant or resource permissions.

Further Reading

Conclusion

Collection query options are part of the API contract and part of the server’s resource budget. Keep them bounded, predictable, permission-aware, and backed by measured query plans.

Category

Related Posts

API Request, Response, and Error Shapes Clients Can Trust

Design consistent API request and response envelopes, validation errors, and problem details so client developers can handle success and failure reliably.

#api-design #http #error-handling

API Resource Names, Relationships, and Collections

Model API URLs around stable resources and relationships, with clear collection behavior that clients can navigate without learning server internals.

#api-design #rest #resource-modeling

HTTP Methods, Status Codes, and Headers in API Design

Choose HTTP methods that match the operation, return status codes clients can act on, and use headers for metadata, caching, and safe retries.

#api-design #http #rest