<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Deepak Satyam</title>
    <description>The latest articles on DEV Community by Deepak Satyam (@deepaksatyam).</description>
    <link>https://dev.to/deepaksatyam</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3873472%2F44a11f85-e938-4451-91ab-7b5f3b7c398f.png</url>
      <title>DEV Community: Deepak Satyam</title>
      <link>https://dev.to/deepaksatyam</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/deepaksatyam"/>
    <language>en</language>
    <item>
      <title>9 API changes that look backwards-compatible but aren't</title>
      <dc:creator>Deepak Satyam</dc:creator>
      <pubDate>Tue, 16 Jun 2026 14:30:00 +0000</pubDate>
      <link>https://dev.to/deepaksatyam/9-api-changes-that-look-backwards-compatible-but-arent-1bk0</link>
      <guid>https://dev.to/deepaksatyam/9-api-changes-that-look-backwards-compatible-but-arent-1bk0</guid>
      <description>&lt;p&gt;"We didn't break anything — we just cleaned up the response."&lt;/p&gt;

&lt;p&gt;Famous last words. Four hours later a mobile app is crashing and a partner integration is returning garbage, because "cleaned up" quietly meant "changed the shape of data other people depend on."&lt;/p&gt;

&lt;p&gt;The tricky thing about breaking an API is that &lt;strong&gt;the most dangerous changes don't look dangerous.&lt;/strong&gt; They look like tidying. They pass code review, pass your tests, and ship green — because the breakage doesn't live in your repo. It lives in code you can't see, owned by people who didn't get a heads-up.&lt;/p&gt;

&lt;p&gt;Here are nine changes that feel backwards-compatible, why each one isn't, and how to stay safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Renaming a field
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;userId&lt;/code&gt; → &lt;code&gt;user_id&lt;/code&gt;, "for consistency." Every client still reading &lt;code&gt;userId&lt;/code&gt; now gets &lt;code&gt;undefined&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A rename is a &lt;strong&gt;delete plus an add&lt;/strong&gt; — and the delete is the part that breaks people.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Safe move:&lt;/strong&gt; add the new field, keep the old one, deprecate it loudly, remove it later (if ever).&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Making a response field optional or nullable
&lt;/h2&gt;

&lt;p&gt;It was always there; now it's sometimes &lt;code&gt;null&lt;/code&gt;. The client that did &lt;code&gt;payment.currency.toUpperCase()&lt;/code&gt; crashes.&lt;/p&gt;

&lt;p&gt;Loosening a response you &lt;em&gt;send&lt;/em&gt; tightens the assumptions you &lt;em&gt;break&lt;/em&gt; downstream.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Safe move:&lt;/strong&gt; treat "always present" as a promise. If a field genuinely must become optional, that's a versioned change, not a patch.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Removing a field "nobody uses"
&lt;/h2&gt;

&lt;p&gt;You can grep your own codebase. You cannot grep your consumers' code, the partner's integration, the Zapier zap someone built in 2023, or the LLM parsing your response right now.&lt;/p&gt;

&lt;p&gt;"Unused" means "unused by the consumers I happen to know about."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Safe move:&lt;/strong&gt; instrument field-level usage in production &lt;em&gt;before&lt;/em&gt; you remove anything. Measure, don't assume.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Narrowing a type
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;string&lt;/code&gt; → an enum. A wide numeric range → a smaller one. &lt;code&gt;number&lt;/code&gt; → &lt;code&gt;integer&lt;/code&gt;. Every previously-valid value that no longer fits is now a rejected request.&lt;/p&gt;

&lt;p&gt;Widening an input type is safe. &lt;strong&gt;Narrowing it is a breaking change&lt;/strong&gt; for everyone already sending the old values.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Safe move:&lt;/strong&gt; only ever loosen the inputs you accept. Tightening needs a version bump.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Changing a default value
&lt;/h2&gt;

&lt;p&gt;A field defaults to &lt;code&gt;false&lt;/code&gt;; you flip it to &lt;code&gt;true&lt;/code&gt; "because that's what most people want anyway." Every client relying on the old default silently changes behavior — no error, no failed request, just &lt;em&gt;different results&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Defaults are part of the contract even though nothing in the schema visibly "changed."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Safe move:&lt;/strong&gt; treat a default change as a breaking change. It is one.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Adding a required request field
&lt;/h2&gt;

&lt;p&gt;You add &lt;code&gt;tenantId&lt;/code&gt; and mark it required. Every existing client that doesn't send it now gets a &lt;code&gt;400&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Adding &lt;em&gt;optional&lt;/em&gt; fields is safe. Adding &lt;em&gt;required&lt;/em&gt; ones breaks everyone who came before.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Safe move:&lt;/strong&gt; new request fields are optional with a sane default — or they ride a new version.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Changing the error shape or status code
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;400&lt;/code&gt; → &lt;code&gt;422&lt;/code&gt;. &lt;code&gt;{ "error": "..." }&lt;/code&gt; → &lt;code&gt;{ "errors": [...] }&lt;/code&gt;. A success &lt;code&gt;200&lt;/code&gt; → a &lt;code&gt;204&lt;/code&gt; with no body.&lt;/p&gt;

&lt;p&gt;Error handling is code too, and you just broke all of it. The clients that were carefully parsing your error format are now the ones that fail hardest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Safe move:&lt;/strong&gt; error contracts are contracts. Status codes and error bodies are load-bearing — version them like anything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Touching an enum
&lt;/h2&gt;

&lt;p&gt;Renaming a value (&lt;code&gt;"active"&lt;/code&gt; → &lt;code&gt;"ACTIVE"&lt;/code&gt;) or dropping one (&lt;code&gt;"pending"&lt;/code&gt; is gone) breaks every client with an exhaustive switch — or a stored value that's now invalid. Even &lt;em&gt;adding&lt;/em&gt; a value can break strict consumers that didn't expect a new one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Safe move:&lt;/strong&gt; enum values are forever. Add them carefully; never rename or remove without a major version.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Quietly changing serialization
&lt;/h2&gt;

&lt;p&gt;The schema "type" didn't change, but the &lt;em&gt;meaning&lt;/em&gt; did:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dates from Unix seconds to ISO-8601.&lt;/li&gt;
&lt;li&gt;A 64-bit ID that loses precision when JSON serializes it as a float.&lt;/li&gt;
&lt;li&gt;Pagination from offset to cursor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are the nastiest because a schema diff often won't even flag them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Safe move:&lt;/strong&gt; pin formats explicitly, and diff &lt;strong&gt;real payloads&lt;/strong&gt;, not just the spec.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing all nine have in common
&lt;/h2&gt;

&lt;p&gt;Every one of these passes review, passes your tests, and ships green. "Looks backwards-compatible" is a &lt;em&gt;vibe&lt;/em&gt;. "Is backwards-compatible" is a &lt;em&gt;check&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The only reliable guard is to compare every change against the contract that's actually in production — in CI, on every pull request — and fail the build on the change classes above. A human will miss a renamed field in a big diff. A diff tool won't.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Full disclosure: I got annoyed enough by this exact problem that I build a tool for it. But you can get most of the way with &lt;a href="https://github.com/oasdiff/oasdiff" rel="noopener noreferrer"&gt;&lt;code&gt;oasdiff&lt;/code&gt;&lt;/a&gt; or any OpenAPI diff wired into CI. Just put &lt;em&gt;something&lt;/em&gt; between "looks safe" and "is deployed."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Which of these nine has bitten you? I'm betting #1 or #3. 👇&lt;/p&gt;

</description>
      <category>api</category>
      <category>webdev</category>
      <category>programming</category>
      <category>devops</category>
    </item>
    <item>
      <title>7 bugs AI-generated code ships that your tests won't catch</title>
      <dc:creator>Deepak Satyam</dc:creator>
      <pubDate>Mon, 15 Jun 2026 20:02:59 +0000</pubDate>
      <link>https://dev.to/deepaksatyam/7-bugs-ai-generated-code-ships-that-your-tests-wont-catch-2lfh</link>
      <guid>https://dev.to/deepaksatyam/7-bugs-ai-generated-code-ships-that-your-tests-wont-catch-2lfh</guid>
      <description>&lt;p&gt;AI writes most of my code now. I'm not mad about it — it's faster, and most of the time it's &lt;em&gt;right&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;But after months of reviewing what these agents produce all day, I've noticed they fail in a very specific, very consistent way. Not random garbage — &lt;strong&gt;plausible, well-formatted, test-passing code that's wrong in ways the test suite never notices.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here are the seven I now look for on every AI-assisted PR, and how to catch each one before it reaches production.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The breaking change to your own API
&lt;/h2&gt;

&lt;p&gt;The agent "cleans up" a response DTO — renames a field, drops one that looks unused, makes something optional. Clean diff. CI green.&lt;/p&gt;

&lt;p&gt;Except the field lived in a contract another team, a mobile app, or a partner integration depends on. The break isn't in this repo, so nothing here can see it — and the tests pass because the agent updated them in the same commit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Catch it:&lt;/strong&gt; diff your OpenAPI/schema against the version that's actually deployed, in CI, and fail on removed/renamed/narrowed fields. Humans miss a renamed key in a 600-line diff. A diff tool never does.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The N+1 query that passes every test
&lt;/h2&gt;

&lt;p&gt;The agent writes an ORM loop that looks perfectly idiomatic and fires one query per row. On your 3-row test fixture it's instant. On 50,000 production rows it melts the database.&lt;/p&gt;

&lt;p&gt;Tests run on tiny fixtures, so this is invisible until a customer with real data hits it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Catch it:&lt;/strong&gt; assert query counts in your tests (&lt;code&gt;assertNumQueries(2)&lt;/code&gt; and friends), load-test the hot paths, and turn on slow-query logging in staging.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Happy-path-only code
&lt;/h2&gt;

&lt;p&gt;Agents optimize for the example in your prompt. You showed it the success case, so it wrote the success case. Null inputs, empty lists, a timeout, a 500 from a downstream service — silently unhandled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Catch it:&lt;/strong&gt; property-based testing (throw a thousand weird inputs at it), and explicitly review for the unhappy path. "What happens when this is null/empty/slow/down?" is still your job.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The concurrency bug
&lt;/h2&gt;

&lt;p&gt;The code is correct — single-threaded. Check-then-act on a shared resource, an unguarded counter, a missing transaction. Your test suite runs serially, so it never reproduces the race.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Catch it:&lt;/strong&gt; concurrency tests that actually run in parallel, design for idempotency, and lean on database constraints as the backstop the application logic forgot.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The confidently outdated API
&lt;/h2&gt;

&lt;p&gt;The model's training data has a half-life. It'll reach for a deprecated method, a flag that was removed two versions ago, or a function signature that's &lt;em&gt;almost&lt;/em&gt; right. It compiles, it runs, and it quietly rots.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Catch it:&lt;/strong&gt; pin your dependencies, lint for deprecations, and trust the official docs over the model's confidence. "It worked" is not "it's current."&lt;/p&gt;

&lt;h2&gt;
  
  
  6. The security footgun
&lt;/h2&gt;

&lt;p&gt;String-interpolated SQL. A missing authorization check. A secret that ends up in a log line. CORS set to &lt;code&gt;*&lt;/code&gt; because that made the error go away. Your tests assert &lt;em&gt;behavior&lt;/em&gt;, not &lt;em&gt;safety&lt;/em&gt;, so all of these pass.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Catch it:&lt;/strong&gt; SAST in CI, a dedicated security-focused review pass, and a rule that an agent's "it works" is never the bar for auth or input-handling code.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. The yes-man test
&lt;/h2&gt;

&lt;p&gt;This is the subtle one, and the most dangerous.&lt;/p&gt;

&lt;p&gt;When the same agent writes the code &lt;strong&gt;and&lt;/strong&gt; the test in one pass, the test stops being an independent check. It asserts whatever the code happens to do — including the bug. You get a green checkmark and zero protection, because the test and the code were written to agree with each other.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- assertThat(json).contains("\"currency\":\"USD\"");
&lt;/span&gt;&lt;span class="gi"&gt;+ assertThat(json).doesNotContainKey("currency");   // "fixed" the test to match the change
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That test didn't catch the breaking change. It &lt;em&gt;ratified&lt;/em&gt; it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Catch it:&lt;/strong&gt; write the test from the spec &lt;strong&gt;before&lt;/strong&gt; the agent touches the implementation, and treat any test modified in the same commit as the code it covers with deep suspicion.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern underneath all seven
&lt;/h2&gt;

&lt;p&gt;Every one of these has the same root: AI made &lt;em&gt;writing&lt;/em&gt; code nearly free, but it can't see the things that live &lt;strong&gt;outside the function it's editing&lt;/strong&gt; — your other services, your production data volume, your concurrency, your consumers, your threat model.&lt;/p&gt;

&lt;p&gt;The bottleneck moved from &lt;em&gt;typing&lt;/em&gt; to &lt;em&gt;verifying&lt;/em&gt;. And a test the agent wrote for its own code isn't verification — it's the agent agreeing with itself.&lt;/p&gt;

&lt;p&gt;So the leverage now isn't a cleverer prompt. It's guardrails the agent can't fake: contract checks, query budgets, security scans, and tests written independently of the code they guard.&lt;/p&gt;

&lt;p&gt;What's the dumbest thing an AI agent has confidently shipped for you? I collect these. 👇&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>webdev</category>
      <category>testing</category>
    </item>
    <item>
      <title>The most dangerous line of code your AI agent writes is the test that passes</title>
      <dc:creator>Deepak Satyam</dc:creator>
      <pubDate>Tue, 09 Jun 2026 14:30:00 +0000</pubDate>
      <link>https://dev.to/deepaksatyam/the-most-dangerous-line-of-code-your-ai-agent-writes-is-the-test-that-passes-23ko</link>
      <guid>https://dev.to/deepaksatyam/the-most-dangerous-line-of-code-your-ai-agent-writes-is-the-test-that-passes-23ko</guid>
      <description>&lt;p&gt;I shipped a breaking change to production three weeks ago. CI was green. 142 tests passed. Code review was approved. The PR was clean enough that I barely read it.&lt;/p&gt;

&lt;p&gt;A mobile client started 500ing four hours later.&lt;/p&gt;

&lt;p&gt;Here's the part that kept me up that night: every single safety net I had was &lt;em&gt;working as designed&lt;/em&gt;. That's what scared me. The system didn't fail. The system did exactly what I told it to do, and the thing still broke.&lt;/p&gt;

&lt;p&gt;Let me walk you through how, because I think a lot of you are about to hit the same wall, if you haven't already.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually happened
&lt;/h2&gt;

&lt;p&gt;I asked an agent to "clean up the user response DTO." Reasonable. There was a field, &lt;code&gt;phoneNumber&lt;/code&gt;, that was always populated for legacy reasons but new accounts left it null. The agent did something genuinely sensible: it made the field optional and, where the value was null, dropped it from the JSON entirely instead of serializing &lt;code&gt;"phoneNumber": null&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Cleaner payload. Smaller response. Better, by most definitions of better.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- { "id": 1042, "name": "Priya", "phoneNumber": null }
&lt;/span&gt;&lt;span class="gi"&gt;+ { "id": 1042, "name": "Priya" }
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You see the problem. An older Android build did &lt;code&gt;response.phoneNumber.length&lt;/code&gt; on a screen nobody on my team had opened in a year. Field present-but-null? Fine. Field &lt;em&gt;missing&lt;/em&gt;? Crash. And you can't force-update an app that's sitting on someone's phone in a tunnel.&lt;/p&gt;

&lt;p&gt;But that's not the interesting part. Breaking changes are old news. The interesting part is &lt;em&gt;why nothing caught it.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The tests passed because the agent wrote the tests
&lt;/h2&gt;

&lt;p&gt;This is the shift nobody's pricing in yet.&lt;/p&gt;

&lt;p&gt;For twenty years, the implicit contract of a test suite was: a human wrote the assertion &lt;em&gt;as a statement of intent&lt;/em&gt;, separately from the implementation. The test and the code disagreed on purpose sometimes, and that disagreement was the whole point. The test was an independent witness.&lt;/p&gt;

&lt;p&gt;When the same agent writes the implementation and the test in the same pass, that independence is gone. The agent changed the DTO, then updated the assertion to match the DTO it just wrote:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- assertThat(json).contains("\"phoneNumber\":null");
&lt;/span&gt;&lt;span class="gi"&gt;+ assertThat(json).doesNotContainKey("phoneNumber");
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That test didn't &lt;em&gt;fail to catch&lt;/em&gt; the breaking change. It &lt;strong&gt;ratified&lt;/strong&gt; it. It went green specifically &lt;em&gt;because&lt;/em&gt; the behavior changed. Your test suite stopped being a description of what the system promises and became a description of what the system currently does. Those are not the same thing, and the gap between them is exactly where every breaking change lives.&lt;/p&gt;

&lt;p&gt;I've started calling these "yes-man tests." They agree with whatever just got written. A suite full of them gives you the emotional comfort of a green checkmark with none of the protection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why agents are structurally blind to this
&lt;/h2&gt;

&lt;p&gt;It's not that the models are dumb. It's that a breaking change is, almost by definition, invisible from inside the repo.&lt;/p&gt;

&lt;p&gt;The blast radius of &lt;code&gt;phoneNumber&lt;/code&gt; going missing isn't in my backend. It's in an Android repo I don't own, a partner's integration I've never seen, a Zapier zap some customer built in 2024, an LLM-powered agent on the &lt;em&gt;other&lt;/em&gt; side that's now parsing my API and will hallucinate around the missing field instead of erroring. The agent optimizes for what it can see: does it compile, does the test pass, does it satisfy the prompt. Compatibility is a property of the &lt;em&gt;boundary between systems&lt;/em&gt;, and the agent only ever sees one side of the boundary.&lt;/p&gt;

&lt;p&gt;This is also why "just review the PR" doesn't save you at scale. I review fine when I'm reading 40 lines I wrote. I review &lt;em&gt;terribly&lt;/em&gt; when I'm rubber-stamping 600 lines of plausible, well-formatted, test-covered code that an agent generated in nine seconds. The volume is the attack vector. We 10x'd our output and quietly 10x'd the rate of silent contract drift right along with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's actually started working for me
&lt;/h2&gt;

&lt;p&gt;I'm not anti-agent. I write more code with them than without now and I'm not going back. But I changed how I think about the safety layer:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The contract is the source of truth, not the code.&lt;/strong&gt; I freeze an OpenAPI spec (or a Pact, or even a checked-in JSON sample of every response) and diff &lt;em&gt;generated output against the frozen contract&lt;/em&gt; in CI. Not "do the tests pass" — "did the response shape change in a way that breaks a consumer." Field removed, type narrowed, required-now-optional, enum value dropped, 200→204. Those are the five that cost real money. They're also mechanically detectable, which means a machine should be guarding them, not my tired eyes at 11pm.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Treat agent-written tests as guilty until proven independent.&lt;/strong&gt; If a test changed in the same commit as the code it tests, it is not evidence of anything. I make the agent write characterization tests against the &lt;em&gt;old&lt;/em&gt; behavior first, in a separate step, before it's allowed to touch the implementation. Annoying. Works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Breaking-change detection belongs in CI, not in code review.&lt;/strong&gt; Humans are bad at spotting a missing key in a 600-line diff. Computers are perfect at it. This is the one job I will never hand back to a person.&lt;/p&gt;

&lt;p&gt;The mental model that finally clicked for me: &lt;strong&gt;the bottleneck was never writing code.&lt;/strong&gt; Agents proved that. The bottleneck is &lt;em&gt;knowing whether the thing you just wrote broke someone who isn't in the room.&lt;/em&gt; That problem got dramatically harder the moment we started generating code faster than we can possibly reason about its consequences.&lt;/p&gt;

&lt;p&gt;We're all very excited about how much code we can produce now. I'd start getting equally excited about how much of it we can &lt;em&gt;verify we didn't break.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;What's the dumbest breaking change that's slipped past &lt;em&gt;your&lt;/em&gt; CI lately? I want to know I'm not alone here.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>devops</category>
    </item>
    <item>
      <title>Your API has thousands of LLM consumers. None of them can read your changelog.</title>
      <dc:creator>Deepak Satyam</dc:creator>
      <pubDate>Thu, 04 Jun 2026 20:13:40 +0000</pubDate>
      <link>https://dev.to/deepaksatyam/your-api-has-thousands-of-llm-consumers-none-of-them-can-read-your-changelog-5c9e</link>
      <guid>https://dev.to/deepaksatyam/your-api-has-thousands-of-llm-consumers-none-of-them-can-read-your-changelog-5c9e</guid>
      <description>&lt;p&gt;Sometime in the last eighteen months, the consumer base of every public API changed, and almost nobody updated their testing strategy.&lt;/p&gt;

&lt;p&gt;If you publish an API today — even an internal one with an OpenAPI spec on a Confluence page — you have consumers you didn't onboard, didn't issue a key to, didn't notify on deprecation, and can't reach with a changelog. Those consumers are language models, and the agents people are building on top of them. Some of them learned your API last week. Some learned it eighteen months ago and got their knowledge frozen there. None of them got the email when you renamed &lt;code&gt;user_id&lt;/code&gt; to &lt;code&gt;userId&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is a real category of consumer, in production, in volume, today. And the testing discipline that was supposed to catch breaking changes — consumer-driven contract testing, the Pact-shaped world — wasn't designed for it. The mismatch is wider than most teams realize, and it's quietly producing a class of bugs that nobody is currently tracking as a bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  The frozen-consumer problem
&lt;/h2&gt;

&lt;p&gt;A traditional API consumer is a known, named, versioned codebase. Your mobile app, version 4.3.2. Your partner's billing service. The Python SDK you publish. You can enumerate them. You can test against them. When you change something, you can call the team and tell them. If you adopted Pact in the last decade you have a broker that tells you, automatically, which consumers your provider change would break — before you ship it.&lt;/p&gt;

&lt;p&gt;An LLM consumer is none of those things.&lt;/p&gt;

&lt;p&gt;It is a &lt;em&gt;population&lt;/em&gt;, not a codebase. It does not version itself in any way you can address. It has a training cutoff, after which your API documentation is frozen in its weights. It has a tool-use prompt at runtime, after which any &lt;em&gt;additional&lt;/em&gt; knowledge of your API is whatever some agent author happened to paste in. It does not pull your changelog. It will not retry against the new schema. It will, with terrifying confidence, call the old endpoint with the old fields, parse the old shape, and degrade silently.&lt;/p&gt;

&lt;p&gt;Call this the &lt;strong&gt;frozen consumer&lt;/strong&gt;: a real, behaviorally-relevant user of your API whose understanding of your schema is anchored to a moment in time you do not control, that you cannot query, and that you cannot update with a deprecation header.&lt;/p&gt;

&lt;p&gt;If your team ships an OpenAPI spec, it is in the training corpus. If your endpoints respond to documented requests, agents are calling them. KushoAI's &lt;em&gt;State of Agentic API Testing 2026&lt;/em&gt; report found that 41% of public APIs experience schema drift within 30 days of any given snapshot, and 63% within 90 days — meaning the vast majority of LLM consumers in production are calling an API that has already changed since their training data was assembled. The drift is the default state. The "synchronized consumer" was always a flattering fiction, but with human consumers you could at least notify them. You can't notify a population.&lt;/p&gt;

&lt;h2&gt;
  
  
  The new breaking-change taxonomy
&lt;/h2&gt;

&lt;p&gt;The old taxonomy of breaking changes — removed endpoints, removed required fields, changed types, narrowed enums — is still right. It's just incomplete.&lt;/p&gt;

&lt;p&gt;There are now categories of change that are &lt;strong&gt;not breaking for human-written consumers&lt;/strong&gt; but &lt;strong&gt;catastrophic for frozen consumers&lt;/strong&gt;. Three matter:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Lexical breaks
&lt;/h3&gt;

&lt;p&gt;You rename &lt;code&gt;user_id&lt;/code&gt; to &lt;code&gt;userId&lt;/code&gt;. Your changelog notes it. Your SDKs auto-regenerate. Your typed clients refuse to compile until updated. Every human consumer is either fine, or loud about not being fine.&lt;/p&gt;

&lt;p&gt;The LLM consumer is silent. It will keep generating requests with &lt;code&gt;user_id&lt;/code&gt; for as long as that token cluster wins next-token prediction, which is to say: indefinitely for many models, until the next retraining cycle. The model isn't broken. The model is doing exactly what models do — predicting the most likely token sequence based on the training distribution it learned.&lt;/p&gt;

&lt;p&gt;The same applies to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;snake_case&lt;/code&gt; ↔ &lt;code&gt;camelCase&lt;/code&gt; migrations&lt;/li&gt;
&lt;li&gt;Plural ↔ singular collection naming (&lt;code&gt;/users/{id}/order&lt;/code&gt; vs &lt;code&gt;/users/{id}/orders&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Header prefix changes (&lt;code&gt;X-Request-Id&lt;/code&gt; → &lt;code&gt;Request-Id&lt;/code&gt;, dropping the &lt;code&gt;X-&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Path versioning shifts (&lt;code&gt;/v1/items&lt;/code&gt; → &lt;code&gt;/api/items&lt;/code&gt; with content negotiation)&lt;/li&gt;
&lt;li&gt;Acronym casing (&lt;code&gt;userID&lt;/code&gt; ↔ &lt;code&gt;userId&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For human consumers and traditional contract tests these are "find-replace" changes that the typecheck catches in seconds. For frozen consumers, they're invisible cliffs. And field &lt;em&gt;additions&lt;/em&gt; are not safe either: KushoAI's data shows additions account for 86% of all observed drift events, and LLMs reliably hallucinate field names from related-domain APIs to fill perceived gaps — so an "additive" change can still produce calls with phantom fields the model believes you accept.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Semantic drift inside stable shapes
&lt;/h3&gt;

&lt;p&gt;The shape of the response is unchanged. The meaning is not.&lt;/p&gt;

&lt;p&gt;You added two new values to an existing enum: &lt;code&gt;status: "active" | "inactive"&lt;/code&gt; becomes &lt;code&gt;status: "active" | "inactive" | "trialing" | "paused"&lt;/code&gt;. Strictly additive. Your typed consumers expand their switch statements (or your TypeScript users find out at runtime — same outcome, different timing).&lt;/p&gt;

&lt;p&gt;For an LLM consumer, the new values are now &lt;em&gt;out of distribution&lt;/em&gt;. It learned a binary. It will branch on a binary. &lt;code&gt;"trialing"&lt;/code&gt; will route through the "inactive" branch with some probability and the "active" branch with the rest, depending on the agent's prompt, the model temperature, and the surrounding context. It will not raise. It will not log. It will just route some non-trivial fraction of customers to the wrong code path.&lt;/p&gt;

&lt;p&gt;The most dangerous shape of this isn't even enums. It's response codes whose semantics drift: a &lt;code&gt;422&lt;/code&gt; that used to mean "validation failed, retry never" but now also gets emitted by a new ML-backed validator that means "validation failed, retry maybe." Same code. Different meaning. Frozen consumers will keep treating it as terminal. Human consumers update their retry policy when you announce it.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Hallucinated endpoints and resurrected fields
&lt;/h3&gt;

&lt;p&gt;This is the inverse problem. Your API gets &lt;em&gt;smaller&lt;/em&gt;, and an LLM keeps calling the removed parts.&lt;/p&gt;

&lt;p&gt;Frozen consumers will confidently call endpoints you sunset two years ago. They will populate request bodies with deprecated fields that your server now rejects (in the lucky case) or silently ignores (in the worse one). They will believe in pagination tokens you stopped issuing.&lt;/p&gt;

&lt;p&gt;There's a documented failure mode for this — researchers call it &lt;em&gt;functional hallucination&lt;/em&gt;: the agent calls an endpoint that doesn't exist, or sends a string &lt;code&gt;"fifty-percent"&lt;/code&gt; to a field requiring the integer &lt;code&gt;50&lt;/code&gt;. Roughly 20% of package references in LLM-generated code are hallucinated, and 43% of those hallucinations &lt;em&gt;repeat across generations&lt;/em&gt;. These are not random one-off fabrications. They are stable confabulations the model has learned to repeat. Your removed endpoint has a half-life measured in model generations, not deploy cycles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your contract tests don't catch any of this
&lt;/h2&gt;

&lt;p&gt;Pact, the canonical consumer-driven contract testing tool, works like this: the consumer declares what it expects from the provider. The expectation is published to a broker. The provider, in CI, verifies that its current code satisfies every published expectation. If a provider change would break any registered consumer, the verification fails and the merge doesn't happen.&lt;/p&gt;

&lt;p&gt;This is a beautiful system, and I'm a fan — &lt;a href="https://dev.to/deepaksatyam/pact-is-great-its-also-why-most-teams-dont-do-contract-testing-5c14"&gt;I wrote a piece last week&lt;/a&gt; about how Pact is genuinely great and dramatically underused. But re-read the contract: the consumer &lt;em&gt;declares&lt;/em&gt; what it expects. The consumer is a participant. The contract is mutual.&lt;/p&gt;

&lt;p&gt;A frozen consumer is not a participant. It doesn't sign anything. It doesn't publish to a broker. It doesn't even know which version of your API it learned. It has, instead, a &lt;em&gt;phantom contract&lt;/em&gt;: a statistical model of your schema, derived from text someone scraped at some point, with no version, no expiration, no notification channel.&lt;/p&gt;

&lt;p&gt;You cannot run a Pact test against a phantom contract because there is no contract object to fetch. There is no canonical "what LangChain agents trained on a 2023-cutoff model expected from your &lt;code&gt;/billing&lt;/code&gt; endpoint" — there is a &lt;em&gt;distribution&lt;/em&gt; of expectations, varying by base model, fine-tune, surrounding prompt context, and which retrieval-augmented documents the agent author happened to attach.&lt;/p&gt;

&lt;p&gt;Pact assumed a population of N consumers where N is countable and addressable. Frozen consumers are a population where N is large, varies daily, and each individual "consumer" is the joint product of a base model, a prompt, and your now-stale documentation. The math of contract testing assumes a finite, named set. The new consumer base is neither.&lt;/p&gt;

&lt;p&gt;This is not Pact's fault. Pact is correct within its assumptions. The assumptions about &lt;em&gt;who your consumers are&lt;/em&gt;, on the other hand, no longer hold for any API with a public-facing spec.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to actually do
&lt;/h2&gt;

&lt;p&gt;I don't have a clean answer here, and nobody else does either yet. But three practices are starting to converge, and they're all cheap enough that you can adopt them this quarter without waiting for the discipline to mature.&lt;/p&gt;

&lt;h3&gt;
  
  
  Treat your OpenAPI spec as your AI compatibility contract
&lt;/h3&gt;

&lt;p&gt;Your OpenAPI document is no longer documentation. It is the canonical artifact that a population of frozen consumers will read once and remember forever. Descriptions matter more than they used to. Examples matter more than they used to. The names of fields matter &lt;em&gt;vastly&lt;/em&gt; more than they used to.&lt;/p&gt;

&lt;p&gt;This has consequences for how you make changes. Renaming a field for human-readability is no longer a free improvement. The cost is not "update the SDK." The cost is "every agent backed by a model trained between $LAST_CUTOFF and the next major training run will silently use the wrong name, indefinitely." That cost is real and you should price it into the change.&lt;/p&gt;

&lt;p&gt;The corollary: if you must rename, accept the old name in parallel for at least one model-generation cycle — call it 18 months — and emit it in responses too. The cost of dual-shape support is finite, bounded, and visible. The cost of a population of agents quietly calling you wrong is, depending on what your API does, anywhere from "support tickets" to "incident."&lt;/p&gt;

&lt;h3&gt;
  
  
  Test against the actual consumer
&lt;/h3&gt;

&lt;p&gt;The cheapest, highest-signal test you are not running: take your OpenAPI spec, hand it to Claude or GPT or Gemini &lt;em&gt;with no other context&lt;/em&gt;, and ask the model to construct a valid call to each endpoint. Then run the call. See what happens.&lt;/p&gt;

&lt;p&gt;This sounds dumb. It catches things contract tests cannot, because it &lt;em&gt;is&lt;/em&gt; the actual behavior your actual consumers will exhibit. If the model consistently misnames a field, misreads an enum, hallucinates a required parameter, or calls a deprecated endpoint, you have just discovered a production bug that no Pact test will ever surface.&lt;/p&gt;

&lt;p&gt;A minimal version, in pseudo-Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ai_compat_check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="n"&gt;findings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;instruction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Construct a valid request. Return JSON.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;actually_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;findings&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can run this in CI alongside your contract tests. Treat divergence as a &lt;em&gt;finding&lt;/em&gt;, not a failure — sometimes the model is doing something dumb you don't need to fix. But sometimes it's pointing at a real ambiguity in your spec, or a real frozen-consumer trap.&lt;/p&gt;

&lt;h3&gt;
  
  
  Publish a structured deprecation channel agents can actually read
&lt;/h3&gt;

&lt;p&gt;Right now your deprecation strategy is: blog post, changelog, email to known consumers. The first two don't reach frozen consumers at all. The third reaches them only if a human is in the loop to update the agent's instructions.&lt;/p&gt;

&lt;p&gt;The emerging fix is the Model Context Protocol (MCP) and similar machine-readable surfaces. An MCP server is a structured, queryable contract that an agent can pull &lt;em&gt;at runtime&lt;/em&gt;. It doesn't rely on the model's training data. It tells the agent, right now, what the current shape is.&lt;/p&gt;

&lt;p&gt;Publishing an MCP surface alongside your REST API is the closest thing to a registered-consumer relationship you can get with the LLM population. It won't reach agents that don't use MCP, but the population that does is growing fast, and being there early is cheap.&lt;/p&gt;

&lt;h2&gt;
  
  
  The category-level take
&lt;/h2&gt;

&lt;p&gt;Consumer-driven contract testing was the testing discipline of the 2010s. It assumed a knowable, addressable, code-bearing consumer. That assumption still holds for most of your traffic. It does not hold for an emerging, growing, increasingly-important slice of it.&lt;/p&gt;

&lt;p&gt;AI-consumer compatibility testing is the same problem in a fundamentally different shape. The tooling doesn't exist yet. There are no Pact-equivalents for the frozen-consumer case, because nobody has figured out yet what the broker is supposed to broker. I expect the next several years of API testing tooling to be about figuring this out, the same way the 2010s were about figuring out contracts.&lt;/p&gt;

&lt;p&gt;Until then: assume you have consumers you can't see, write your OpenAPI as though it's a contract you can't take back, and test against the models that are actually calling you.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I think and write about this kind of thing for a living — most of my time goes into &lt;a href="https://specshield.io/" rel="noopener noreferrer"&gt;SpecShield&lt;/a&gt;, where I work on API breaking-change detection. If you've shipped a "non-breaking" change that broke an AI consumer in production, I'd really like to hear it in the comments — I'm starting to collect cases.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>ai</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Pact is great. It's also why most teams don't do contract testing</title>
      <dc:creator>Deepak Satyam</dc:creator>
      <pubDate>Tue, 02 Jun 2026 19:32:04 +0000</pubDate>
      <link>https://dev.to/deepaksatyam/pact-is-great-its-also-why-most-teams-dont-do-contract-testing-5c14</link>
      <guid>https://dev.to/deepaksatyam/pact-is-great-its-also-why-most-teams-dont-do-contract-testing-5c14</guid>
      <description>&lt;p&gt;In 2014, &lt;a href="https://docs.pact.io" rel="noopener noreferrer"&gt;Pact&lt;/a&gt; created an entire engineering discipline. Consumer-driven contract testing — the idea that the API consumer, not the provider, should define what compatibility means — was a genuine breakthrough. It solved problems that integration testing couldn't, with mathematical certainty, in CI. The Pact Foundation's specification has been ported to twelve languages. &lt;a href="https://pactflow.io" rel="noopener noreferrer"&gt;PactFlow&lt;/a&gt;, the commercial parent, was acquired by SmartBear in 2023. Contract testing as a category exists because Pact exists.&lt;/p&gt;

&lt;p&gt;And yet, by every survey I can find, fewer than 5% of microservices teams actually do it.&lt;/p&gt;

&lt;p&gt;That's not a Pact failure. It's a category failure, and Pact is the closest thing to a positive proof of why.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pact thesis
&lt;/h2&gt;

&lt;p&gt;Pact's premise is simple and correct:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The consumer (e.g., a frontend) declares what it needs from a provider (e.g., a backend service): which endpoints it calls, which fields it reads from the response.&lt;/li&gt;
&lt;li&gt;The consumer publishes this contract to a shared broker.&lt;/li&gt;
&lt;li&gt;The provider, in CI, verifies that its current code satisfies every consumer's contract.&lt;/li&gt;
&lt;li&gt;If a provider PR would break a consumer, the verification fails and the PR doesn't merge.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a higher-fidelity check than integration testing (which is environment-coupled and slow) and higher-fidelity than spec-based linting (which doesn't know what the consumer actually uses). The consumer-driven angle is what makes it special. The consumer is the source of truth for "what matters." The provider can change anything else freely.&lt;/p&gt;

&lt;p&gt;When Pact works at a team, it works beautifully. I've talked to engineers at companies running 50+ services on Pact who say they haven't had a contract-related production incident in two years. That's real.&lt;/p&gt;

&lt;h2&gt;
  
  
  The adoption tax
&lt;/h2&gt;

&lt;p&gt;The reason 95% of teams don't do contract testing isn't that they don't see the value. It's the implementation tax. Five specific costs:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Every consumer team needs to adopt Pact's DSL.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Pact contracts aren't generated from existing tests. They're written using Pact's matcher DSL inside your test framework. In JavaScript it looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Pact&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;consumer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-app&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;orders-service&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addInteraction&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order 123 exists&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;uponReceiving&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a request for order 123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;withRequest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/orders/123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;willRespondWith&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;like&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order-123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;eachLike&lt;/span&gt;&lt;span class="p"&gt;({...})&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not a difficult API to learn, but every consumer team that wants to participate has to: add the Pact library to their test suite, refactor their existing integration tests into Pact interactions, learn the matcher DSL (&lt;code&gt;like&lt;/code&gt;, &lt;code&gt;eachLike&lt;/code&gt;, &lt;code&gt;term&lt;/code&gt;, &lt;code&gt;integer&lt;/code&gt;, etc.), wire up the Pact mock server, and set up CI publishing of generated contracts.&lt;/p&gt;

&lt;p&gt;For a 10-service organization, this is a quarter of work per consumer team. For a Java + Go + Node + Python polyglot organization, it's a quarter per consumer per language stack.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. You need a Pact broker.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The broker is the central service that stores published contracts and orchestrates verification. You can self-host (Pact Broker open-source) or buy PactFlow. Self-hosting means another internal service to maintain. Buying PactFlow means a per-user subscription. Either way, there's now an operational dependency that didn't exist before.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The mental model doesn't reuse existing artifacts.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most teams already write OpenAPI specs. Most teams already have integration tests. Pact's model says: "your OpenAPI is the wrong shape for consumer-driven testing, and your integration tests should be rewritten as Pact interactions." The cognitive cost of "we have to do contract testing in this third way that doesn't reuse what we have" is high.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. The provider team has to verify in CI.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Providers run &lt;code&gt;pact-broker verify&lt;/code&gt; in their CI. This pulls every consumer contract, hits the provider with each consumer's expected requests, and checks responses. Each provider needs to be runnable in a way that lets the verifier hit it (often: spin up the service in CI, plus its database, plus mock dependencies). For services with complex setups, this is non-trivial CI engineering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. The investment is N+M, the value to any one team is small until coverage is high.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you have 8 consumer teams and 3 provider teams, all 8 consumers need to adopt Pact AND all 3 providers need to set up verification. There's no incremental adoption path where one team adopting Pact gives that team meaningful value. You need the whole graph or you have nothing.&lt;/p&gt;

&lt;p&gt;This isn't Pact being badly designed. This is Pact being optimized for fidelity, and fidelity has a price tag.&lt;/p&gt;

&lt;h2&gt;
  
  
  The contract-testing chasm
&lt;/h2&gt;

&lt;p&gt;The result of the adoption tax is what I call the contract-testing chasm:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Teams that have crossed it (full Pact adoption) report it as transformative.&lt;/li&gt;
&lt;li&gt;Teams that haven't crossed it have OpenAPI specs that drift from reality, integration tests that fail in CI for unrelated reasons, and the occasional "consumer team didn't realize we changed the API" incident at 2am.&lt;/li&gt;
&lt;li&gt;Most teams, when they look at the cost of crossing, decide it isn't worth it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So we have a tool that's mathematically correct, has a passionate user base, and has been around for over a decade — and the median microservices team still has API drift incidents quarterly.&lt;/p&gt;

&lt;p&gt;The conclusion from this is not "Pact is bad." It's that the bar to entry for contract testing is too high for the bulk of teams that need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The OpenAPI-first alternative
&lt;/h2&gt;

&lt;p&gt;A different shape of contract testing has been emerging in the last few years. The idea:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Provider already publishes an OpenAPI spec (most do).&lt;/li&gt;
&lt;li&gt;Consumer publishes a SUBSET — either a hand-written OpenAPI fragment describing what they read, or (more recently) one auto-generated from observed traffic (HAR files from browser DevTools, Playwright tests, or mitmproxy in front of integration tests).&lt;/li&gt;
&lt;li&gt;A central engine compares: "does the consumer's subset fit inside the provider's full spec?"&lt;/li&gt;
&lt;li&gt;If yes, compatible. If no, the deploy is blocked.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is mathematically the same check as Pact (does the consumer's needs ⊆ provider's promise?). The difference is the inputs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pact&lt;/strong&gt;: hand-coded DSL on both sides + broker&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenAPI-first&lt;/strong&gt;: existing OpenAPI on the provider + a subset or auto-generated contract on the consumer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trade-off is fidelity vs cost. Pact has higher fidelity — the DSL captures behavioral expectations like "this response should match this regex pattern" or "this field should be present and of type X with these constraints." OpenAPI-first has lower fidelity: it captures shapes and types, not runtime behavior. But OpenAPI-first has much lower cost: no DSL to learn, no broker to maintain, language-agnostic, generated from artifacts the team already has.&lt;/p&gt;

&lt;p&gt;For the 95% of teams that don't have Pact today, "lower fidelity than Pact but actually shipped" beats "higher fidelity than Pact but never shipped."&lt;/p&gt;

&lt;h2&gt;
  
  
  What different tools do in this space
&lt;/h2&gt;

&lt;p&gt;Several products operate in the OpenAPI-first contract-testing space, and they've roughly converged on the same shape:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://specmatic.io" rel="noopener noreferrer"&gt;Specmatic&lt;/a&gt;&lt;/strong&gt; is OSS, framed as Contract-Driven Development. The closest pure alternative to Pact in spirit, with broader scope (also handles mocking + stubs).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://microcks.io" rel="noopener noreferrer"&gt;Microcks&lt;/a&gt;&lt;/strong&gt; is CNCF-incubating. Focuses on mocking AND conformance, multi-protocol (OpenAPI, AsyncAPI, gRPC, GraphQL).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://bump.sh" rel="noopener noreferrer"&gt;Bump.sh&lt;/a&gt;&lt;/strong&gt; does OpenAPI diff + docs, less on the gate side.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/oasdiff/oasdiff" rel="noopener noreferrer"&gt;oasdiff&lt;/a&gt;&lt;/strong&gt; is the OSS OpenAPI diff CLI — limited to spec-to-spec comparison, not contract testing per se, but covers the most common "did we break our consumers?" check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://specshield.io" rel="noopener noreferrer"&gt;SpecShield&lt;/a&gt;&lt;/strong&gt; is what I work on. &lt;strong&gt;Disclosure up front: I am the founder.&lt;/strong&gt; We do OpenAPI + Pact-ingest contract testing with a can-i-deploy gate and HAR auto-capture. Free CLI, paid hosted dashboard.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm not making the case that SpecShield is the best of these. I'm making the case that this &lt;em&gt;category&lt;/em&gt; exists because Pact's adoption tax is real, and several teams have independently converged on roughly the same shape for solving it. If you compare any of these tools head-to-head with Pact on fidelity, Pact wins. If you compare on adoption cost, Pact loses. The right answer depends on which of those constraints binds for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd actually recommend
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;If your team currently does no contract testing:&lt;/strong&gt; Start with simple OpenAPI diff in CI. oasdiff is free, ~10 minutes to wire up, catches the most common breaking changes. That's the minimum viable check. From there, evaluate whether you need richer contract testing (you probably do, if you have &amp;gt;5 services with cross-team dependencies).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If your team currently uses Pact:&lt;/strong&gt; Stay with Pact. The migration cost away from Pact is higher than the value of switching unless you have specific pain points (broker maintenance burden, polyglot stack friction, consumer teams refusing to adopt the DSL). Pact is genuinely good at what it does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If your team tried Pact and abandoned it:&lt;/strong&gt; That's the OpenAPI-first audience. The tools mentioned above are designed for this case.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you're a platform engineer at a 50+ engineer org:&lt;/strong&gt; look at this seriously. The cost of contract testing has come down significantly in the last 24 months. The argument for "we'd love to but it's too much work" is weaker now than it was in 2022.&lt;/p&gt;

&lt;h2&gt;
  
  
  The category insight
&lt;/h2&gt;

&lt;p&gt;Contract testing as a category has been stuck at ~5% adoption since around 2018. The blocker isn't education — engineers know about Pact. It isn't awareness — the value prop is clear. It isn't tooling quality — Pact is mature and well-maintained.&lt;/p&gt;

&lt;p&gt;The blocker is the height of the bar to entry, and the bar's height was set by Pact's fidelity choices ten years ago. The teams that have made contract testing work have been the ones willing to absorb the tax — that self-selects for organizations with: centralized platform teams that can do the adoption work for service teams, pre-existing investment in microservices maturity, or a specific past incident that justified the cost.&lt;/p&gt;

&lt;p&gt;Most teams don't meet any of those criteria but still ship microservices and still have API drift problems. They deserve a tool that meets them where they already are — with the OpenAPI they already maintain, the test runs they already do, the CI they already operate.&lt;/p&gt;

&lt;p&gt;That's the category Pact created the demand for, and the category Pact's choice of fidelity-over-adoption has left open for someone else to fill. Multiple tools are now trying. Time will tell which framing wins.&lt;/p&gt;

&lt;p&gt;In the meantime: if you're a team running microservices and you don't have any kind of contract checking in CI, that's a worth-fixing problem regardless of which tool you pick.&lt;/p&gt;

</description>
      <category>openapi</category>
      <category>api</category>
      <category>microservices</category>
      <category>testing</category>
    </item>
    <item>
      <title>Optic is dead. A 2026 migration guide for OpenAPI breaking changes</title>
      <dc:creator>Deepak Satyam</dc:creator>
      <pubDate>Sat, 23 May 2026 18:29:19 +0000</pubDate>
      <link>https://dev.to/deepaksatyam/optic-is-dead-a-2026-migration-guide-for-openapi-breaking-changes-4go2</link>
      <guid>https://dev.to/deepaksatyam/optic-is-dead-a-2026-migration-guide-for-openapi-breaking-changes-4go2</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://specshield.io/blog/optic-is-dead-migration-guide" rel="noopener noreferrer"&gt;the SpecShield blog&lt;/a&gt;. I'm a co-founder of SpecShield, one of the migration targets discussed below — full disclosure upfront. The post tries to be honest about where SpecShield fits and where other tools fit better.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Optic's GitHub repo was &lt;strong&gt;archived on January 12, 2026&lt;/strong&gt;. Last release: &lt;strong&gt;v1.0.9 in August 2025&lt;/strong&gt;. The &lt;code&gt;useoptic.com&lt;/code&gt; domain no longer resolves.&lt;/li&gt;
&lt;li&gt;Optic was acquired by &lt;strong&gt;Atlassian in April 2024&lt;/strong&gt; but was never folded into Atlassian Compass.&lt;/li&gt;
&lt;li&gt;The MIT-licensed source is still on GitHub, but &lt;strong&gt;abandoned&lt;/strong&gt; — no security patches, no new rules, no support, and your CI will eventually break when a transitive dependency goes EOL.&lt;/li&gt;
&lt;li&gt;If you used Optic primarily for &lt;strong&gt;spec-to-spec diffing and breaking-change checks in CI&lt;/strong&gt;, the most direct migration path is to &lt;strong&gt;&lt;a href="https://specshield.io" rel="noopener noreferrer"&gt;SpecShield&lt;/a&gt;&lt;/strong&gt; (Web UI + CLI + GitHub App + free GitHub Action) or &lt;strong&gt;&lt;a href="https://github.com/oasdiff/oasdiff" rel="noopener noreferrer"&gt;oasdiff&lt;/a&gt;&lt;/strong&gt; (open-source CLI, no UI).&lt;/li&gt;
&lt;li&gt;If you used Optic for &lt;strong&gt;OpenAPI generation from test traffic&lt;/strong&gt; — there is no direct replacement. You'll need to change workflow.&lt;/li&gt;
&lt;li&gt;Below: a feature-by-feature comparison table, a step-by-step migration walkthrough, and an honest list of what's hard to replace.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What actually happened to Optic
&lt;/h2&gt;

&lt;p&gt;For the last six years, Optic was the open-source default for OpenAPI breaking-change detection. It had what most developer-tool startups don't: a clean CLI, a sensible rule system, MIT licensing, deep CI integration, and a small but devoted community.&lt;/p&gt;

&lt;p&gt;Here's the timeline of how it ended:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;April 2024&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Atlassian acquires Optic Labs. Optic team joins Atlassian.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;April 2024 – August 2025&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Release cadence slows. Most commits become dependency bumps. Issues pile up unanswered.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;August 10, 2025&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Final release: &lt;strong&gt;v1.0.9&lt;/strong&gt;. No release notes hint that it's the last.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;August 2025 – January 2026&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Silence. No releases, no maintainer responses. The community asks "is Optic still maintained?" — no official answer.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;January 12, 2026&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The &lt;code&gt;opticdev/optic&lt;/code&gt; GitHub repo is archived (made read-only). The &lt;code&gt;useoptic.com&lt;/code&gt; domain stops resolving shortly after.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;There was no formal sunset announcement, no migration guide from the Optic team, no archived-but-here's-a-fork blessing. It just stopped.&lt;/p&gt;

&lt;p&gt;The expectation in the community after the Atlassian acquisition was that Optic would become part of &lt;a href="https://www.atlassian.com/software/compass" rel="noopener noreferrer"&gt;Atlassian Compass&lt;/a&gt; — Atlassian's developer-experience product. That integration never shipped. As of May 2026, Compass remains focused on service catalogs and DORA metrics, with no API drift detection capability.&lt;/p&gt;

&lt;p&gt;The 1.5k-star repo has 92 forks, but &lt;strong&gt;no fork has emerged as a community successor&lt;/strong&gt;. If you keep depending on &lt;code&gt;@useoptic/optic&lt;/code&gt; from npm, you're depending on unmaintained code that will eventually break when its TypeScript / Node / npm peer dependencies hit EOL.&lt;/p&gt;

&lt;p&gt;You need to migrate. The good news: this is a four-hour job for most teams, not a four-week project.&lt;/p&gt;




&lt;h2&gt;
  
  
  What you actually used Optic for (and what you'll need to replace)
&lt;/h2&gt;

&lt;p&gt;Optic had five distinct capabilities. Different teams used different subsets. Identify which ones applied to you before picking a replacement — choosing wrong is the single biggest migration mistake.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Spec-to-spec diffing and breaking-change detection
&lt;/h3&gt;

&lt;p&gt;The most common use case. You had &lt;code&gt;openapi.yaml&lt;/code&gt; in your repo, you ran &lt;code&gt;optic diff&lt;/code&gt; in CI on every PR, and the action commented on the PR with breaking-change warnings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Replacement difficulty:&lt;/strong&gt; Easy. Multiple drop-in alternatives exist.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. OpenAPI rule-based linting
&lt;/h3&gt;

&lt;p&gt;Optic had its own ruleset format (&lt;code&gt;.optic.dev.yml&lt;/code&gt;) for things like "all paths must be kebab-case" or "every endpoint needs a description".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Replacement difficulty:&lt;/strong&gt; Medium. You'll switch to &lt;a href="https://github.com/stoplightio/spectral" rel="noopener noreferrer"&gt;Spectral&lt;/a&gt;, which is the de-facto standard for OpenAPI linting and has a much larger ecosystem of rulesets. Your custom rules will need to be re-expressed in Spectral's format.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. OpenAPI generation from captured traffic (&lt;code&gt;optic capture&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;This was Optic's most novel feature. You'd run your test suite with the Optic proxy in front of it, and Optic would generate (or update) the OpenAPI spec from observed traffic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Replacement difficulty:&lt;/strong&gt; Hard. There is no direct open-source replacement with the same maturity. Alternatives include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://netflix.github.io/pollyjs/" rel="noopener noreferrer"&gt;Pollyjs&lt;/a&gt; (record/replay HTTP, generic)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.speakeasy.com/docs" rel="noopener noreferrer"&gt;Speakeasy&lt;/a&gt; (different workflow — spec drives SDK, not traffic drives spec)&lt;/li&gt;
&lt;li&gt;Hand-maintaining the OpenAPI spec (which is what most teams end up doing)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you relied heavily on &lt;code&gt;optic capture&lt;/code&gt;, your migration is more painful than spec-diff users. Plan accordingly.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. CI integration via GitHub Action
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;useoptic/optic-action&lt;/code&gt; GitHub Action posted PR comments with diff results.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Replacement difficulty:&lt;/strong&gt; Easy. Several drop-in alternatives exist with similar or better PR comment design.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Hosted UI for browsing diffs and history
&lt;/h3&gt;

&lt;p&gt;Optic Cloud (the SaaS layer) gave you a web UI to browse diffs across releases and share them with non-developer stakeholders.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Replacement difficulty:&lt;/strong&gt; Medium. The hosted UI tools available today either cost more (Bump.sh, Stoplight) or look less polished. SpecShield's web UI is a direct equivalent in scope and is in active development.&lt;/p&gt;




&lt;h2&gt;
  
  
  Your migration options at a glance
&lt;/h2&gt;

&lt;p&gt;Honest comparison of the realistic alternatives, as of May 2026:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What it replaces&lt;/th&gt;
&lt;th&gt;Pricing&lt;/th&gt;
&lt;th&gt;Hosted UI&lt;/th&gt;
&lt;th&gt;OSS engine&lt;/th&gt;
&lt;th&gt;PR comments&lt;/th&gt;
&lt;th&gt;Active development&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://specshield.io" rel="noopener noreferrer"&gt;SpecShield&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Optic's spec-diff, breaking-change detection, hosted UI, CI integration, BDCT (bonus)&lt;/td&gt;
&lt;td&gt;Free tier; paid from $29/mo&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;Closed core, free GitHub Action coming&lt;/td&gt;
&lt;td&gt;✅ Yes (GitHub App + Action)&lt;/td&gt;
&lt;td&gt;✅ Active&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/oasdiff/oasdiff" rel="noopener noreferrer"&gt;oasdiff&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Spec-diff and breaking-change detection (CLI only)&lt;/td&gt;
&lt;td&gt;Free / OSS (Apache 2.0)&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;✅ Yes (Go)&lt;/td&gt;
&lt;td&gt;✅ Via official GitHub Action&lt;/td&gt;
&lt;td&gt;✅ Active&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/stoplightio/spectral" rel="noopener noreferrer"&gt;Spectral&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Optic's linting (not diffing)&lt;/td&gt;
&lt;td&gt;Free / OSS (Apache 2.0)&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;✅ Yes (TS)&lt;/td&gt;
&lt;td&gt;Via custom integration&lt;/td&gt;
&lt;td&gt;Active but slower post-SmartBear acquisition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://bump.sh" rel="noopener noreferrer"&gt;Bump.sh&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hosted docs + changelog (a different workflow than Optic's CI flow)&lt;/td&gt;
&lt;td&gt;From $50/mo&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;❌ No native PR comments&lt;/td&gt;
&lt;td&gt;✅ Active&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://redocly.com/docs/cli/" rel="noopener noreferrer"&gt;Redocly CLI&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Linting + diff (basic)&lt;/td&gt;
&lt;td&gt;Free CLI; paid platform&lt;/td&gt;
&lt;td&gt;✅ Yes (paid)&lt;/td&gt;
&lt;td&gt;Partial OSS&lt;/td&gt;
&lt;td&gt;Via custom integration&lt;/td&gt;
&lt;td&gt;✅ Active&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three patterns will cover ~95% of Optic migrations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;You used Optic for spec-diff in CI + you want a UI&lt;/strong&gt; → &lt;strong&gt;SpecShield&lt;/strong&gt; (most direct replacement, smallest mental model shift)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You used Optic for spec-diff in CI + you don't need a UI&lt;/strong&gt; → &lt;strong&gt;oasdiff&lt;/strong&gt; (CLI-only, Apache 2.0, will outlive any SaaS company)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You used Optic for linting&lt;/strong&gt; → &lt;strong&gt;Spectral&lt;/strong&gt; (regardless of which diff tool you pick)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The rest of this post focuses on path #1, because it's the most common and the least documented. If you're on path #2 or #3, the official docs for &lt;a href="https://github.com/oasdiff/oasdiff" rel="noopener noreferrer"&gt;oasdiff&lt;/a&gt; and &lt;a href="https://meta.stoplight.io/docs/spectral/" rel="noopener noreferrer"&gt;Spectral&lt;/a&gt; are excellent and you don't need a migration guide.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why SpecShield is the most direct replacement for Optic users
&lt;/h2&gt;

&lt;p&gt;I built SpecShield specifically for the spec-first, CI-driven workflow Optic championed. The mental model is identical:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You commit your OpenAPI spec to git&lt;/li&gt;
&lt;li&gt;On every PR, the new spec is compared to the base branch&lt;/li&gt;
&lt;li&gt;Breaking changes are flagged before merge&lt;/li&gt;
&lt;li&gt;You get a beautiful diff report (in the UI, the CLI, &lt;em&gt;and&lt;/em&gt; as a PR comment)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What's the same as Optic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OpenAPI 3.0 and 3.1 support&lt;/li&gt;
&lt;li&gt;Multi-file spec support (with &lt;code&gt;$ref&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Breaking-change classification (removed paths, type changes, new required fields, etc.)&lt;/li&gt;
&lt;li&gt;CI/CD integration via GitHub App and (soon) free GitHub Action&lt;/li&gt;
&lt;li&gt;CLI distributed via npm (&lt;code&gt;npx specshield compare ...&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What's better than Optic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bi-Directional Contract Testing (BDCT)&lt;/strong&gt; — verify provider/consumer compatibility, with a &lt;code&gt;can-i-deploy&lt;/code&gt; gate. Optic never had this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compatibility matrix&lt;/strong&gt; for multi-service architectures&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Beautiful, screenshot-worthy PR comments&lt;/strong&gt; (Optic's were utilitarian)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Active development&lt;/strong&gt; — we ship roughly weekly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free tier&lt;/strong&gt; with no time limit (Optic Cloud's free tier was always limited)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What's different (some teams will see these as downgrades):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SpecShield is a SaaS product with a free tier. Optic was BYO-infrastructure if you stayed on the open-source CLI.&lt;/li&gt;
&lt;li&gt;The core diff engine isn't open-source today (the free GitHub Action will be, when it ships later in 2026).&lt;/li&gt;
&lt;li&gt;We're a smaller team than Optic was at its peak — though we ship faster.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're moving primarily because Optic stopped getting maintained and you want something that &lt;em&gt;will&lt;/em&gt; be maintained, those tradeoffs probably work in your favor. If you fundamentally need OSS-or-nothing, see oasdiff.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step-by-step migration: Optic → SpecShield
&lt;/h2&gt;

&lt;p&gt;Here's the actual migration for a team that was using Optic's CLI and GitHub Action. Expect this to take 30–60 minutes end-to-end, including PR review time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 0 — Audit what you're currently using
&lt;/h3&gt;

&lt;p&gt;Before deleting anything, find every place Optic is referenced in your repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Find Optic config files&lt;/span&gt;
find &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;".optic.dev.yml"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"optic.yml"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;".optic*"&lt;/span&gt;

&lt;span class="c"&gt;# Find Optic CI workflows&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rE&lt;/span&gt; &lt;span class="s2"&gt;"useoptic/optic-action|optic diff|optic lint"&lt;/span&gt; .github/

&lt;span class="c"&gt;# Find Optic CLI invocations in scripts&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rE&lt;/span&gt; &lt;span class="s2"&gt;"@useoptic/optic|optic capture|optic verify"&lt;/span&gt; package.json scripts/

&lt;span class="c"&gt;# Find references in docs&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-ri&lt;/span&gt; &lt;span class="s2"&gt;"optic"&lt;/span&gt; README.md docs/ 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make a checklist of every file you found. You'll touch each one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1 — Install the SpecShield CLI
&lt;/h3&gt;

&lt;p&gt;The SpecShield CLI is published to npm as &lt;code&gt;specshield&lt;/code&gt;. You don't need to globally install it — &lt;code&gt;npx&lt;/code&gt; is fine for CI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Test it locally — no signup needed for the diff command&lt;/span&gt;
npx specshield compare old-openapi.yaml new-openapi.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see output that includes the same categories Optic produced (breaking, non-breaking, additions), formatted as a clean terminal table.&lt;/p&gt;

&lt;p&gt;If you want JSON output for CI scripting, use the &lt;code&gt;--json&lt;/code&gt; flag (with &lt;code&gt;--output&lt;/code&gt; to also write to a file):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Print JSON to stdout AND save to a file&lt;/span&gt;
npx specshield compare old.yaml new.yaml &lt;span class="nt"&gt;--json&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; diff.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The JSON shape is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"breaking"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"additions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"modifications"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"warnings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"breakingChanges"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"additions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"modifications"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"warnings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2 — Replace your Optic GitHub workflow
&lt;/h3&gt;

&lt;p&gt;If you had something like this &lt;code&gt;.github/workflows/api-check.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# OLD — Optic workflow&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;API contract check&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;check&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;useoptic/optic-action@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;openapi.yaml&lt;/span&gt;
          &lt;span class="na"&gt;base_ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.pull_request.base.ref }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace it with this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# NEW — SpecShield CLI workflow&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;API contract check&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;check&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Extract base spec from base branch&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;git show origin/${{ github.event.pull_request.base.ref }}:openapi.yaml &amp;gt; /tmp/base.yaml&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Compare specs&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;compare&lt;/span&gt;
        &lt;span class="na"&gt;continue-on-error&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;{&lt;/span&gt;
            &lt;span class="s"&gt;echo '## 🛡️ SpecShield · API contract check'&lt;/span&gt;
            &lt;span class="s"&gt;echo ''&lt;/span&gt;
            &lt;span class="s"&gt;echo '```&lt;/span&gt;
&lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="nv"&gt;endraw %&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="s1"&gt;'&lt;/span&gt;
            &lt;span class="s"&gt;npx&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;specshield&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;compare&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;/tmp/base.yaml&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;openapi.yaml&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;--fail-on-breaking&lt;/span&gt;
            &lt;span class="s"&gt;echo&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;
&lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="nv"&gt;raw %&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;```&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;
          &lt;span class="s"&gt;}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;/tmp/comment.md&lt;/span&gt;

      &lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;name:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Comment&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;on&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;PR&lt;/span&gt;
        &lt;span class="s"&gt;if:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="s"&gt;uses:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;marocchino/sticky-pull-request-comment@v2&lt;/span&gt;
        &lt;span class="s"&gt;with:&lt;/span&gt;
          &lt;span class="s"&gt;path:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;/tmp/comment.md&lt;/span&gt;

      &lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;name:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Fail&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;build&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;if&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;breaking&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;changes&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;were&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;found&lt;/span&gt;
        &lt;span class="s"&gt;if:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;steps.compare.outcome&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;==&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'failure'&lt;/span&gt;
        &lt;span class="s"&gt;run&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;exit &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few notes on this pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The CLI auto-disables ANSI color codes when not running in a TTY, so the output inside the fenced code block renders cleanly on GitHub.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;continue-on-error: true&lt;/code&gt; on the compare step lets the PR comment post even when breaking changes are detected. The final step then fails the build explicitly so PR checks still gate the merge.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;marocchino/sticky-pull-request-comment&lt;/code&gt; updates the existing comment in place on subsequent commits rather than spamming the PR.&lt;/li&gt;
&lt;li&gt;A dedicated SpecShield Action (in development) will render a richer markdown comment with a color-coded summary table and collapsible per-endpoint diffs. Until that ships, the fenced-code-block approach above is the cleanest path.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 3 — Migrate your custom rules (if you had any)
&lt;/h3&gt;

&lt;p&gt;This is the step that takes the longest if you'd customized Optic's rule engine. Optic rules looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# OLD — .optic.dev.yml&lt;/span&gt;
&lt;span class="na"&gt;ruleset&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;breaking-changes&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;examples&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kebab-case-paths&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;operation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;in&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;any&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;rule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;after&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;must&lt;/span&gt;
      &lt;span class="na"&gt;assert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;operation.path.split('/').every(s =&amp;gt; /^[a-z0-9-]*$/.test(s))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SpecShield doesn't currently expose a Spectral-compatible custom rule API. For now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Breaking-change rules&lt;/strong&gt;: covered by SpecShield's built-in detector — no migration needed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Style/lint rules&lt;/strong&gt; (kebab-case paths, required descriptions, etc.): migrate these to a separate Spectral step in your CI:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;OpenAPI lint&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx @stoplight/spectral-cli lint openapi.yaml --ruleset .spectral.yaml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With a &lt;code&gt;.spectral.yaml&lt;/code&gt; like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;extends&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;spectral:oas&lt;/span&gt;
&lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;paths-kebab-case&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Paths should be kebab-case&lt;/span&gt;
    &lt;span class="na"&gt;given&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$.paths.*~&lt;/span&gt;
    &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;warn&lt;/span&gt;
    &lt;span class="na"&gt;then&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;function&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pattern&lt;/span&gt;
      &lt;span class="na"&gt;functionOptions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^(\/[a-z0-9-]*)+$&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We're working on first-class custom-rule support in SpecShield. Until then, the two-step approach above is the cleanest path.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4 — Remove Optic dependencies
&lt;/h3&gt;

&lt;p&gt;Once your new workflow is green:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Remove npm dep&lt;/span&gt;
npm uninstall @useoptic/optic

&lt;span class="c"&gt;# Remove config&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; .optic.dev.yml

&lt;span class="c"&gt;# Remove the old workflow (or just edit it)&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; .github/workflows/optic-check.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Commit the migration as a single PR — easier to review, easier to revert if anything's off.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5 — Set up the dashboard (optional but recommended)
&lt;/h3&gt;

&lt;p&gt;If you want history, team collaboration, BDCT, and a &lt;code&gt;can-i-deploy&lt;/code&gt; gate beyond the per-PR diff:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sign up free at &lt;a href="https://specshield.io" rel="noopener noreferrer"&gt;specshield.io&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Install the SpecShield GitHub App on your repo&lt;/li&gt;
&lt;li&gt;Your existing CLI runs will automatically appear in your dashboard's compare history&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No re-configuration needed — the GitHub App and CLI write to the same backend.&lt;/p&gt;




&lt;h2&gt;
  
  
  What SpecShield does NOT replace from Optic (being honest)
&lt;/h2&gt;

&lt;p&gt;Three Optic capabilities don't have a clean SpecShield replacement today. If you depended on these, you need a plan.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. &lt;code&gt;optic capture&lt;/code&gt; — generating specs from traffic
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;No direct replacement in SpecShield.&lt;/strong&gt; Our model assumes you maintain the spec by hand (or generate it from code via a framework annotation library). If &lt;code&gt;optic capture&lt;/code&gt; was central to your workflow, evaluate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Continuing to use Optic's archived &lt;code&gt;capture&lt;/code&gt; command standalone (it'll keep working for a while — it's not network-dependent)&lt;/li&gt;
&lt;li&gt;Replacing with &lt;a href="https://netflix.github.io/pollyjs/" rel="noopener noreferrer"&gt;Pollyjs&lt;/a&gt; for record/replay&lt;/li&gt;
&lt;li&gt;Switching to a spec-first workflow (which is where the industry has moved, for what it's worth)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;optic capture&lt;/code&gt;-style functionality is on our roadmap but isn't shipping in 2026.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The MIT-licensed core
&lt;/h3&gt;

&lt;p&gt;Optic's diff engine was MIT. SpecShield's core engine is closed-source as of May 2026. The free GitHub Action will be Apache 2.0 when it ships, but the engine behind the hosted product is proprietary.&lt;/p&gt;

&lt;p&gt;If "must be fully open-source" is a non-negotiable requirement, &lt;strong&gt;use oasdiff&lt;/strong&gt; — it's the most mature open-source option and is unaffected by Optic's shutdown.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The exact ruleset format
&lt;/h3&gt;

&lt;p&gt;Optic's rule DSL is unique. Your rules don't port 1:1 to anything. You'll re-author them in either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SpecShield's built-in rules (which cover ~80% of the common cases by default)&lt;/li&gt;
&lt;li&gt;Spectral's rule DSL (for the remaining 20% of custom style rules)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plan ~1 hour per custom rule for the rewrite.&lt;/p&gt;




&lt;h2&gt;
  
  
  When SpecShield is NOT the right migration target
&lt;/h2&gt;

&lt;p&gt;Honest recommendations for the cases where another tool is better:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Your situation&lt;/th&gt;
&lt;th&gt;Recommended migration target&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"I just want a CLI that does breaking-change detection, no SaaS"&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;oasdiff&lt;/strong&gt;. It will outlive every SaaS in this list.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"We had thousands of dollars of Optic Cloud customization and need a high-touch enterprise vendor"&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Bump.sh&lt;/strong&gt; or &lt;strong&gt;Stoplight&lt;/strong&gt; for documentation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"We need OpenAPI capture from traffic"&lt;/td&gt;
&lt;td&gt;Stay on Optic's archived CLI for now; &lt;strong&gt;Speakeasy&lt;/strong&gt; if you can shift to spec-first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"We need OpenAPI linting and nothing else"&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Spectral&lt;/strong&gt; (standalone, free)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"We want a fully self-hosted option with no cloud component"&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;oasdiff&lt;/strong&gt; + &lt;strong&gt;Spectral&lt;/strong&gt;, both run on your own infra&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"We're a microservice org with consumer/provider contracts"&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;SpecShield&lt;/strong&gt; (has BDCT built in) or &lt;strong&gt;PactFlow&lt;/strong&gt; (the dedicated Pact-broker SaaS)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The honest read on the post-Optic landscape: there's no single tool that covers everything Optic did. Most teams will end up with &lt;strong&gt;two tools&lt;/strong&gt; in their pipeline — one for diff/breaking-change detection, one for linting — where they had one before.&lt;/p&gt;




&lt;h2&gt;
  
  
  What we learned from Optic's end
&lt;/h2&gt;

&lt;p&gt;A few takeaways worth sitting with, especially if you build or pay for developer tools:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;OSS + VC is a fragile combination.&lt;/strong&gt; Optic raised funding, was acquired, and shut down within a typical VC fund lifecycle. The OSS code still exists but nobody is paid to maintain it. If a tool is mission-critical to your CI, weight its funding model and sustainability &lt;em&gt;along with&lt;/em&gt; its features.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Acquisition isn't continuity.&lt;/strong&gt; "Acquired by [BigCo]" sounds reassuring at the time. It almost never is. Plan for the acquired tool to be gone within 18 months unless its product roadmap is publicly aligned with the acquirer's stated direction.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Spec-first won.&lt;/strong&gt; Optic's bet was that you should auto-generate specs from traffic. The industry consensus has moved the other way — the spec is the source of truth, code and tests are generated from it. That shift is part of why the spec-from-capture approach didn't become indispensable.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Beauty matters.&lt;/strong&gt; Optic's PR comments were functional but plain. The tools that have taken its place (and the ones that will replace whatever replaces them) compete heavily on UI/UX. Engineers screenshot and share comments that look good. That's distribution.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Migrate today, not next quarter
&lt;/h2&gt;

&lt;p&gt;Three reasons to do this migration &lt;em&gt;now&lt;/em&gt;, not after your next release:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Optic's npm package will eventually break.&lt;/strong&gt; &lt;code&gt;@useoptic/optic&lt;/code&gt; depends on TypeScript, Node, and dozens of transitive packages, several of which will hit EOL within 12 months. When that happens, your CI will silently start failing in ways that are hard to debug because the underlying tool is unmaintained.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Your team is forgetting Optic's quirks.&lt;/strong&gt; The longer you wait, the harder the migration. The engineer who set up your Optic workflow may not be at the company in 12 months.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The migration is small if you do it now.&lt;/strong&gt; Most teams can complete the steps above in under an hour. Wait until a CI break forces it, and you'll be doing it in a panic on a Friday afternoon.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Try SpecShield in 5 minutes
&lt;/h2&gt;

&lt;p&gt;If you've read this far and want to try SpecShield as your Optic replacement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# No signup needed for the CLI diff command&lt;/span&gt;
npx specshield compare old-openapi.yaml new-openapi.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the dashboard, history, GitHub App, and BDCT: sign up free at &lt;a href="https://specshield.io/signup" rel="noopener noreferrer"&gt;specshield.io/signup&lt;/a&gt; — no credit card, generous free tier. Pricing details at &lt;a href="https://specshield.io/pricing" rel="noopener noreferrer"&gt;specshield.io/pricing&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For migration support, email me at &lt;a href="mailto:founder@specshield.io"&gt;founder@specshield.io&lt;/a&gt; — happy to help any Optic refugee migrate, gratis, regardless of whether you end up using SpecShield. The migration matters more than the tool.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Was this guide helpful? It's part of a series on the post-Optic API tooling landscape. Next post: a deep technical comparison of the breaking-change rules across SpecShield, oasdiff, and Optic's archived ruleset — including the edge cases each catches that the others miss.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Have feedback or corrections? Drop a comment below or email me directly. I'd rather get this right than get this fast.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>openapi</category>
      <category>api</category>
      <category>devops</category>
      <category>testing</category>
    </item>
    <item>
      <title>How to Detect API Breaking Changes in OpenAPI (Step-by-Step Guide)</title>
      <dc:creator>Deepak Satyam</dc:creator>
      <pubDate>Sat, 11 Apr 2026 12:14:50 +0000</pubDate>
      <link>https://dev.to/deepaksatyam/how-to-detect-api-breaking-changes-in-openapi-step-by-step-guide-28i</link>
      <guid>https://dev.to/deepaksatyam/how-to-detect-api-breaking-changes-in-openapi-step-by-step-guide-28i</guid>
      <description>&lt;p&gt;APIs are the backbone of modern applications. But one small change in your API can silently break production systems.&lt;/p&gt;

&lt;p&gt;If you’re working with microservices or public APIs, detecting breaking changes is &lt;strong&gt;not optional — it’s critical&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In this guide, you’ll learn:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What API breaking changes are&lt;/li&gt;
&lt;li&gt;Why they are dangerous&lt;/li&gt;
&lt;li&gt;How to detect them in OpenAPI/Swagger&lt;/li&gt;
&lt;li&gt;How to automate detection in CI/CD&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🔍 What is an API Breaking Change?
&lt;/h2&gt;

&lt;p&gt;An API breaking change is any modification that causes existing clients to fail.&lt;/p&gt;

&lt;h3&gt;
  
  
  Common Examples:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Removing a field from response&lt;/li&gt;
&lt;li&gt;Changing data type (e.g., &lt;code&gt;string&lt;/code&gt; → &lt;code&gt;integer&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Renaming endpoints&lt;/li&gt;
&lt;li&gt;Making optional fields required&lt;/li&gt;
&lt;li&gt;Removing query parameters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 Even a small change can break mobile apps, frontend apps, or partner integrations.&lt;/p&gt;




&lt;h2&gt;
  
  
  💥 Why Breaking Changes Are Dangerous
&lt;/h2&gt;

&lt;p&gt;Most API failures in production happen because of &lt;strong&gt;undetected contract changes&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Real-world impact:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;🚫 Frontend crashes&lt;/li&gt;
&lt;li&gt;📉 Partner integrations fail&lt;/li&gt;
&lt;li&gt;🔥 Production incidents&lt;/li&gt;
&lt;li&gt;😡 Bad user experience&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🧩 Traditional Approach (Problem)
&lt;/h2&gt;

&lt;p&gt;Most teams rely on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Manual reviews ❌&lt;/li&gt;
&lt;li&gt;Post-deployment testing ❌&lt;/li&gt;
&lt;li&gt;Consumer-driven contract tools (complex setup) ⚠️&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 These approaches are either &lt;strong&gt;slow or unreliable&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 Better Approach: OpenAPI Diff
&lt;/h2&gt;

&lt;p&gt;If you are already using OpenAPI/Swagger:&lt;/p&gt;

&lt;p&gt;👉 You can compare two API specs&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Old version (base)&lt;/li&gt;
&lt;li&gt;New version (target)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 Detect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Breaking changes&lt;/li&gt;
&lt;li&gt;Non-breaking changes&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🛠️ How to Detect API Breaking Changes (Step-by-Step)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Get Your OpenAPI Specs
&lt;/h3&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# base.yaml&lt;/span&gt;
&lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;/user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;responses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;200&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;OK&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Step 2: Modify API (Simulate Change)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# target.yaml&lt;/span&gt;
&lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;/user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;responses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;200&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;OK&lt;/span&gt;
          &lt;span class="c1"&gt;# field removed or changed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Step 3: Compare Specs
&lt;/h3&gt;

&lt;p&gt;Use a CLI tool like SpecShield:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;specshield compare base.yaml target.yaml &lt;span class="nt"&gt;--fail-on-breaking&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Step 4: Detect Issues
&lt;/h3&gt;

&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;❌ Breaking Change Detected:
- Response schema changed for /user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  ⚡ Automate in CI/CD
&lt;/h2&gt;

&lt;p&gt;You can integrate this into your pipeline:&lt;/p&gt;

&lt;h3&gt;
  
  
  Example: GitHub Actions
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Detect API Breaking Changes&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;specshield compare base.yaml target.yaml --fail-on-breaking&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👉 If breaking change detected → build fails&lt;/p&gt;




&lt;h2&gt;
  
  
  🔥 Why Use SpecShield?
&lt;/h2&gt;

&lt;p&gt;SpecShield is a CLI tool designed specifically for OpenAPI-based API validation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Benefits:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;🚀 Simple CLI (no complex setup)&lt;/li&gt;
&lt;li&gt;🔍 Detect breaking changes instantly&lt;/li&gt;
&lt;li&gt;⚡ CI/CD friendly&lt;/li&gt;
&lt;li&gt;🧩 Works with OpenAPI/Swagger&lt;/li&gt;
&lt;li&gt;🔄 Lightweight alternative to Pact&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  ⚔️ SpecShield vs Pact
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;SpecShield&lt;/th&gt;
&lt;th&gt;Pact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Setup&lt;/td&gt;
&lt;td&gt;Simple CLI&lt;/td&gt;
&lt;td&gt;Complex&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Focus&lt;/td&gt;
&lt;td&gt;OpenAPI diff&lt;/td&gt;
&lt;td&gt;Consumer contracts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speed&lt;/td&gt;
&lt;td&gt;Fast&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Use case&lt;/td&gt;
&lt;td&gt;API schema validation&lt;/td&gt;
&lt;td&gt;Consumer testing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;👉 If you use OpenAPI → SpecShield is faster and simpler&lt;/p&gt;




&lt;h2&gt;
  
  
  🎯 Best Practices
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Always version your API&lt;/li&gt;
&lt;li&gt;Never deploy without diff check&lt;/li&gt;
&lt;li&gt;Automate in CI/CD&lt;/li&gt;
&lt;li&gt;Monitor breaking changes&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🧠 Final Thoughts
&lt;/h2&gt;

&lt;p&gt;API breaking changes are one of the most common causes of production issues.&lt;/p&gt;

&lt;p&gt;👉 The solution is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use OpenAPI&lt;/li&gt;
&lt;li&gt;Compare versions&lt;/li&gt;
&lt;li&gt;Automate detection&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🚀 Get Started
&lt;/h2&gt;

&lt;p&gt;Install SpecShield:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; specshield
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run your first check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;specshield compare base.yaml target.yaml &lt;span class="nt"&gt;--fail-on-breaking&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🔗 Learn More
&lt;/h2&gt;

&lt;p&gt;Visit: &lt;a href="https://specshield.io" rel="noopener noreferrer"&gt;https://specshield.io&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  💬 Conclusion
&lt;/h2&gt;

&lt;p&gt;If you're serious about API reliability, detecting breaking changes should be part of your workflow.&lt;/p&gt;

&lt;p&gt;👉 Start small&lt;br&gt;
👉 Automate early&lt;br&gt;
👉 Prevent production failures&lt;/p&gt;




&lt;p&gt;Happy coding 🚀&lt;/p&gt;

</description>
      <category>openapi</category>
      <category>api</category>
      <category>npm</category>
      <category>specshield</category>
    </item>
  </channel>
</rss>
