# Pagination, Filtering and Sorting — Routebase

> Offset against cursor and what each costs when the data changes underneath, why limits are not optional, and how much of the filter and sort syntax is actually standardised.

Canonical page: https://routebase.dev/guides/api-design/api-pagination-filtering-and-sorting/
Chapter 5 of 10 · API Design · Last reviewed 2026-09-13 · The Routebase Team

Every collection endpoint eventually returns more than anyone wants in one response. Adding pagination afterwards is a breaking change, and shipping it on the first day costs a query parameter.

## Offset and cursor answer different questions

Offset pagination names a position, so the caller asks to skip a number of rows and take the next few. Cursor pagination names an item, so the caller asks for everything after the token the previous page handed back.

The distinction is invisible on static data and decisive on data that moves.

_Figure: Between the two requests a new row is inserted at the top of the ordering. The offset request skips the same number of rows as before, so the row that was last on page one is now first on page two and the caller sees it twice. The cursor points at an item rather than a count, so the second page starts exactly where the first one ended, and a deletion instead of an insertion is the same story with a row silently skipped._

That duplication and skipping is not a rare edge case on a busy collection. It is the normal behaviour of offset pagination whenever writes and reads overlap, and it produces bugs that reproduce only under load.

## What each one gives up

Cursor pagination is the safer default, and it is not free.

A cursor cannot jump. There is no page seven, because the token encodes a position in one specific traversal, so a caller who wants deep navigation has to walk. A cursor also usually arrives without a total count, since counting the whole set defeats the reason the query was fast. Both of those are user-visible in any interface with numbered pages.

Offset gives you exactly those two things and pays for them with correctness under concurrency and with performance at depth, because a large offset still makes the database walk the rows it is skipping.

So the decision is about the collection rather than about the technique. A slow-changing reference list that users page through visually is a reasonable place for offset. An event feed, an audit log or anything a machine iterates is a place for cursors.

Whichever you choose, the cursor format is your business. There is no standard for it, and treating it as opaque in the documentation keeps it changeable, because a caller who decodes your token has turned an implementation detail into a contract.

## Limits are part of the design

Two numbers need to exist before the first caller arrives, namely a default page size and a maximum.

The default matters because a caller who omits the parameter has to get something sensible rather than the whole table. The maximum matters because otherwise the parameter is a request for as much work as the caller feels like asking for, which is a denial of service you built yourself.

When a caller asks for more than the maximum, clamping is friendlier than rejecting, provided the response states the size that was actually applied. Silently returning fewer items than requested without saying so is the version that produces a client which thinks it has reached the end.

## Filtering and sorting are conventions

There is no specification for filter syntax. Every style you have seen, whether that is a parameter per field, a bracketed operator or a small query language, is a house convention, and none of them is more correct than the others.

That means the value is entirely in consistency, which is covered in its own [chapter](/guides/api-design/consistency-across-an-api/). Two decisions are worth making explicitly. Decide whether an unknown filter parameter is ignored or rejected, because ignoring it turns a typo into a silently wrong result set. And decide whether multiple filters combine with and or with or, since callers will assume one of them.

Sorting needs the same treatment plus one extra rule. A sort that is not total is not stable, so two rows with the same value can swap places between requests and break pagination in both styles. Appending a unique tiebreaker to every sort is the fix, and it is invisible to the caller.

The one standardised piece in this area is where the links live. [RFC 8288](https://www.rfc-editor.org/rfc/rfc8288) defines the `Link` header field, and the `next` and `prev` relation types are registered, so pagination links have a defined home outside your body envelope.

## In Routebase

Pagination parameters are the clearest case for defining a parameter once and referencing it everywhere, because they repeat on every list endpoint in the API.

A parameter component fixes the name, location, type, format, default and whether it is required. Each component also lists the endpoints that use it, so changing the maximum page size shows you the blast radius first. Specifications imported from OpenAPI keep their referenced parameters as components rather than flattening them into copies.

Two style guide rules touch this chapter. One flags a `GET` that returns a collection without pagination parameters. The other flags a success response that returns a bare array instead of an object, which is what pushes list responses towards an envelope with somewhere to put paging information. Both ship below error severity and are configurable per organisation and per project. Routebase does not ship a pagination envelope of its own, so the shape is yours to define, and the shared library is where it belongs once a second project needs the same one. The [Parameters](https://docs.routebase.dev/parameters/) and [Components](https://docs.routebase.dev/components/) guides cover reuse, and the [Style Guide](https://docs.routebase.dev/style-guide/) covers the rules.

## Frequently asked questions

### What is the difference between offset and cursor pagination?

Offset pagination asks for a position in the result set, so page three means skip the first two pages worth of rows. Cursor pagination asks for everything after a specific item, using a token the server issued with the previous page. The difference shows up when rows are inserted or deleted between two requests, because an offset then points somewhere else than it did and a cursor still points at the same item.

### Is cursor pagination always better than offset?

It is the safer default on data that changes, and it gives up two things people want. A cursor cannot jump to page seven, and it usually comes without a total count, because computing one defeats the reason cursors are fast. On a small, slow-changing collection where users expect numbered pages, offset is the better answer.

### Should an API have a maximum page size?

Yes, and it should apply whether or not the caller asks for one. Without a default the first caller who omits the parameter gets the whole table, and without a maximum a caller who wants everything asks for it in one request. Clamping an oversized request to the maximum is friendlier than rejecting it, as long as the response says what was applied.

### Is there a standard for API filtering and sorting?

Not for the syntax. There is no specification that says filters look like one thing or that sort parameters take a leading minus for descending, so every convention you see is a house style. The one standardised piece nearby is the Link header from RFC 8288, whose next and prev relation types give pagination links a defined home.

---

[Routebase](https://routebase.dev/) — [Sign up](https://app.routebase.dev/): Every account starts with a 14-day Pro trial — no credit card required.
