Claude Code API Contract Testing Guide (2026)
Updated April 2026 for the latest Claude Code release. The approach below reflects current api contract behavior after API specification tooling updates and OpenAPI 3.1 adoption.
API contract testing ensures that services communicate reliably without integration failures. When working with microservices or external API integrations, contract testing validates that the interface between providers and consumers remains consistent. Claude Code offers several skills that streamline this workflow, making it accessible for developers across different experience levels.
This guide covers practical approaches to API contract testing using Claude Code, focusing on real-world implementation patterns you can apply immediately.
Understanding Contract Testing Fundamentals
Contract testing operates on a simple principle: define what a service provides, then verify implementations match that specification. Unlike traditional integration testing that requires all services running simultaneously, contract tests validate interfaces independently.
Two primary approaches exist: consumer-driven contracts and provider-driven contracts. Consumer-driven contracts, where the consuming service defines expected behavior, work well when you control both ends of an integration. Provider-driven contracts suit situations where external services define the API specification.
Contract Testing vs. Integration Testing vs. E2E Testing
Before diving into implementation, it helps to understand where contract testing fits in the testing pyramid:
| Test Type | Scope | Speed | Brittle? | Best For |
|---|---|---|---|---|
| Unit tests | Single function | Very fast | Low | Business logic |
| Contract tests | Service interface | Fast | Low | API boundaries |
| Integration tests | Multiple services live | Medium | Medium | Happy path flows |
| E2E tests | Full system | Slow | High | Critical user journeys |
Contract tests occupy a valuable middle ground. They run without requiring live dependencies (making them fast and reliable in CI), yet they catch the specific category of bugs that unit tests miss: mismatches at service boundaries. A function that perfectly transforms data internally can still break when the upstream API changes its field names or response shape.
Claude Code skills like the tdd skill help structure your testing workflow, while supermemory enables tracking of contract changes across your projects.
Setting Up Contract Testing with Claude Code
Begin by using the essential skills for contract testing workflows. In Claude Code, skills are invoked directly in conversation. reference the tdd skill, pdf skill, or api-testing skill by name when describing your task to Claude Code.
The tdd skill provides test scaffolding and organization patterns. The api-testing skill offers HTTP request utilities. For documentation, the pdf skill helps generate contract specification documents.
Create a dedicated test directory structure:
tests/
contracts/
schemas/
expectations/
consumer-tests/
provider-tests/
This separation matters for several reasons. Schemas live in one place and are shared between consumer and provider test suites. Expectations files capture the consumer’s view of what the provider must deliver. Keeping consumer and provider tests in separate directories makes it easy to run them independently in CI. for example, running only consumer tests when a consuming service changes, and only provider tests when the API implementation changes.
Installing Dependencies
For a JavaScript/Node.js project, set up a typical contract testing stack:
npm install --save-dev jest @types/jest
npm install --save-dev ajv # JSON Schema validation
npm install --save-dev openapi-fetch # Type-safe OpenAPI client
npm install --save-dev nock # HTTP request mocking
For a Python project, the equivalent setup:
pip install pytest pytest-asyncio
pip install jsonschema
pip install responses # HTTP mocking
pip install pact-python # If using Pact framework
Writing Your First Contract Test
Define your API contract using OpenAPI specifications or a simpler JSON schema approach. Here’s an example contract for a user service endpoint:
contracts/user-service.yaml
openapi: 3.0.0
info:
title: User Service API
version: 1.0.0
paths:
/users/{id}:
get:
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: User found
content:
application/json:
schema:
type: object
required: [id, email, created_at]
properties:
id:
type: string
email:
type: string
format: email
created_at:
type: string
format: date-time
'404':
description: User not found
content:
application/json:
schema:
type: object
required: [error]
properties:
error:
type: string
Notice the addition of required arrays to the schema. This is a critical detail that most tutorial contracts omit. A schema without required constraints will pass even when fields are missing entirely. Contract testing that doesn’t enforce required fields is not testing the contract. it’s just checking that the response is valid JSON.
Using the tdd skill, generate test scaffolding. Open the Claude REPL and invoke:
/tdd Generate test scaffolding from contracts/user-service.yaml and place tests in tests/consumer-tests/
This creates test files that verify your consumer handles the contract correctly.
Consumer-Side Contract Testing
Consumer tests validate that your application correctly handles API responses according to the contract. The consumer owns these tests. they express what the consumer needs from the provider, not what the provider happens to deliver today.
This distinction matters when contracts evolve. If you’re a consumer and the provider adds optional fields to a response, your consumer tests should continue passing. If the provider removes a required field or changes a field type, your consumer tests should immediately fail.
// tests/consumer-tests/user-endpoint.test.js
const { test, describe, expect, beforeEach } = require('@jest/globals');
const nock = require('nock');
const { getUser } = require('../../src/services/user-service');
// Load the contract schema for validation
const Ajv = require('ajv');
const addFormats = require('ajv-formats');
const contractSchema = require('../../tests/contracts/schemas/user-response.json');
const ajv = new Ajv();
addFormats(ajv);
const validateUser = ajv.compile(contractSchema);
describe('User Service Consumer Contract', () => {
beforeEach(() => {
// Mock the HTTP layer to return contract-compliant responses
nock('http://user-service')
.get('/users/user-123')
.reply(200, {
id: 'user-123',
email: '[email protected]',
created_at: '2026-01-15T10:30:00Z'
});
nock('http://user-service')
.get('/users/nonexistent')
.reply(404, { error: 'User not found' });
});
test('returns user object with required fields', async () => {
const user = await getUser('user-123');
expect(user).toHaveProperty('id');
expect(user).toHaveProperty('email');
expect(user).toHaveProperty('created_at');
});
test('response validates against contract schema', async () => {
const user = await getUser('user-123');
const isValid = validateUser(user);
expect(isValid).toBe(true);
if (!isValid) {
console.error('Schema validation errors:', validateUser.errors);
}
});
test('validates email format from contract', async () => {
const user = await getUser('user-123');
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
expect(user.email).toMatch(emailRegex);
});
test('handles user not found per contract', async () => {
const result = await getUser('nonexistent');
expect(result).toBeNull();
});
});
The schema validation step is the most important part. Rather than manually asserting individual field names, compiling and running the OpenAPI schema as a validator catches any deviation from the contract shape automatically. As the contract evolves, you update the schema file and all downstream assertions update with it.
The tdd skill organizes these tests and provides clear output when contract violations occur.
Provider-Side Contract Validation
Provider tests ensure your API implementation matches the declared contract. These tests run against a live instance of your service (typically in CI against a test environment) and make real HTTP requests.
// tests/provider-tests/user-service-validation.test.js
const { test, describe, expect, beforeAll } = require('@jest/globals');
const BASE_URL = process.env.USER_SERVICE_URL || 'http://localhost:3000';
// Helper: validate a response body against the contract schema
const Ajv = require('ajv');
const addFormats = require('ajv-formats');
const ajv = new Ajv();
addFormats(ajv);
const userResponseSchema = require('../../tests/contracts/schemas/user-response.json');
const validateUserResponse = ajv.compile(userResponseSchema);
describe('User Service Provider Contract', () => {
beforeAll(async () => {
// Seed test data if needed
await fetch(`${BASE_URL}/test/seed`, { method: 'POST' });
});
test('GET /users/{id} returns 200 with valid user', async () => {
const response = await fetch(`${BASE_URL}/users/user-123`);
const body = await response.json();
expect(response.status).toBe(200);
expect(response.headers.get('content-type')).toContain('application/json');
const isValid = validateUserResponse(body);
expect(isValid).toBe(true);
if (!isValid) {
console.error('Provider contract violation:', validateUserResponse.errors);
}
});
test('GET /users/{id} returns 404 for nonexistent user', async () => {
const response = await fetch(`${BASE_URL}/users/invalid-id-that-does-not-exist`);
const body = await response.json();
expect(response.status).toBe(404);
expect(body).toHaveProperty('error');
expect(typeof body.error).toBe('string');
});
test('response content-type matches contract', async () => {
const response = await fetch(`${BASE_URL}/users/user-123`);
expect(response.headers.get('content-type')).toContain('application/json');
});
test('id field in response matches requested id', async () => {
const response = await fetch(`${BASE_URL}/users/user-123`);
const body = await response.json();
// Provider must return the correct user, not just a valid user shape
expect(body.id).toBe('user-123');
});
});
The final test. verifying the response ID matches the requested ID. catches a subtle but common bug: services that return valid-shaped responses for the wrong resource. Schema validation ensures structure; this test ensures correctness.
Automating Contract Validation
Integrate contract tests into your CI/CD pipeline. The supermemory skill tracks contract versions and notifies you when changes require test updates:
.github/workflows/contract-tests.yml
name: Contract Tests
on:
push:
branches: [main, develop]
pull_request:
jobs:
consumer-contract-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- name: Run consumer contract tests
run: npm test -- --testPathPattern=consumer-tests
provider-contract-tests:
runs-on: ubuntu-latest
services:
user-service:
image: ghcr.io/your-org/user-service:${{ github.sha }}
ports:
- 3000:3000
options: >-
--health-cmd="curl -f http://localhost:3000/health"
--health-interval=5s
--health-timeout=3s
--health-retries=10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- name: Run provider contract tests
env:
USER_SERVICE_URL: http://localhost:3000
run: npm test -- --testPathPattern=provider-tests
- name: Validate OpenAPI specification
run: npx @redocly/cli lint contracts/user-service.yaml
Running consumer and provider tests as separate jobs is intentional. Consumer tests can pass even when the provider service isn’t available (because they mock the HTTP layer). Provider tests require a live service but don’t depend on the consumer codebase. This separation lets both teams work independently and merge changes with confidence.
Handling Contract Evolution
APIs evolve over time. Establish a process for managing breaking changes:
- Version your contracts: Include version numbers in file names or OpenAPI info section
- Document changes: Use the pdf skill to generate changelogs
- Implement backward compatibility: Support both old and new contract versions during transitions
- Automate notifications: Set up alerts when contract tests fail
// Contract version checking utility
function checkContractVersion(response, expectedVersion) {
const actualVersion = response.headers['x-api-version'];
if (actualVersion !== expectedVersion) {
console.warn(`Contract version mismatch: expected ${expectedVersion}, got ${actualVersion}`);
}
return actualVersion === expectedVersion;
}
Classifying Changes as Breaking vs. Non-Breaking
Not all API changes break consumers. Understanding the distinction saves unnecessary coordination overhead:
Non-breaking changes (safe to deploy without consumer coordination):
- Adding optional fields to responses
- Adding new endpoints
- Relaxing validation constraints (e.g., increasing max string length)
- Adding new enum values to fields consumers don’t switch on
Breaking changes (require consumer coordination):
- Removing fields from responses
- Changing field types (string to number, etc.)
- Renaming fields
- Changing HTTP status codes for existing scenarios
- Making previously optional fields required
- Removing endpoints
A simple rule: if a consumer following the current contract would break after the change, it’s a breaking change. Version your contracts when introducing breaking changes and run both old and new contract test suites during the transition window.
// Supporting dual contract versions during migration
const CONTRACT_VERSIONS = {
'v1': require('./contracts/schemas/user-response-v1.json'),
'v2': require('./contracts/schemas/user-response-v2.json')
};
function validateAgainstVersion(data, version) {
const schema = CONTRACT_VERSIONS[version];
if (!schema) throw new Error(`Unknown contract version: ${version}`);
const validate = ajv.compile(schema);
return { valid: validate(data), errors: validate.errors };
}
Contract Pinning for External APIs
When consuming external APIs you don’t control, pin your contract to the version you tested against. Save a snapshot of the actual API response and use it as your contract baseline:
// scripts/capture-contract-snapshot.js
const response = await fetch('https://external-api.example.com/users/test-user');
const body = await response.json();
// Save as your contract baseline
fs.writeFileSync(
'tests/contracts/snapshots/external-user-api.json',
JSON.stringify(body, null, 2)
);
Commit these snapshots to version control. When the external API changes and starts returning different shapes, your snapshot comparison tests immediately flag the drift before it causes production bugs.
Testing Error Contracts
Error responses are contracts too. Many teams thoroughly test success paths but leave error handling unspecified. This leads to consumers that work under normal conditions but fail unpredictably when errors occur.
describe('User Service Error Contracts', () => {
test('400 Bad Request returns structured error', async () => {
const response = await fetch(`${BASE_URL}/users/`, {
// Invalid: empty ID
});
const body = await response.json();
expect(response.status).toBe(400);
expect(body).toMatchObject({
error: expect.any(String),
code: expect.any(String)
});
});
test('401 Unauthorized returns WWW-Authenticate header', async () => {
const response = await fetch(`${BASE_URL}/users/user-123`);
// When called without auth token
expect(response.status).toBe(401);
expect(response.headers.get('www-authenticate')).toBeTruthy();
});
test('500 Internal Server Error has sanitized error message', async () => {
// Trigger an internal error via a known-bad input
const response = await fetch(`${BASE_URL}/users/__trigger_500__`);
const body = await response.json();
expect(response.status).toBe(500);
// Error messages must not leak stack traces or internal paths
expect(body.error).not.toMatch(/at Object\./);
expect(body.error).not.toMatch(/node_modules/);
});
});
Error contract tests serve a second purpose beyond correctness: they document expected failure behavior for other developers. A test named 401 Unauthorized returns WWW-Authenticate header is better documentation than any comment.
Best Practices for Contract Testing
Keep these principles in mind when implementing contract testing:
- Test contracts, not implementation: Focus on the interface, not internal logic
- Keep contracts versioned: Never overwrite contracts without preserving history
- Run tests frequently: Execute contract tests on every commit
- Use clear naming: Name test files to indicate which contract they validate
- Document assumptions: Note any implicit expectations not captured in the schema
- Test error contracts: Happy path coverage is not enough
- Use schema validation libraries: Don’t hand-write field assertions. compile the schema and validate against it
- Mock at the HTTP layer, not the function layer: Consumer tests should mock HTTP calls, not function stubs, to ensure transport-layer behavior is correct
The tdd skill encourages test-first development, which naturally aligns with contract testing workflows. Define the contract first, write consumer tests against that contract, then implement the provider to satisfy those tests. The frontend-design skill can help if you’re building test dashboards or reporting interfaces to display contract validation results across your system.
Conclusion
API contract testing with Claude Code skills provides a solid framework for maintaining reliable service integrations. By implementing consumer and provider tests, validating both success and error shapes with schema validation, versioning your contracts, and automating validation in your CI/CD pipeline, you catch interface mismatches before they cause production issues.
The key mindset shift: treat every API boundary as a formal contract, not an informal agreement. Formal contracts are written down, versioned, and mechanically verified. Informal agreements drift silently until something breaks in production at an inconvenient time.
Start with a single service endpoint, establish your contract testing patterns with schema-based validation, then expand coverage across your system. Add error contract tests as a second pass. The investment pays dividends in reduced debugging time, fewer late-night incidents, and increased confidence during deployments. especially when multiple teams are delivering changes simultaneously.
Try it: Paste your error into our Error Diagnostic for an instant fix.
Last verified: April 2026. If this approach no longer works, check Mendeley Chrome Extension — Honest Review 2026 for updated steps.
Related Reading
- Claude Code MCP Server Setup: Complete Guide 2026
- Claude Code Permissions Model Security Guide 2026
- Claude Code for Beginners: Complete Getting Started Guide
- Best Claude Skills for Developers in 2026
- Advanced Claude Skills Hub
Built by theluckystrike. More at zovo.one
Get started → Generate your project setup with our Project Starter.
Configure MCP → Build your server config with our MCP Config Generator.