Tutorial

Catch Breaking API Changes in CI

A hands-on guide to detecting breaking schema changes — removed fields, type changes, new required parameters — with API Contract Guardian CLI and GitHub Actions integration.
May 17, 2026 · 9 min read · DevForge
Share this article:

Your team ships a minor API change on Friday afternoon. The diff looks tiny — just renaming a field and making a new parameter required. Tests pass. You merge.

Monday morning: three mobile apps are broken. The renamed field returns null for 40% of users. The new required parameter wasn't sent by any client built before last week. Your partner integrations are returning 400 errors. And the webhook consumers? They're silently dropping payloads because the schema changed without warning.

This is the breaking API change problem: it's not whether your server works — it's whether every client that depends on you still works. And traditional testing doesn't catch it, because your server returns perfectly valid responses. The breakage is on the client side.

In this tutorial, you'll learn how to detect breaking API changes with API Contract Guardian, a CLI tool that diffs OpenAPI specs, classifies every change by severity, and gates your CI pipeline so incompatible changes never reach production silently.

┌────────────┐ ┌───────────────────┐ ┌────────────┐ │ Old Spec │ ──→ │ │ ←── │ New Spec │ │ (main) │ │ API Contract │ │ (PR branch)│ └────────────┘ │ Guardian │ └────────────┘ │ │ ┌────────────┐ │ Breaking Change │ ┌────────────┐ │ PR Diff │ ──→ │ Report │ ──→ │ CI Gate │ │ +1 field │ │ │ │ ❌ Block │ └────────────┘ └───────────────────┘ └────────────┘ "removed field: user.email" ❌ Merge blocked "changed type: id int→string" ❌ Merge blocked "added required: consent" ❌ Merge blocked

What You'll Set Up

By the end of this tutorial, you'll have:

  1. API Contract Guardian installed and ready on your machine
  2. Two OpenAPI spec files — baseline and a PR with breaking changes
  3. Diff detection running with colored, classified output
  4. JSON output for CI/CD pipeline integration
  5. A GitHub Actions workflow that blocks PRs with breaking changes
⚡ Already have API Contract Guardian? Jump to CI Integration or check suite pricing.

1. Install API Contract Guardian

API Contract Guardian is a Python CLI — install it in seconds:

$ pip install api-contract-guardian

Or install directly from GitHub:

$ pip install git+https://github.com/Coding-Dev-Tools/api-contract-guardian.git

Verify it's working:

$ api-contract-guardian --help

You should see the available commands: diff, check, and init.

2. Create Sample OpenAPI Specs

Let's simulate a real API versioning scenario. Create two spec files — one for your current production API, and one for a PR that introduces changes.

openapi-base.yaml (production)

openapi: "3.1.0"
info:
  title: User Service API
  version: "2.4.0"
paths:
  /users:
    get:
      summary: List users
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
      responses:
        "200":
          description: Paginated user list
          content:
            application/json:
              schema:
                type: object
                properties:
                  users:
                    type: array
                    items:
                      $ref: "#/components/schemas/User"
  /users/{id}:
    get:
      summary: Get user by ID
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        "200":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
components:
  schemas:
    User:
      type: object
      required:
        - id
        - name
        - email
      properties:
        id:
          type: integer
        name:
          type: string
        email:
          type: string
          format: email
        avatar_url:
          type: string
          format: uri
        role:
          type: string
          enum: [admin, member, guest]

Now save this as openapi-pr.yaml — the same API, but with several breaking changes sneaked in:

openapi: "3.1.0"
info:
  title: User Service API
  version: "2.5.0"
paths:
  /users:
    get:
      summary: List users
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
        - name: consent          # ← NEW REQUIRED param
          in: query
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Paginated user list
          content:
            application/json:
              schema:
                type: object
                properties:
                  users:
                    type: array
                    items:
                      $ref: "#/components/schemas/User"
  /users/{id}:
    get:
      summary: Get user by ID
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string        # ← CHANGED from integer to string!
      responses:
        "200":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
components:
  schemas:
    User:
      type: object
      required:
        - id
        - name
        # email REMOVED from required!  # ← Was required, now optional
        - consent                     # ← NEW required field
      properties:
        id:
          type: string                # ← CHANGED from integer
        name:
          type: string
        email:
          type: string
          format: email
        # avatar_url DELETED entirely   # ← Removed field
        role:
          type: string
          enum: [admin, member, guest, superadmin]  # ← New enum value (non-breaking)
🚨 Spot the breaks? Three breaking changes hidden in this diff: (1) id changed from integer to string — any client parsing it as an int will crash. (2) avatar_url was deleted — any client referencing it gets null. (3) consent is now required in both the GET /users query and the User schema — any client not sending it gets a 400. These are exactly the kinds of changes that pass server-side tests but break every existing consumer.

3. Diff the Specs

Compare the two specs directly:

$ api-contract-guardian diff openapi-base.yaml openapi-pr.yaml

API Contract Guardian will output a classified report showing every change, grouped by severity:

══════════════════════════════════════════════════════════════════════
         API Contract Guardian — Breaking Change Report
══════════════════════════════════════════════════════════════════════

🔴 BREAKING (4)
────────────────────────────────────────────────────────────────────
  User.id              type changed   integer → string
  User.avatar_url      field removed  was: string ($uri)
  GET /users           new required   consent (query param)
  User                 new required   consent (schema field)

🟠 WARNING (1)
────────────────────────────────────────────────────────────────────
  User.email           required removed  (breaking if clients depend on it)

🔵 INFO (2)
────────────────────────────────────────────────────────────────────
  User.role            enum added     +superadmin
  info.version         version bump   2.4.0 → 2.5.0

API Contract Guardian classifies each change automatically:

4. Check a Single Endpoint

Sometimes you just want to check one endpoint, not the whole spec:

$ api-contract-guardian check openapi-base.yaml openapi-pr.yaml --endpoint "/users/{id}"

This isolates the diff to a specific path — useful for large APIs where you want to focus on the change you just made.

5. JSON Output for CI

For pipeline integration, use JSON output:

$ api-contract-guardian diff openapi-base.yaml openapi-pr.yaml --output json

This produces structured JSON:

{
  "base": "openapi-base.yaml",
  "target": "openapi-pr.yaml",
  "breaking": [
    {
      "location": "components.schemas.User.properties.id",
      "change": "type_changed",
      "from": "integer",
      "to": "string",
      "impact": "Clients parsing id as integer will crash or produce wrong values"
    },
    {
      "location": "components.schemas.User.properties.avatar_url",
      "change": "field_removed",
      "impact": "Clients referencing avatar_url will receive null or KeyError"
    },
    {
      "location": "paths./users.get.parameters.consent",
      "change": "new_required_param",
      "impact": "Requests without consent param will receive 400 Bad Request"
    },
    {
      "location": "components.schemas.User.required",
      "change": "new_required_field",
      "field": "consent",
      "impact": "Responses without consent field will fail schema validation"
    }
  ],
  "warnings": [
    {
      "location": "components.schemas.User.required",
      "change": "required_removed",
      "field": "email",
      "impact": "Clients expecting email to always be present may break"
    }
  ],
  "infos": [
    { "location": "components.schemas.User.properties.role", "change": "enum_value_added", "value": "superadmin" },
    { "location": "info.version", "change": "version_bump", "from": "2.4.0", "to": "2.5.0" }
  ],
  "exit_code": 1
}

The exit_code field tells you the result:

6. CI/CD Integration (GitHub Actions)

Here's the real payoff — gating your pipeline so that breaking API changes never ship silently. Add this GitHub Actions workflow to your repo:

# .github/workflows/api-contract-check.yml
name: API Contract Check

on:
  pull_request:
    paths:
      - 'openapi/**'
      - 'docs/api/**'

jobs:
  check-contract:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2

      - name: Install API Contract Guardian
        run: pip install api-contract-guardian

      - name: Diff OpenAPI specs
        run: |
          api-contract-guardian diff \
            openapi/openapi-base.yaml \
            openapi/openapi-pr.yaml \
            --output silent || echo "::warning::Breaking API changes detected!"
💡 Pro tip: Use --output silent in CI. The exit code is all you need — non-zero means breaking changes were found. Capture the full JSON report as a CI artifact for debugging.

For a more detailed pipeline that surfaces the actual breaking changes in the PR conversation:

      - name: Generate contract report
        id: contract
        run: |
          api-contract-guardian diff \
            openapi/openapi-base.yaml \
            openapi/openapi-pr.yaml \
            --output json > contract-report.json
          echo "breaking=$(jq -r '.exit_code' contract-report.json)" >> $GITHUB_OUTPUT
          cat contract-report.json

      - name: Comment PR with breaking changes
        if: steps.contract.outputs.breaking == '1'
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const report = JSON.parse(fs.readFileSync('contract-report.json'));
            const breaking = report.breaking.map(b =>
              `- 🔴 \`${b.location}\`: ${b.change} — ${b.impact}`
            ).join('\n');
            await github.rest.issues.createComment({
              ...context.repo,
              issue_number: context.issue.number,
              body: `## ⚠️ Breaking API Changes Detected\n\n${breaking}\n\nFix these or add backward-compatible paths before merging.`
            });

Now every PR that touches your OpenAPI spec gets automatically checked. If breaking changes are found, the PR gets a comment detailing every breaking change and the CI check fails — no more silent compatibility breaks.

7. Initialize a Contract Project

For teams that manage multiple APIs, use a config file:

$ api-contract-guardian init .

This creates an .api-contract.yaml file where you can define baseline specs, paths, and rules:

baseline: openapi/openapi.yaml
specs:
  - openapi/openapi.yaml
  - openapi/admin-api.yaml
  - openapi/partner-api.yaml
rules:
  type_changed: breaking
  field_removed: breaking
  new_required_param: breaking
  enum_value_added: info
  new_optional_field: info
ignore:
  - "info.version"
  - "x-internal"

Then run with just:

$ api-contract-guardian diff --config .api-contract.yaml

What Counts as Breaking?

API Contract Guardian uses a comprehensive classification system. Here's the full reference:

Change TypeSeverityExample
Field removed🔴 Breakingavatar_url deleted from User schema
Type changed🔴 Breakingid changed from integer to string
New required param🔴 Breakingconsent added as required query param
New required field🔴 Breakingconsent added to User required list
Required removed🟠 Warningemail no longer required
Enum value removed🔴 Breakingguest removed from role enum
Enum value added🔵 Infosuperadmin added to role enum
New optional field🔵 Infonickname added (not required)
Format changed🟠 Warningemail format changed to email
Endpoint removed🔴 BreakingDELETE /users/{id} removed
New endpoint🔵 InfoGET /users/search added

Real-World API Breakage Horror Stories

Still think manual review is enough? Here's what happens when breaking changes slip through:

✅ The fix is one pip install away. Adding API Contract Guardian to your CI pipeline takes 10 minutes and catches these issues before they become outages.

Next Steps

API Contract Guardian is one of 11 tools in the DevForge suite, all designed to catch problems before they reach production:

🔌 MCP bridge: All tools work with click-to-mcp — turn any of them into an MCP server so Claude Code, Codex, and Cursor can use them directly.

Read the full docs →

Stay in the Loop

PyPI publishing is coming soon. Get notified when we ship.