Engineering

Contract Testing Between Frontend and Backend Teams

Learn how consumer-driven contract testing with Pact catches breaking API changes between React frontends and Node.js backends before they reach production.

Aarav Sharma

Aarav Sharma

October 31, 20269 min read
Share
A renamed API field that still returns 201, caught by a contract test before deploy

Imagine a React checkout page that has been working perfectly for weeks. Then the backend team deploys a small API change: transactionId is renamed to paymentId. The API still returns a successful 200 or 201 response, so the backend tests pass, but the frontend suddenly stops displaying the payment confirmation.

This is the type of problem contract testing for frontend-backend workflows is designed to prevent. When frontend and backend teams deploy independently, they need a reliable way to verify that both sides still agree on how an API behaves.

So today let's look at what API contracts are, how consumer-driven contract testing works, how Pact JS (an API contract testing tool) connects React and Node.js applications, and how to catch a breaking change before it ships.

Quick Answer

Contract testing checks that a backend still returns what a frontend actually depends on. The frontend's test records each request it makes and the response it expects, and that becomes a contract file. The backend replays the contract against its real API before it deploys, so a renamed field or a changed status code fails a test instead of breaking production. Pact is the most common tool for this in JavaScript, and it works with React frontends and Node.js backends.

What Contract Testing Between Frontend and Backend Means

Before the apps are deployed, contract testing between the frontend and backend teams confirms that the backend still supports the API functionality that the frontend depends on. The backend functions as the supplier, and the frontend as the customer. Requests, answers, headers, status codes, and other actions that the customer truly depends on can all be described in the contract.

What is Contract Testing?

API contract testing confirms that two independently deployed systems still agree on the messages they exchange. The consumer (usually the frontend or another service) records the requests it makes and the responses it expects. The provider (the backend) later verifies that its real implementation can satisfy those recorded interactions.

Frontend-backend architecture

  • Consumer: The frontend application that calls the API.
  • Provider: The backend application that provides the API response.
  • Contract: a machine-readable file (often JSON) describing the expected interactions.
  • Mock server: a temporary stand-in for the provider that the consumer tests against.
  • Contract broker (or shared repository): the place contracts are published so both teams can see them.
  • Provider verification: the backend replaying the contract against its own running service.

What makes Contract Testing different from other testing formats?

Contract testing vs schema validation. Schema validation verifies that an API adheres to a specified schema. Contract testing goes one step further by determining whether the supplier meets the real requirements of a customer.

Contract testing vs integration testing. Integration testing usually checks whether multiple components work together. Contract testing lets each team test independently against a documented agreement.

Contract testing vs end-to-end testing. End-to-end testing validates a complete user journey, for example: Login -> Add Product -> Checkout -> Payment -> Order Confirmation. Contract testing instead focuses on individual API interactions within that journey.

For more deeper reference, see the Pact documentation and the OpenAPI specification.

The Problem

Even when their own tests are successful, frontend and backend teams may unintentionally break one another. It can be done by the following scenarios:

  • Renamed or removed response fields
  • Changed data types (number → string)
  • Required fields becoming optional or vice versa
  • Different status codes for the same scenario
  • Altered error-body structure
  • Unexpected authentication or header requirements

A practical scenario: a React checkout page expects the payment API to return a transactionId:

{
  "transactionId": "txn_123",
  "status": "success"
}

The backend team renames the field and restructures the payment service:

{
  "paymentId": "pay_123",
  "status": "success"
}

The backend endpoint still works. The frontend still reads the old field:

const result = await createPayment(payment);
showConfirmation(result.transactionId);

Now the frontend receives undefined.

The Solution: Contract Testing with React and Node.js Using Pact

What Is Pact and How Does It Work?

Before implementing contract testing, it is important to understand what Pact actually does and why both frontend and backend teams benefit from using it.

Pact is a consumer-driven contract testing tool. It allows the frontend team, which acts as the API consumer, to define the API interactions it depends on. Pact then generates a contract from those expectations, which the backend team can use to verify that its API implementation still provides the required behavior.

The frontend consumer test runs against a Pact mock server and produces a contract file, which is shared and replayed by provider verification against the real Node.js API, giving a pass or fail before deploy

Requirements for the example

  • Node.js 18 or later
  • A React or Next.js frontend
  • A Node.js backend using Express
  • Working knowledge of JavaScript or TypeScript
  • Jest or Vitest as the test runner
  • The @pact-foundation/pact package
  • Optional: a Pact Broker or PactFlow account for sharing contracts across CI/CD

Step 1: Set Up the Project Structure

contract-testing-demo/
├── frontend/
│   ├── src/
│   │   └── api/
│   │       └── orderApi.js
│   ├── tests/
│   │   └── order.pact.test.js
│   └── package.json
└── backend/
    ├── src/
    │   └── server.js
    ├── tests/
    │   └── order.provider.test.js
    └── package.json

Step 2: Create the Frontend API Client

Set up a React app if you don't already have one:

npm create vite@latest frontend -- --template react
cd frontend
npm install

For contract testing, the part that matters is the API client, not the UI itself. Create frontend/src/api/orderApi.js. The base URL is a parameter, so the test can point the same function at Pact's mock server:

const API_URL = "http://localhost:3000";

export async function createOrder(order, baseUrl = API_URL) {
  const response = await fetch(`${baseUrl}/orders`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(order),
  });

  if (!response.ok) {
    throw new Error("Order creation failed");
  }

  return response.json();
}

The actual application calls the backend using this function:

const order = { productId: "prod_123", quantity: 1 };
const result = await createOrder(order);
console.log(result.orderId);

The response the frontend expects:

{
  "orderId": "ord_123",
  "status": "created"
}

Step 3: Install Pact JS

npm install --save-dev @pact-foundation/pact

Pact JS gives JavaScript and Node.js applications the tools they need to create consumer contracts and perform provider verification.

Step 4: Write the Consumer Contract Test

Create frontend/tests/order.pact.test.js:

import { PactV4, MatchersV3 } from "@pact-foundation/pact";
import { createOrder } from "../src/api/orderApi";

const { like } = MatchersV3;

const provider = new PactV4({
  consumer: "CheckoutFrontend",
  provider: "OrderAPI",
  dir: "./pacts",
});

describe("Order API Contract", () => {
  test("creates an order", async () => {
    await provider
      .addInteraction()
      .given("the order service is available")
      .uponReceiving("a request to create an order")
      .withRequest("POST", "/orders", (builder) => {
        builder.headers({ "Content-Type": "application/json" });
        builder.jsonBody({ productId: "prod_123", quantity: 1 });
      })
      .willRespondWith(201, (builder) => {
        builder.headers({ "Content-Type": "application/json" });
        builder.jsonBody({
          orderId: like("ord_123"),
          status: like("created"),
        });
      })
      .executeTest(async (mockServer) => {
        const result = await createOrder(
          { productId: "prod_123", quantity: 1 },
          mockServer.url
        );
        expect(result.orderId).toBeDefined();
        expect(result.status).toBe("created");
      });
  });
});

Some parts to understand:

  • Consumer and provider names: Consumer CheckoutFrontend is the React application, the consumer because it consumes the Order API. Provider OrderAPI is the Node.js application, the provider because it provides the Order API.
  • Expected request: .withRequest(...) tells Pact: "when the frontend calls the backend, it sends a POST to /orders with this body."
  • Expected response: .willRespondWith(201, ...) tells Pact: "the frontend expects a 201 with an orderId and a status."
  • Calling the real API client: createOrder({...}, mockServer.url). Within the contract test, we are not manually submitting another fetch() call. The real React API client is being called, pointed at the Pact mock server. In other words, the contract test confirms what the program actually sends.

Step 5: Run the Consumer Test

Inside frontend/package.json:

{
  "scripts": {
    "test:pact": "jest tests/order.pact.test.js"
  }
}

Run the script:

npm run test:pact

When it passes, Pact starts a local mock server, verifies that createOrder() sends the request specified in the test, returns the defined answer, and creates a contract file:

frontend/
└── pacts/
    └── CheckoutFrontend-OrderAPI.json

That JSON file is the actual agreement between frontend and backend. You don't hand-write it; the consumer test generates it.

Step 6: Build the Node.js Provider

Set up the backend. The server uses import, so set the package type to module:

mkdir backend && cd backend
npm init -y
npm pkg set type=module
npm install express
npm install --save-dev @pact-foundation/pact

Create backend/src/server.js:

import express from "express";

const app = express();
app.use(express.json());

app.post("/orders", (req, res) => {
  const { productId, quantity } = req.body;

  if (!productId || !quantity) {
    return res.status(400).json({
      error: "productId and quantity are required",
    });
  }

  res.status(201).json({
    orderId: "ord_123",
    status: "created",
  });
});

app.listen(3000, () => {
  console.log("Order API running on port 3000");
});

Start it:

node src/server.js

Step 7: Verify the Provider Against the Contract

Create backend/tests/order.provider.test.js:

import path from "node:path";
import { Verifier } from "@pact-foundation/pact";

describe("Order API Provider Verification", () => {
  test("verifies the Order API contract", async () => {
    await new Verifier({
      provider: "OrderAPI",
      providerBaseUrl: "http://localhost:3000",
      pactUrls: [
        path.resolve(process.cwd(), "../frontend/pacts/CheckoutFrontend-OrderAPI.json"),
      ],
    }).verifyProvider();
  });
});

Run it while the server is running:

npm test

Pact reads the consumer's contract, replays POST /orders against the real, running backend, and compares the actual response to what the contract expects. If the real response matches, the test passes.

Step 8: See Pact Catch a Breaking Change

Let's say the backend developer modifies the response now:

// before
res.status(201).json({ orderId: "ord_123", status: "created" });

// after
res.status(201).json({ paymentId: "pay_123", status: "created" });

The code still compiles. The endpoint still returns 201. A backend unit test that only checks the status code would still pass. However, orderId is required by the contract, and it is missing, so provider verification fails.

The contract expects a 201 with orderId and status, the backend now returns paymentId instead, and provider verification fails because orderId is missing

When Should Teams Use Contract Testing?

Use it when:

  • Frontend and backend deploy independently.
  • You have microservices, multiple frontend clients, mobile apps, or external API consumers.
  • A small field change can block checkout, authentication, account management, payments, or core dashboards.

When integration risk is truly low and a single small team owns both a stable frontend and backend, lighter schema validation might be sufficient.

Trade-offs

Contract tests only cover what the consumer wrote down. If the frontend's test never mentions a field, the backend can change that field freely and the contract will still pass. The protection is as good as the interactions the consumer team records.

They also add moving parts. Both teams need the same Pact version, a place to share contracts (a Pact Broker or a shared repository), and CI steps on both sides. Provider states such as "the order service is available" need setup code on the backend, and that code has to be kept up to date as the data model changes.

Contract tests do not replace other tests. They check the shape of each request and response, not whether the business logic is right or whether a full user journey works. Teams still need unit tests for logic and a small number of end-to-end tests for critical journeys.

If one small team owns both the frontend and the backend and deploys them together, a shared schema or typed API client can catch the same mistakes with less setup.

FAQ

What is contract testing between frontend and backend teams?

Contract testing confirms that the requests and answers that a frontend depends on are still fulfilled by a backend API. The backend functions as the supplier, and the frontend as the customer.

Is the frontend the consumer in contract testing?

Yes. The frontend (or any client) is the consumer; the backend is the provider. Consumer-driven contracts start from the needs of the consumer.

Does Pact replace end-to-end testing?

No. Pact verifies the HTTP (or message) contract between two services. End-to-end tests still check full user journeys, browser behaviour, and multi-service orchestration.

Can React and Node.js use Pact for contract testing?

Yes. Both React frontends and Express (or other Node.js) backends can take part because the official @pact-foundation/pact package works with JavaScript and TypeScript.

What API changes can contract tests catch?

Renamed or removed fields, type changes, altered status codes, unexpected headers, request-shape mismatches, and changes to error response structure that the consumer relies on.

Should teams use OpenAPI or Pact?

Use OpenAPI for API design, documentation, and client generation. Use Pact (or similar consumer-driven tools) when you need to protect real consumers from breaking changes. Many mature teams use both.

Conclusion

When frontend and backend teams release separately, API compatibility issues will inevitably arise. A frontend feature that relies on the prior behavior may be subtly broken by a seemingly innocuous backend change, such as renaming a field, altering a data type, or changing an error response.

By transforming the API expectations between customers and suppliers into an automatically verifiable agreement, contract testing bridges this gap. Teams can verify compatibility as part of the development and CI/CD process rather than depending just on API documentation, team communication, or end-to-end testing.

Contract testing, when used in conjunction with unit, integration, and end-to-end testing, helps frontend and backend teams find API incompatibilities before release, rather than after a client discovers a malfunction. Need help setting up contract tests between your teams? Contact us.

Aarav Sharma

Aarav Sharma

Lead Software Engineer

Aarav leads product engineering at Matlab Infotech, where he has shipped mobile and web platforms across healthcare, fintech, and SaaS. He writes about pragmatic engineering and shipping fast without cutting corners.

Related articles

Let's Collaborate

Tell us about your project and we'll come back with a plan, a timeline, and a quote.

Project Type

Budget

Task Message

Your Contacts