skip to content

Which combinations of a connection's first/after/last/before must a server reject?

level: middleimportance: should knowfreq 46%

answer

  1. One mandated refusal, several local choices
  2. Discouraged is not forbidden
  3. Below zero is the specified error
  4. Nothing is said about no arguments
  5. A cap the convention never gives you

basics

~20 s

Only a negative first or last is an error the Relay pagination algorithm mandates. Supplying both first and last is discouraged but defined. Neither count, and a request above your maximum page size, are unspecified — the server decides.

solid answer

~50 s

The Relay cursor-connections pagination algorithm mandates exactly one refusal: if `first` or `last` is negative, throw an error. Everything else is local policy. Supplying `first` and `last` together is *strongly discouraged* by that specification but not forbidden, and the algorithm still defines the outcome — narrow by the cursors, keep the first N, then keep the last M of what remains. Supplying neither count is not addressed at all, so a server must choose between a default page size and a request error demanding one of them; returning every edge is the option that looks like a default and is really an unbounded read. A maximum page size is likewise unspecified and effectively mandatory. Whether to clamp an over-large `first` or reject it is a design call: erroring is honest, clamping silently rewrites the request the client actually wrote.

code

graphql · 7 lines
graphql
query BothCounts {
  campaign(id: "cmp-4718") {
    donations(after: "b3B*Mjo3NDE5", first: 10, last: 2) {
      edges { cursor node { amountMinor } }
    }
  }
}

go deeper

for a junior

Know that the counts are whole numbers and that asking for a negative page is a client error, not something the server should quietly fix. Recall that most servers cap how many edges one page may contain.

for a middle

Be able to separate the mandated error from the discouraged combination from the unspecified cases, and to work through what supplying both counts produces step by step rather than calling it invalid.

for a senior

Show that a connection without a default and a maximum is an unbounded read with a paginated shape, and argue the clamp-versus-error tradeoff in terms of what it does to existing callers.

for a principal

Own the policy as a platform contract: one default and one ceiling applied consistently across every connection, decided before the first release rather than retrofitted after an incident, and documented because the convention will not document it for you.

## Exactly one refusal is specified Read the Relay cursor-connections pagination algorithm looking for the word *error* and you find it in one place, twice: if `first` is set and is less than zero, throw an error; if `last` is set and is less than zero, throw an error. That is the whole of the mandated validation. `first: -1` is not clamped to zero, not ignored, and not quietly treated as "give me everything" — it is a bad request, and the honest response is a request error naming the argument. Everything else people assume is a rule is either **defined but discouraged**, or **not addressed at all**. Being able to sort a connection's argument handling into those three buckets — specified error, specified but discouraged, unspecified — is the actual interview question, because it is the difference between a candidate who has read the convention and one who has absorbed folklore. ## Both counts at once: defined, and discouraged The specification calls supplying both `first` and `last` **strongly discouraged**, on the grounds that it leads to confusing queries and confusing results. It does not forbid it, and the algorithm does not special-case it. The steps run in order: 1. Narrow by the cursors. 2. If `first` is set, keep the first N, discarding from the end. 3. If `last` is set, keep the last M of *that*, discarding from the start. So `first: 10, last: 2` over a campaign's donation list means "take the first ten donations after the cursor, then hand me only the last two of those ten" — donations nine and ten. It is a well-defined window, it is just one almost nobody means to ask for. A server may reject the combination as its own policy, but doing so is a local rule, not the convention speaking. ## Neither count: your decision, and you must make one The convention says nothing about a client that selects `donations` with no `first` and no `last`. Two defensible answers exist, and a team must pick one deliberately: - **Apply a default page size.** Friendly, and the request always succeeds. The risk is that clients never learn they are paginating. - **Reject the request** with a request error demanding `first` or `last`. Blunt, and it makes the contract explicit at the cost of breaking naive first attempts. What is *not* defensible is the third option, which is what an unconfigured connection usually does: return every edge. That is an unbounded read of the whole relation dressed up as a paginated field. ## The maximum, and the incident that teaches it A maximum page size is also unspecified, and it is not optional. In a graph fronting eleven backing services, one `donations` connection shipped with neither a default nor a cap. A reporting client selected it with no arguments, the field read the campaign's entire donation history — 1,284,517 edges — and the request took the service down at month end. The team's first reaction was to put a short-lived response cache in front of the field, which stopped the outage and immediately created a worse one: the donation totals on the campaign page were now minutes stale, and fundraisers were reading numbers that no longer matched the ledger. Caching a field to hide an unbounded read trades an availability bug for a correctness bug. The real fix was three lines of policy on the connection: a default of 23 edges when neither count is given, a hard maximum of 97, and a request error when a client asks for more. ## Clamp or error above the maximum? Both are used, and the tradeoff is worth being able to argue. **Clamping** — silently serving 97 when the client asked for 500 — never breaks a caller and is easy to roll out. But it changes the request the client wrote, and a client that trusts the count it asked for will misread a full page as a partial one. Any client relying on the returned count as a termination signal is now wrong. **Erroring** is louder and honest: the caller learns the ceiling exists and learns it at the moment it was crossed, rather than discovering it as a subtle data bug later. It does break existing callers on the day you introduce the cap, which is exactly why the cap should be part of the field's first release rather than a later rescue. A middle path — clamp, and surface the applied limit in the response `extensions` — keeps callers working while making the truncation observable. Nothing in the convention describes it, so it is a local contract you must document. ## What to say in an interview Say the negative-count error is the only refusal the convention mandates; say both counts together are legal but discouraged, and describe the window it produces; then say that the default and the maximum are yours to choose and that a connection without a maximum is not really paginated. That last sentence is the one that signals production experience.

  • What window does `first: 10, last: 2` actually produce?
    The cursors narrow the range first. Then `first: 10` keeps the first ten edges, discarding from the end. Then `last: 2` keeps the last two of those ten, discarding from the start — so you receive the ninth and tenth edges of the range. It is well defined, which is why the specification only discourages it rather than banning it; the objection is that almost nobody means to ask for that window.
  • Should an over-large `first` be clamped or rejected?
    Rejecting is more honest: the caller learns the ceiling at the moment it is crossed instead of discovering later that a full page was not the page it asked for. Clamping never breaks a caller, which is why it is common, but it silently rewrites the request and misleads any client that treats the returned count as a termination signal. If you clamp, surface the applied limit in the response `extensions` so the truncation is at least observable.
  • Is `first: 0` an error?
    No. The specified error is for a count *less than* zero. Zero passes the check and simply keeps no edges after the cursors have narrowed the range, so the client gets an empty `edges` list while the connection's other fields still resolve. Servers that reject zero are imposing a local rule.

saying these in an interview costs you the question

  • Says the specification forbids first with last
  • Clamps a negative count to zero instead of erroring
  • Assumes the convention defines a default page size
  • Ships a connection with no maximum page size
  • Caches an unbounded field instead of bounding it
  • Thinks omitting both counts means return everything

context