skip to content

A GraphQL connection exposes both edges and a plural nodes field — what do clients lose by selecting nodes?

level: seniorimportance: should knowfreq 37%

answer

  1. A convenience field, fewer levels of nesting
  2. Something per-item quietly goes missing
  3. Not required by the Relay server specification
  4. Resume points collapse to page boundaries
  5. The tempting bad fix moves fields down

basics

~20 s

Selecting nodes skips the edge layer, so clients lose every per-item cursor and every field the edge carries. They can still resume from the slice's boundary cursor, but never from an arbitrary item in the middle.

solid answer

~50 s

`nodes` is a convenience field many APIs add to a connection: a flat list of the items, skipping the edge wrapper. It is **not** part of the Relay server specification, which requires only `edges` and `pageInfo`; the convention permits extra fields, so `nodes` is a legal addition, not a violation. What a client gives up is everything that lives on the edge — the per-item `cursor` and any relationship field such as a per-flight price or status. The practical consequence is resume granularity: without per-item cursors, a client can only continue from the boundary cursor the page info reports, so it cannot resume from the 14th of 30 items after a partial render, and it cannot build a bookmark to an item. The ergonomic gain is real, though: dropping two nesting levels per hop matters when a document is already deep. Offering both is the usual answer, since removing `edges` later is a breaking change.

code

graphql · 13 lines
graphql
query CabinTwoWays($flightId: ID!) {
  flight(id: $flightId) {
    seatMap(first: 30) {
      nodes { designator }
      edges {
        cursor
        priceCents
        node { designator }
      }
      pageInfo { endCursor }
    }
  }
}

go deeper

for a junior

Recognise that nodes returns the items directly while edges returns wrapper objects, and that the cursor is only visible through edges. Knowing which one a query needs is enough here.

for a middle

Explain the mechanics: nodes is the same slice with the edge layer projected away, so per-item cursors and every edge field disappear from the response. Say plainly that nodes is conventional, not required.

for a senior

Demonstrate the operational consequences — resume granularity collapses to page boundaries, and a server resolving the two fields separately can return mismatched, half-empty pages. Recommend exposing both and resolving the page once.

for a principal

Own the schema-standard question: whether every connection in the organisation offers both fields, how cost limits score them comparably so clients do not game the cheaper one, and how you would ever retire one across clients you do not control.

## What `nodes` is A connection under the Relay server convention must have `edges` and `pageInfo`. Nothing forbids extra fields, and one extra field became near-universal: a plural `nodes` that returns the items directly. ```graphql type SeatConnection { edges: [SeatEdge!]! nodes: [Seat!]! pageInfo: PageInfo! } ``` Both fields describe the same slice; `nodes` is `edges.map(e => e.node)` on the server side. It is a convenience, and it is **conventional, not specified** — an API that omits it is fully compliant, and an API that offers only `nodes` and no `edges` is not a connection at all under the convention. ## Why teams add it Nesting. A seat map is reached through a graph, and every connection on the path costs two extra levels of selection. A document that walks itinerary → segment → flight → seat map → seat features → amenity detail is already deep before edges are counted; adding four connections on that path pushes it from 11 to 19 levels deep, and 19 levels is genuinely painful to read, to indent, and to hold in your head during review. Every level also costs bytes in the request and in the response, and depth is what most depth-limiting rules count. The second reason is client ergonomics. A component that just wants a list of seats writes `nodes { designator }` and gets an array. With `edges` it writes `edges { node { designator } }` and then unwraps in the view layer — a small tax paid at every single call site. ## What is actually lost **1. The per-item cursor — the important one.** Cursors live on edges. A client reading `nodes` receives items with no positions. It can still page forward, because the page info carries the slice's boundary cursors, but its resume points are page boundaries only. That is fine for infinite scroll and wrong for anything finer: - A client renders a 30-seat page, the user acts on the 14th seat, and the app wants a deep link that resumes the list *there*. With `nodes`, there is no cursor for item 14. - A retrying background sync that got through part of a page has to re-request the whole page. - Any "jump to this item's neighbourhood" feature has to be built from a searchable key on the item instead, which is a different, weaker mechanism. **2. Every edge field.** If the edge carries per-flight `priceCents` and `status`, `nodes` cannot see them. Teams then face the tempting bad fix: move those fields onto the item type so `nodes` becomes sufficient — which reintroduces the parent-dependent-field problem and can corrupt a normalized client cache. This is the quiet way a `nodes` shortcut damages a schema: not by existing, but by making relationship data look inconvenient. **3. Alignment guarantees.** If a document selects `edges` and `nodes` in the same request, they should describe the same items in the same order. Nothing in the type system says so; it is on the server to make both fields read the same materialised slice. A server that resolves them as two independent fetches can return a half-empty, mismatched response — 30 nodes and 27 edges, because rows changed between two queries — and the client's index-based zip silently misaligns. Resolve the page once and project both fields from it. ## The judgement, as a senior owner Offer both. `nodes` is an additive change and costs you nothing to add; removing `edges` later is a breaking change and removes the only path to per-item cursors. Then: - **Keep relationship data on the edge** even though `nodes` cannot reach it, and document that the two fields are not interchangeable when edge fields exist. - **Materialise the page once** so both fields project from one result set. - **Watch usage.** Field-level usage data tells you whether anyone still selects `edges` on a given connection. Where nothing does, the connection may genuinely be one whose edges carry nothing but a cursor. - **Cost limits.** If a cost or complexity limiter scores selections, `nodes` and `edges { node }` should score comparably; otherwise clients pick a field to dodge the limiter rather than to express intent. ## The interview answer Weak answers treat this as syntax preference. The strong answer is three beats: `nodes` is a convention layered on a convention and is not required by the Relay server specification; the loss is per-item cursors and edge fields, which specifically costs fine-grained resume and bookmarking; and the right default is to expose both while keeping edge data on the edge, because the shortcut's real risk is that it pressures relationship fields into the wrong type.

  • Does adding a nodes field make a connection non-compliant with the Relay server convention?
    No. The convention requires a connection type to have `edges` and `pageInfo` and says nothing against extra fields, so `nodes` is a legal addition. The reverse is not true: a type offering only `nodes`, with no `edges`, is not a connection under the convention, because there is then no place for per-item cursors.
  • A client selects both nodes and edges in one request and zips them by index. What can go wrong?
    Nothing in the type system guarantees the two fields describe the same items in the same order. A server that resolves them as two independent fetches can return mismatched lengths when rows change between them, and the client's index-based zip misaligns silently. Resolve the page once and project both fields from that single materialised slice.
  • Your API has offered nodes for a year and edges is barely selected. Would you remove edges?
    Not on usage data alone. Removing a field is a breaking change and it removes the only route to per-item cursors, so any future bookmarking or fine-grained resume feature is blocked. Check whether the edge carries relationship fields, check usage across every known client version, and prefer deprecating over deleting if you move at all.

It is the difference between a passenger list and a boarding manifest: the list tells you who is aboard, the manifest tells you where each one sits and what they paid.

saying these in an interview costs you the question

  • Believes nodes is required by the Relay server specification
  • Says nodes and edges are fully interchangeable
  • Moves relationship fields onto the item so nodes suffices
  • Assumes nodes and edges always align by index
  • Thinks removing edges is a safe additive change
  • Cannot name what a per-item cursor enables

context