DEV Community

1xApi
1xApi

Posted on • Originally published at 1xapi.com

How to Implement API Contract Testing with Pact in Node.js (2026 Guide)

How to Implement API Contract Testing with Pact in Node.js (2026 Guide)

As of March 2026, microservices architecture remains the standard for building scalable applications. But with multiple services communicating via APIs, how do you ensure that changes in one service do not break your consumers? That is where contract testing comes in — and Pact is the leading tool for the job.

In this guide, you will learn what contract testing is, why it matters, and how to implement it in Node.js using Pact (v3.x as of early 2026).

What is Contract Testing?

Contract testing verifies that an API provider and its consumers agree on the API structure. Instead of spinning up both services, each consumer defines what it expects from the API, and the provider verifies it can satisfy those expectations.

Think of it like a legal contract: the consumer specifies what it needs (the "expectations"), and the provider signs off that it delivers exactly that.

Why Contract Testing Beats Integration Tests

Aspect Integration Tests Contract Tests
Setup Requires all services running Tests run in isolation
Speed Slow (full stack) Fast (unit-level)
Failure isolation One failure affects all Pinpoints the broken side
CI/CD Heavy resource usage Lightweight

Setting Up Pact in Node.js

Install the core packages:

npm install @pact-foundation/pact@^14.0.0 @pact-foundation/pact-core@^14.0.0
Enter fullscreen mode Exit fullscreen mode

Project Structure

my-api/
├── provider/
│   ├── index.js
│   └── pact/
│       └── provider.test.js
└── consumer/
    ├── index.js
    ├── client.js
    └── pact/
        └── consumer.test.js
Enter fullscreen mode Exit fullscreen mode

Writing Consumer Tests (The Expectation)

The consumer defines what it expects from the API. Let us say we have a user service that expects a list of users:

// consumer/pact/consumer.test.js
const { like, term } = require("@pact-foundation/pact-core");
const pact = require("@pact-foundation/pact").PactV4;

describe("User Consumer", () => {
  const provider = pact({
    consumer: "user-web-app",
    provider: "user-api",
    port: 4000,
    log: path.resolve(process.cwd(), "logs", "pact.log"),
    dir: path.resolve(process.cwd(), "pacts"),
    logLevel: "INFO",
  });

  beforeAll(() => provider.setup());
  afterAll(() => provider.finalize());

  describe("fetching users", () => {
    it("returns a list of users", async () => {
      // Define the expected response structure
      provider
        .given("users exist")
        .uponReceiving("a request for all users")
        .withRequest({
          method: "GET",
          path: "/api/users",
          headers: {
            Accept: "application/json",
          },
        })
        .willRespondWith({
          status: 200,
          headers: {
            "Content-Type": "application/json",
          },
          body: like({
            users: like([
              {
                id: like(1),
                name: like("John Doe"),
                email: like("john@example.com"),
              },
            ]),
          }),
        });

      await provider.executeTest(async (mockServer) => {
        const response = await fetch(`${mockServer.url}/api/users`);
        const data = await response.json();

        expect(response.status).toBe(200);
        expect(data.users).toHaveLength(1);
        expect(data.users[0].name).toBe("John Doe");
      });
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

Key Concepts in Consumer Tests

  • Given: Sets up the provider state (e.g., "users exist")
  • uponReceiving: Describes the consumer action
  • withRequest: Specifies the HTTP request
  • willRespondWith: Defines the expected response
  • like(): Creates flexible matchers (ignores actual values, validates structure)
  • term(): Creates matchers for specific patterns (regex)

Writing Provider Tests (The Verification)

The provider verifies it can satisfy all registered contracts:

// provider/pact/provider.test.js
const { VerifierV4 } = require("@pact-foundation/pact-core");
const path = require("path");

describe("User API Provider", () => {
  const verifier = new VerifierV4({
    provider: "user-api",
    providerBaseUrl: "http://localhost:3000",
    pactUrls: [
      path.resolve(
        process.cwd(),
        "../consumer/pacts/user-web-app-user-api.json"
      ),
    ],
    stateHandlers: {
      "users exist": () => {
        // Set up test data in your database
        console.log("Setting up: users exist");
        // Your database setup logic here
      },
    },
    requestFilters: [
      // Add auth headers if needed
      (req, res, next) => {
        req.headers["Authorization"] = "Bearer test-token";
        next();
      },
    ],
  });

  it("validates the expectations of user-web-app", async () => {
    const result = await verifier.verify();
    console.log("Contract verification result:", result);
  });
});
Enter fullscreen mode Exit fullscreen mode

Running Contract Tests in CI/CD

Consumer CI (Runs on Every Commit)

# .github/workflows/consumer-tests.yml
name: Consumer Contract Tests

on: [push]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "22"
      - run: npm ci
      - run: npm test
        env:
          CI: true
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: pact-contracts
          path: pacts/
Enter fullscreen mode Exit fullscreen mode

Provider CI (Runs Before Deployment)

# .github/workflows/provider-tests.yml
name: Provider Contract Tests

on:
  push:
    branches: [main]
  pull_request:

jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "22"
      - run: npm ci
      - run: npm run start:provider &
      - run: npm test:provider
        env:
          CI: true
Enter fullscreen mode Exit fullscreen mode

Publishing Contracts to Pact Broker

For teams with multiple services, use a Pact Broker to share contracts:

const { Publisher } = require("@pact-foundation/pact-core");

async function publishContracts() {
  const publisher = new Publisher({
    pactFilesOrDirs: ["./pacts"],
    pactBroker: "https://your-broker.pact.dius.com.au",
    pactBrokerToken: process.env.PACT_BROKER_TOKEN,
    consumerVersion: process.env.GITHUB_SHA?.substring(0, 7),
  });

  await publisher.publish();
}

publishContracts();
Enter fullscreen mode Exit fullscreen mode

Best Practices for Contract Testing (2026)

  1. Start with consumer tests — They are easier to write and catch issues early
  2. Use semantic versioning — Tag contracts with version numbers
  3. Run provider tests before deployment — Prevent breaking changes from reaching production
  4. Keep contracts small — Test the critical paths, not every edge case
  5. Use flexible matchers — Prefer like() over exact values to reduce test flakiness

Common Pitfalls

  • Testing too much detail (breaks easily when irrelevant fields change)
  • Not running provider tests in CI
  • Forgetting to publish contracts to the broker
  • Hardcoding URLs instead of using environment variables

Conclusion

Contract testing with Pact is essential for maintaining reliable API integrations in 2026. It catches breaking changes early, reduces test flakiness, and enables independent service deployments. Start with consumer tests, add provider verification, and integrate both into your CI/CD pipeline.

For more advanced scenarios, explore Pact support for asynchronous messaging, XML APIs, and GraphQL contracts.

Top comments (0)