API contract testing without Pact's overhead: a lighter workflow for small teams
Consumer-driven contract testing is a good idea that small teams consistently fail to adopt. The Pact implementation in particular asks for a lot: consumer test suites generating pactfiles, a broker (hosted or self-hosted) exchanging them with provider verification, can-I-deploy checks, and a discipline spanning two repositories and two teams. When the teams are two engineers in the same Slack channel, the broker is ceremony. But the problem contract testing solves is real and severe: the provider changes something the consumer depended on, and nobody finds out until staging. You can get most of the safety with a much lighter workflow built around the OpenAPI document.
What contract testing actually guarantees
Strip away the tooling and there are two directions of mismatch:
- Provider provides less than the consumer uses. A field the consumer reads is removed or renamed; a status code changes; an enum value disappears.
- Consumer sends something the provider does not accept. A request body shape drifts; a required parameter is omitted; a content type changes.
Traditional integration tests catch these only when the exact path is exercised against the exact deployed version, which in practice means after merge. Contract testing moves the check left: both sides are verified against a shared description before deployment. The shared description is the valuable part. Whether it lives in a broker with consumer-defined expectations or in an OpenAPI document in git is an implementation choice.
The lightweight alternative: one spec, two verification directions
Use the OpenAPI document as the contract and verify both sides against it in CI — no broker, no pactfiles.
Provider side: does the running service honor the spec?
- Validate recorded or generated responses against the document. The strongest cheap version: capture real traffic from staging (HAR or a proxy), then validate every response body against the operation's schema, including status codes and content types.
- Run scenario tests — request sequences with extracted variables — against staging on every backend release. These exercise the journeys consumers actually take, not just schema shapes.
- For request validation, replay captured consumer requests against the spec in strict mode; malformed calls fail the build.
Consumer side: does the client use only what the spec promises?
- Generate the client's TypeScript types from the released spec (see generating a TypeScript client). A removed field becomes a compile error in the consumer's CI.
- In the consumer's tests, satisfy the API boundary with mocks derived from the spec's examples rather than hand-written fixtures — MSW handlers generated or seeded from the document keep edge cases contract-accurate.
Between them: the breaking-change gate.
- Every provider PR diffs its spec against the last released version and blocks breaking changes (the full rule set is in detecting breaking API changes in CI).
- Every consumer PR builds against the latest released spec, never against an unreleased branch.
That triangle — provider validates against spec, consumer typechecks against spec, releases gate on spec diffs — catches the same two mismatch directions Pact catches.
What you give up, and whether it matters
| Capability | Pact | OpenAPI-light |
|---|---|---|
| Consumer-defined expectations per consumer | Yes | No — spec is provider-authored |
| Broker matrix / can-I-deploy | Yes | Version tags + CI ordering |
| Multiple consumers with different needs | Strong | Requires versioned specs or feature negotiation |
| Message/pact for event-driven | Yes | Via webhook/SSE schemas + scenarios |
| Setup and maintenance cost | High | Low — tools already in the repo |
| Works across independent organizations | Yes | No (you need shared governance) |
The honest gap: Pact lets each consumer say "I specifically depend on these four fields and nothing else," and the provider can change anything not claimed without consultation. The OpenAPI-light model treats the whole documented surface as supported and relies on deprecation windows. Inside one team or one company with a shared repository and release cadence, that is fine — the deprecation policy and diff gate carry the load. Across separate companies consuming your public API, consumer-driven contracts or a versioning guarantee earn their keep.
The workflow in a week
- Get a real spec. If one exists, validate it against staging traffic and fix the gaps — specs are usually optimistic about required fields and error responses. If not, scan the codebase to produce a first draft and review the flagged gaps rather than accepting invented schemas.
- Put the spec in the provider repo (or a shared contracts repo) with a release tag per published version.
- Add the provider CI jobs: bundle, lint, diff against the released tag, and replay/validate traffic.
- Add the consumer CI step: generate types from the released tag; seed test mocks from examples.
- Write five scenario journeys covering the critical business flows and run them against staging before release.
- Establish the deprecation rule: breaking changes only behind a new version; deprecated operations carry sunset metadata; removals need a released major version.
When to graduate to Pact
Signs the lightweight setup has stopped fitting: three or more independent consumer teams arguing about release timing; consumers in other organizations you cannot coordinate with in one CI system; a genuine need to know, per consumer, which fields are safe to remove; or event-driven contracts where provider and consumer deploy on completely independent cadences. At that point the broker's matrix is solving a real coordination problem and the overhead is justified. Until then, a broker you half-maintain gives you the worst of both worlds — ceremony without coverage.
The core insight is that contract testing's value was never the pactfile format; it was making the shared description executable in both directions. An OpenAPI document that drives provider validation, consumer types, mocks, and scenarios is already an executable contract.
Powerduck is built around that loop: one local spec drives mocks, scenario tests runnable against mock and staging, response validation, and the diff/review workflow before publishing — with webhook and SSE contracts included. The demo runs the journeys on a sample document.
What to read next: scenario testing for REST APIs defines the journey format that replaces most provider-side pact tests, and a 200 is not done explains why schema validation alone is not enough.