Resource library

QA How-To

How to Test GraphQL Federation Contracts With Pact (2026)

Learn to test GraphQL federation contracts with Pact using runnable TypeScript consumer tests, provider verification, precise matchers, and CI checks.

18 min read | 3,507 words

TL;DR

Generate consumer Pacts for named operations at the router boundary, then replay them against a deterministic federated provider. Pair Pact with schema composition checks because the two techniques detect different failures.

Key Takeaways

  • Contract the federated router when that is the endpoint consumers call.
  • Generate Pact interactions by exercising the real GraphQL client.
  • Use Pact V4 GraphQL builders for named operations, variables, and response matchers.
  • Seed every participating subgraph through deterministic provider states.
  • Run federation composition checks separately because Pact verifies runtime interactions.
  • Publish immutable consumer and provider versions before using deployment gates.

To test GraphQL federation contracts with Pact, write consumer tests against the federated graph's public GraphQL endpoint, generate a Pact for each operation your client uses, and replay those requests against a running router or a router-equivalent provider in CI. Pact verifies the HTTP request, operation, variables, and response shape that matter to the consumer. Pair it with federation schema composition checks, because Pact does not prove that every subgraph composes into a valid supergraph.

This tutorial builds a small TypeScript consumer for a federated product graph, a Pact V4 GraphQL consumer test, an executable Apollo GraphQL provider, and a provider verification test. The same shape works when Apollo Router, Cosmo Router, or another federation gateway fronts your subgraphs. If you need the broader testing context first, read the GraphQL API testing guide and the contract testing guide.

TL;DR

Check Best target What a failure means
Consumer Pact Pact mock server The client no longer sends or accepts the agreed operation
Provider verification Running router endpoint The deployed graph cannot satisfy a recorded consumer interaction
Federation composition Subgraph SDL plus federation tooling Subgraph schemas cannot form the intended supergraph
Resolver tests Individual subgraph Business logic or data mapping is wrong
End-to-end test Real router and selected dependencies Wiring, authentication, or infrastructure is broken

Treat the router as the Pact provider when consumers call the router. Record only operations the consumer genuinely executes, use matchers for values that can vary, and keep exact assertions for enums, nullability behavior, and error codes your client branches on.

What You Will Build

You will create:

  • A typed Catalog Web client that posts a named ProductCard query to /graphql.
  • A Pact consumer test using the 2026 Pact V4 GraphQL builder.
  • A local Apollo Server that behaves like the federated graph's public boundary.
  • A provider verification test that replays the generated pact.
  • A CI workflow that runs the consumer and provider contract stages separately.

The example contract covers a product entity with id, sku, name, price, and shippingEstimate. In a real supergraph, those fields might be resolved by catalog, pricing, and fulfillment subgraphs. The consumer still sees one GraphQL response, so its contract belongs at the router boundary.

Prerequisites

Use Node.js 22.18.0, npm 10.9.3, TypeScript 5.9.2, Vitest 3.2.4, GraphQL.js 16.11.0, Apollo Server 5.0.0, and @pact-foundation/pact 16.0.2. Pact JS 16 makes the V4 Pact interface the default export and provides addGraphQLInteraction directly.

Create an empty project and install exact versions:

mkdir federation-pact-demo
cd federation-pact-demo
npm init -y
npm install @apollo/server@5.0.0 graphql@16.11.0
npm install --save-dev @pact-foundation/pact@16.0.2 vitest@3.2.4 typescript@5.9.2 @types/node@22.15.30

Verify the toolchain:

node --version
npm --version
npm ls @pact-foundation/pact vitest typescript graphql @apollo/server

The first two lines should report v22.18.0 and 10.9.3. The dependency tree should contain the six pinned package versions. If you use later patch releases, commit the lockfile and run the complete tutorial before adopting them.

Step 1: Configure the TypeScript Contract Project

Replace package.json with scripts that separate consumer generation, provider verification, and the combined contract gate:

{
  "name": "federation-pact-demo",
  "private": true,
  "type": "module",
  "scripts": {
    "test:consumer": "vitest run tests/consumer.pact.test.ts",
    "test:provider": "vitest run tests/provider.pact.test.ts",
    "test:contract": "npm run test:consumer && npm run test:provider"
  },
  "dependencies": {
    "@apollo/server": "5.0.0",
    "graphql": "16.11.0"
  },
  "devDependencies": {
    "@pact-foundation/pact": "16.0.2",
    "@types/node": "22.15.30",
    "typescript": "5.9.2",
    "vitest": "3.2.4"
  }
}

Add tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "types": ["node", "vitest/globals"]
  },
  "include": ["src", "tests"]
}

Create directories for source, tests, and generated contracts:

mkdir -p src tests pacts

Pact writes JSON files into pacts. Commit those files only if your team reviews contracts in Git. Teams using a Pact Broker normally publish them from the consumer pipeline instead.

Verify Step 1: run the TypeScript compiler without emitting files.

npx tsc --noEmit

At this point it should exit with code 0 and print no TypeScript errors. Empty included directories are valid.

Step 2: Implement the Federated Graph Consumer

Create src/catalog-client.ts. This is production-style client code, not a test-only HTTP call:

export type ProductCard = {
  id: string;
  sku: string;
  name: string;
  price: {
    amount: number;
    currency: string;
  };
  shippingEstimate: string;
};

type ProductCardData = {
  product: ProductCard | null;
};

type GraphQLErrorResponse = {
  errors?: Array<{ message: string }>;
};

export const PRODUCT_CARD_QUERY = `
  query ProductCard($sku: ID!) {
    product(sku: $sku) {
      id
      sku
      name
      price {
        amount
        currency
      }
      shippingEstimate
    }
  }
`;

export async function fetchProductCard(
  baseUrl: string,
  sku: string
): Promise<ProductCard> {
  const response = await fetch(`${baseUrl}/graphql`, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({
      operationName: "ProductCard",
      query: PRODUCT_CARD_QUERY,
      variables: { sku }
    })
  });

  const payload = (await response.json()) as ProductCardData & GraphQLErrorResponse;

  if (!response.ok || payload.errors?.length) {
    throw new Error(payload.errors?.[0]?.message ?? `GraphQL HTTP ${response.status}`);
  }
  if (!payload.product) {
    throw new Error(`Product ${sku} was not found`);
  }

  return payload.product;
}

A named operation makes broker reports readable and lets gateways apply operation-level metrics or allowlists. The client checks both the HTTP status and GraphQL errors because a GraphQL server can return HTTP 200 with an errors array. It also treats a null product as a domain failure, which means the Pact response must demonstrate the non-null path this screen needs.

Verify Step 2: type-check the client.

npx tsc --noEmit

Expect no output and exit code 0. If fetch is unknown, confirm that Node 22 is active or add DOM to the tsconfig lib list.

Step 3: Test GraphQL Federation Contracts With Pact on the Consumer Side

Write the consumer contract with Pact V3. Record only the operation your client actually sends, and match on shape, not exact values.

import { PactV3, MatchersV3 } from "@pact-foundation/pact";
import { getProductWithReviews } from "../src/graphClient";

const { like, eachLike } = MatchersV3;
const provider = new PactV3({ consumer: "web-bff", provider: "federation-router" });

it("sends the agreed federated operation and accepts the supergraph shape", () => {
  provider
    .uponReceiving("a product-with-reviews query spanning two subgraphs")
    .withRequest({
      method: "POST",
      path: "/graphql",
      headers: { "Content-Type": "application/json" },
      body: like({ query: like("query ProductWithReviews"), variables: { id: "p-1" } }),
    })
    .willRespondWith({
      status: 200,
      body: { data: { product: { id: like("p-1"), name: like("Kettle"),
        reviews: eachLike({ id: like("r-1"), rating: like(5) }) } } },
    });

  return provider.executeTest(async (mock) => {
    const res = await getProductWithReviews(mock.url, "p-1");
    expect(res.product.reviews[0].rating).toBeGreaterThanOrEqual(0);
  });
});

Verify: npx vitest run test/consumer.pact.test.ts writes a pact file under ./pacts and fails if the client stops sending the agreed operation.

Create tests/consumer.pact.test.ts:

import path from "node:path";
import { describe, expect, it } from "vitest";
import { Matchers, Pact } from "@pact-foundation/pact";
import {
  fetchProductCard,
  PRODUCT_CARD_QUERY
} from "../src/catalog-client.js";

const { decimal, like, regex } = Matchers;

const pact = new Pact({
  consumer: "CatalogWeb",
  provider: "FederatedProductGraph",
  dir: path.resolve(process.cwd(), "pacts"),
  logLevel: "warn"
});

describe("CatalogWeb product card contract", () => {
  it("renders a sellable product returned by the federated graph", async () => {
    const interaction = pact
      .addGraphQLInteraction()
      .given("product SKU-123 is sellable")
      .uponReceiving("a ProductCard query for SKU-123")
      .withOperation("ProductCard")
      .withVariables({ sku: "SKU-123" })
      .withRequest("POST", "/graphql")
      .withQuery(PRODUCT_CARD_QUERY)
      .willRespondWith(200, (response) => {
        response.headers({
          "content-type": regex(
            "application/json(; ?charset=utf-8)?",
            "application/json; charset=utf-8"
          )
        });
        response.jsonBody({
          data: {
            product: {
              id: like("prod-123"),
              sku: "SKU-123",
              name: like("Mechanical Keyboard"),
              price: {
                amount: decimal(129.99),
                currency: regex("^[A-Z]{3}
quot;, "USD") }, shippingEstimate: like("2 business days") } } }); }); await interaction.executeTest(async (mockServer) => { const product = await fetchProductCard(mockServer.url, "SKU-123"); expect(product.sku).toBe("SKU-123"); expect(product.price.currency).toBe("USD"); expect(product.shippingEstimate).toContain("business days"); }); }); });

The Pact V4 GraphQL builder creates the correct POST body from the operation, variables, and query. Do not also add a hand-built JSON request body. That duplicates responsibility and makes query formatting differences harder to diagnose.

The exact SKU is intentional because the provider state promises that specific fixture. The id, name, amount, and estimate use matchers because their examples are not protocol constants. Currency uses a regular expression so the provider may return another ISO-style three-letter code without weakening the response into any string.

Verify Step 3: generate the pact.

npm run test:consumer
node -e "const p=require('./pacts/CatalogWeb-FederatedProductGraph.json'); console.log(p.consumer.name, p.provider.name, p.interactions.length)"

Vitest should pass one test. The second command should print CatalogWeb FederatedProductGraph 1. This proves a contract file was generated, not merely that an assertion passed.

Step 4: Build a Provider at the Federation Boundary

Create src/provider.ts:

import { ApolloServer } from "@apollo/server";
import { startStandaloneServer } from "@apollo/server/standalone";

const typeDefs = `#graphql
  type Price {
    amount: Float!
    currency: String!
  }

  type Product {
    id: ID!
    sku: ID!
    name: String!
    price: Price!
    shippingEstimate: String!
  }

  type Query {
    product(sku: ID!): Product
  }
`;

type Product = {
  id: string;
  sku: string;
  name: string;
  price: { amount: number; currency: string };
  shippingEstimate: string;
};

const products = new Map<string, Product>();

const resolvers = {
  Query: {
    product: (_parent: unknown, args: { sku: string }) => products.get(args.sku) ?? null
  }
};

export function seedSellableProduct(): void {
  products.set("SKU-123", {
    id: "prod-123",
    sku: "SKU-123",
    name: "Mechanical Keyboard",
    price: { amount: 129.99, currency: "USD" },
    shippingEstimate: "2 business days"
  });
}

export async function startProvider(port = 0): Promise<{
  url: string;
  stop: () => Promise<void>;
}> {
  const server = new ApolloServer({ typeDefs, resolvers });
  const { url } = await startStandaloneServer(server, {
    listen: { host: "127.0.0.1", port }
  });

  return {
    url: url.replace(/\/$/, ""),
    stop: () => server.stop()
  };
}

This executable provider represents the router's public schema. In production verification, point Pact at the actual router started with isolated test subgraphs. The local implementation is useful for learning and for teams whose router test harness exposes the same operation without booting every service.

The in-memory map is reset only by process lifetime, so the provider state handler must seed data before Pact replays the interaction. Avoid calling shared staging databases from provider verification. A deterministic fixture makes a contract failure describe compatibility, not environment health.

Verify Step 4: run a one-off GraphQL request against an ephemeral server.

node --import=tsx -e "import('./src/provider.ts').then(async m=>{m.seedSellableProduct();const s=await m.startProvider();const r=await fetch(s.url+'graphql',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({query:'query { product(sku:"SKU-123") { sku } }'})});console.log(await r.text());await s.stop()})"

Install tsx@4.20.3 as a dev dependency before this command if your environment does not provide TypeScript stripping:

npm install --save-dev tsx@4.20.3

The response should contain data.product.sku equal to SKU-123. Add tsx to package.json when you keep this verification command in project documentation.

Step 5: Verify the Pact Against the GraphQL Provider

Verify against the composed router, not a single subgraph, so federation wiring is exercised.

import { Verifier } from "@pact-foundation/pact";
import { seedProduct, seedReviews } from "./support/state";

new Verifier({
  provider: "federation-router",
  providerBaseUrl: process.env.ROUTER_URL ?? "http://localhost:4000",
  pactUrls: ["./pacts/web-bff-federation-router.json"],
  stateHandlers: {
    "product p-1 exists across subgraphs": async () => {
      await seedProduct("p-1");
      await seedReviews("p-1", 2);
    },
  },
}).verifyProvider().then(() => console.log("Pact verification complete"));

Verify: start the router, then run this verifier. A satisfied contract exits zero; a missing field or removed resolver fails with the exact interaction that broke.

Create tests/provider.pact.test.ts:

import path from "node:path";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { Verifier } from "@pact-foundation/pact";
import {
  seedSellableProduct,
  startProvider
} from "../src/provider.js";

let providerUrl = "";
let stopProvider: () => Promise<void>;

beforeAll(async () => {
  const provider = await startProvider();
  providerUrl = provider.url;
  stopProvider = provider.stop;
});

afterAll(async () => {
  await stopProvider();
});

describe("FederatedProductGraph provider contract", () => {
  it("satisfies every CatalogWeb interaction", async () => {
    const result = await new Verifier({
      provider: "FederatedProductGraph",
      providerBaseUrl: providerUrl,
      pactUrls: [
        path.resolve(
          process.cwd(),
          "pacts/CatalogWeb-FederatedProductGraph.json"
        )
      ],
      stateHandlers: {
        "product SKU-123 is sellable": async () => {
          seedSellableProduct();
        }
      },
      logLevel: "info"
    }).verifyProvider();

    expect(result).toContain("Pact Verification Complete");
  });
});

The verifier reads the pact, invokes the matching provider state handler, and sends the recorded POST to /graphql under providerBaseUrl. The provider name must match the consumer pact exactly. The state string must also match character for character, which is why stable domain language works better than implementation details such as "row 42 exists."

If your real router requires authentication, add a requestFilter that inserts a test token at verification time. Do not record a live bearer token in the consumer pact. The contract should capture the required authorization behavior while secrets remain outside the artifact.

Verify Step 5: run both sides from a clean pacts directory after moving any old artifact out of the way.

npm run test:consumer
npm run test:provider

Both Vitest commands should pass. Provider output should list one successful interaction and end with Pact Verification Complete.

Step 6: Add a Breaking Change to Prove the Contract Works

A contract suite earns trust when you have watched it reject a realistic federation regression. In src/provider.ts, temporarily rename shippingEstimate to deliveryEstimate in both the Product type and fixture, then run:

npm run test:provider

The ProductCard query should fail validation because the public graph no longer offers shippingEstimate. Pact reports a response mismatch rather than silently updating the consumer's expectation. Restore shippingEstimate and run the command again.

Now try a subtler failure: keep the field but return null. GraphQL non-null propagation can turn data.product into null or even data into null, depending on where the non-null field appears. Pact then identifies the missing object expected by CatalogWeb. This is exactly the boundary behavior a schema-only diff cannot fully predict, because runtime resolver behavior controls null propagation.

Pact is not a substitute for composition. A subgraph can rename an internal field yet preserve the router response through entity resolution, which should not break this consumer contract. Conversely, two subgraphs can fail federation composition before any request reaches a resolver. Run composition checks alongside Pact, as explained in validating GraphQL deprecation with schema diff.

Verify Step 6: after restoring the provider, run:

npm run test:contract

The combined command must finish with two passing Vitest runs. Never leave the deliberate breaking change in the branch.

Step 7: Model Federation-Specific Provider States

Federated operations often gather fields from multiple owners. Keep one consumer interaction, but make the provider state describe the graph-wide business condition:

stateHandlers: {
  "product SKU-123 is sellable": async () => {
    await catalogFixtures.upsertProduct({
      id: "prod-123",
      sku: "SKU-123",
      name: "Mechanical Keyboard"
    });
    await pricingFixtures.setPrice("prod-123", {
      amount: 129.99,
      currency: "USD"
    });
    await fulfillmentFixtures.setEstimate("prod-123", "2 business days");
  }
}

This snippet shows the shape for a real router harness. Implement catalogFixtures, pricingFixtures, and fulfillmentFixtures in your own test infrastructure before using it. They should call approved fixture endpoints or local repositories, not production services. The runnable tutorial uses seedSellableProduct because those organization-specific fixture APIs cannot be universal.

Do not split the ProductCard consumer expectation into three Pacts merely because three subgraphs own fields. CatalogWeb does not communicate with those subgraphs. Splitting at implementation boundaries would allow each subgraph to pass while the router still returns an unusable combined response.

Use direct subgraph contracts only when another service genuinely calls that subgraph endpoint. For detailed boundary selection, compare Pact vs OpenAPI for contract testing and testing backend contracts without production.

Verify Step 7: list the provider states embedded in the generated pact.

node -e "const p=require('./pacts/CatalogWeb-FederatedProductGraph.json'); console.log(p.interactions.map(i=>i.providerStates?.map(s=>s.name)))"

The output should include product SKU-123 is sellable exactly once.

Step 8: Publish and Gate Contracts in CI

Local pactUrls are ideal for the tutorial. In a multi-repository system, publish the consumer pact to a Pact Broker, verify it from the provider pipeline, and use can-i-deploy before release. Keep consumer version and branch metadata tied to immutable CI values such as the Git commit SHA and branch name.

A minimal GitHub Actions verification job looks like this:

name: provider-contract

on:
  pull_request:
  push:
    branches: [main]

jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22.18.0
          cache: npm
      - run: npm ci
      - run: npm run test:consumer
      - run: npm run test:provider
        env:
          CI: "true"

For separate repositories, replace local pactUrls with brokerUrl, consumerVersionSelectors, and credentials supplied through CI secrets. Set publishVerificationResult only in CI, and use the provider Git SHA as providerVersion. Publishing results from laptops pollutes the broker's deployment evidence.

A safe delivery gate asks whether the exact consumer and provider versions can deploy to the target environment. It should not simply ask whether main passed recently. Versioned evidence matters when a router, supergraph schema, and client move on different schedules.

Verify Step 8: inspect the workflow syntax locally and rerun the contract gate:

npm run test:contract

Then open the GitHub Actions run and confirm both the consumer artifact generation and provider replay appear in logs. For broader API pipeline design, use the API testing roadmap.

How Test GraphQL Federation Contracts With Pact Without Brittle Tests

Match semantics, not random examples. An exact enum or currency code is appropriate when consumer logic branches on that value. A generated database id should use like or regex. Arrays need enough examples to exercise consumer behavior, but matching rules should allow valid changes in length unless the exact count is part of the agreement.

Keep queries named and stable. GraphQL permits insignificant whitespace, yet tooling and persisted-query systems may hash the exact document. Pact's GraphQL helper handles query structure for the interaction, but your production client should still use the same exported document the test imports.

Record the smallest response selection the consumer uses. GraphQL already lets the consumer declare required fields. Adding fields "for completeness" expands the contract and creates false coupling. Test a separate interaction when another screen uses a materially different operation or provider state.

Test errors intentionally. Add interactions for NOT_AUTHORIZED, a missing entity, or a domain error only when the client has behavior for those cases. Assert GraphQL errors extensions.code exactly if it controls UI flow. Use broad matchers for human-readable error messages unless copy is contractual.

Keep schema composition, resolver unit tests, Pact verification, and a few end-to-end paths. Each catches a different class of defect. The GraphQL query complexity security testing guide adds abuse and resource-limit coverage that consumer contracts deliberately do not address.

Troubleshooting

Problem: addGraphQLInteraction is not a function -> Confirm @pact-foundation/pact 16.x is installed and import Pact, not PactV2. Pact JS 16 aliases the V4 implementation as Pact. Delete stale lockfile changes only through your normal dependency update process, then inspect npm ls output.

Problem: the mock server reports a request mismatch -> Log the client's outgoing operationName, variables, path, and content-type. Import the same PRODUCT_CARD_QUERY constant in production code and the Pact test. Do not normalize the query in one location but not the other.

Problem: provider verification returns 404 -> Set providerBaseUrl to the server root when the interaction path is /graphql. If the base URL already ends in /graphql, the replay may target the wrong combined path. Print the ephemeral URL and enable Pact info logging.

Problem: verification fails because product is null -> Make the provider state seed every subgraph dependency before replay. In a federated fixture harness, wait for writes to become visible before resolving the handler. Prefer synchronous local stores or explicit readiness polling over arbitrary sleeps.

Problem: content-type differs by charset -> Use a regex matcher that accepts application/json with an optional UTF-8 charset, as the consumer test does. Do not accept unrelated media types. Confirm the real client sends application/json.

Problem: the pact passes but federation composition fails -> This is expected separation of concerns. Run the federation composition or schema check before provider verification. Pact proves recorded operations at runtime; it does not validate ownership directives, entity keys, or composition rules across all subgraphs.

Interview Questions and Answers

The interviewQnA collection below covers the main design questions: choosing the provider boundary, separating composition from consumer contracts, matching GraphQL responses, handling provider states, and gating deployment. A strong explanation should connect each tool to the failure it detects rather than claiming Pact replaces every GraphQL test.

Common Mistakes

  • Pointing the consumer pact at a subgraph even though production clients call the router.
  • Treating a successful schema composition as proof that resolvers return consumer-compatible data.
  • Matching every value exactly, which makes prices, ids, and display text unnecessarily brittle.
  • Matching every value loosely, which misses enum, error-code, and nullability changes that alter client behavior.
  • Generating pacts from hand-written requests instead of exercising the production GraphQL client.
  • Sharing staging data across provider verification jobs, causing nondeterministic provider states.
  • Publishing verification results from local machines with mutable version labels.
  • Omitting negative interactions when the UI has explicit behavior for GraphQL errors.

Where To Go Next

First, add one negative ProductCard interaction for a product that is not sellable, including the exact errors.extensions.code your client handles. Then run the pact against your real federation router with isolated test subgraphs.

Deepen the implementation with API contract testing with Pact, review operation coverage in the modern GraphQL API testing guide, and protect the wider schema with OpenAPI and schema testing concepts. Practice explaining the boundary choices in API testing interview questions, or use the hands-on scenarios in QAJobFit practice.

Conclusion

To test GraphQL federation contracts with Pact effectively, contract the public router operations that real consumers execute, generate those contracts from production client code, and verify them against a deterministic graph provider. Use precise matchers, named operations, and graph-wide provider states so failures identify actual compatibility risks.

Keep federation composition checks beside Pact rather than inside it. Together, composition protects the supergraph structure while Pact protects consumer-visible request and response behavior. Run both before deployment, then gate exact versions through your broker when repositories release independently.

Interview Questions and Answers

Where do you place the Pact provider boundary in GraphQL federation?

I place it at the endpoint the consumer calls. For a web client that calls a federation router, the router is the Pact provider even if several subgraphs own selected fields. I use direct subgraph contracts only for consumers that genuinely bypass the router.

What does Pact catch that a federation composition check does not?

Composition proves that subgraph schemas can form a supergraph under federation rules. Pact replays an actual consumer operation and checks runtime status, response shape, matchers, and errors. It can catch resolver nulls, incorrect values, and router behavior that a schema-only check cannot.

How do you prevent GraphQL Pact tests from becoming brittle?

I keep exact matches for values that control consumer behavior, such as enum values or error codes. I use type, decimal, and regex matchers for ids, prices, and display text that can legitimately change. I also select only fields used by the operation.

How do provider states work across multiple subgraphs?

A router-level provider state describes a business condition such as a sellable product. Its handler creates compatible catalog, pricing, and fulfillment fixtures before verification. The state should be deterministic, isolated per test, and independent of production data.

Should a contract test use a hand-written GraphQL request or the production client?

It should call the production client against Pact's mock server. That verifies serialization of operationName, query, variables, headers, path, and response handling. A hand-written test request can pass while the real client remains broken.

How would you gate deployment with Pact in a federated architecture?

I publish pacts with immutable consumer commit versions and publish provider verification results with the router or graph commit version. Before deployment, can-i-deploy evaluates the exact versions for the target environment. Composition checks run earlier in the same provider pipeline.

How do you test GraphQL nullability behavior with Pact?

I create interactions for the response paths the consumer explicitly handles. Provider verification then catches cases where a non-null resolver returns null and GraphQL propagates that null upward. I keep separate negative contracts when the client has intentional fallback or error behavior.

Frequently Asked Questions

Can Pact test a GraphQL federation router?

Yes. A GraphQL router exposes HTTP requests and responses, so Pact can record consumer operations and replay them against that endpoint. Treat the router as the provider when it is the system the consumer actually calls.

Does Pact validate Apollo Federation schema composition?

No. Pact validates recorded runtime interactions, not federation directives or supergraph composition. Run a federation-aware composition or schema check in the same pipeline.

Should each GraphQL subgraph have its own consumer Pact?

Only when a real consumer calls that subgraph directly. If an application calls the federated router, one router-level interaction should describe the combined response even when several subgraphs resolve its fields.

How should GraphQL errors be represented in Pact?

Create a dedicated interaction for each error behavior the consumer handles. Match stable fields such as errors.extensions.code exactly, and use flexible matchers for messages unless the wording is part of the product contract.

Why use named GraphQL operations in contract tests?

Named operations make Pact reports, router metrics, traces, and allowlists easier to interpret. The test should import the same operation document used by the production client.

Where should provider state data be created for a federated query?

The state handler should seed all subgraphs that contribute to the router response. Use isolated fixture APIs or local stores and wait for data readiness before Pact replays the operation.

Can Pact replace GraphQL end-to-end tests?

No. Pact gives fast compatibility evidence for specific consumer interactions, while a small end-to-end suite checks real infrastructure, authentication, and routing. Resolver tests and composition checks also remain necessary.

Related Guides