{"slug": "your-openapi-spec-is-a-contract-nobody-is-checking-it-i-built-a-cli-that-detects", "title": "Your OpenAPI spec is a contract. Nobody is checking it. I built a CLI that detects spec↔code drift with zero dependencies.", "summary": "A developer built oas-drift, a zero-dependency Python CLI that detects drift between an OpenAPI spec and a Python codebase by statically analyzing routes with the ast module. The tool reports three drift classes — code-only routes, method mismatches, and spec-only endpoints — and always exits 0, leaving build-failure policy to CI via its JSON output. It was validated against FastAPI's full-stack-fastapi-template, where prefixed router paths produce predictable false-positive pairs.", "body_md": "\"The generated client keeps calling DELETE /items/{id}.\"\n\n\"The server returns 405 Method Not Allowed.\"\n\nAn OpenAPI spec is not just documentation. SDKs, frontend types, API docs, mock servers — everything is generated *from* the spec. When the implementation drifts from it, **everything generated starts lying quietly**.\n\nThis is about **oas-drift**, a CLI I built that detects that drift — with **zero dependencies** (Python 3.11+ standard library only, no LLM).\n\n[https://github.com/sunnydachs/oas-drift](https://github.com/sunnydachs/oas-drift)\n\nYou give it an OpenAPI JSON spec and a Python codebase. It finds three classes of drift:\n\n```\n# scan the current directory against a spec (read-only)\noas-drift --spec openapi.json\n\n# scan a specific root, machine-readable output\noas-drift --spec openapi.json ./src --json\n```\n\nHere is a real run against a deliberately-drifted demo app:\n\n```\noas-drift — scanned src\n  spec: 5 endpoint(s) | code: 4 route(s)\n\n/health  ➕ CODE ONLY\n    route implemented (GET) but not defined in spec — src/app.py:21\n/items/{id}  ⚠️ METHOD MISMATCH\n    /items/{id}: in spec but not implemented: DELETE; implemented but not in spec: POST — src/app.py:17\n/users/{id}  ⚠️ METHOD MISMATCH\n    /users/{id}: in spec but not implemented: DELETE — src/app.py:13\n/admin/stats  ⬜ SPEC ONLY\n    defined in spec (GET) but no matching route in codebase\n/items  ⬜ SPEC ONLY\n    defined in spec (GET) but no matching route in codebase\n\nsummary: {\"code_only\": 1, \"method_mismatch\": 2, \"spec_only\": 2} | ok: 2\n```\n\n\"The spec is the contract\" only works if *someone* checks both sides. Code review sees the diff against the last commit — not against a spec written three months ago.\n\nThis was the core design decision. Most CI-facing drift tools fail the build when they find anything. oas-drift's exit code is `0` either way. Three reasons:\n\n`/health` endpoint missing from the spec is usually fine. A METHOD MISMATCH on a payment route is not. Which drift fails the build is policy — the tool shouldn't decide that for you.`ast` module — never imports, never executes, never writes. Same input → same report, always.\nIf you *do* want to fail on specific statuses, wire it into CI with `--json` and `jq` — the report is machine-readable by design.\n\nSame principles as the sibling tools I shipped this month — **doc-drift** (README↔code) and **plan-drift** (tracking plan↔code). Deterministic work deserves deterministic tools.\n\nThe detail I obsessed over: path parameters and router prefixes. The rule:\n\n`/users/{id}` in the spec matches `prefix=\"/items\"` serving `/{id}` does not match a spec's `/items/{id}`.\nI validated this against a real, widely-used codebase — the backend of FastAPI's official **full-stack-fastapi-template** (25 files, 14 paths, 23 routes detected). Scanning it with a spec written in prefixed paths produces exactly the false-positive pair this rule predicts:\n\n```\n/items/{id}  ⬜ SPEC ONLY\n    defined in spec (DELETE, GET, PUT) but no matching route in codebase\n/{id}  ➕ CODE ONLY\n    route implemented (DELETE, GET, PUT) but not defined in spec — backend/app/api/routes/items.py:48, ...\n```\n\nTwo names for the same route. oas-drift doesn't guess — it reports what literally exists. If your project uses router prefixes, normalize the spec side first (the surest source is the `/openapi.json` your app actually serves).\n\nAnd the important detection works: flipping the implemented `POST /login/access-token` to `PUT` in a test spec gets reported precisely:\n\n```\n/login/access-token  ⚠️ METHOD MISMATCH\n    /login/access-token: in spec but not implemented: PUT; implemented but not in spec: POST — backend/app/api/routes/login.py:23\n```\n\n`@app.get(f\"/users/{id}\")` isn't extracted; string literals only.\nThese are documented in the README. The current version prioritizes minimal, honest detection over coverage.\n\nSpecs drift the moment the code moves and nobody updates them. A contract is only a contract if something checks it — **oas-drift** is that checking part, built read-only, fully deterministic, and dependency-free.\n\n*This is a personal OSS project with no warranty. If you hit bugs or have suggestions, GitHub issues are the best way to reach me.*", "url": "https://wpnews.pro/news/your-openapi-spec-is-a-contract-nobody-is-checking-it-i-built-a-cli-that-detects", "canonical_source": "https://dev.to/sunnydachs/your-openapi-spec-is-a-contract-nobody-is-checking-it-i-built-a-cli-that-detects-spec-code-drift-23ah", "published_at": "2026-09-19 07:13:34+00:00", "updated_at": "2026-09-19 07:24:23.489123+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools"], "entities": ["oas-drift", "FastAPI", "full-stack-fastapi-template", "Python", "doc-drift", "plan-drift"], "alternates": {"html": "https://wpnews.pro/news/your-openapi-spec-is-a-contract-nobody-is-checking-it-i-built-a-cli-that-detects", "markdown": "https://wpnews.pro/news/your-openapi-spec-is-a-contract-nobody-is-checking-it-i-built-a-cli-that-detects.md", "text": "https://wpnews.pro/news/your-openapi-spec-is-a-contract-nobody-is-checking-it-i-built-a-cli-that-detects.txt", "jsonld": "https://wpnews.pro/news/your-openapi-spec-is-a-contract-nobody-is-checking-it-i-built-a-cli-that-detects.jsonld"}}