Skip to main content

Pagination, Filtering, and Sorting Without Bad Endpoints

How to design list endpoints that stay predictable as volume grows, filters combine, and clients need to navigate without surprises.

Andrews Ribeiro

Andrews Ribeiro

Founder & Engineer

Track

Senior Full Stack Interview Trail

Step 9 / 14

The problem

Almost every API starts with an innocent list endpoint.

Something like:

GET /orders

At first it works. Then new needs appear:

  • filter by status
  • sort by date
  • search by customer
  • paginate instead of returning everything
  • show total results

If the team keeps adding those things through improvisation, the endpoint turns into a bag of parameters with no clear contract.

And the result is usually bad on both sides:

  • the client does not know what to expect
  • the database pays for weak queries

Mental model

A list endpoint needs to answer four questions clearly:

  1. Which data set am I looking at?
  2. Which slice of that set do I want?
  3. In what order does that slice appear?
  4. How do I navigate it in parts without getting lost?

If one of those answers stays vague, the API starts creating surprises.

And list endpoints with surprises become bugs very easily.

Breaking the problem down

Filtering needs clear semantics

Filtering is not just “accept any query param.”

You need to make clear:

  • which fields can be filtered
  • whether the filter is exact, ranged, or textual
  • how filters combine
  • what happens when a value is invalid

When that is not clear, each client invents its own expectation.

Sorting needs to be stable

This point goes unnoticed until pagination starts breaking.

If you sort only by created_at, two rows may have the same value.

If new items arrive between one page and the next, the client may see:

  • repeated item
  • skipped item
  • inconsistent navigation

That is why list sorting usually needs a tie-breaker.

Something like:

  • created_at desc
  • id desc

That makes the order deterministic.

Pagination is not just cutting data into blocks

The two most common strategies are:

  • offset
  • cursor

Offset is simple:

?limit=20&offset=40

It works well in smaller lists, administrative reports, and scenarios where the user wants to go to “page 3.”

But it suffers more when:

  • the table is very large
  • sorting is expensive
  • new data keeps arriving all the time

Cursor is usually better when the list is alive and the order needs to stay stable:

?limit=20&cursor=eyJjcmVhdGVkX2F0Ijoi...

It is not magic. It simply models the idea of “continue from here” better.

The response needs to match the cost

Not every list endpoint needs to return:

  • total
  • page_count
  • numbered pages

In some cases, calculating the exact total on every request is too expensive.

So a better contract may be:

  • list of items
  • next_cursor
  • has_more

Promise less, but promise something reliable.

Good endpoints also protect themselves

Overly open listing endpoints invite abuse and regressions.

It is worth limiting:

  • maximum limit
  • which fields may be sorted
  • which filters are allowed
  • combinations that are too expensive

This is not anti-product.

It is part of the contract.

A field that looks nice in UI should not automatically become free-form sort just because it exists on screen.

Exposed sorting without criteria is often just a fast way to push hidden cost into the database.

Simple example

Imagine an orders endpoint:

GET /orders?status=paid&created_from=2026-03-01&sort=created_at:desc,id:desc&limit=20

Response:

{
  "items": [
    {
      "id": "ord_103",
      "status": "paid",
      "created_at": "2026-03-23T10:30:00Z"
    }
  ],
  "next_cursor": "eyJjcmVhdGVkX2F0IjoiMjAyNi0wMy0yM1QxMDozMDowMFoiLCJpZCI6Im9yZF8xMDMifQ==",
  "has_more": true
}

What this contract makes clear:

  • status is an exact filter
  • created_from defines a time slice
  • sorting has a main field and a tie-breaker
  • navigation continues through a cursor

Now compare that with a confusing endpoint:

GET /orders?filter=paid&sort=recent&page=2

Here almost everything is missing:

  • what exactly does filter filter?
  • what does recent mean?
  • what stable order does page=2 depend on?

The problem is not that the syntax is short.

It is that the contract is weak.

Common mistakes

  • Mixing textual search, exact filters, and ranges inside the same generic parameter.
  • Sorting by an unstable field and then blaming pagination.
  • Exposing any field for sort without thinking about index and cost.
  • Returning exact total every time even when that cost is too high.
  • Choosing offset or cursor because of trend, not because of list behavior.

How a senior thinks about it

People with more experience look at listings as a product contract and an operational contract at the same time.

The reasoning usually sounds like:

This endpoint needs to be predictable for the consumer and sustainable for the operator.

That changes the conversation.

It is no longer about “which query param looks nicer.”

It becomes about:

  • clear semantics
  • deterministic ordering
  • controlled cost
  • evolution without breaking clients

What the interviewer wants to see

In interviews, this topic appears a lot when you are asked to design a list API.

The evaluator wants to see whether you think beyond superficial CRUD.

Your level rises when you:

  • talk about stable ordering before pagination breaks
  • separate the simplicity of offset from the more stable behavior of cursor
  • mention the cost of total, free-form sort, and filters without indexes
  • treat listing as a predictable contract, not CRUD with makeup

A strong answer often sounds like this:

I would treat listing as a contract. First I would define clear filters, then deterministic ordering, and only after that would I choose between offset and cursor. If the list changes a lot and grows a lot, cursor is usually safer.

A bad listing endpoint does not fail only in the database. It fails in the confidence of whoever tries to use the API without guessing behavior.

Quick summary

What to keep in your head

Practice checklist

Use this when you answer

You finished this article

Part of the track: Senior Full Stack Interview Trail (9/14)

Next article REST vs GraphQL vs RPC: When Each One Fits Previous article Workload Affinity Without Turning Scaling Into a Lottery

Keep exploring

Related articles