In modern distributed microservice architectures, frontend applications (React / Next.js) consume data from dozens of independent upstream backend services over REST, GraphQL, or gRPC interfaces. Traditional integration testing approaches suffer from severe architectural limitations:
The architectural solution is Consumer-Driven Contracts (CDC) standardized via the Pact specification. In this model, the consumer (frontend) explicitly defines its precise data requirements, compiles them into an immutable JSON contract (Pact File), and publishes it to a central registry (Pact Broker). Upstream backend providers verify these contracts on their side before either application is cleared for deployment.
Consumer-Driven Contracts decouple frontend and backend verification timelines while guaranteeing runtime integration integrity.
+-------------------------------------------------------------------------+
| PHASE 1: Frontend Consumer (React / Vitest) |
| |
| 1. Test executes real HTTP call against local Pact Mock Server |
| 2. Mock Server verifies request parameters and fulfills stubbed data |
| 3. Generates artifact: order-web-order-service.json |
+-------------------------------------------------------------------------+
|
v pact-broker publish
+-------------------------------------------------------------------------+
| Pact Broker |
| - Stores contract indexed by consumer Git commit SHA |
| - Tracks compatibility matrix: Consumer SHA <-> Provider SHA |
| - Fires webhook to backend repository when contract specifications mutate|
+-------------------------------------------------------------------------+
|
v Trigger CI Provider Verification
+-------------------------------------------------------------------------+
| PHASE 2: Backend Provider (Node.js / Go) |
| |
| 1. Pulls contract from Pact Broker |
| 2. Sets Provider State (e.g. "order with ID ord-9910 exists in DB") |
| 3. Replays recorded contract requests against live server controllers |
| 4. Validates response schemas using flexible Matchers |
| 5. Publishes verification results back to Pact Broker |
+-------------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------------+
| PHASE 3: Deployment Quality Gate |
| |
| $ pact-broker can-i-deploy --pacticipant OrderWeb |
| --version $GIT_SHA |
| --to-environment production |
| | |
| +------------------+------------------+ |
| | | |
| [PASS] [FAIL] |
| v v |
| Deployment Approved Deployment Blocked |
| (Backend in production is compatible) (Incompatible API version) |
+-------------------------------------------------------------------------+OrderApiClient.ts#A typed HTTP client responsible for fetching order data:
export interface OrderItem {
id: string;
sku: string;
quantity: number;
unitPrice: number;
}
export interface OrderSummary {
orderId: string;
status: 'PENDING' | 'CONFIRMED' | 'SHIPPED';
totalAmount: number;
items: OrderItem[];
createdAt: string;
}
export class OrderApiClient {
constructor(private readonly baseUrl: string) {}
async getOrder(orderId: string): Promise<OrderSummary> {
const response = await fetch(`${this.baseUrl}/api/v2/orders/${orderId}`, {
method: 'GET',
headers: {
Accept: 'application/json',
},
});
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Order not found: ${orderId}`);
}
throw new Error(`Failed to fetch order: ${response.statusText}`);
}
const data = (await response.json()) as OrderSummary;
return data;
}
}order-consumer.pact.spec.ts#Using @pact-foundation/pact (version PactV3) with Vitest. The test starts a local mock server that records requests, verifies expectations, and compiles the output into a JSON contract artifact.
import { describe, it, expect } from 'vitest';
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
import path from 'path';
import { OrderApiClient } from '../OrderApiClient';
const { like, eachLike, regex, integer, decimal } = MatchersV3;
// Initialize consumer-provider contract definition
const provider = new PactV3({
consumer: 'OrderWebClient',
provider: 'OrderBackendService',
dir: path.resolve(process.cwd(), 'pacts'),
});
describe('Orders API Contract: OrderWebClient -> OrderBackendService', () => {
it('successfully retrieves order summary by ID', async () => {
// 1. Declare expected interaction specification
provider
.given('an order with ID ord-9910 exists in the database')
.uponReceiving('a request for order ord-9910')
.withRequest({
method: 'GET',
path: '/api/v2/orders/ord-9910',
headers: {
Accept: 'application/json',
},
})
.willRespondWith({
status: 200,
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
body: {
orderId: like('ord-9910'),
status: regex(/^(PENDING|CONFIRMED|SHIPPED)$/, 'CONFIRMED'),
totalAmount: decimal(1499.5),
createdAt: regex(
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/,
'2026-09-10T14:30:00.000Z'
),
items: eachLike(
{
id: like('item-001'),
sku: like('KB-MECH-RGB'),
quantity: integer(2),
unitPrice: decimal(749.75),
},
{ min: 1 }
),
},
});
// 2. Dispatch real HTTP call against local Pact mock server
await provider.executeTest(async (mockServer) => {
const client = new OrderApiClient(mockServer.url);
const order = await client.getOrder('ord-9910');
// 3. Assert typed client handling
expect(order.orderId).toBe('ord-9910');
expect(order.status).toBe('CONFIRMED');
expect(order.items.length).toBeGreaterThanOrEqual(1);
expect(order.items[0].sku).toBe('KB-MECH-RGB');
});
});
});Upon test completion, the compiled contract file pacts/OrderWebClient-OrderBackendService.json is published to the central Pact Broker:
# CI environment variables
export PACT_BROKER_BASE_URL="https://broker.internal.company.net"
export PACT_BROKER_TOKEN="secret-broker-auth-token"
export GIT_COMMIT=$(git rev-parse HEAD)
export GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
# Publish contract specifying commit hash and active branch
npx pact-broker publish ./pacts \
--broker-base-url "$PACT_BROKER_BASE_URL" \
--broker-token "$PACT_BROKER_TOKEN" \
--consumer-app-version "$GIT_COMMIT" \
--branch "$GIT_BRANCH"provider.pact.spec.ts#The backend provider fetches active contracts from the broker, launches a test instance, and leverages Provider State handlers to seed the test database before replaying requests:
import { Verifier } from '@pact-foundation/pact';
import { describe, it, beforeAll, afterAll } from 'vitest';
import { app } from '../src/server';
import type { Server } from 'http';
describe('Pact Provider Verification: OrderBackendService', () => {
let server: Server;
const PORT = 8089;
beforeAll(async () => {
// Launch server on dedicated test port
server = app.listen(PORT);
});
afterAll(async () => {
server.close();
});
it('satisfies contracts from all active consumers in Pact Broker', async () => {
const verifier = new Verifier({
provider: 'OrderBackendService',
providerBaseUrl: `http://127.0.0.1:${PORT}`,
pactBrokerUrl: process.env.PACT_BROKER_BASE_URL,
pactBrokerToken: process.env.PACT_BROKER_TOKEN,
// Publish verification status back to the broker
publishVerificationResult: !!process.env.CI,
providerVersion: process.env.GIT_COMMIT || '1.0.0',
providerVersionBranch: process.env.GIT_BRANCH || 'main',
// Handle declared provider states
stateHandlers: {
'an order with ID ord-9910 exists in the database': async () => {
// Seed required state in database or mock store
await mockDatabase.seedOrder({
id: 'ord-9910',
status: 'CONFIRMED',
totalAmount: 1499.5,
items: [
{ id: 'item-001', sku: 'KB-MECH-RGB', quantity: 2, unitPrice: 749.75 },
],
createdAt: new Date().toISOString(),
});
},
},
});
// Execute contract verification suite
await verifier.verifyProvider();
});
});can-i-deploy in GitHub Actions#Prior to executing deployment routines, CI inspects the version compatibility matrix. If the target production environment does not contain a verified provider version, the pipeline halts with a non-zero exit code:
name: Deploy Consumer Frontend
on:
push:
branches: [main]
jobs:
contract-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
# Generate contracts via Vitest
- run: pnpm test:pact
# Publish generated contracts to Pact Broker
- name: Publish Pact Contract
run: |
npx pact-broker publish ./pacts \
--broker-base-url ${{ secrets.PACT_BROKER_URL }} \
--broker-token ${{ secrets.PACT_BROKER_TOKEN }} \
--consumer-app-version ${{ github.sha }} \
--branch main
quality-gate:
needs: contract-test
runs-on: ubuntu-latest
steps:
# Query compatibility matrix: is this consumer version verified against prod?
- name: Can I Deploy?
run: |
npx pact-broker can-i-deploy \
--pacticipant OrderWebClient \
--version ${{ github.sha }} \
--to-environment production \
--broker-base-url ${{ secrets.PACT_BROKER_URL }} \
--broker-token ${{ secrets.PACT_BROKER_TOKEN }}
- name: Deploy to Cloud
run: pnpm run deploy:prodUsing literal static values turns contract tests into brittle database state assertions. If the backend returns a valid response with an alternate monetary amount or dynamic timestamp, verification fails despite flawless contract compliance.
// ❌ WRONG: Rigid static literals
body: {
totalAmount: 1499.50, // Fails if database returns an order totaling 2000.00
createdAt: "2026-09-10T12:00:00Z", // Fails on dynamic timestamp generation
}
// ✅ CORRECT: Flexible schema matchers
body: {
totalAmount: MatchersV3.decimal(1499.50), // Asserts floating-point number type
createdAt: MatchersV3.iso8601DateTime(), // Asserts valid ISO-8601 string format
}Contract tests validate protocol shape and communication contracts (headers, status codes, payload shapes). Attempting to encode every permutation of discounts, tax calculations, and business rules into Pact bloats contracts with redundant scenarios. Rule of thumb: 1 contract for 200 OK + 1 for 404 Not Found + 1 for 400 Bad Request. Deep calculation logic belongs in backend unit tests.
Deploying without validating compatibility matrices removes safety guarantees: if the backend renamed a field in a pending branch but has not deployed it to production, the frontend will crash for users with TypeError: Cannot read properties of undefined.
Solution: Always enforce pact-broker can-i-deploy as an automated gate in CI.
Declare expected HTTP endpoints, status codes, and payload schemas using MatchersV3 in @pact-foundation/pact.
Run Vitest contract suite to verify client logic against Pact mock server and compile pacts JSON artifacts.
Upload contract artifacts tagged with the current Git commit SHA and branch via pact-broker publish.
Replay contract requests against the backend provider using stateHandlers to initialize database preconditions.
Query compatibility matrix in CI/CD pipeline to verify compatibility before promoting builds to production.