Skip to content

Blog Article

API Contract Testing Between Coding Agents

API contract testing catches incompatible provider changes before agent-built clients ship. Run a small failing and passing check, then plan real verification.

By AppHandoff Team · Published · 11 min read

Illustration of an API contract beside a review sheet and pen
EngineeringAPIAI Coding

API contract testing checks whether a service provides the request and response behavior a client depends on. When one coding agent builds the provider and another builds the consumer, a shared interface description is a starting agreement; an executable check reveals whether their code still agrees. This guide shows a tiny local failure and repair, then explains how to turn that lesson into a real consumer and provider verification workflow. The example is invented and bounded; it is not a test of AppHandoff or a customer system.

The immediate risk is simple. A backend agent returns { state: 'ready' } while a frontend agent reads response.status. Both files may type-check in isolation, and an OpenAPI file may still describe status. The screen can nonetheless show the wrong state at runtime. Testing the actual boundary gives the next agent a concrete failure to resolve. The API contracts guide covers how to agree and version the interface; here the focus is proving the implemented interaction.

Choose the consumer behavior that can break

Start with one operation and one reason the caller cares. In this illustrative build-status API, GET /v1/builds/42 should return a 200 JSON object with a string status. The consumer renders ready as ‘Ready’ and any other valid status as ‘Waiting’. The provider agent owns the response; the consumer agent owns the display function. If the provider renames the field to state, the consumer must not silently present an unknown value as a valid waiting build. An ordinary unit test for the display function is useful, but by itself it cannot show what the provider sends.

The consumer should identify only the fields and semantics it relies on. Pact's consumer testing guidance recommends assertions about changes that affect the consumer and warns against unnecessarily strict response matching. A contract that freezes timestamps or unrelated fields creates work without protecting this screen. For the real HTTP interaction, specify the successful request and the response fields the client uses. This local fixture checks a string status and the expected ‘Ready’ result for its supplied example; other status values need their own agreed semantics and cases. Another client may depend on different fields; its contract should reflect its own behavior, not copy this one's assumptions.

Run a small failure before changing the provider

Save the following entire example as contract-check.cjs. It runs with Node's built-in assertion module and needs no package install. The invented provider function returns the old shape by default and the accepted shape when passed fixed. The consumer function checks the field it uses and throws a clear error if that field is absent. This is an illustrative executable assertion, not a Pact suite, HTTP test, or MCP protocol conformance check. The full snippet is the exact code used for the local results below.

const assert = require('node:assert/strict');

function provider(version) {
  return version === 'fixed'
    ? { status: 'ready' }
    : { state: 'ready' };
}

function consumer(body) {
  assert.equal(typeof body.status, 'string',
    'provider must return string status');
  return body.status === 'ready' ? 'Ready' : 'Waiting';
}

assert.equal(consumer(provider(process.argv[2])), 'Ready');
console.log('contract check passed');

Run node contract-check.cjs first. It exits with an assertion failure: provider must return string status, because the response contains state instead. Then run node contract-check.cjs fixed. It prints contract check passed. The published fixture was executed locally on October 9, 2026; the first exited 1 and the second exited 0. The second result proves only that this in-memory example accepts the repaired shape. It does not show that any deployed endpoint returns it or that authentication and errors behave correctly.

Notice that the assertion is on the consumer's actual read of the field, not merely on a sample object matching a schema. If the consumer had instead used body.state, the agreement would need a deliberate revision or the provider would need to retain that field. Decide that together rather than editing both copies until a test goes green. Keep the failing output as evidence of what was broken; rerun on the revision proposed for review. A passing result from an earlier revision does not certify subsequent changes.

Move from a local assertion to contract verification

A production contract workflow has two sides. A consumer test exercises the real API client against an expected interaction and produces an agreed contract artifact. Provider verification then runs that interaction against the provider version under consideration. Pact's provider verification documentation describes checking a provider against published consumer contracts. This is stronger than our in-memory function because the test is tied to real client code and provider behavior. It is still scoped to the interactions represented in those contracts: an omitted scenario remains untested.

  1. Select the consumer operation that can break and record the accepted request, response fields, status codes, and provider state.
  2. Exercise the real consumer API client against that interaction; keep the resulting contract artifact tied to the consumer revision.
  3. Verify the provider revision against the relevant consumer contracts, including the state needed to reproduce each interaction.
  4. Keep provider functional tests for authorization, persistence, and business rules; add integration or browser proof for the user path when needed.
  5. Record the exact revisions, environment, failing and passing results, and any client or provider versions that were not checked.

For the build-status call, the consumer test should invoke its actual fetch wrapper and assert how a status response becomes a UI state. Provider verification should exercise the actual route under a reproducible build state, not call the toy provider() function. A 401 path may need its own interaction if the consumer handles it specially. A backend functional test should separately prove that one account cannot read another account's build. Contract verification checks compatibility; it does not replace ownership or security tests. The definition of done guide shows how to attach each kind of evidence to a reviewable revision.

Make the verification failure actionable for both agents. Record the consumer version that expects status, the provider version that supplied state, the request and expected response, and the assertion that failed. The provider owner can then decide whether the implementation accidentally changed or the accepted interface needs a revision. If the latter, the consumer owner must update the real client and its check. Run the two sides again on the proposed versions before a reviewer calls them compatible. Avoid a test that merely constructs a preferred JSON object and compares it with itself: such a test can stay green while the route still returns the old shape. Keep separately observed route behavior and any skipped environment checks visible. This makes a failing run useful even when the team cannot complete the release in the same session.

Keep OpenAPI and executable evidence distinct

The OpenAPI Specification defines a format for describing HTTP APIs. An OpenAPI document can make the intended path, schema, response codes, and security requirements legible to both agents. Parsing the file checks syntax; schema validation can check a sample against the description. Neither action proves that the frontend reads the intended field or that the backend currently returns it. The local red result above illustrates that gap: the intended status field can be perfectly documented while an implementation returns state.

Keep the description and checks connected to the same revision. When a provider change adds an optional field, a tolerant consumer may continue working; a strict schema or client decoder might reject it. When the change removes or renames a field a consumer uses, test that interaction before accepting the provider revision. If the contract must change, let the affected consumer owner confirm the new behavior, then rerun the relevant checks. Do not infer universal compatibility from one passing client: other consumers may use fields this example ignores.

Coordinate two agents without overstating the proof

Give the provider agent the operation and the accepted contract revision. Give the consumer agent the same reference and a named behavior to exercise. In their shared work item, keep separate links to the description, consumer check, provider verification, and any user-flow proof. If either agent changes status to state, the other should see the proposal before implementation diverges. The human review guide explains how a reviewer can approve or revise a consequential change based on evidence rather than an agent's confident summary.

AppHandoff can hold that shared work item and evidence. Its documented MCP endpoint is https://api.apphandoff.com/mcp. The current tool catalog includes bootstrap, get, find, ticket, plan, message, project, and decide_lifecycle_proposal. An authorized agent can resolve a project with bootstrap and read a known work item with get; writes depend on scope and the current rules. The last tool handles a signed-in human's lifecycle approval card. It is not a model's general permission to approve its own code. The MCP overview explains the product connection.

The official July 2026 MCP tools specification defines tool listing and calls. It does not make the toy JavaScript fixture an MCP test. For an MCP-facing provider, separately verify the current protocol version, discovery, transport, authentication, tool input and output behavior, and failure paths in the environment you claim to support. A label such as ‘contract test passed’ should name the exact interaction and versions; it should not imply universal client compatibility or a live service check that nobody performed.

A reviewable handoff for the next change

Before accepting an agent-built API change, ask which consumer can break, which field or status it relies on, and which provider revision was verified. Include a red example when fixing a divergence and a green run after the repair. State the limits: our local fixture shows one field-name failure and repair; a real team's contract checks must exercise its actual client and provider. The pull request template generator can format a review checklist from details you enter. It is deterministic and does not inspect code, run the contract checks, verify a provider, or approve a change. Keep those results attached to the work item so the next agent can distinguish an agreement from a tested implementation.

Frequently asked questions

What is API contract testing?

API contract testing checks whether a provider and its consumer agree on the requests and responses the consumer actually uses. A useful check exercises client behavior and verifies the corresponding provider behavior against the same accepted interaction.

Is an OpenAPI document an API contract test?

No. OpenAPI can describe an HTTP interface, but a document alone does not execute a client or provider. Validate the description, then run checks against the implementations and record which versions passed.

Does the local example prove an MCP integration works?

No. The example tests one invented JavaScript provider response and consumer function. It does not use an MCP transport, JSON-RPC, OAuth, a live service, or a Pact provider verification run.