What do the first/after and last/before arguments select on a GraphQL connection?
answer
- Two directions, four arguments
- One ordering, never rearranged
- Cursors narrow, counts slice
- Dropped from the end versus the start
- A tail is not a reversal
basics
~10 sfirst/after pages forward: take the first N edges after a cursor. last/before pages backward: take the last N edges before a cursor. Both slice the same fixed ordering, and neither reverses it.
solid answer
~50 sA connection field under the Relay server specification accepts four pagination arguments: `first: Int` and `after: String` for forward paging, `last: Int` and `before: String` for backward paging. The connection has one fixed order and the arguments only choose a window inside it. The cursors narrow the range first — `after` drops everything up to and including the edge that cursor names, `before` drops everything from that edge onward, so both bounds are exclusive. The counts then slice what is left: `first: N` keeps the first N, discarding from the end; `last: N` keeps the last N, discarding from the start. The order of what comes back is unchanged, so `last: 5` returns the final five edges in the connection's order, not reversed. A field may offer the forward pair, the backward pair, or both.
code
graphql · 23 linesquery FirstPage {
campaign(id: "cmp-4718") {
donations(first: 23) {
edges { cursor node { amountMinor donatedAt } }
}
}
}
query NextPage {
campaign(id: "cmp-4718") {
donations(first: 23, after: "b3B*Mjo3NDE5") {
edges { cursor node { amountMinor donatedAt } }
}
}
}
query TailOfTheList {
campaign(id: "cmp-4718") {
donations(last: 23) {
edges { cursor node { amountMinor donatedAt } }
}
}
}go deeper
Be ready to name all four arguments and say which pair goes with which direction, then state that the cursors are exclusive bounds. Knowing that last gives you the tail in normal order, not a reversed list, is the point interviewers listen for.
Explain the two-step mechanic out loud: cursors narrow the range, counts then slice it from one end. Be able to work a nine-edge example and show that forward and backward arguments can address the identical page.
Show why the anchoring end matters under concurrent writes, and why the number of edges returned is never an answer to whether more data exists. Interviewers want to hear you reach for page info instead of counting rows.
Own the API-shape argument: whether a field declares one pair or both is a contract decision that constrains every client and every backing store you can put behind it later. Be ready to say when offering backward paging is not worth the implementation cost.
## Four arguments over one ordering A connection field under the Relay server specification — the cursor-connections convention that grew up around GraphQL rather than a part of the GraphQL specification itself — accepts up to four pagination arguments: ```graphql type Campaign { donations( first: Int after: String last: Int before: String ): DonationConnection! } ``` `first`/`after` are the **forward** pair, `last`/`before` the **backward** pair. The convention requires a connection field to accept forward arguments, backward arguments, or both. A field that only ever pages forward may legitimately declare `first` and `after` alone, and a document that then passes `last` fails validation with an unknown-argument error before anything executes. Behind the field sits one ordered sequence of edges — every donation to a charity campaign, say, newest first. **Nothing in the four arguments changes that sequence.** They only choose a window inside it. ## Cursors narrow the range; counts slice it The pagination algorithm runs in two steps, and separating them explains nearly every confusing result people get from connections. **Step one applies the cursors.** `after: "c"` removes every edge up to *and including* the edge whose cursor equals `"c"`. `before: "c"` removes every edge from that edge onward, again including it. Both bounds are exclusive, which is why the edge you paged from never reappears at the top of the next page. Supplying both leaves the open range strictly between them. **Step two applies the counts.** `first: N` keeps the first N edges of what survived step one, discarding from the **end**. `last: N` keeps the last N, discarding from the **start**. That is the entire model. A cursor answers *where in the ordering*; a count answers *how many, and from which side*. ## A worked slice Take a campaign whose donations are D1 through D9 in the connection's order: | Arguments | Edges returned | |---|---| | `first: 3` | D1, D2, D3 | | `after: <D3>, first: 3` | D4, D5, D6 | | `last: 3` | D7, D8, D9 | | `before: <D7>, last: 3` | D4, D5, D6 | The last two rows are the interesting ones: **forward and backward paging can address exactly the same page.** They differ only in which end of the ordering the client anchored to, and that matters when rows are being inserted while a user pages. A client anchored to the newest donation and walking forward drifts differently from one anchored to the oldest and walking backward — same window, different behaviour under concurrent writes. ## `last` selects a tail; it does not reverse anything The single most common misreading is that `last`/`before` gives you the results backwards. It does not. `last: 3` over D1…D9 returns **D7, D8, D9 in that order** — the tail of the sequence, still in the connection's declared order. The word "reverse" appears nowhere in the algorithm; the algorithm only ever *removes* edges from one end or the other. The confusion has a real source. A server usually cannot cheaply read the tail of a large ordered relation, so it flips its scan — orders the underlying rows the other way, takes N of them, and then must flip the fetched rows back before it builds edges. When a team forgets that final flip, backward pages arrive reversed and the bug looks like a specification quirk. It is not; it is an implementation slip. ## Combining the pairs The pairs are not sealed off from each other. `after` with `last` is well defined: narrow to everything after the cursor, then keep the final N of that range. `before` with `first` is likewise defined: everything before the cursor, then the first N. Both are unusual but legal, and a server that returns an error for them is imposing its own rule rather than following the convention. Supplying `first` and `last` together is a different matter — defined by the algorithm, but explicitly discouraged by the specification because the resulting window is hard to reason about. Omitting the counts entirely is not addressed by the convention at all. Every server therefore decides for itself what an argument-free `donations` field means: a default page size, or a refusal demanding one of the counts. ## What the returned count does not tell you A short page is ambiguous on its own. It may mean the range ran out, or that the server capped the count you asked for. The number of edges in `edges` is not an answer to "is there more?" — the connection's page info fields exist precisely to answer that, and they are the only honest place to look. Treating "fewer edges than I asked for" as "end of list" is a bug that only shows up once a server introduces a maximum page size.
- Must a connection field accept all four of these arguments?No. The convention requires the forward pair, the backward pair, or both. A feed that only ever scrolls one way can declare `first` and `after` alone; a document that then sends `last` fails validation with an unknown-argument error before execution starts. Accepting all four when only forward paging is implemented is worse than declaring two, because the client learns about the gap at runtime instead of at validation.
- What does `first: 0` mean?It is a legal request for an empty page. Zero is not negative, so the algorithm's error case does not apply: the cursors narrow the range, then zero edges are kept. The `edges` list comes back empty while the connection's other fields are still resolved, so clients sometimes use it as a cheap metadata-only probe rather than as a mistake to reject.
- Can a client supply `after` and `before` together with no count?Yes — that asks for the open range strictly between the two cursors, with no limit on how many edges fall inside it. The algorithm defines it precisely. In practice a server should still impose its own page size, because a bounded-looking request over a wide range can be an unbounded read of the relation.
The ordering is a fixed shelf of donation receipts. A cursor says which receipt to stand beside; the count says how many to take and which way to reach. You never rearrange the shelf.
saying these in an interview costs you the question
- Says last/before returns edges in reverse order
- Treats after as a numeric offset into the list
- Claims first and last must both be supplied
- Includes the edge the cursor names in the page
- Assumes every connection accepts all four arguments
- Reads a short page as proof there is no more data