How to Implement @requires for Computed Fields

This guide shows exactly how to compute a field in one subgraph from data owned by another, using the @requires directive so the Apollo Router fetches the dependency before your resolver runs. It is the hands-on companion to using @external and @requires for field resolution, focused on the single task of wiring a computed field correctly under Subgraph Implementation & Entity Resolution.

When to Use This Pattern

  • A field you own is a pure function of fields another subgraph owns — a total, a flag, a derived label — and you do not want to duplicate the source data.
  • The computation belongs logically in your service (it uses your business rules, clients, or constants) even though the inputs do not.
  • The inputs change rarely enough, or the read volume is low enough, that an extra gated fetch per entity is acceptable — or you can offset it with @provides or caching.

If instead two subgraphs both legitimately resolve the field, you want @shareable, not @requires. If the inputs are already loaded by an upstream list resolver, prefer @provides to skip the hop.

Prerequisites

Three homes for a computed field Computing in the owning subgraph needs no directive but puts foreign logic in the wrong service. Computing with requires keeps ownership correct at the cost of a sequenced fetch. Computing in the client duplicates the rule across every consumer. Order.discountedTotal has to be computed somewhere in the data owner no directive, no extra fetch Fast, but the pricing rules now live in the catalog service that does not own them. Two teams deploy together forever after. with @requires one sequenced fetch The rules stay with the team that owns them and the field appears exactly where clients expect it. Costs one round trip that cannot be parallelised. in the client no server cost at all Every consumer implements the same rule, and they will not stay identical. Only reasonable for purely presentational values.

Implementation Walkthrough

Suppose a catalog subgraph owns Product.price and Product.currency, and a pricing subgraph must expose totalPrice in USD. The pricing subgraph does not own price or currency, so it redeclares them as @external and lists them in @requires. The router then fetches those fields from catalog, attaches them to the entity representation, and hands the populated reference to the pricing resolver.

# pricing subgraph — schema.graphql
extend schema
  @link(url: "https://specs.apollo.dev/federation/v2.9",
        import: ["@key", "@external", "@requires"])

type Product @key(fields: "id") {
  id: ID!
  # owned by the catalog subgraph; declared @external so @requires can name them.
  price: Float! @external
  currency: String! @external
  # owned here; the router fetches price+currency before this resolver runs.
  totalPrice: Float! @requires(fields: "price currency")
}
// pricing subgraph — resolvers.ts
import { GraphQLResolveInfo } from 'graphql';

interface ProductReference {
  __typename: 'Product';
  id: string;
  price?: number;     // injected by the router via @requires
  currency?: string;  // injected by the router via @requires
}

interface Context {
  exchangeRates: { getRate(from: string, to: string): Promise<number> };
}

export const resolvers = {
  Product: {
    totalPrice: async (
      reference: ProductReference,
      _args: Record<string, unknown>,
      context: Context,
      _info: GraphQLResolveInfo,
    ): Promise<number | null> => {
      const { price, currency } = reference;

      // Guard against a partial upstream payload — never assume the fetch succeeded.
      if (price == null || currency == null) return null;

      // The inputs came from the router; we only run our own business logic here.
      const rate = await context.exchangeRates.getRate(currency, 'USD');
      return Number((price * rate).toFixed(2));
    },
  },
};

The key discipline is that the resolver reads price and currency from the reference object and never re-fetches them. The router has already done that work as a separate fetch in the query plan; calling catalog again from inside this resolver would defeat the purpose of @requires and add an invisible, untraced round trip.

For nested inputs, the fields argument is a full selection set. To require a tax rate nested under a metadata object, write @requires(fields: "price currency metadata { taxRate }") and read reference.metadata?.taxRate in the resolver.

A few details separate a correct implementation from one that merely composes. The @external redeclarations must mirror the owning subgraph’s SDL exactly, nullability included — price: Float! in catalog must be price: Float! @external in pricing, not price: Float @external. If the types diverge, composition rejects the schema with a field-type-mismatch error rather than a missing-external error, which can be confusing because the field is marked external. Treat the @external block as a copy of the owner’s signature, not an approximation.

It is equally important that the resolver does no I/O against the owning subgraph. The whole value of @requires is that the router has already fetched price and currency as a planned step; your resolver’s job is purely the business computation — converting currency, applying a coupon, formatting a label. If you find yourself calling the catalog API from inside totalPrice, you have effectively rebuilt the dependency by hand, except now it is invisible to the query planner and to your traces. The router cannot batch it, cache it, or show it in a trace span, so a latency regression there is far harder to diagnose than one the planner created. Keep the resolver pure with respect to the network and let the directive own the fetch.

Finally, decide deliberately what a missing input means. The resolver above returns null when price or currency is absent, which is the right default when the field is nullable. If totalPrice is non-nullable, returning null will collapse the parent object, so you must either supply a safe default or accept that a degraded upstream nulls the whole Product. There is no universally correct answer — only a choice you should make on purpose and document, because it determines how the graph behaves during a partial outage.

What the router does to satisfy one @requires The router resolves the entity key, fetches the required fields from their owner, merges them into the representation, and only then calls the resolver that declared the requires. Four steps, and only the last one is your resolver 1. key in hand { __typename, id } 2. fetch required from the owning subgraph 3. merge into the representation 4. your resolver reads them off ref Where it goes wrong If step 2 returns null for a field, step 4 receives undefined — so guard rather than assume. Steps 1 to 3 are the router's work and are invisible in your code, which is why a @requires field looks free in the resolver and costs a full round trip in the trace.

Verification Steps

First, confirm composition accepts the directives:

rover subgraph check "$APOLLO_GRAPH_REF" \
  --name pricing \
  --schema ./pricing/schema.graphql

Then run the operation and inspect the plan. With tracing enabled you should see two sequential fetches — catalog first, pricing second:

query GetProductTotal {
  product(id: "prod_123") {
    id
    totalPrice
  }
}

The planner produces, in order: a catalog fetch returning { id price currency }, then a pricing _entities fetch that receives { __typename: "Product", id: "prod_123", price: 19.99, currency: "EUR" } and resolves totalPrice. Confirm the expected response shape:

{ "data": { "product": { "id": "prod_123", "totalPrice": 21.45 } } }

If totalPrice comes back null where you expected a number, enable APOLLO_ROUTER_LOG=debug and check the _entities payload sent to pricing — a missing price or currency means catalog did not return it.

Every field in the selection travels on every request A requires selection listing two fields adds two values per entity to the intermediate payload. Listing a whole nested object adds all of its fields, multiplied by the length of any list being resolved. Selection size × list length = bytes you pay for narrow @requires(fields: "price currency") over-broad @requires(fields: "pricing { … }") — every nested field 0 intermediate payload per entity On a list of two hundred orders the difference is not a rounding error — it is the fetch's whole cost. List the exact leaves you read in the resolver. If the selection grows, that is a signal the field is being computed in the wrong subgraph.

Common Mistakes & Gotchas

Forgetting @external on a required field. Composition fails with Field "Product.totalPrice" requires "price" but "price" is not declared @external in subgraph "pricing". Every field named in @requires must be redeclared @external in the requiring subgraph, with a type that matches the owner exactly — including nullability.

Re-fetching the required data in the resolver. Calling the catalog API again from inside totalPrice adds a hidden hop the router cannot see or trace. Read the values from the reference object; that is the entire point of @requires.

Throwing on a partial payload. If the upstream subgraph occasionally returns null for price, an unguarded resolver throws Cannot return null for non-nullable field. Add explicit null guards and decide whether to return null or a safe default — see entity resolution fallback strategies for partial data.

Frequently Asked Questions

Can @requires reference nested fields or arrays?

Yes. The fields argument is a selection set, so @requires(fields: "metadata { taxRate }") works for nested objects, and arrays are passed through whole — your resolver iterates them. Top-level scalars are space-separated.

What happens if the owning subgraph fails to resolve a required field?

The router passes null or undefined into your resolver for that field. Always guard against missing inputs and return null or a typed default rather than throwing, so one degraded entity does not collapse the whole response.

Does adding @requires hurt performance?

It adds a serially gated fetch on the critical path. Keep the selection set minimal, batch the owning resolver with a DataLoader, and consider @provides or caching to remove the hop — covered in optimizing reference resolvers for performance.

What happens if a required field is null?

Your resolver receives undefined for it and must decide. There is no automatic short-circuit: the router fetched what it could, and a null is a legitimate answer from the owning subgraph. Guard explicitly and return null for the computed field rather than letting an arithmetic operation produce NaN, which will serialise happily and confuse whoever reads the response.

Can the required fields be selected by the client at the same time?

Yes, and it costs nothing extra. If the client selects price alongside discountedTotal, the router is already fetching price for your requirement and simply also returns it. The two uses share one fetch, which is why adding a @requires for a field clients commonly select is much cheaper than adding one for a field nobody asks for.

Should the computed field be nullable?

Almost always, yes. It depends on data from another service, so its availability is bounded by that service’s availability. Declaring it non-null means an upstream hiccup nulls the whole parent object instead of one field, which turns a degraded response into a missing one.