API Contract — Complete Guide to Service Agreements
In this tutorial, you will learn about API Contract. We cover key concepts, practical examples, and best practices to help you master this topic.
An API contract is a formal agreement between provider and consumer defining endpoints, request/response formats, error codes, authentication, and rate limits for reliable integration between services.
What You'll Learn
- What an API contract includes and why it matters
- OpenAPI as a contract specification language
- Consumer-driven Contract Testing
Why It Matters
Without a formal contract, providers can unknowingly break consumers. An API contract acts as a single source of truth that both sides agree to, preventing integration surprises.
Real-World Use
Durga Antivirus Pro publishes OpenAPI 3.1 contracts for all APIs. Internal Microservices use consumer-driven contracts: each consumer publishes their expected contract, and CI verifies the provider matches all consumer contracts.
flowchart LR
P["Provider"] --> C["API Contract (OpenAPI)"]
C --> Con1["Consumer 1"]
C --> Con2["Consumer 2"]
C --> Con3["Consumer 3"]
Con1 --> CT["Contract Test"]
CT --> CI["CI Pipeline"]
CI --> P
style C fill:#dbeafe,stroke:#2563eb
Code Examples
# OpenAPI 3.1 contract example
openapi: "3.1.0"
info:
title: Durga Antivirus Threat API
version: "2.0.0"
description: Contract for threat intelligence queries
paths:
/threats:
get:
summary: List threats
parameters:
- name: severity
in: query
schema:
type: string
enum: [low, medium, high, critical]
description: Filter by threat severity
- name: page
in: query
schema:
type: integer
minimum: 1
default: 1
responses:
"200":
description: Paginated threat list
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/Threat"
meta:
type: object
properties:
page: { type: integer }
total: { type: integer }
"401":
description: Missing or invalid authentication
Expected output: OpenAPI contract defines every endpoint, parameter, response, and error schema.
# Consumer-driven contract test
import requests
from openapi_core import create_spec
import yaml
# Load provider's contract
with open('openapi.yaml') as f:
spec = create_spec(yaml.safe_load(f))
# Consumer's expected behavior (contract test)
def test_threats_endpoint_contract():
response = requests.get(
'https://api.example.com/threats?severity=high',
headers={'Authorization': 'Bearer test-token'}
)
# Validate response against contract
result = spec.validate_response(response)
assert not result.errors, f"Contract broken: {result.errors}"
# Verify consumer-specific expectations
data = response.json()
assert 'data' in data, "Missing data field"
assert 'meta' in data, "Missing meta field"
assert all('severity' in t for t in data['data']), "Missing severity in threats"
Expected output: Contract test validates that the API response matches the OpenAPI spec and consumer expectations.
// Pact consumer-driven contract
const { PactV3, MatchersV3 } = require('@pact-foundation/pact');
const { like } = MatchersV3;
const provider = new PactV3({
consumer: 'ThreatDashboard',
provider: 'ThreatAPI',
});
describe('Threat API contract', () => {
it('returns threats matching the contract', () => {
provider
.given('threats exist')
.uponReceiving('a request for threats')
.withRequest({
method: 'GET',
path: '/threats',
headers: { Authorization: 'Bearer token' },
})
.willRespondWith({
status: 200,
headers: { 'Content-Type': 'application/json' },
body: {
data: like([{ id: like(1), name: like('Ransomware'), severity: like('high') }]),
meta: { page: like(1), total: like(42) },
},
});
return provider.executeTest(async (mockServer) => {
const res = await axios.get(`${mockServer.url}/threats`, {
headers: { Authorization: 'Bearer token' },
});
expect(res.data.data[0].id).toBeDefined();
});
});
});
Expected output: Pact contract test verifies provider meets consumer expectations in isolation.
Common Mistakes
1. No Contract at All
Without a contract, provider and consumer have different understandings of the API, causing integration failures.
2. Contract Drift
The implementation changes but the contract is not updated. Keep contract and code in sync via CI checks.
3. Overly Permissive Contracts
Using additionalProperties: true or allowing nullable everywhere defeats contract validation.
4. No Versioning in the Contract
A contract without version info cannot track changes over time. Always version your contract.
5. Ignoring Consumer Contracts
Provider-only contracts miss what consumers actually need. Use consumer-driven contracts to capture real requirements.
Practice Questions
- What is an API contract and why is it important?
- How does consumer-driven contract testing differ from provider contract testing?
- What is contract drift and how do you prevent it?
- Why should contracts be versioned?
- What is the role of OpenAPI in API contracts?
Answers:
- An API contract is a formal specification of the API interface that both provider and consumer agree to.
- Provider contracts describe what the API does; consumer contracts describe what consumers expect.
- Contract drift is when implementation diverges from the spec. Prevent by validating against the contract in CI.
- Versioning tracks how the contract evolves; breaking changes require a new version.
- OpenAPI provides a standard, machine-readable format for defining API contracts.
Challenge: Create a consumer-driven contract for a payment API. Write a Pact test from the consumer perspective, publish the OpenAPI contract from the provider, and set up CI to verify both match.
FAQ
Mini Project
Create an API contract for a task management API using OpenAPI 3.1. Include: CRUD endpoints, pagination, error schemas, authentication, and rate limit headers. Write consumer-driven Pact tests for two consumers with different expectations.
What's Next
Learn about API testing strategies for contract verification, or explore API documentation for translating contracts into developer guides.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro