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

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.

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.

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.