
Api Contract Testing
- 797 installs
- 305 repo stars
- Updated March 4, 2026
- aj-geddes/useful-ai-prompts
API Contract Testing is a backend testing skill that verifies API contracts between consumers and providers using contract testing, Pact, and schema validation for developers who must catch breaking service changes befor
About
API Contract Testing is an installable skill from aj-geddes/useful-ai-prompts that guides contract testing between microservice consumers and providers. It validates that API changes do not break dependent consumers without requiring full integration test suites. Coverage includes consumer-driven contracts, Pact workflows, schema validation, and compatibility checks before ship. Developers reach for API Contract Testing when services evolve independently and integration tests are too slow or brittle to catch contract drift. The skill documents overview, quick start, reference guides, and best practices for honoring published API contracts.
- Verifies request/response formats, data types, and behavior without full integration tests
- Prevents breaking changes in microservices and third-party integrations
- Supports consumer-driven contract testing with Pact and OpenAPI/Swagger validation
- Catches contract violations early in CI pipelines
- Validates API versioning and backward compatibility
Api Contract Testing by the numbers
- 797 all-time installs (skills.sh)
- +10 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #557 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/aj-geddes/useful-ai-prompts --skill api-contract-testingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 797 |
|---|---|
| repo stars | ★ 305 |
| Security audit | 3 / 3 scanners passed |
| Last updated | March 4, 2026 |
| Repository | aj-geddes/useful-ai-prompts ↗ |
How do you prevent breaking API contract changes?
Automatically verify that API changes between services will not break dependent consumers before they ship.
Who is it for?
Backend developers maintaining microservices who need consumer-driven contract tests instead of heavyweight end-to-end integration suites.
Skip if: Teams with a single monolithic API and no independent consumer services that require contract isolation.
When should I use this skill?
A developer mentions Pact, consumer-driven contracts, schema validation, or breaking API changes between services.
What you get
Contract test plans, Pact consumer-provider validations, and schema compatibility checks confirming services honor published API contracts.
- contract test specifications
- schema validation checklist
Files
API Contract Testing
Table of Contents
Overview
Contract testing verifies that APIs honor their contracts between consumers and providers. It ensures that service changes don't break dependent consumers without requiring full integration tests. Contract tests validate request/response formats, data types, and API behavior independently.
When to Use
- Testing microservices communication
- Preventing breaking API changes
- Validating API versioning
- Testing consumer-provider contracts
- Ensuring backward compatibility
- Validating OpenAPI/Swagger specifications
- Testing third-party API integrations
- Catching contract violations in CI
Quick Start
Minimal working example:
// tests/pact/user-service.pact.test.ts
import { PactV3, MatchersV3 } from "@pact-foundation/pact";
import { UserService } from "../../src/services/UserService";
const { like, eachLike, iso8601DateTimeWithMillis } = MatchersV3;
const provider = new PactV3({
consumer: "OrderService",
provider: "UserService",
port: 1234,
dir: "./pacts",
});
describe("User Service Contract", () => {
const userService = new UserService("http://localhost:1234");
describe("GET /users/:id", () => {
test("returns user when found", async () => {
await provider
.given("user with ID 123 exists")
.uponReceiving("a request for user 123")
.withRequest({
method: "GET",
path: "/users/123",
headers: {
// ... (see reference guides for full implementation)Reference Guides
Detailed implementations in the references/ directory:
| Guide | Contents |
|---|---|
| Pact for Consumer-Driven Contracts | Pact for Consumer-Driven Contracts |
| OpenAPI Schema Validation | OpenAPI Schema Validation |
| JSON Schema Validation | JSON Schema Validation |
| REST Assured for Java | REST Assured for Java |
| Contract Testing with Postman | Contract Testing with Postman |
| Pact Broker Integration | Pact Broker Integration |
Best Practices
✅ DO
- Test contracts from consumer perspective
- Use matchers for flexible matching
- Validate schema structure, not specific values
- Version your contracts
- Test error responses
- Use Pact broker for contract sharing
- Run contract tests in CI
- Test backward compatibility
❌ DON'T
- Test business logic in contract tests
- Hard-code specific values in contracts
- Skip error scenarios
- Test UI in contract tests
- Ignore contract versioning
- Deploy without contract verification
- Test implementation details
- Mock contract tests
Contract Testing with Postman
Contract Testing with Postman
// postman-collection.json
{
"info": {
"name": "User API Contract Tests"
},
"item": [
{
"name": "Get User",
"request": {
"method": "GET",
"url": "{{baseUrl}}/users/{{userId}}"
},
"test": "
pm.test('Response status is 200', () => {
pm.response.to.have.status(200);
});
pm.test('Response matches schema', () => {
const schema = {
type: 'object',
required: ['id', 'email', 'name'],
properties: {
id: { type: 'string' },
email: { type: 'string', format: 'email' },
name: { type: 'string' },
age: { type: 'integer' }
}
};
pm.response.to.have.jsonSchema(schema);
});
pm.test('Email format is valid', () => {
const data = pm.response.json();
pm.expect(data.email).to.match(/^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$/);
});
"
}
]
}JSON Schema Validation
JSON Schema Validation
# tests/contract/test_schema_validation.py
import pytest
import jsonschema
from jsonschema import validate
import json
# Define schemas
USER_SCHEMA = {
"type": "object",
"required": ["id", "email", "name"],
"properties": {
"id": {"type": "string"},
"email": {"type": "string", "format": "email"},
"name": {"type": "string"},
"age": {"type": "integer", "minimum": 0, "maximum": 150},
"role": {"type": "string", "enum": ["user", "admin"]},
"createdAt": {"type": "string", "format": "date-time"},
},
"additionalProperties": False
}
ORDER_SCHEMA = {
"type": "object",
"required": ["id", "userId", "total", "status"],
"properties": {
"id": {"type": "string"},
"userId": {"type": "string"},
"total": {"type": "number", "minimum": 0},
"status": {
"type": "string",
"enum": ["pending", "paid", "shipped", "delivered", "cancelled"]
},
"items": {
"type": "array",
"items": {
"type": "object",
"required": ["productId", "quantity", "price"],
"properties": {
"productId": {"type": "string"},
"quantity": {"type": "integer", "minimum": 1},
"price": {"type": "number", "minimum": 0},
}
}
}
}
}
class TestAPIContracts:
def test_get_user_response_schema(self, api_client):
"""Validate user endpoint response against schema."""
response = api_client.get('/api/users/123')
assert response.status_code == 200
data = response.json()
# Validate against schema
validate(instance=data, schema=USER_SCHEMA)
def test_create_user_request_schema(self, api_client):
"""Validate create user request body."""
valid_user = {
"email": "test@example.com",
"name": "Test User",
"age": 30,
}
response = api_client.post('/api/users', json=valid_user)
assert response.status_code == 201
# Response should also match schema
validate(instance=response.json(), schema=USER_SCHEMA)
def test_invalid_request_rejected(self, api_client):
"""Invalid requests should be rejected."""
invalid_user = {
"email": "not-an-email",
"age": -5, # Invalid age
}
response = api_client.post('/api/users', json=invalid_user)
assert response.status_code == 400
def test_order_response_schema(self, api_client):
"""Validate order endpoint response."""
response = api_client.get('/api/orders/order-123')
assert response.status_code == 200
validate(instance=response.json(), schema=ORDER_SCHEMA)
def test_order_items_array_validation(self, api_client):
"""Validate nested array schema."""
order_data = {
"userId": "user-123",
"items": [
{"productId": "prod-1", "quantity": 2, "price": 29.99},
{"productId": "prod-2", "quantity": 1, "price": 49.99},
]
}
response = api_client.post('/api/orders', json=order_data)
assert response.status_code == 201
result = response.json()
validate(instance=result, schema=ORDER_SCHEMA)OpenAPI Schema Validation
OpenAPI Schema Validation
// tests/contract/openapi.test.ts
import request from "supertest";
import { app } from "../../src/app";
import OpenAPIValidator from "express-openapi-validator";
import fs from "fs";
import yaml from "js-yaml";
describe("OpenAPI Contract Validation", () => {
let validator;
beforeAll(() => {
const spec = yaml.load(fs.readFileSync("./openapi.yaml", "utf8"));
validator = OpenAPIValidator.middleware({
apiSpec: spec,
validateRequests: true,
validateResponses: true,
});
});
test("GET /users/:id matches schema", async () => {
const response = await request(app).get("/users/123").expect(200);
// Validate against OpenAPI schema
expect(response.body).toMatchObject({
id: expect.any(String),
email: expect.stringMatching(/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/),
name: expect.any(String),
age: expect.any(Number),
createdAt: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T/),
});
});
test("POST /users validates request body", async () => {
const invalidUser = {
email: "invalid-email", // Should fail validation
name: "Test",
};
await request(app).post("/users").send(invalidUser).expect(400);
});
});Pact Broker Integration
Pact Broker Integration
# .github/workflows/contract-tests.yml
name: Contract Tests
on: [push, pull_request]
jobs:
consumer-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm ci
- run: npm run test:pact
- name: Publish Pacts
run: |
npx pact-broker publish ./pacts \
--consumer-app-version=${{ github.sha }} \
--broker-base-url=${{ secrets.PACT_BROKER_URL }} \
--broker-token=${{ secrets.PACT_BROKER_TOKEN }}
provider-tests:
runs-on: ubuntu-latest
needs: consumer-tests
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm ci
- run: npm run test:pact:provider
- name: Can I Deploy?
run: |
npx pact-broker can-i-deploy \
--pacticipant=UserService \
--version=${{ github.sha }} \
--to-environment=production \
--broker-base-url=${{ secrets.PACT_BROKER_URL }} \
--broker-token=${{ secrets.PACT_BROKER_TOKEN }}Pact for Consumer-Driven Contracts
Pact for Consumer-Driven Contracts
Consumer Test (Jest/Pact)
// tests/pact/user-service.pact.test.ts
import { PactV3, MatchersV3 } from "@pact-foundation/pact";
import { UserService } from "../../src/services/UserService";
const { like, eachLike, iso8601DateTimeWithMillis } = MatchersV3;
const provider = new PactV3({
consumer: "OrderService",
provider: "UserService",
port: 1234,
dir: "./pacts",
});
describe("User Service Contract", () => {
const userService = new UserService("http://localhost:1234");
describe("GET /users/:id", () => {
test("returns user when found", async () => {
await provider
.given("user with ID 123 exists")
.uponReceiving("a request for user 123")
.withRequest({
method: "GET",
path: "/users/123",
headers: {
Authorization: like("Bearer token"),
},
})
.willRespondWith({
status: 200,
headers: {
"Content-Type": "application/json",
},
body: {
id: like("123"),
email: like("user@example.com"),
name: like("John Doe"),
age: like(30),
createdAt: iso8601DateTimeWithMillis("2024-01-01T00:00:00.000Z"),
role: like("user"),
},
})
.executeTest(async (mockServer) => {
const user = await userService.getUser("123");
expect(user.id).toBe("123");
expect(user.email).toBeDefined();
expect(user.name).toBeDefined();
});
});
test("returns 404 when user not found", async () => {
await provider
.given("user with ID 999 does not exist")
.uponReceiving("a request for non-existent user")
.withRequest({
method: "GET",
path: "/users/999",
})
.willRespondWith({
status: 404,
headers: {
"Content-Type": "application/json",
},
body: {
error: like("User not found"),
code: like("USER_NOT_FOUND"),
},
})
.executeTest(async (mockServer) => {
await expect(userService.getUser("999")).rejects.toThrow(
"User not found",
);
});
});
});
describe("POST /users", () => {
test("creates new user", async () => {
await provider
.given("user does not exist")
.uponReceiving("a request to create user")
.withRequest({
method: "POST",
path: "/users",
headers: {
"Content-Type": "application/json",
},
body: {
email: like("newuser@example.com"),
name: like("New User"),
age: like(25),
},
})
.willRespondWith({
status: 201,
headers: {
"Content-Type": "application/json",
},
body: {
id: like("new-123"),
email: like("newuser@example.com"),
name: like("New User"),
age: like(25),
createdAt: iso8601DateTimeWithMillis(),
role: "user",
},
})
.executeTest(async (mockServer) => {
const user = await userService.createUser({
email: "newuser@example.com",
name: "New User",
age: 25,
});
expect(user.id).toBeDefined();
expect(user.email).toBe("newuser@example.com");
});
});
});
describe("GET /users/:id/orders", () => {
test("returns user orders", async () => {
await provider
.given("user 123 has orders")
.uponReceiving("a request for user orders")
.withRequest({
method: "GET",
path: "/users/123/orders",
query: {
limit: "10",
offset: "0",
},
})
.willRespondWith({
status: 200,
body: {
orders: eachLike({
id: like("order-1"),
total: like(99.99),
status: like("completed"),
createdAt: iso8601DateTimeWithMillis(),
}),
total: like(5),
hasMore: like(false),
},
})
.executeTest(async (mockServer) => {
const response = await userService.getUserOrders("123", {
limit: 10,
offset: 0,
});
expect(response.orders).toBeDefined();
expect(Array.isArray(response.orders)).toBe(true);
expect(response.total).toBeDefined();
});
});
});
});Provider Test (Verify Contract)
// tests/pact/user-service.provider.test.ts
import { Verifier } from "@pact-foundation/pact";
import path from "path";
import { app } from "../../src/app";
import { setupTestDB, teardownTestDB } from "../helpers/db";
describe("Pact Provider Verification", () => {
let server;
beforeAll(async () => {
await setupTestDB();
server = app.listen(3001);
});
afterAll(async () => {
await teardownTestDB();
server.close();
});
test("validates the expectations of OrderService", () => {
return new Verifier({
provider: "UserService",
providerBaseUrl: "http://localhost:3001",
pactUrls: [
path.resolve(__dirname, "../../pacts/orderservice-userservice.json"),
],
// Provider state setup
stateHandlers: {
"user with ID 123 exists": async () => {
await createTestUser({ id: "123", name: "John Doe" });
},
"user with ID 999 does not exist": async () => {
await deleteUser("999");
},
"user 123 has orders": async () => {
await createTestUser({ id: "123" });
await createTestOrder({ userId: "123" });
},
},
})
.verifyProvider()
.then((output) => {
console.log("Pact Verification Complete!");
});
});
});REST Assured for Java
REST Assured for Java
// ContractTest.java
import io.restassured.RestAssured;
import io.restassured.module.jsv.JsonSchemaValidator;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;
public class UserAPIContractTest {
@Test
public void getUserShouldMatchSchema() {
given()
.pathParam("id", "123")
.when()
.get("/api/users/{id}")
.then()
.statusCode(200)
.body(JsonSchemaValidator.matchesJsonSchemaInClasspath("schemas/user-schema.json"))
.body("id", notNullValue())
.body("email", matchesPattern("^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$"))
.body("age", greaterThanOrEqualTo(0));
}
@Test
public void createUserShouldValidateRequest() {
String userJson = """
{
"email": "test@example.com",
"name": "Test User",
"age": 30
}
""";
given()
.contentType("application/json")
.body(userJson)
.when()
.post("/api/users")
.then()
.statusCode(201)
.body("id", notNullValue())
.body("email", equalTo("test@example.com"))
.body("createdAt", matchesPattern("\\d{4}-\\d{2}-\\d{2}T.*"));
}
@Test
public void getUserOrdersShouldReturnArray() {
given()
.pathParam("id", "123")
.queryParam("limit", 10)
.when()
.get("/api/users/{id}/orders")
.then()
.statusCode(200)
.body("orders", isA(java.util.List.class))
.body("orders[0].id", notNullValue())
.body("orders[0].status", isIn(Arrays.asList(
"pending", "paid", "shipped", "delivered", "cancelled"
)))
.body("total", greaterThanOrEqualTo(0));
}
@Test
public void invalidRequestShouldReturn400() {
String invalidUser = """
{
"email": "not-an-email",
"age": -5
}
""";
given()
.contentType("application/json")
.body(invalidUser)
.when()
.post("/api/users")
.then()
.statusCode(400)
.body("error", notNullValue());
}
}#!/bin/bash
# scaffold-tests.sh - Generate test file scaffolding
# Usage: ./scaffold-tests.sh <source_file> [--framework jest|pytest|mocha]
set -euo pipefail
SOURCE_FILE="${{1:?Usage: $0 <source_file> [--framework jest|pytest|mocha]}}"
FRAMEWORK="${{2:-jest}}"
echo "Scaffolding tests for: $SOURCE_FILE (framework: $FRAMEWORK)"
# TODO: Implement test scaffolding logic
# - Parse source file for exported functions/classes
# - Generate test stubs for each export
# - Include setup/teardown boilerplate
# - Add common assertion patterns
echo "Test scaffolding complete."
// Test Template
// TODO: Customize for your testing framework and project
describe('ModuleName', () => {
// Setup
beforeEach(() => {
// TODO: Add test setup
});
afterEach(() => {
// TODO: Add cleanup
});
describe('functionName', () => {
it('should handle the happy path', () => {
// TODO: Add assertion
});
it('should handle edge cases', () => {
// TODO: Add edge case tests
});
it('should handle errors gracefully', () => {
// TODO: Add error handling tests
});
});
});
Related skills
How it compares
Choose API Contract Testing for cross-service contract guarantees; rely on integration tests only when you need full end-to-end behavioral coverage beyond API surface contracts.
FAQ
What problem does API Contract Testing solve?
API Contract Testing verifies that providers honor contracts expected by consumers, catching breaking API changes without running full integration tests across every dependent service.
Does API Contract Testing support Pact?
API Contract Testing explicitly covers Pact, consumer-driven contracts, and schema validation as core methods for verifying compatibility between services.
Is Api Contract Testing safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.