Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

API Contract Testing with Pact

By Kokil Thapa | Last reviewed: August 2026

Integration failures between services are among the most expensive bugs to fix in production, especially when multiple teams own different parts of the system. API Contract Testing with Pact solves this by verifying that consumers and providers agree on the shape of HTTP interactions before code ever reaches a shared environment. Instead of relying on fragile end-to-end tests or outdated Swagger documentation, you define executable contracts that serve as both test suite and living specification.

This approach shifts integration validation left into the unit test phase. For developers building distributed systems—whether Laravel REST APIs, headless eCommerce backends, or legal-tech portals integrating with government systems—Pact eliminates the "works on my machine" problem across service boundaries. It replaces the hope that an upstream API hasn't changed with mathematical certainty derived from verified contracts.

What is API Contract Testing with Pact and why does it matter?

Traditional API testing falls into two inadequate buckets: unit tests that mock external dependencies (verifying nothing about the real world) and integration tests that require fully deployed infrastructure (slow, flaky, expensive). API Contract Testing with Pact occupies the critical middle ground. It treats the interface between services as a first-class artifact that must be tested independently of either service's internal implementation.

The core insight is asymmetry. In any API relationship, the consumer knows exactly what it needs from the provider, but the provider rarely knows every way its API is consumed. Pact formalizes this reality through Consumer-Driven Contracts (CDC). The consumer writes a test describing the specific request it will make and the response it expects. This interaction is serialized into a JSON contract file. The provider later loads this contract and runs it against its actual application, failing the build if the real response doesn't match the agreed-upon structure.

Consumer Service(Laravel / Vue / Mobile)Pact TestDefines ExpectationGET /users/1 → 200Pact BrokerContract StorageVersioning & TagsVerification StatusProvider Service(Laravel / Symfony API)Verification TestReplays ContractAgainst Real AppContract acts as executable specification between independent deployable units
API Contract Testing with Pact workflow: consumer defines expectations, broker stores versioned contracts, provider verifies compatibility during CI

In my experience working on production Laravel applications that integrate with payment gateways like eSewa and Khalti, or legal-tech portals connecting to external document verification services, the pain point is always the same: the upstream API changes silently, and your application breaks at runtime. Pact prevents this category of failure entirely. When I've implemented contract testing on client projects involving multiple microservices or third-party integrations, the result isn't just fewer bugs—it's faster deployment cycles because teams stop waiting for staging environments to validate compatibility.

How do you set up Pact PHP for Laravel contract testing?

Setting up API Contract Testing with Pact in a Laravel ecosystem requires the pact-php/pact package, which provides FFI bindings to the underlying Rust implementation. As of 2026, ensure you're running PHP 8.2 or higher (PHP 8.4 is recommended for Laravel 12.x) and Composer 2.7+. The setup differs depending on whether you're writing consumer tests, provider verification tests, or both.

Installing dependencies and configuring the consumer

For the consumer side (the service making HTTP requests), install the Pact library as a dev dependency:

composer require --dev pact-php/pact:^10.0

Create a dedicated test class extending PHPUnit's TestCase. Unlike standard feature tests that hit a real or mocked HTTP endpoint, Pact tests spin up a local mock server that behaves exactly according to your defined interactions:

<?php

namespace Tests\Contract;

use PHPUnit\Framework\TestCase;
use PactPhp\PactBuilder;
use PactPhp\InteractionBuilder;

class UserApiClientTest extends TestCase
{
    private PactBuilder $pact;

    protected function setUp(): void
    {
        $this->pact = PactBuilder::create('UserDashboard', 'UserService')
            ->setPort(7200)
            ->start();
    }

    protected function tearDown(): void
    {
        $this->pact->stop();
    }

    public function test_get_user_returns_expected_structure(): void
    {
        $this->pact
            ->given('A user with ID 42 exists')
            ->uponReceiving('A request for user details')
            ->withRequest('GET', '/api/users/42')
            ->willRespondWith(200, [
                'Content-Type' => 'application/json',
            ], [
                'id' => 42,
                'name' => PactBuilder::like('Kokil Thapa'),
                'email' => PactBuilder::regex('[\w.-]+@[\w.-]+\.\w+', 'kokil@example.com'),
                'role' => PactBuilder::oneOf(['admin', 'editor', 'viewer'], 'editor'),
            ]);

        // Execute the actual client code against the mock
        $client = new \App\Services\UserApiClient('http://localhost:7200');
        $user = $client->getUser(42);

        // Assert your domain logic works with the response
        $this->assertEquals(42, $user->id);
        $this->assertStringContains('@', $user->email);
    }
}

Several details matter here. The like(), regex(), and oneOf() matchers are critical—they tell Pact to verify structure rather than exact values. Without them, your contract becomes brittle and fails whenever test data changes. The given() clause establishes provider state; this string must match exactly what the provider uses to set up test fixtures during verification.

Publishing contracts to the Pact Broker

After consumer tests pass, they generate a JSON pact file in ./pacts/. You must publish this to a Pact Broker for the provider to retrieve. Configure publishing via environment variables in your CI pipeline:

# In GitLab CI or GitHub Actions
export PACT_BROKER_BASE_URL=https://pact-broker.yourcompany.com
export PACT_BROKER_TOKEN=$PACT_BROKER_API_KEY
export PACT_CONSUMER_VERSION=$CI_COMMIT_SHA
export PACT_BRANCH=$CI_COMMIT_BRANCH

vendor/bin/phpunit tests/Contract/
vendor/bin/pact-publish pacts/ --broker-base-url=$PACT_BROKER_BASE_URL

Never commit pact files to version control. They are build artifacts tied to specific commits. The broker handles versioning, tagging (e.g., main, prod), and tracking which provider versions have verified which consumer versions.

How does provider verification work in a Laravel application?

The provider side is where API Contract Testing with Pact delivers its real value. Instead of trusting documentation or hoping staging matches production, you replay every registered consumer interaction against your actual Laravel application during the provider's test suite. If any interaction fails, the provider's build fails before deployment.

Pact BrokerProvider TestLaravel AppDatabase / FixturesFetch PactsBoot ApplicationSetup State (Seed User 42)Replay GET /users/42200 + Body MatchPublish Result
Provider verification sequence: fetch contracts, boot Laravel, seed state per interaction, replay requests, compare responses, publish verification status

Writing the provider verification test

Create a separate test class for provider verification. This test boots your full Laravel application and configures Pact to route incoming contract-defined requests through it:

<?php

namespace Tests\Contract;

use PHPUnit\Framework\TestCase;
use PactPhp\Verifier\Verifier;
use Illuminate\Support\Facades\DB;

class UserServiceProviderTest extends TestCase
{
    public function test_verify_all_consumer_pacts(): void
    {
        $verifier = Verifier::create('UserService')
            ->setProviderBaseUrl('http://localhost:8000')
            ->setBrokerUrl(getenv('PACT_BROKER_BASE_URL'))
            ->setBrokerToken(getenv('PACT_BROKER_TOKEN'))
            ->addConsumerVersionSelector([
                'tag' => 'main',
                'latest' => true,
            ])
            ->enableStateChangeEndpoint('/test/pact-state')
            ->verify();

        $this->assertTrue($verifier->isSuccessful());
    }
}

The enableStateChangeEndpoint is essential. When Pact replays an interaction with a given('A user with ID 42 exists') clause, it calls this endpoint on your provider before sending the request. You implement this as a test-only route that sets up the required database state:

// routes/test.php (only loaded in testing environment)
Route::post('/test/pact-state', function (\Illuminate\Http\Request $request) {
    $state = $request->input('state');

    match ($state) {
        'A user with ID 42 exists' => DB::table('users')->upsert([
            'id' => 42,
            'name' => 'Test User',
            'email' => 'test@example.com',
            'role' => 'editor',
        ], ['id']),
        'No users exist' => DB::table('users')->truncate(),
        default => throw new \RuntimeException("Unknown state: {$state}"),
    };

    return response()->json(['status' => 'ok']);
});

A common mistake I've seen on real client projects is treating state setup as optional or using shared fixtures. Each interaction must be isolated. Truncate relevant tables before seeding, or use transactions rolled back after each state change. Flaky provider verification usually traces back to state leakage between interactions.

When should you use Pact versus OpenAPI or end-to-end tests?

API Contract Testing with Pact doesn't replace other testing strategies—it complements them. Understanding when each tool applies prevents over-engineering or dangerous gaps. Here's how they compare in practice for Laravel and PHP-based distributed systems:

CriteriaPact (Contract Tests)OpenAPI / Swagger ValidationEnd-to-End Integration Tests
What it verifiesSpecific consumer-provider interactions with exact field matchingSchema conformance (types, required fields, status codes)Full business flows across deployed services
Execution speedFast (seconds, runs in unit test phase)Fast (static analysis or lightweight validation)Slow (minutes, requires infrastructure)
Catches breaking changesYes, before deploymentOnly if schema is updated and validatedYes, but after deployment to staging
Requires deployed servicesNo (mock server for consumer, local boot for provider)NoYes (staging or production-like environment)
Maintenance burdenModerate (contracts evolve with consumer needs)Low-High (depends on discipline keeping spec current)High (flaky, slow, complex setup)
Best forMicroservices, third-party API wrappers, team boundariesPublic APIs, SDK generation, documentationCritical user journeys, cross-system workflows
Nepal legal-tech contextInternal service boundaries (auth ↔ case management)Public-facing legal information APIsFull court-marriage booking flow with payment

Use Pact when two independently deployable services communicate over HTTP and at least one team owns each side. Use OpenAPI when publishing a public API or generating client SDKs. Reserve end-to-end tests for smoke-testing critical paths after deployment. On a recent legal-tech portal project, we used Pact for internal service contracts between the authentication service and case-management backend, OpenAPI for the public lawyer directory API, and a small E2E suite for the complete marriage-registration submission flow. Each tool addressed a distinct risk profile.

How do you integrate Pact verification into CI/CD pipelines safely?

The power of API Contract Testing with Pact only materializes when verification blocks deployments automatically. Manual contract checking defeats the purpose. Here's the pattern I've used across multiple Deployer 7 + GitLab CI pipelines for Laravel services:

  1. Consumer CI: Run contract tests → publish pacts tagged with branch name → trigger provider verification webhook (optional).
  2. Provider CI: Fetch latest consumer pacts for target branch → boot Laravel with test database → run verification → publish results back to broker.
  3. Deployment gate: Provider deployment proceeds only if verification passed for all main-tagged consumer pacts.
  4. Canary safety: Tag successful provider builds with prod after deployment; consumers can select prod-verified pacts for release confidence.
Consumer CIRun Pact TestsPublish + Tag BranchPact BrokerStore Versioned PactsTrack VerificationProvider CIVerify Against Real AppPublish ResultsAll Verified?(main tag)NOBlock DeployNotify TeamYESDeploy ProviderTag prod on SuccessSafe for Consumers
CI/CD integration: consumer publishes pacts, provider verifies before deploy, failed verification blocks release, success tags production-safe version

A practical gotcha: provider verification needs a running Laravel instance with a clean database. In GitLab CI, I typically use a services: block with MySQL or PostgreSQL, run migrations, start PHP-FPM or the built-in server in the background, execute verification, then tear down. Don't try to verify against a remote staging environment—that reintroduces the coupling Pact exists to eliminate. For teams managing CI/CD pipelines in Nepal with limited infrastructure budgets, self-hosted Pact Broker (Docker image) on the same EC2 instance as GitLab Runner keeps costs under Rs 3,000/month (~USD 22) while providing full functionality.

Another consideration: selective verification. Fetching all consumer pacts on every provider build wastes time. Use consumer version selectors to target only relevant pacts:

->addConsumerVersionSelector([
    'tag' => 'main',
    'latest' => true,
    'fallbackTag' => 'prod',
])

This fetches the latest main-tagged pact for each consumer, falling back to prod if no main-tagged version exists. During provider feature branches, you might instead select pacts tagged with the same branch name to test co-developed changes before merging.

Implementing API Contract Testing with Pact for sustainable integration quality

Adopting API Contract Testing with Pact is an investment in architectural clarity as much as test coverage. Start small: pick one high-risk integration boundary (payment callback handler, auth token exchange, document verification endpoint) and implement bilateral contracts there. Resist the urge to convert all existing integration tests immediately; let the team learn the matcher semantics and state management patterns on a non-critical path first.

Remember that contracts are communication artifacts. When a consumer adds a new expectation, treat the resulting pact change like a code review item—discuss it with the provider team before merging. The broker's diff view makes this straightforward. Over time, this conversation replaces the post-deployment incident channel as your primary integration coordination mechanism.

If you're building distributed PHP systems and want to eliminate integration surprises without drowning in E2E test maintenance, reach out to discuss your architecture. I've helped teams in Nepal and globally implement contract testing strategies that actually stick, balancing rigor with the operational realities of small engineering teams.

Frequently Asked Questions

Pact verifies that an HTTP API consumer and provider adhere to a shared JSON contract without requiring both services running simultaneously during tests.

Pact generates verifiable contracts from consumer code rather than static documentation, ensuring the provider actually satisfies specific consumer expectations before deployment.

Yes, using pact-php for consumers and providers. It integrates with PHPUnit and supports Laravel 12 on PHP 8.2+, though setup requires configuring the standalone binary correctly.

Absolutely. The @pact-foundation/pact package works natively with Node 22 LTS. For Vue apps, write consumer tests in Jest or Vitest to validate API calls against your backend contracts before integration.

Pact itself is open-source and free. Implementation costs are purely engineering time, typically Rs 75,000–150,000 (USD 550–1,100) for initial setup and training across a small Laravel or Node stack.

Consumer-driven contracts start with frontend or client requirements defining the API shape, preventing over-engineering. Provider-driven contracts define capabilities first. In my experience building legal-tech portals, consumer-driven prevents building unused endpoints that waste development budget.

Deploy the official Pact Broker Docker image or use PactFlow. Configure PACT_BROKER_BASE_URL in your .env file. Use pact-php-cli to publish verification results after CI runs. On production Ubuntu servers, I typically run the broker behind Nginx with basic auth to protect sensitive contract data.

This usually stems from mismatched standalone binary versions or incorrect base URLs. Ensure pact-php downloads the correct platform binary during composer install. Verify PACT_MOCK_SERVICE_PORT is consistent. In my experience deploying Laravel apps via Deployer 7, environment variable drift between local and CI environments causes most false negatives.

Define auth headers explicitly in each interaction using withHeader(). Never hardcode real tokens. Use matcher functions like Pact\Matcher\Regex for dynamic values like JWTs. For Laravel Sanctum APIs, mock the token format rather than validating actual credentials, keeping contract tests focused on structure, not security logic.

Yes, but it requires careful configuration. Use withMultipart() in consumer tests and define Content-Type boundaries explicitly. Binary matching can be flaky. On eCommerce projects handling product image uploads, I have found it more reliable to contract-test the metadata endpoint separately and handle binary validation in dedicated integration tests.

Use tags and branches in Pact Broker to isolate service versions. Tag consumers and providers by git branch or release version. Enable selective verification so providers only verify relevant consumer pacts. This prevents cascading failures when one team deploys independently, a pattern essential for maintaining separate Laravel and Node services.

Pact Broker flags the verification failure immediately in CI. The provider build fails before merging, preventing breaking changes from reaching production. Teams receive webhook notifications linking to the specific broken interaction. This feedback loop catches regressions that traditional integration tests often miss until staging environments.

Use semantic versioning tags in Pact Broker alongside feature branches. Publish pacts with branch names during development, then tag verified pacts with release versions. Providers should support multiple consumer versions during transition periods. Deprecate old interactions explicitly rather than removing them abruptly to maintain backward compatibility.

No. Pact validates interface compliance, not business logic or database state. You still need integration tests for complex workflows like payment processing via eSewa or Khalti. Use Pact to guarantee the API shape remains stable, reducing brittle E2E tests. Reserve full-stack testing for critical user journeys where multiple systems interact.

Check the Pact Broker diff output showing expected versus actual payloads. Common issues include extra fields, wrong types, or missing matchers. Use Pact::like() for flexible matching instead of exact equality. In Laravel applications, ensure Form Request validation rules align with consumer expectations. Mismatches often reveal undocumented API behavior changes.

Share this article

Quick Contact Options
Choose how you want to connect me: