/ Use case

API Contract Testing

Catch the renamed field before your mobile app does

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:

  • $.id must exist
  • $.name must exist
  • $.email must exist
  • $.avatar may or may not exist (no assertion)

Field Types

// Expected types:
{
  "count": 42,
  "name": "Product",
  "active": true,
  "tags": ["sale", "new"],
  "metadata": {"key": "value"}
}

Assertions:

  • $.count is number
  • $.name is string
  • $.active is boolean
  • $.tags is array
  • $.metadata is object

Enum Values

// Expected enum:
{
  "status": "active" | "pending" | "cancelled"
}

Assertions:

  • $.status in ["active", "pending", "cancelled"]

Nested Objects

// Expected structure:
{
  "user": {
    "profile": {
      "address": {
        "city": "string"
      }
    }
  }
}

Assertions:

  • $.user.profile.address.city must exist
  • $.user.profile.address.city is string

Arrays

// Expected array structure:
{
  "items": [
    {"id": "string", "name": "string"}
  ]
}

Assertions:

  • $.items is array
  • $.items.length >= 0
  • $.items[0].id exists (if non-empty)
  • $.items[0].name exists (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 Email
Low Optional field removed Log only

Alert Response

When a contract assertion fails:

  1. Identify the change: which field, which type?
  2. Check recent deploys: who changed what?
  3. Assess impact: which clients read that field?
  4. Decide: roll back, or fix forward?
  5. 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

Write your first assertion.

Five monitors, thirty-second checks, no card, no call.