skip to content

In Apollo Client 4, what is a link, and why must HttpLink be the last link in the chain you pass to ApolloClient?

level: juniorimportance: should knowfreq 36%

answer

  1. middleware around every operation
  2. out through the chain, back up again
  3. forward(operation) calls the next link
  4. a link that never calls forward
  5. ApolloLink.from keeps array order

basics

~20 s

An Apollo Client link is one step in the request pipeline, seeing each operation on the way out and its result on the way back. HttpLink terminates the chain by sending the request, so links after it never run.

solid answer

~40 s

In Apollo Client 4 the `ApolloClient` constructor requires a `link`, usually a chain built with `ApolloLink.from([...])`. Each link's request handler receives the `operation` and a `forward` function; `forward(operation)` hands the operation to the next link and returns an RxJS Observable of the result, which the link can inspect or transform on the way back up. Cross-cutting concerns become separate links: `SetContextLink` adds headers, `RetryLink` re-sends after network failures, `ErrorLink` reacts to errors, `ApolloLink.split` routes by a predicate. The chain has to end in a terminating link such as `HttpLink`, which calls `fetch` instead of `forward`. Anything listed after it is never called, and a custom link that forgets to call `forward` turns into an accidental terminating link, leaving the request pending.

code

ts · 19 lines
ts
import { ApolloClient, ApolloLink, HttpLink, InMemoryCache } from "@apollo/client";
import { SetContextLink } from "@apollo/client/link/context";
import { ErrorLink } from "@apollo/client/link/error";
import { RetryLink } from "@apollo/client/link/retry";

declare function readAccessToken(): string;

const link = ApolloLink.from([
  new ErrorLink(({ error, operation }) => {
    console.warn(`${operation.operationName ?? "anonymous"} failed:`, error.message);
  }),
  new RetryLink(),
  new SetContextLink((prevContext) => ({
    headers: { ...prevContext.headers, authorization: `Bearer ${readAccessToken()}` },
  })),
  new HttpLink({ uri: "/graphql" }),
]);

export const client = new ApolloClient({ link, cache: new InMemoryCache() });

go deeper

for a junior

Recall that a link is one step in a pipeline every operation passes through, that forward hands it on, and that HttpLink ends the chain because it actually sends the request.

for a middle

Explain the round trip: array order on the way out, reverse order on the way back, forward returning an RxJS Observable, and why a link that never calls forward silently terminates the chain.

for a senior

Show that ordering is a design decision: which links wrap which, what each can observe, and how a misplaced link hides errors or skips headers in a real production chain.

for a principal

Frame the chain as the client's single request policy point, and argue which concerns belong in links versus components, so that auth, retries and telemetry stay consistent across teams.

## What a link is In Apollo Client 4 every operation a component runs, whether a query from `useQuery` or a mutation from `useMutation`, leaves the client through a **link chain**. A **link** is an instance of `ApolloLink` (or of a subclass such as `HttpLink`) with a request handler. The handler receives two things: - the **operation**: the parsed document, its `variables`, its `operationName`, and a mutable **context** read with `operation.getContext()` and written with `operation.setContext(...)`; - a **forward** function that passes the operation to the next link and returns an RxJS **Observable** of the result. Apollo Client 4 made links classes and moved them onto RxJS. `rxjs` is a peer dependency, links return RxJS Observables, and you transform results with `.pipe()` and operators such as `map` and `tap`. The old helper functions (`createHttpLink`, `setContext`, `onError`) and the bare `from`/`concat`/`split` functions still exist but are deprecated in favour of the classes and the `ApolloLink.from` / `ApolloLink.split` statics. The constructor also changed: `new ApolloClient` **requires** a `link`. The 3.x shortcut of passing `uri` or `headers` straight to the client was removed, so even the smallest setup writes `link: new HttpLink({ uri })`. ## Out through the chain, back up again A chain behaves like middleware. The request travels **down** the chain in array order. Each link may change the context, for example by adding headers, and then calls `forward(operation)`. The result travels back **up** the same links in reverse order, and each link may observe or rewrite it on the way. 1. `ApolloLink.from([a, b, c])` builds one composed link that runs `a`, then `b`, then `c`. 2. `c` is the **terminating link**. It does not call `forward`. It performs the request and emits the result. 3. The result flows back through `b` and then `a`, and finally reaches the cache and the hook. Because the result passes back through every earlier link, a link near the front of the array wraps everything after it. That is why ordering questions come up in interviews: a link can only affect, or observe, the links that come after it. ## The terminating link `HttpLink` (and `BatchHttpLink`) are terminating links. `HttpLink` reads `headers`, `uri` and other options from the operation context, serialises the operation, calls `fetch`, and parses the response. It never calls `forward`, so: - a logging link placed **after** `HttpLink` never runs, and it cannot see the response; - a custom link that returns something without calling `forward(operation)` becomes an **accidental terminating link**, and the operation it swallows is never sent; - when `ApolloLink.split(test, left, right)` ends the chain, each branch must end in its own terminating link, because only one branch runs per operation and nothing follows it. ## A dashboard's chain Take an internal analytics dashboard that talks to a GraphQL API behind a gateway. A typical chain looks like this: | Position | Link | Job | |---|---|---| | 1 | `ErrorLink` | react to failures once they come back up | | 2 | `RetryLink` | re-send after network failures, with backoff | | 3 | `SetContextLink` | put the current access token in `headers` | | 4 | `HttpLink` | send the request with `fetch` (terminating) | Each concern is small, testable and swappable. Replacing the transport means replacing one link, not rewriting the error handling. ## How links pass information: the context Links do not call each other's methods. They communicate through the **operation context**, a per-operation object that starts from whatever the hook passed in its `context` option: - an upstream link writes to it, for example `SetContextLink` adding `headers`; - a downstream link reads it, for example `HttpLink` reading `headers`, `uri` and `credentials` when it builds the `fetch` call; - after the response arrives, `HttpLink` stores the raw `Response` in the context, so a link above it can inspect it on the way back. A small custom link shows both directions at once. It writes a start time into the context and then pipes `tap` onto `forward(operation)` to log how long the round trip took. Because each operation gets its own context, two dashboard panels loading at the same moment never see each other's values. ## Mistakes interviewers listen for - **Order does not matter.** It does. Links run in array order on the way out and in reverse on the way back. - **Code after HttpLink sees the response.** It never runs. Observe responses from a link placed before the terminator, by piping onto `forward(operation)`. - **forward returns a Promise.** In Apollo Client 4 it returns an RxJS Observable. That lets one operation emit several results, as with `@defer` or subscriptions. - **Passing `uri` to ApolloClient.** That option is gone in version 4. Pass `link: new HttpLink({ uri })` instead. A junior answer that covers the pipeline, the `forward` call, and the terminating link at the end is complete. The ordering consequences are where the harder follow-ups begin.

  • How does an Apollo Client 4 link change or observe a result on its way back up the chain?
    It pipes RxJS operators onto the Observable that `forward(operation)` returns. For example, `forward(operation).pipe(map(result => ...))` rewrites a result, and `tap` records timing without changing it. Apollo Client 4 uses RxJS, so there is no method chaining on the Observable any more, only `.pipe()` with operators.
  • When would a dashboard's chain use ApolloLink.split, and what must each branch end with?
    `ApolloLink.split(test, left, right)` runs `left` when the predicate returns true and `right` otherwise, for example to send operations flagged in their context to a separate export endpoint. Only one branch runs per operation. When the split ends the chain, each branch needs its own terminating link, such as a second `HttpLink`. If `right` is omitted, operations that fail the test are forwarded to whatever follows the split.

A link chain works like a row of mailroom desks: each desk can stamp an outgoing envelope and read the reply as it comes back, and the last desk actually posts it. A desk placed after the post box never sees a single envelope.

saying these in an interview costs you the question

  • Links run independently, so their order in the array does not matter.
  • A logging link placed after HttpLink can read the server's response.
  • Calling forward(operation) returns a Promise of the parsed response body.
  • In Apollo Client 4 you can still pass uri straight to the ApolloClient constructor.
  • A custom link can skip calling forward and the request still goes out.