skip to content

What fields does PageInfo carry on a GraphQL connection, and which of them can be null?

level: juniorimportance: must knowfreq 63%

answer

  1. Four fields, two different nullability answers
  2. A client always needs a yes or no
  3. Cursors mark this page's two ends
  4. An empty page has no edges
  5. Relay server specification, not the GraphQL spec

basics

~20 s

PageInfo carries four fields: two non-null booleans, hasNextPage and hasPreviousPage, and two nullable strings, startCursor and endCursor. The cursors are nullable because an empty page has no edges, so there is no first or last cursor to report.

solid answer

~50 s

The Relay server specification defines `PageInfo` as `hasNextPage: Boolean!`, `hasPreviousPage: Boolean!`, `startCursor: String` and `endCursor: String`. The two booleans claim whether more edges exist after the end and before the start of *this* slice. `startCursor` is the cursor of the first edge in the page just returned and `endCursor` the cursor of its last edge — boundary markers for the slice, not for the whole collection. The booleans are non-null because a client asking "should I offer a Next control?" always needs a yes or a no. The cursors are nullable because a page can legitimately come back empty — a filter that matches nothing, or a client that paged exactly to the end — and an empty edge list has no first or last edge to take a cursor from. None of this is in the GraphQL specification; it is a convention that became near-universal.

code

graphql · 16 lines
graphql
type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

type ArtworkConnection {
  edges: [ArtworkEdge!]!
  pageInfo: PageInfo!
}

type ArtworkEdge {
  node: Artwork!
  cursor: String!
}

go deeper

for a junior

Recall the four field names and, crucially, which two are nullable. Be ready to say that PageInfo comes from the Relay server specification rather than from the GraphQL specification itself.

for a middle

Explain why the split exists: a client always needs a yes or no for a Next control, while an empty page has no first or last edge to take a cursor from. Say clearly that the two cursors describe the page returned, not the collection.

for a senior

Show you have shipped this. An empty edge list is a normal result rather than an error, an empty-string cursor is a latent bug, and endCursor must come from an edge the client actually received or every later page starts one object too late.

for a principal

Own the contract angle: those four names are a floor that client code and generated types depend on, so adding a field to PageInfo is safe while renaming or dropping one breaks every consumer of the convention at once.

## Where PageInfo comes from The GraphQL specification defines a type system and an execution algorithm. It says nothing about pagination — no `PageInfo`, no cursors, no paging arguments. Everything on this page comes from the **Relay server specification**, a short document describing a cursor-connection convention that servers could adopt so a client which understood the shape could page any field. It has been adopted far beyond the client it was written for, and today most GraphQL APIs that page use it. Calling `PageInfo` "part of GraphQL" is the single most common mistake on this subject: it is a near-universal convention, not a specified rule. ## The declaration ```graphql type PageInfo { hasNextPage: Boolean! hasPreviousPage: Boolean! startCursor: String endCursor: String } ``` Two booleans, non-null. Two cursors, nullable. The asymmetry in those nullability modifiers is deliberate, and it is what an interviewer probes. ## What each field means Take a museum collection graph, where a gallery exposes the objects currently hung in it: ```graphql type Gallery { artworks(first: Int, after: String, last: Int, before: String): ArtworkConnection! } type ArtworkConnection { edges: [ArtworkEdge!]! pageInfo: PageInfo! } type ArtworkEdge { node: Artwork! cursor: String! } ``` - **`startCursor`** is the cursor of the **first edge in the page just returned** — not the first object in the gallery. - **`endCursor`** is the cursor of the **last edge in the page just returned**. It is the value a client hands back to continue forward. - **`hasNextPage`** claims that further edges exist after the end of this page, under this ordering and this filter. - **`hasPreviousPage`** claims the same before the start of this page. The two cursors are boundary markers for *this slice*. They are not object identifiers, and what a cursor encodes internally is a separate subject — to a client it is an opaque string it echoes back untouched. ## Why the booleans are non-null A client rendering a list has to decide whether to offer a Next control, and `null` is not a decision. Declaring the fields `Boolean!` forces the server to commit to `true` or `false` on every response. The subtlety that follows from that — and it is worth knowing even at this level — is that a server which cannot cheaply determine the answer still has to say something, and the something it says is `false`. So `false` is not always a claim that nothing is there. ## Why the cursors are nullable Because a page can be empty, and an empty page has no edge to take a cursor from. Two entirely ordinary ways to get one: 1. **The filter matches nothing.** A gallery closed for a rehang currently holds zero objects; `artworks(first: 24)` returns an empty page. 2. **The client paged exactly to the end.** The previous response ended flush with the last object, and the follow-up request continuing from `endCursor` returns nothing. The honest response in both cases: ```json { "data": { "gallery": { "artworks": { "edges": [], "pageInfo": { "hasNextPage": false, "hasPreviousPage": false, "startCursor": null, "endCursor": null } } } } } ``` Note two things. `edges` is an **empty list, not null** — the field is declared `[ArtworkEdge!]!` and an empty page is a normal result, not an error. And the cursors are `null`, not `""`: an empty string is a cursor *value*, and a client will happily echo it back as a paging argument, at which point the server has to decide what an empty cursor means. Returning `""` is a bug that surfaces days later. ## The handshake these fields exist for A client reads a page, and if `hasNextPage` is true it repeats the request with the same page size, continuing from `endCursor`. It stops when `hasNextPage` is false. That loop is the entire reason `endCursor` exists, and it explains a rule servers break surprisingly often: `endCursor` must be the cursor of an edge the client actually **received**. A server that over-fetches one row to decide `hasNextPage` and then takes `endCursor` from that extra row will make every subsequent page start one object too late, silently losing one object per page for the length of the crawl. ## Can PageInfo be extended? The four fields are a floor, not a ceiling — a server may add fields to its own `PageInfo`. Removing or renaming one of the four is a breaking change for any client written against the convention, which is what makes this shape worth memorising rather than re-deriving. ## What an interviewer listens for The nullability split *and its reason*; "start and end describe this page" rather than "the collection"; and the word "convention" somewhere in the answer.

  • Why is edges an empty list rather than null when a page has no results?
    Because an empty result is a normal outcome, not a failure. The connection declares `edges: [ArtworkEdge!]!`, so returning null there would be a non-null violation and would blow a hole in the response where a perfectly good empty page belongs. Null and empty carry different meanings, and clients that iterate the list handle empty for free.
  • Can a page have no edges but still report hasNextPage true?
    Yes. Requesting zero items is legal, and the convention's algorithm asks whether more edges remain than the number requested — with zero requested, the answer is true whenever anything matches at all. So you get an empty edge list, null cursors and `hasNextPage: true`. It is a useful corner because it disproves the shortcut "an empty page means the end of the list".
  • Is a server allowed to add fields to its PageInfo type?
    Yes — the four fields are a minimum. Servers commonly add their own extras, and adding a field is a backward-compatible change. Renaming or removing one of the four is not: any client written against the convention selects those names by hand or through generated code, and they vanish from the schema at once.

PageInfo is the label on a slice of cake, not on the cake: it tells you where this slice was cut from and whether more remains on either side, and a slice with nothing on it has no edges to point at.

saying these in an interview costs you the question

  • Claiming PageInfo is defined by the GraphQL specification
  • Saying startCursor points at the first item in the whole collection
  • Saying endCursor identifies the first item of the next page
  • Returning an empty string cursor instead of null on an empty page
  • Expecting hasNextPage to be null when the server does not know
  • Returning null for edges when a page matches nothing

context