API Contract Testing
API contracts define what clients expect from your APIs. When responses deviate from the contract — missing fields, wrong types, changed formats — client applications break. APIAssert helps you continuously verify your APIs honor their contracts.
The Problem
API contract violations cause real problems:
- Mobile apps crash — Required field becomes optional
- Frontend breaks — String becomes number, parsing fails
- Integrations fail — Partner's code expects specific format
- Silent data loss — Renamed fields aren't mapped correctly
These issues often slip past unit tests and only surface in production.
How APIAssert Helps
Validate Response Structure
Assert that expected fields exist:
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
Ensure fields have correct types:
Assertions:
✓ $.id is string
✓ $.age is number
✓ $.active is boolean
✓ $.tags is array
✓ $.metadata is object
Check Value Constraints
Validate values meet expectations:
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 APIAssert 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
- Contract violation detected in staging before production deploy
- Alert fired when
$.user.nameassertion failed - Breaking change caught before mobile app update
- Fix applied — kept
namefield alongsidefull_name
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
Different clients may have different needs:
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 APIAssert 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 contract violation detected:
- Identify the change — What field/type changed?
- Check recent deploys — Who changed what?
- Assess impact — Which clients affected?
- Decide action — Rollback or fix forward?
- Update contracts — If intentional change
Getting Started
- Document your contracts — What fields are required?
- Create monitors — One per major endpoint
- Add structure assertions — Required fields exist
- Add type assertions — Fields have expected types
- Add value assertions — Enums, ranges, patterns
- Set alerts — Critical for breaking changes
Related Features
- Response Validation — Assertion capabilities
- GraphQL Monitoring — GraphQL-specific contracts