Siddhant Deval
Siddhant DevalAuthor
Senior Full-Stack Engineer·Oct 20, 2026·12 min read

Consuming a Federated Supergraph: The Client View

From the client's perspective, the supergraph is a single, unified schema — but the query you write determines which subgraphs the router fetches, how entity stitching assembles the response, and what partial data you receive when one domain is unavailable.

Technical Series

GraphQL Frontend Engineering

Part 8 of 8

Consuming a Federated Supergraph: The Client View

The bug report is simple: "The Orders section is sometimes blank — F5 usually fixes it." The frontend engineer opens DevTools. HTTP 200. The GraphQL response has data.user with name and email populated, and data.user.orders is null. No errors array. No network error. Just null.
The platform team explains after an hour: the Orders subgraph had a brief outage at 14:22. The Apollo Router returned partial data — user from the Users subgraph resolved correctly, orders from the Orders subgraph returned null because the subgraph was unreachable. The router didn't fail the entire request. It returned what it had. And because the client used the default errorPolicy: 'none', the errors array was there — but the component never checked it.
Actually, one more thing: the id field wasn't included in the user selection. The router couldn't perform entity stitching between the Users and Orders subgraphs. The orders field returned null even when the Orders subgraph was healthy. Two separate bugs, both invisible, both silent.
This is the federated supergraph from the client's perspective. One unified schema, one HTTP request, one response — assembled from multiple domain services that can fail independently. Writing queries against a supergraph requires knowing three things that don't apply to monolithic GraphQL: @key entity fields, partial data semantics per subgraph, and how to read a query plan. This article covers all three.
A GraphQL client is not a data fetcher — it is a local replica of your server's data graph. In a federated supergraph, that graph spans domain boundaries. The replica must reflect those domain boundaries correctly, including their independent failure modes.

1. What a Supergraph Is from the Client's Perspective

The Apollo Router presents the supergraph as a single, unified schema. From the client's perspective, there is no visible boundary between subgraphs. You write queries against one schema, send them to one endpoint, and receive one response.
graphql
# This query crosses two subgraph boundaries — invisible to the client
query GetUserWithOrders($userId: ID!) {
  user(id: $userId) {
    name        # Resolved by: Users subgraph
    email       # Resolved by: Users subgraph
    orders {    # Resolved by: Orders subgraph
      id
      total
    }
  }
}
Internally, the router:
  1. Parses the query into a query plan — a graph of fetch operations and entity stitching steps
  2. Executes the fetch to the Users subgraph in parallel with available fields
  3. Uses the @key entity reference to correlate User entities across subgraphs
  4. Fetches orders from the Orders subgraph using the User.id as the entity key
  5. Stitches the responses into a single unified result
None of this is visible to the client. But knowing it exists is essential for debugging, query optimization, and understanding why User.id must be in your selection set.

2. The @key Field: Why You Must Include It

This is the most common and most silent federated query bug. The @key directive on the server defines which field identifies a User entity across subgraphs. From the backend schema (covered in the companion Backend series, Part 3):
graphql
# Users subgraph
type User @key(fields: "id") {
  id: ID!
  name: String!
  email: String!
}

# Orders subgraph
type User @key(fields: "id") {
  id: ID!
  orders: [Order!]!
}
id is the @key field — the distributed foreign key the router uses to stitch User entities across subgraph boundaries.

2.1 What Happens When @key Is Missing

graphql
# ❌ Missing the @key field — entity stitching fails silently
query GetUserWithOrders($userId: ID!) {
  user(id: $userId) {
    name    # Returns correctly from Users subgraph
    email   # Returns correctly from Users subgraph
    orders { # SILENTLY RETURNS NULL — router can't stitch without id
      id
      total
    }
  }
}
The router cannot correlate the User from the Users subgraph with the User representation in the Orders subgraph without the @key field. The orders field returns null. No error is emitted — the router treats the missing stitching key as a missing entity reference.

2.2 The Correct Query: Always Include @key Fields

graphql
# ✅ @key field included — entity stitching works correctly
query GetUserWithOrders($userId: ID!) {
  user(id: $userId) {
    id      # ← The @key field — required for cross-subgraph entity references
    name
    email
    orders {
      id
      total
    }
  }
}
Correct query including the @key field (right, green) vs query missing the @key field causing silent null on cross-subgraph fields (left, red)
Correct query including the @key field (right, green) vs query missing the @key field causing silent null on cross-subgraph fields (left, red)
Pro Tip & Optimization
Fragment colocation is the systematic solution to this class of bug. If your UserOrders fragment always includes id, every query that spreads it will always include the @key field. This is one of the structural benefits of fragment colocation beyond refactoring safety.

3. Partial Data: What the Client Receives When a Subgraph Fails

When the Orders subgraph is unavailable, the router does not return an HTTP 500. It returns the data it has — from the Users subgraph — and includes an error for the failed field:
json
{
  "data": {
    "user": {
      "id": "1",
      "name": "Alice",
      "email": "alice@example.com",
      "orders": null
    }
  },
  "errors": [
    {
      "message": "Failed to fetch from subgraph 'orders'",
      "path": ["user", "orders"],
      "extensions": {
        "code": "SUBGRAPH_ERROR",
        "subgraph": "orders"
      }
    }
  ]
}
This is the same partial data pattern from Part 6, applied at subgraph granularity. The client-side handling is identical:
typescript
const { data, error } = useQuery(GET_USER_WITH_ORDERS, {
  variables: { userId },
  errorPolicy: 'all', // ← Required — 'none' discards the partial data
})

// Find the subgraph-specific error
const ordersSubgraphError = error?.graphQLErrors.find(
  e => e.extensions?.code === 'SUBGRAPH_ERROR' && e.path?.includes('orders')
)
Crucial Requirement
errorPolicy: 'all' is not optional for federated graphs — it is the minimum correct configuration. With the default errorPolicy: 'none', a single subgraph outage throws an error and discards the entire response, including data from healthy subgraphs. The user sees a full error page instead of a graceful degraded experience.

3.1 Fragment-Level Error Boundaries for Subgraph Zones

Each subgraph boundary is a natural Error Boundary boundary. Wrap each data zone from a different subgraph in its own Error Boundary:
typescript
function UserDashboard({ userId }: { userId: string }) {
  return (
    <div>
      {/* Users subgraph — critical, unlikely to fail */}
      <ErrorBoundary fallback={<UserProfileError />}>
        <Suspense fallback={<ProfileSkeleton />}>
          <UserProfile userId={userId} />
        </Suspense>
      </ErrorBoundary>

      {/* Orders subgraph — isolated failure zone */}
      <ErrorBoundary
        fallback={<OrdersUnavailable message="Orders are temporarily unavailable" />}
      >
        <Suspense fallback={<OrdersSkeleton />}>
          <OrderHistory userId={userId} />
        </Suspense>
      </ErrorBoundary>

      {/* Inventory subgraph — isolated failure zone */}
      <ErrorBoundary fallback={<RecommendationsUnavailable />}>
        <Suspense fallback={<RecommendationsSkeleton />}>
          <ProductRecommendations userId={userId} />
        </Suspense>
      </ErrorBoundary>
    </div>
  )
}
When the Orders subgraph fails, only <OrdersUnavailable /> renders. The profile and recommendations remain functional. This is the production-correct architecture for any federated UI.

4. Reading a Query Plan in Apollo Studio

The query plan is the router's decomposition of a supergraph query into subgraph fetches. Reading it is the correct debugging technique for slow federated queries — it shows exactly which subgraph fetches are sequential vs. parallel and where the latency is concentrated.
Access the query plan in Apollo Studio's Explorer tab after executing a query. A plan for a simple two-subgraph query looks like this:
QueryPlan {
  Parallel {
    Fetch(service: "users") {
      user(id: $userId) {
        id name email
      }
    }
    Sequence {
      Fetch(service: "users") {
        user(id: $userId) {
          __typename
          id
        }
      }
      Flatten(path: "user") {
        Fetch(service: "orders") {
          ... on User {
            __typename
            id
          } {
            orders { id total }
          }
        }
      }
    }
  }
}

4.1 What the Plan Tells You

  • Parallel: These fetches execute concurrently — their latencies don't add up
  • Sequence: These fetches execute serially — the second waits for the first. A long sequence is a performance bottleneck.
  • Flatten: The router is stitching an entity — correlating the @key field from one subgraph's response with the representation in another
When the plan shows Sequence { Fetch A → Flatten → Fetch B → Flatten → Fetch C }, the total latency is A + B + C. Restructuring the query to allow parallel fetches, or restructuring the schema to add a field that eliminates the sequential dependency, is the performance optimization.
Client operation flows through the Apollo Router: query plan decomposes into parallel Users and Orders subgraph fetches, entity stitching assembles the response, client cache receives unified data
Client operation flows through the Apollo Router: query plan decomposes into parallel Users and Orders subgraph fetches, entity stitching assembles the respo…

5. @defer Across Subgraph Boundaries

Apollo Router ≥ 1.25 supports @defer across subgraph boundaries. Deferred fields from a slow subgraph stream in after the fast subgraph's data has already been delivered to the client:
graphql
query GetUserDashboard($userId: ID!) {
  user(id: $userId) {
    # Fast: Users subgraph — delivered immediately
    id
    name
    email

    # Slow: Orders subgraph — deferred, streams when ready
    ... @defer {
      orders {
        id
        total
        createdAt
      }
    }

    # Slow: Recommendations subgraph — deferred, streams when ready
    ... @defer {
      recommendations {
        id
        title
        relevanceScore
      }
    }
  }
}
typescript
function UserDashboard({ userId }: { userId: string }) {
  const { data } = useSuspenseQuery(GET_USER_DASHBOARD, {
    variables: { userId },
  })

  return (
    <div>
      {/* Users subgraph data — renders immediately from first chunk */}
      <UserHeader user={data.user} />

      {/* Deferred — Suspense boundary streams in when Orders subgraph responds */}
      <Suspense fallback={<OrdersSkeleton />}>
        <OrderHistory user={data.user} />
      </Suspense>

      {/* Deferred — independent Suspense boundary for Recommendations */}
      <Suspense fallback={<RecommendationsSkeleton />}>
        <ProductRecommendations user={data.user} />
      </Suspense>
    </div>
  )
}
@defer across subgraph boundaries lets fast subgraphs deliver their data immediately without waiting for slow subgraphs. The user sees the profile header instantly; the orders and recommendations fill in as each subgraph responds. This is meaningfully better than blocking the entire render on the slowest subgraph.
Architectural Note
Cross-series link: The server-side implementation of @defer in Apollo Router — multipart HTTP response configuration, subgraph-level defer support, and the router version requirements — is covered in GraphQL Backend & API Design, Part 3 (Federation & Subgraph Architecture).

Summary

ConceptRule
Supergraph transparencyThe supergraph presents a single schema to the client — the router decomposes queries across subgraphs transparently; the client never needs to know which subgraph owns which field.
@key fields@key entity fields must be present in every query selection that crosses a subgraph boundary — missing them causes the router to return null for the entity, with no error, silently.
Subgraph failureSubgraph failure returns null for the affected entity fields, not a gateway-level HTTP error — errorPolicy: 'all' is required to receive both partial data and the error detail.
Query plan debuggingReading the query plan in GraphOS is the correct debugging technique for slow federated queries — it reveals sequential vs. parallel fetch ordering and identifies the bottleneck subgraph.
@defer and federation@defer works across subgraph boundaries in Apollo Router ≥ 1.25 — use it to render critical cross-domain data immediately while slow subgraph responses stream in.

This is Part 8 and the final article in the GraphQL Frontend Engineering series.
Continue with the companion series: The server-side architecture that makes everything in this series possible — schema design, resolver performance, DataLoader, federation subgraph authoring, subscriptions server setup, observability, schema security, evolution patterns, and testing — is covered in GraphQL Backend & API Design, a companion 8-part series for backend and platform engineers.
Research & Synthesis Note

This article was developed with AI-assisted deep search, specification cross-referencing, and technical research synthesis.

#GraphQL#Federation#Supergraph#Apollo Router#Apollo Client#Entity Stitching#Distributed Systems
Siddhant Deval

Written by Siddhant Deval

Senior Full-Stack Engineer building high-scale architectures, browser performance engineering systems, and SaaS platforms.