<?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: Priya</title>
    <description>The latest articles on DEV Community by Priya (@priya3054).</description>
    <link>https://dev.to/priya3054</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%2F4030991%2F61564797-3410-441f-bf65-e5e1c2f7ed05.png</url>
      <title>DEV Community: Priya</title>
      <link>https://dev.to/priya3054</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/priya3054"/>
    <language>en</language>
    <item>
      <title>Contract-Testing My Own Zerodha Clone: What Specmatic Found in Code I Thought Was Done</title>
      <dc:creator>Priya</dc:creator>
      <pubDate>Wed, 15 Jul 2026 21:07:41 +0000</pubDate>
      <link>https://dev.to/priya3054/contract-testing-my-own-zerodha-clone-what-specmatic-found-in-code-i-thought-was-done-i9e</link>
      <guid>https://dev.to/priya3054/contract-testing-my-own-zerodha-clone-what-specmatic-found-in-code-i-thought-was-done-i9e</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I have a MERN-stack Zerodha clone in my portfolio — a real-time trading dashboard with a Node/Express backend, a React frontend and dashboard, MongoDB, and Razorpay for payments. It worked. It demoed well. Like most side projects, "working" mostly meant "worked the last time I clicked through signup → login → place an order → pay."&lt;/p&gt;

&lt;p&gt;During my internship at &lt;a href="https://specmatic.io/" rel="noopener noreferrer"&gt;Specmatic&lt;/a&gt;, most of my days go into working with &lt;a href="https://docs.specmatic.io/contract_driven_development/contract_testing" rel="noopener noreferrer"&gt;contract testing&lt;/a&gt; — turning OpenAPI specs into executable tests that verify an API actually honors what it promises. Starting the contract testing journey with my own project — the obvious experiment became: what happens if I point the same approach at something I'd already called 'finished'?&lt;/p&gt;

&lt;p&gt;This post is that experiment — the setup, the real bugs it surfaced, and the CI gates it left behind. Repo: &lt;a href="https://github.com/priya3054/zerodha-specmatic" rel="noopener noreferrer"&gt;priya3054/zerodha-specmatic&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The application:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backend&lt;/strong&gt;: Node.js / Express, port &lt;code&gt;3002&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend + Dashboard&lt;/strong&gt;: React&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database&lt;/strong&gt;: MongoDB&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payments&lt;/strong&gt;: Razorpay&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The contract:&lt;/strong&gt; a single OpenAPI 3.0 spec at &lt;code&gt;contracts/openapi.yaml&lt;/code&gt;, with examples — some inline, most external — under &lt;code&gt;contracts/openapi_examples/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How Specmatic fits in:&lt;/strong&gt; &lt;code&gt;specmatic.yaml&lt;/code&gt; (Config v3) points at the spec, the running backend, and the examples directory, then generates and runs test scenarios from all three:&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;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;

&lt;span class="na"&gt;systemUnderTest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;definitions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;definition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;filesystem&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;contracts&lt;/span&gt;
          &lt;span class="na"&gt;specs&lt;/span&gt;&lt;span class="pi"&gt;:&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="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;zerodhaApiSpec&lt;/span&gt;
                &lt;span class="na"&gt;path&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;runOptions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;openapi&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
        &lt;span class="na"&gt;baseUrl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8080"&lt;/span&gt;
        &lt;span class="c1"&gt;# reports routes actually registered on the real backend, so Specmatic&lt;/span&gt;
        &lt;span class="c1"&gt;# can flag "Missing In Spec" / "Not Implemented" during coverage analysis&lt;/span&gt;
        &lt;span class="na"&gt;actuatorUrl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://backend:3002/actuator/mappings&lt;/span&gt;
        &lt;span class="na"&gt;workflow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ids&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;/create-order&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;200"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;extract&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BODY.id"&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;examples&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;directories&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./contracts/openapi_examples&lt;/span&gt;

&lt;span class="na"&gt;specmatic&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;settings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;schemaResiliencyTests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;all&lt;/span&gt;
  &lt;span class="na"&gt;governance&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;successCriteria&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;enforce&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;maxMissedOperationsInSpec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
      &lt;span class="na"&gt;minCoveragePercentage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two lines carry most of the weight here. &lt;code&gt;schemaResiliencyTests: all&lt;/code&gt; tells Specmatic to stop testing only the requests I thought to write examples for, and instead generate every schema-valid &lt;em&gt;and&lt;/em&gt; schema-invalid variation itself — wrong types, boundary values, missing fields — and assert the API rejects what it should. &lt;code&gt;minCoveragePercentage: 100&lt;/code&gt; with &lt;code&gt;maxMissedOperationsInSpec: 0&lt;/code&gt; turns the contract into a gate: an endpoint the spec documents but the backend never implements (or vice versa) fails the build, not just a silent gap in a report.&lt;/p&gt;

&lt;p&gt;Everything runs through &lt;code&gt;docker-compose.yml&lt;/code&gt; on the open-source &lt;code&gt;specmatic/specmatic:2.49.1&lt;/code&gt; image — Docker is the only local dependency.&lt;/p&gt;

&lt;p&gt;From that one spec and config, Specmatic currently generates and runs &lt;strong&gt;178 tests across 20 operations, at 100% API coverage&lt;/strong&gt; — the vast majority of them schema-resiliency variations I never wrote by hand. The HTML report breaks it down per path: &lt;code&gt;/auth/signup&lt;/code&gt; alone gets 2 positive tests for its 200 and 23 generated negative tests asserting the 400 path, &lt;code&gt;/auth/login&lt;/code&gt; gets 8 tests hammering the 401 path with malformed credentials, and so on.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5aoxmd845kk18m4tycrp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5aoxmd845kk18m4tycrp.png" alt="Specmatic contract test report — 178/178 passing at 100% coverage" width="800" height="358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Specmatic actually found
&lt;/h2&gt;

&lt;p&gt;This is the part I didn't expect. I assumed running contract tests on my own, already-working project would mostly be a formality. Instead, the first real runs surfaced bugs I'd never have found by clicking through the UI:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;/newOrder&lt;/code&gt; had no input validation at all.&lt;/strong&gt; The original handler saved &lt;code&gt;req.body&lt;/code&gt; straight to MongoDB — no type checks, nothing. The contract, meanwhile, already declared &lt;code&gt;qty&lt;/code&gt; as an integer with a minimum of 1:&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;# contracts/openapi.yaml&lt;/span&gt;
&lt;span class="na"&gt;qty&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;integer&lt;/span&gt;
  &lt;span class="na"&gt;minimum&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;example&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Schema resiliency mutated &lt;code&gt;qty&lt;/code&gt; to &lt;code&gt;5.5&lt;/code&gt;, &lt;code&gt;0&lt;/code&gt;, &lt;code&gt;"5"&lt;/code&gt;, and worse, and the handler accepted every one of them. The fix that's in the backend now checks type, integer-ness, and range explicitly before anything touches the database:&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="c1"&gt;// backend/index.js&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/newOrder&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;qty&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;qty&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BUY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SELL&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mode&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid order payload&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="c1"&gt;// ...proceeds to save the order&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the paired example that pins this down as an actual test case, not just a code comment — Specmatic matches request/response examples by name and shape, so a &lt;code&gt;qty: 0&lt;/code&gt; request has to sit next to its expected &lt;code&gt;400&lt;/code&gt;:&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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;contracts/openapi_examples/placeNewOrder_application_json_&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="err"&gt;_application_json_&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;.json&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;"http-request"&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;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/newOrder"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"body"&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"INFY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"qty"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1555.45&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BUY"&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="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"http-response"&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;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"body"&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;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Invalid order payload"&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="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;p&gt;&lt;strong&gt;&lt;code&gt;/auth/login&lt;/code&gt; could crash on malformed input.&lt;/strong&gt; Non-string &lt;code&gt;username&lt;/code&gt;/&lt;code&gt;password&lt;/code&gt; values reached &lt;code&gt;passport.authenticate&lt;/code&gt; completely unguarded. Fixed with an explicit type check before authentication ever runs:&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="c1"&gt;// backend/routes/auth.js&lt;/span&gt;
&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/login&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&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;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Password or username is incorrect&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="nx"&gt;passport&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;local&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="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;})(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&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;&lt;strong&gt;&lt;code&gt;/verify-payment&lt;/code&gt; accepted invalid &lt;code&gt;amount&lt;/code&gt; values.&lt;/strong&gt; A &lt;code&gt;null&lt;/code&gt; or string amount crashed the DB update with a 500; a boolean silently coerced to &lt;code&gt;0&lt;/code&gt;/&lt;code&gt;1&lt;/code&gt; and the payment still came back "success." The handler now validates &lt;code&gt;amount&lt;/code&gt; is a real number before it ever reaches &lt;code&gt;User.findByIdAndUpdate&lt;/code&gt;:&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="c1"&gt;// backend/index.js&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isNaN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failure&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Signup wasn't idempotent.&lt;/strong&gt; Re-running the same contract test twice failed on duplicate-user creation — a problem that only shows up once you're running the same suite repeatedly, which manual testing never does.&lt;/p&gt;

&lt;p&gt;None of these came from QA. They came from &lt;code&gt;schemaResiliencyTests: all&lt;/code&gt; doing exactly what it's supposed to do: generating the malformed, unexpected, "nobody would type this by hand" requests that a real client eventually will.&lt;/p&gt;

&lt;h2&gt;
  
  
  Catching a breaking change before it merges
&lt;/h2&gt;

&lt;p&gt;The other half of the setup is the backward-compatibility gate, and I wanted to see it actually catch something rather than take it on faith. I changed &lt;code&gt;qty&lt;/code&gt; in the contract from &lt;code&gt;integer&lt;/code&gt; to &lt;code&gt;string&lt;/code&gt; on a branch called &lt;code&gt;breaking-qty-change&lt;/code&gt; — the kind of change that looks harmless in a diff and breaks every existing consumer.&lt;/p&gt;

&lt;p&gt;The check that catches it runs in CI (Gate 1), diffing the current spec against a committed baseline copy in &lt;code&gt;baseline/contracts/openapi.yaml&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;# .github/workflows/01-contract-repo-ci.yml (Backward compatibility check)&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;Backward compatibility check (Specmatic)&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;docker run --rm \&lt;/span&gt;
      &lt;span class="s"&gt;-v ${{ github.workspace }}:/usr/src/app \&lt;/span&gt;
      &lt;span class="s"&gt;--entrypoint sh \&lt;/span&gt;
      &lt;span class="s"&gt;specmatic/specmatic:2.49.1 \&lt;/span&gt;
      &lt;span class="s"&gt;-c '&lt;/span&gt;
        &lt;span class="s"&gt;mkdir -p /tmp/compat-check/contracts &amp;amp;&amp;amp; cd /tmp/compat-check&lt;/span&gt;
        &lt;span class="s"&gt;git init -q &amp;amp;&amp;amp; git checkout -q -b main&lt;/span&gt;
        &lt;span class="s"&gt;git config user.email ci@ci.local &amp;amp;&amp;amp; git config user.name CI&lt;/span&gt;
        &lt;span class="s"&gt;cp /usr/src/app/baseline/contracts/openapi.yaml contracts/openapi.yaml&lt;/span&gt;
        &lt;span class="s"&gt;git add contracts/openapi.yaml &amp;amp;&amp;amp; git commit -q -m baseline&lt;/span&gt;
        &lt;span class="s"&gt;git checkout -q -b current&lt;/span&gt;
        &lt;span class="s"&gt;cp /usr/src/app/contracts/openapi.yaml contracts/openapi.yaml&lt;/span&gt;
        &lt;span class="s"&gt;git add contracts/openapi.yaml &amp;amp;&amp;amp; git commit -q -m current&lt;/span&gt;
        &lt;span class="s"&gt;specmatic backward-compatibility-check --base-branch=main --target-path=contracts/openapi.yaml&lt;/span&gt;
      &lt;span class="s"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the breaking change in place, the check fails immediately and points at the exact line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;REQUEST.BODY.qty (contracts/openapi.yaml:208:17)
"type string in the new specification, but type number in the old specification"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And once reverted, the same check passes cleanly across every operation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Verdict for spec /tmp/compat-check/contracts/openapi.yaml:
  (COMPATIBLE) The spec is backward compatible with the corresponding spec from main

Files checked: 1 (Passed: 1, Failed: 0)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's 40 compatibility checks across 19 operations on every push — and the breaking &lt;code&gt;qty&lt;/code&gt; change never merged. This is exactly the scenario the gate exists for: not "does my backend match my spec right now," but "am I about to ship a change that breaks whoever's already calling this API."&lt;/p&gt;

&lt;h2&gt;
  
  
  Mocking Razorpay instead of hitting it
&lt;/h2&gt;

&lt;p&gt;Payments are the one dependency I didn't want anywhere near a test run. Instead of a hand-rolled mock, I used Specmatic's &lt;a href="https://docs.specmatic.io/contract_driven_development/service_virtualization" rel="noopener noreferrer"&gt;service virtualization&lt;/a&gt; to stub Razorpay directly from the contract — a &lt;code&gt;razorpay-stub&lt;/code&gt; service that comes up alongside &lt;code&gt;mongo&lt;/code&gt; and the real &lt;code&gt;backend&lt;/code&gt; in &lt;code&gt;docker-compose.yml&lt;/code&gt;. Contract tests exercise the full path — auth, order creation, payment verification — without real Razorpay credentials anywhere in CI.&lt;/p&gt;

&lt;p&gt;The same mechanism, pointed at &lt;code&gt;contracts/openapi.yaml&lt;/code&gt; instead, gives a &lt;code&gt;stub&lt;/code&gt; profile that serves a fake backend on port &lt;code&gt;9000&lt;/code&gt; — useful independent of testing, since it means dashboard work can happen against realistic responses before the real endpoint exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proving multi-step flows, not just single endpoints
&lt;/h2&gt;

&lt;p&gt;A single-endpoint contract test can't tell you whether an ID returned by one call actually gets used correctly by the next one. &lt;code&gt;specmatic.yaml&lt;/code&gt;'s &lt;code&gt;workflow.ids&lt;/code&gt; block handles exactly that for the funds flow — extracting the order ID from &lt;code&gt;POST /create-order&lt;/code&gt;'s response so it can be threaded into &lt;code&gt;POST /verify-payment&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="na"&gt;workflow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ids&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;/create-order&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;200"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;extract&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BODY.id"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This runs as its own CI step (Gate 2), separate from the coverage-gated contract run:&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;# .github/workflows/02-provider-ci.yml&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;Run workflow test (create-order -&amp;gt; verify-payment ID propagation)&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;docker compose --profile workflow up \&lt;/span&gt;
      &lt;span class="s"&gt;--build --abort-on-container-exit --exit-code-from specmatic-workflow-test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the full user journey — signup, login, place an order, pay — I modeled it as an Arazzo workflow (&lt;code&gt;ZerodhaTradeFlow.arazzo.yaml&lt;/code&gt;), built visually in Specmatic Studio and exported: &lt;strong&gt;signup → check username → login → get-me → stream live events → get holdings → get positions → place order → create order → verify payment&lt;/strong&gt;, chained in one run. Each step declares a success condition (&lt;code&gt;$statusCode == 200&lt;/code&gt;) and its outputs feed the next step's inputs — the Razorpay order ID from &lt;code&gt;createOrder&lt;/code&gt; flows into &lt;code&gt;verifyPayment&lt;/code&gt; the same way a real user session would carry it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsl0e7wqak9ss3vt3s5kw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsl0e7wqak9ss3vt3s5kw.png" alt="Arazzo workflow flowchart in Specmatic Studio — each step chained on onSuccess" width="800" height="296"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Running it end to end: &lt;strong&gt;11 steps, 11 passing, 100% workflow coverage.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fashig41zi4a9po54cnl7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fashig41zi4a9po54cnl7.png" alt="Arazzo workflow test run — 11/11 steps covered" width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Studio also turned out to be the fastest way to manage the example files themselves — it lists every path/response pair in the contract, shows which named example backs it, and can generate and validate new examples against the schema without hand-writing JSON:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk5tbq5hl6i19dw15ejma.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk5tbq5hl6i19dw15ejma.png" alt="Specmatic Studio examples view — per-endpoint examples with validate/generate" width="800" height="352"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring it into CI
&lt;/h2&gt;

&lt;p&gt;Three gates run on every push, using the direct &lt;code&gt;docker run&lt;/code&gt;/&lt;code&gt;docker compose&lt;/code&gt; form so a red CI run reproduces exactly with the same command locally:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Contract Repo CI&lt;/strong&gt; — Spectral lint, example validation, and the backward-compatibility check above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provider CI&lt;/strong&gt; — the contract test plus schema resiliency tests against the real backend, gated on 100% coverage via the &lt;code&gt;actuatorUrl&lt;/code&gt; route-reporting endpoint, followed by the workflow test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consumer CI&lt;/strong&gt; — consumer stub tests against the frontend.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuxcdhh8pmkezhyf98u5g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuxcdhh8pmkezhyf98u5g.png" alt="All three CI gates green on GitHub — Consumer, Contract Repo, and Provider" width="800" height="353"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;actuatorUrl&lt;/code&gt; piece is worth calling out on its own: Specmatic can only flag "the backend implements a route the contract doesn't document" if it has a way to ask the backend what it actually implements. I added a small &lt;code&gt;/actuator/mappings&lt;/code&gt; endpoint that reports every registered Express route in the same shape Spring Boot's actuator uses, purely so the coverage gate has something to compare the spec against:&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="c1"&gt;// backend/index.js&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/actuator&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/actuator/mappings&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dispatcherServlet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;listRegisteredRoutes&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;methods&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;predicate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`{&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;methods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt; [&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;]}`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;contexts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;application&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;mappings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;dispatcherServlets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;dispatcherServlet&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;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;Without it, &lt;code&gt;maxMissedOperationsInSpec: 0&lt;/code&gt; has nothing to check against — coverage gates are only as good as the signal you feed them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenges Faced
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Workflow ID extraction only reaches body fields, not path parameters (Specmatic v1.14.2).&lt;/strong&gt; The Zerodha API passes everything through request bodies, so the funds flow works end to end — but it's a version constraint worth knowing: check what your &lt;em&gt;pinned&lt;/em&gt; version supports before designing around a feature the latest docs describe.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Authenticating contract tests without weakening them.&lt;/strong&gt; Endpoints like &lt;code&gt;/verify-payment&lt;/code&gt; check &lt;code&gt;req.isAuthenticated()&lt;/code&gt;, but hundreds of generated tests can't each do a login round trip — and disabling auth under test would hide real bugs. The fix: a &lt;code&gt;NODE_ENV === 'test'&lt;/code&gt; middleware that accepts a fixture header for an allowlist of known test users only. Random usernames Specmatic invents still hit the real 401 path, so the negative tests stay honest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Express has no actuator, and the coverage gate needs one.&lt;/strong&gt; &lt;code&gt;maxMissedOperationsInSpec: 0&lt;/code&gt; can only flag undocumented routes if the backend reports what it implements — free in Spring Boot, absent in Express. I built a &lt;code&gt;/actuator/mappings&lt;/code&gt; shim that walks Express's router stack and mimics Spring's format. Without it, "100% coverage" would only mean "100% of what the spec happens to mention."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Stateful endpoints break when the suite runs twice.&lt;/strong&gt; The second full run failed on signup — the user from the first run already existed. 178 tests mutate state, and the suite has to survive its own side effects. Making signup idempotent under test is what made repeated CI runs reliable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Infinite streams don't fit finite HTTP assertions.&lt;/strong&gt; &lt;code&gt;/events&lt;/code&gt; is Server-Sent Events — a connection that never closes, which a contract test can't assert against. Under test, the endpoint emits one sample event and closes, keeping it documented and coverage-counted instead of carved out as an untestable exception.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benefits Achieved
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Real bugs found in code I considered done.&lt;/strong&gt; Five validation and crash bugs that months of manual testing never touched — every one found by a generated negative test, not a scenario I thought to write.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A permanent safety net against breaking changes.&lt;/strong&gt; The backward-compatibility gate has already blocked one breaking change (&lt;code&gt;qty&lt;/code&gt; number → string) from merging, with a failure message pointing at the exact file and line. Future consumers are protected on every push, with zero ongoing effort.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A CI pipeline with no secrets in it.&lt;/strong&gt; Razorpay is stubbed from its own contract, so the complete payment path runs on every push without a single real credential in GitHub Actions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The spec became a guarantee, not documentation.&lt;/strong&gt; With 100% coverage enforced and the actuator flagging undocumented routes, &lt;code&gt;contracts/openapi.yaml&lt;/code&gt; provably matches the running backend on every commit — anyone can build against it without reading the backend source.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frontend development decoupled from the backend.&lt;/strong&gt; The contract-driven stub serves realistic responses on port 9000, so dashboard work needs no MongoDB, no seed data, no running backend — switching to the real service is a one-URL change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;178 tests I didn't write and don't maintain.&lt;/strong&gt; The suite regenerates from the spec on every run. New endpoint in the contract means new tests automatically — no test files to keep in sync, no suite that rots as the API evolves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Learnings
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The happy path is a tiny fraction of your API's real surface.&lt;/strong&gt; Of the 178 tests, maybe a dozen are scenarios I could have written by hand. The rest — 23 malformed-signup variations, 8 bad-credential login probes — are requests nobody writes examples for, and exactly where all five bugs lived.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. "Accepts bad input" is worse than "crashes on bad input."&lt;/strong&gt; The login crash was the loud bug; the scary one was &lt;code&gt;/verify-payment&lt;/code&gt; silently coercing a boolean &lt;code&gt;amount&lt;/code&gt; into a "successful" payment. Crashes get noticed; silent coercion corrupts data quietly — and negative testing is the only systematic way to find the quiet kind.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. A contract that isn't executable drifts into fiction.&lt;/strong&gt; My spec already declared &lt;code&gt;qty&lt;/code&gt; as &lt;code&gt;type: integer, minimum: 1&lt;/code&gt; — the backend just never enforced it. The only spec that stays true is one where code-spec disagreement fails a build.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Testability is an application feature, not a test concern.&lt;/strong&gt; Idempotent signup, the SSE test mode, the actuator endpoint — none live in test files. They're changes to the application, made so it could be verified. Contract testing made the code more disciplined, not just more tested.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. In a payments flow, boundary validation is the whole game.&lt;/strong&gt; Every fixed bug reduces to trusting &lt;code&gt;req.body&lt;/code&gt;. The fixes were trivial — &lt;code&gt;typeof&lt;/code&gt;, &lt;code&gt;Number.isInteger&lt;/code&gt;, an enum check. The expensive part was finding out which checks were missing, and that's exactly what schema resiliency automated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Make the tool prove itself before you trust it.&lt;/strong&gt; I deliberately broke the contract on a branch to watch the compatibility gate fail, then reverted to watch it pass. A gate you've only ever seen pass might be silently checking nothing.&lt;/p&gt;

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

&lt;p&gt;I went into this expecting a formality — point the internship tooling at a portfolio project, collect a green checkmark. What I got was proof that "it works when I use it correctly" and "it's safe to expose to a client I don't control" are two very different claims — and manual testing only ever establishes the first one.&lt;/p&gt;

&lt;p&gt;The final accounting: five real bugs fixed, one breaking change blocked before merge, and &lt;strong&gt;178/178 tests passing at 100% coverage across 20 operations, an 11-step Arazzo workflow, and three green CI gates on every push&lt;/strong&gt; — with the entire suite generated from a single OpenAPI file rather than written by hand.&lt;/p&gt;

&lt;p&gt;If you're working on any project with an API surface, point this at code you already trust. The setup is a spec file, a config file, and Docker. Clone the repo, run &lt;code&gt;docker compose --profile ci up&lt;/code&gt;, and see what your own "finished" project has been hiding:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/priya3054/zerodha-specmatic" rel="noopener noreferrer"&gt;github.com/priya3054/zerodha-specmatic&lt;/a&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>specmatic</category>
      <category>webdev</category>
      <category>node</category>
    </item>
  </channel>
</rss>
