API Contract Testing
Your API contract is the set of fields, types and formats your clients count on. Rename a field, turn a string into a number, drop a key, and those clients break while the endpoint keeps returning 200. Assert checks the contract on every run.
The Problem
A required field goes optional and the mobile app crashes on a null. A string becomes a number and the frontend's parser gives up. A partner's code expects one shape and gets another. A renamed field stops being mapped, so the data quietly stops arriving.
Unit tests pass through all of it, because the endpoint still returns 200.
How Assert Helps
Validate Response Structure
Check that every field a client reads is there:
Monitor: User API Contract
URL: GET /api/users/123
Assertions:
✓ $.id exists
✓ $.email exists
✓ $.created_at exists
✓ $.profile exists
✓ $.profile.name exists
Verify Data Types
Presence is not enough. A count that arrives as a string instead of a number breaks the parser the same way a missing count does:
Assertions:
✓ $.id is string
✓ $.age is number
✓ $.active is boolean
✓ $.tags is array
✓ $.metadata is object
Check Value Constraints
Then the values themselves, when a client only knows how to handle a known set:
Assertions:
✓ $.status in ["active", "pending", "disabled"]
✓ $.email matches email pattern
✓ $.price > 0
✓ $.items.length > 0
✓ $.page <= $.total_pages
Contract Testing Assertions
Required Fields
// Expected contract:
{
"id": "string (required)",
"name": "string (required)",
"email": "string (required)",
"avatar": "string (optional)"
}
Assertions:
$.idmust exist$.namemust exist$.emailmust exist$.avatarmay or may not exist (no assertion)
Field Types
// Expected types:
{
"count": 42,
"name": "Product",
"active": true,
"tags": ["sale", "new"],
"metadata": {"key": "value"}
}
Assertions:
$.countis number$.nameis string$.activeis boolean$.tagsis array$.metadatais object
Enum Values
// Expected enum:
{
"status": "active" | "pending" | "cancelled"
}
Assertions:
$.statusin ["active", "pending", "cancelled"]
Nested Objects
// Expected structure:
{
"user": {
"profile": {
"address": {
"city": "string"
}
}
}
}
Assertions:
$.user.profile.address.citymust exist$.user.profile.address.cityis string
Arrays
// Expected array structure:
{
"items": [
{"id": "string", "name": "string"}
]
}
Assertions:
$.itemsis array$.items.length>= 0$.items[0].idexists (if non-empty)$.items[0].nameexists (if non-empty)
Real-World Example
The Scenario
A mobile app consumes a user profile API. A backend refactor changes user.name to user.full_name. The API returns 200 OK, but the app crashes because it expects name.
The Assert Solution
Monitor: User Profile Contract
URL: GET /api/v1/users/me
Headers:
Authorization: Bearer $TEST_TOKEN
Assertions:
✓ $.user.id exists
✓ $.user.name exists ← Catches the rename!
✓ $.user.name is string
✓ $.user.email exists
✓ $.user.email matches "@"
✓ $.user.created_at exists
✓ $.user.avatar is string or null
Interval: 5 minutes
The Outcome
The staging monitor failed on $.user.name before the deploy reached production. Assert sent one alert with the failing response attached. The team kept name alongside full_name, and the monitor auto-resolved on its next run.
Common API Contracts to Monitor
User/Account APIs
GET /api/users/:id
Required fields:
- $.id (string)
- $.email (string, email format)
- $.created_at (string, ISO date)
- $.status (enum: active, disabled)
Optional fields:
- $.profile.name (string)
- $.profile.avatar (string, URL)
Collection/List APIs
GET /api/products
Required fields:
- $.data (array)
- $.pagination.page (number)
- $.pagination.total (number)
- $.pagination.per_page (number)
Array item contract:
- $.data[*].id (string)
- $.data[*].name (string)
- $.data[*].price (number, > 0)
Error Responses
Any endpoint returning 4xx/5xx
Required fields:
- $.error.code (string)
- $.error.message (string)
Optional fields:
- $.error.details (array)
- $.error.request_id (string)
Webhook Payloads
POST webhook
Required fields:
- $.event (string, enum)
- $.timestamp (string, ISO date)
- $.data (object)
- $.data.id (string)
Best Practices
Version Your Contracts
Monitor each API version separately:
Monitor: User API v1 Contract
URL: /api/v1/users/123
Monitor: User API v2 Contract
URL: /api/v2/users/123
Test Production Data
Use real (or realistic) test accounts:
# Good: Production test user
GET /api/users/test_user_12345
# Less good: Minimal test
GET /api/users/1
Monitor Breaking vs Non-Breaking
Breaking changes (critical):
- Required field removed
- Type changed
- Enum value removed
Non-breaking changes (warning):
- Optional field removed
- New field added
- Enum value added
Create Contract Per Consumer
Two clients can read the same endpoint and need different guarantees from it:
Monitor: Mobile App Contract
- Strict field requirements
- Specific enum values
Monitor: Partner Integration Contract
- Different field subset
- Version-specific
Document Your Contracts
Keep assertions aligned with documentation:
OpenAPI/Swagger → Generate assertions
Assertions → Validate in production
Continuous Contract Testing
Development Workflow
1. Define contract in OpenAPI
2. Create Assert monitors from contract
3. Monitor staging environment
4. Deploy to production
5. Monitor production continuously
When to Alert
| Severity | Condition | Action |
|---|---|---|
| Critical | Required field missing | Page on-call |
| High | Type changed | Slack alert |
| Medium | New unexpected field | |
| Low | Optional field removed | Log only |
Alert Response
When a contract assertion fails:
- Identify the change: which field, which type?
- Check recent deploys: who changed what?
- Assess impact: which clients read that field?
- Decide: roll back, or fix forward?
- Update the contract if the change was intentional.
Getting Started
Write down what each endpoint promises: which fields are required, which types they carry, which enum values are legal. Build one monitor per endpoint from that list and add the assertions in that order, structure first, then types, then values. Send the ones that catch breaking changes to whoever owns the clients.
Related Features
- Response Validation: what you can assert on
- GraphQL Monitoring: contracts for a GraphQL schema