Detect OpenAPI Breaking Changes in CI Before They Break Consumers

· · ~1,050 words

Your API contract is a promise. The moment you delete an endpoint, change a response type, or make a field required, every consumer that depended on the old shape breaks — usually in production, usually at 2 a.m., usually with a senior engineer paged.

OpenAPI specs are machine-readable, which means the breaking-change check can be machine-enforced. You don't need a reviewer to notice that GET /items vanished. A CI step can fail the build the second it happens. This guide shows you how to wire that check in with a single Python CLI — kryptorious-apiguard — and the real output it produces on a deliberately broken spec.

What Counts as a Breaking Change?

Not every diff matters. Adding an optional field is safe; removing an endpoint is not. ApiGuard classifies each change into one of three buckets:

The point of the split is the CI gate: fail on breaking, warn on the rest.

Step 1: Install and Run a Real Diff

Install the CLI:

pip install kryptorious-apiguard
apiguard --help

ApiGuard ships three commands — diff (compare two specs), validate (structural check of one spec), and version. The money command is diff. Below is the actual output from comparing an old spec (v1.0.0) against a "refactored" new spec (v2.0.0) that quietly removed an endpoint:

$ apiguard diff old.yaml new.yaml

                                             🔍 API Breaking Changes Report

┌────┬────────────┬──────────┬────────────────────────────┬────────┬───────────────────────────────────────────────────┐
│ #  │ Severity   │ Rule     │ Path                       │ Method │ Message                                           │
├────┼────────────┼──────────┼──────────┼────────────────────────────┼────────┼───────────────────────────────────────────────────┤
│ 1  │ 🔴         │ PATH-001 │ /items                     │ GET    │ Removed endpoint: GET /items                      │
│    │ CRITICAL   │          │                            │        │                                                   │
└────┴────────────┴──────────┴────────────────────────────┴────────┴───────────────────────────────────────────────────┘

📊 Summary
Total changes: 1
Breaking: 1 | Non-breaking: 0 | Deprecated: 0
🔴 CRITICAL: 1

✖ Found 1 change(s) at or above error severity

The tool exited non-zero (the ✖ Found 1 change(s) at or above error severity line is the CI signal). That single CRITICAL is exactly the kind of change that silently breaks a mobile app in production.

Step 2: Machine-Readable Output for CI

Human-readable tables are nice in logs, but a pipeline needs structured data. ApiGuard emits JSON and SARIF alongside the table. The same diff, as JSON:

$ apiguard diff old.yaml new.yaml --format json

{
  "tool": "kryptorious-apiguard",
  "version": "1.0.0",
  "summary": {
    "total_changes": 1,
    "by_severity": { "critical": 1 },
    "by_category": { "breaking": 1 }
  },
  "changes": [
    {
      "rule_id": "PATH-001",
      "message": "Removed endpoint: GET /items",
      "severity": "critical",
      "category": "breaking",
      "path": "/items",
      "method": "GET"
    }
  ]
}

Pipe that into a gate, post it as a PR comment, or fail the build when by_category.breaking > 0. The exit code already does the heavy lifting — no parsing required for the basic case.

Step 3: Wire It Into Your Pipeline

Most teams keep their spec at openapi.yaml and regenerate it from code, or hand-edit it. Either way, diffing the committed spec against the proposed change on every PR is the safest pattern. A minimal GitHub Actions job:

# .github/workflows/api-contract.yml
name: API Contract Check
on: [pull_request]

jobs:
  contract:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - uses: actions/setup-python@v5
        with: { python-version: '3.12' }
      - run: pip install kryptorious-apiguard
      - name: Diff spec against base
        run: |
          git show origin/${{ github.base_ref }}:openapi.yaml > base.yaml
          apiguard diff base.yaml openapi.yaml --format json

If a PR removes an endpoint or changes a type, this job fails before review — and the author sees exactly which rule fired (PATH-001, TYPE-xxx, REQ-xxx, …) instead of a confused consumer ticket two weeks later.

Why a CLI Instead of a SaaS?

Contract diffing is a solved problem in Java (openapi-diff) and a linter in Spectral, but there was no native, dependency-light Python CLI that drops into a Python shop's CI without a JVM or a hosted service. ApiGuard is the gap-filler: one pip install, runs anywhere Python runs, no account, no network call. For shops already on the Kryptorious toolchain, it slots next to Python Guardian and EnvGuard in the same workflow file.

🔧 ApiGuard is one of 32 free MIT-licensed developer tools.
Want the bundle with DevFlow Premium (multi-env CI, approval gates, infra-as-code)? Get the bundle →
Explore all tools at codegero.github.io.

The Takeaway

A broken contract is the cheapest bug to catch at diff time and the most expensive to catch in production. The check is deterministic, fast, and offline — there is no reason it should live in a reviewer's memory instead of your pipeline. Add the diff step, fail on breaking > 0, and let the build be the bad-news messenger.

← Back to Kryptorious Tools · Get all 32 tools — $9 lifetime →

Keep reading:

← All posts