<?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: shankar subramanian</title>
    <description>The latest articles on DEV Community by shankar subramanian (@shankar_subramanian).</description>
    <link>https://dev.to/shankar_subramanian</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%2F4048282%2Fd978f3c5-6c09-47d1-acca-ad21ee4b6005.jpeg</url>
      <title>DEV Community: shankar subramanian</title>
      <link>https://dev.to/shankar_subramanian</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shankar_subramanian"/>
    <language>en</language>
    <item>
      <title>I turned plain English (and any live website) into API tests</title>
      <dc:creator>shankar subramanian</dc:creator>
      <pubDate>Sun, 26 Jul 2026 22:39:06 +0000</pubDate>
      <link>https://dev.to/shankar_subramanian/i-turned-plain-english-and-any-live-website-into-api-tests-42a1</link>
      <guid>https://dev.to/shankar_subramanian/i-turned-plain-english-and-any-live-website-into-api-tests-42a1</guid>
      <description>&lt;p&gt;Hi 👋 This is my first post here, and I want to share a side project I built as a QA engineer — because it scratches an itch I think a lot of us have.&lt;/p&gt;

&lt;p&gt;Here's the itch: for &lt;strong&gt;one&lt;/strong&gt; business flow, I'd end up building the same thing &lt;strong&gt;three times&lt;/strong&gt; — a Postman collection, a Playwright API test, and a JMeter plan. Same requests, same variables, same assertions, just typed out three different ways. It's slow, it's boring, and the three copies drift apart the moment anything changes.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;&lt;a href="https://github.com/sshankar07/test-flow-agent" rel="noopener noreferrer"&gt;TestFlow Agent&lt;/a&gt;&lt;/strong&gt; to do it once. It works in two ways, and I want to walk through both — because they solve two pretty different situations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 1 — When it's &lt;em&gt;your&lt;/em&gt; service: just describe the flow
&lt;/h2&gt;

&lt;p&gt;The first mode is for when you have control over the API you're testing — it's your team's service, you know the endpoints, maybe it even lives in the same repo.&lt;/p&gt;

&lt;p&gt;In that case, you don't need to record anything. You just type the test in plain English:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create an enrollment for a Texas member.
Get available plans for Texas. Select a Silver plan.
Submit enrollment with effective date 01/01/2026.
Validate enrollment status is ACTIVE.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…and TestFlow Agent reads the intent — the state, the plan, the date, whether it's a happy path or a negative case — and produces the API sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST {{baseUrl}}/auth/token
POST {{baseUrl}}/members
GET  {{baseUrl}}/plans?state=TX
POST {{baseUrl}}/enrollments
GET  {{baseUrl}}/enrollments/{{enrollmentId}}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From that, it generates the Postman collection, the Playwright spec, and the JMeter plan — all sharing the same variables. And because the repo ships a small mock API, you can hit &lt;strong&gt;Run&lt;/strong&gt; and watch the flow actually execute end-to-end (enrollment created, status &lt;code&gt;ACTIVE&lt;/code&gt;, IDs chained). It's not just static text; it runs.&lt;/p&gt;

&lt;p&gt;This part is great for teams testing their &lt;em&gt;own&lt;/em&gt; services: you already know the flow, so describing it is faster than clicking through it.&lt;/p&gt;

&lt;p&gt;But it has a limit — it works best when the flow is known and the service is "yours." Which led me to the part I'm actually more excited about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 2 — When it's &lt;em&gt;any&lt;/em&gt; app: just record it
&lt;/h2&gt;

&lt;p&gt;The second mode needs zero knowledge of the API. &lt;strong&gt;You point it at any website, click through your flow by hand, and it figures out the tests for you.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You enter a URL, hit &lt;strong&gt;Start Discovery&lt;/strong&gt;, and a real browser opens. You log in and do whatever you'd normally do — I tested it against a live &lt;a href="https://opensource-demo.orangehrmlive.com" rel="noopener noreferrer"&gt;OrangeHRM&lt;/a&gt; demo — then hit &lt;strong&gt;Stop&lt;/strong&gt;. Behind the scenes it recorded all the network traffic, and now it hands you the same three artifacts (Postman / Playwright / JMeter), built from what you actually did.&lt;/p&gt;

&lt;p&gt;The catch is that raw browser traffic is &lt;em&gt;messy&lt;/em&gt;, and making it into tests that run &lt;strong&gt;again&lt;/strong&gt; is where most of the real work went. A few of the things it has to get right:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Figuring out what's a variable.&lt;/strong&gt; IDs, tokens, employee numbers — it detects them and chains them across steps, so step 4 uses the ID that step 2 created.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Naming them sensibly.&lt;/strong&gt; It names a variable from the JSON field it came from (&lt;code&gt;employee.id&lt;/code&gt; → &lt;code&gt;employeeId&lt;/code&gt;), not from the URL — because on real apps every URL says "employees" whether or not that's what the ID means.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handling login tokens.&lt;/strong&gt; Captured CSRF tokens are dead by the next run, so instead of replaying a stale one, it re-fetches a fresh token before logging in. That's the difference between a test that logs in and one that just 403s.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cutting the noise.&lt;/strong&gt; Fonts, analytics, i18n bundles, tracking pixels — all filtered out, so you're left with the actual business API calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not corrupting things.&lt;/strong&gt; A tiny one that bit me: an ID of &lt;code&gt;7&lt;/code&gt; must never get find-and-replaced inside a number like &lt;code&gt;4700&lt;/code&gt;. Short IDs are only matched on whole tokens.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don't have to think about any of that — you click through the app, and you get runnable tests. That's the part I think &lt;em&gt;anyone&lt;/em&gt; can benefit from, on &lt;em&gt;any&lt;/em&gt; app, without touching the source code.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick before/after
&lt;/h2&gt;

&lt;p&gt;Here's one real request it captured from that OrangeHRM session:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the browser sent:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;PUT /web/index.php/api/v2/performance/reviews/17/evaluation/employee
{ "ratings": [ { "kpiId": 1, "rating": null } ] }
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What it generated:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;PUT {{baseUrl}}/web/index.php/api/v2/performance/reviews/{{reviewId}}/evaluation/employee
{ "ratings": [ { "kpiId": "{{kpiId}}", "rating": null } ] }
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;reviewId&lt;/code&gt; and &lt;code&gt;kpiId&lt;/code&gt; are now variables that resolve from a generated environment file — so you import it and run, no manual filling-in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;It's open source (MIT) — Node + Express + React — with a one-command Docker start:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/sshankar07/test-flow-agent
&lt;span class="nb"&gt;cd &lt;/span&gt;test-flow-agent
docker compose up &lt;span class="nt"&gt;--build&lt;/span&gt;     &lt;span class="c"&gt;# then open http://localhost:5173&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(The live-recording part opens a real browser, so run that bit natively — the README shows both ways.)&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Repo: &lt;a href="https://github.com/sshankar07/test-flow-agent" rel="noopener noreferrer"&gt;https://github.com/sshankar07/test-flow-agent&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's it — my first dev.to post! If you're in testing/automation, I'd love to know: what would you want it to generate next — k6? RestAssured? A CI pipeline? Drop a comment 🙌&lt;/p&gt;

</description>
      <category>playwright</category>
      <category>automation</category>
      <category>ai</category>
      <category>agentaichallenge</category>
    </item>
    <item>
      <title>Clearing a false positive: my error guard matched too much</title>
      <dc:creator>shankar subramanian</dc:creator>
      <pubDate>Sun, 26 Jul 2026 19:31:12 +0000</pubDate>
      <link>https://dev.to/shankar_subramanian/clearing-a-false-positive-my-error-guard-matched-too-much-4moj</link>
      <guid>https://dev.to/shankar_subramanian/clearing-a-false-positive-my-error-guard-matched-too-much-4moj</guid>
      <description>&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;I built &lt;a href="https://github.com/sshankar07/test-flow-agent" rel="noopener noreferrer"&gt;TestFlow Agent&lt;/a&gt; — an open-source tool that turns plain-English test cases (or a live browser recording) into runnable Postman, Playwright, and JMeter tests.&lt;/p&gt;

&lt;p&gt;Its live-discovery feature launches a &lt;strong&gt;headed&lt;/strong&gt; Chromium via Playwright to record real traffic. That only works where there's a display, so when someone runs the backend inside Docker (no X server), the launch fails. To make that failure friendly, I added a guard that catches the error and returns a clear &lt;code&gt;503&lt;/code&gt; — &lt;em&gt;"live discovery needs a headed browser; run the backend natively."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Good idea. Buggy execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug Fix or Performance Improvement
&lt;/h2&gt;

&lt;p&gt;My guard decided &lt;em&gt;"this is a no-display environment"&lt;/em&gt; by string-matching the launch error message. I included &lt;strong&gt;&lt;code&gt;has been closed&lt;/code&gt;&lt;/strong&gt; in the match:&lt;/p&gt;

&lt;p&gt;The problem: Playwright throws &lt;strong&gt;"Target page, context or browser has been closed"&lt;/strong&gt; for a whole range of &lt;em&gt;unrelated&lt;/em&gt; failures (a crashed context, a closed page mid-call, a killed browser). Every one of those would now be &lt;strong&gt;misdiagnosed&lt;/strong&gt; as "you're in Docker, run natively" — pointing the user at completely the wrong problem.&lt;/p&gt;

&lt;p&gt;An AI code review (GitHub Copilot) flagged it on the PR, and it was dead right: the heuristic was too broad.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Before&lt;/strong&gt; (&lt;code&gt;backend/services/discoveryService.js&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/XServer|X server|&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="sr"&gt;DISPLAY|has been closed/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&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;error&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;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Live discovery needs a headed browser ... run natively&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;launchError&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;After:&lt;/strong&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;// Match only display/X-server markers — NOT generic Playwright phrases like&lt;/span&gt;
&lt;span class="c1"&gt;// "Target page, context or browser has been closed", which are unrelated failures.&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/XServer|X server|&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="sr"&gt;DISPLAY/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&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;error&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;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Live discovery needs a headed browser ... run natively&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;launchError&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;PR:&lt;/strong&gt; &lt;a href="https://github.com/sshankar07/test-flow-agent/pull/5" rel="noopener noreferrer"&gt;https://github.com/sshankar07/test-flow-agent/pull/5&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I verified both directions with a quick check:&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;re&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/XServer|X server|&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="sr"&gt;DISPLAY/i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;...without having a XServer running. Missing X server or $DISPLAY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// true  ✅ real no-display error still caught&lt;/span&gt;
&lt;span class="nx"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Target page, context or browser has been closed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                  &lt;span class="c1"&gt;// false ✅ unrelated error no longer misfires&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  My Improvements
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The guard now fires &lt;strong&gt;only&lt;/strong&gt; on genuine display/X-server errors — no more false "run it natively" advice when the real cause was something else entirely.&lt;/li&gt;
&lt;li&gt;Unrelated Playwright failures once again surface as themselves, instead of being swallowed by a misleading &lt;code&gt;503&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Bonus from the same review pass: the Docker builds moved from &lt;code&gt;npm install&lt;/code&gt; to &lt;code&gt;npm ci&lt;/code&gt; (reproducible installs, no accidental lockfile drift).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Best Use of Sentry
&lt;/h2&gt;

&lt;p&gt;Not used in this submission.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Use of Google AI
&lt;/h2&gt;

&lt;p&gt;Not applicable — though it's worth noting the bug was caught by an &lt;strong&gt;AI code review&lt;/strong&gt; on the pull request, which is a nice argument for keeping AI reviewers in the loop for exactly this kind of over-broad heuristic.&lt;/p&gt;

</description>
      <category>bugsmash</category>
      <category>devchallenge</category>
      <category>testing</category>
      <category>playwright</category>
    </item>
    <item>
      <title>I Dockerized my app and one button broke — thanks, localhost</title>
      <dc:creator>shankar subramanian</dc:creator>
      <pubDate>Sun, 26 Jul 2026 19:30:40 +0000</pubDate>
      <link>https://dev.to/shankar_subramanian/i-dockerized-my-app-and-one-button-broke-thanks-localhost-2e0p</link>
      <guid>https://dev.to/shankar_subramanian/i-dockerized-my-app-and-one-button-broke-thanks-localhost-2e0p</guid>
      <description>&lt;p&gt;Every developer has met this bug. I just met it again in the most on-brand way possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;My side project, &lt;a href="https://github.com/sshankar07/test-flow-agent" rel="noopener noreferrer"&gt;TestFlow Agent&lt;/a&gt;, is three little services:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;strong&gt;mock API&lt;/strong&gt; (the thing under test),&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;backend&lt;/strong&gt; that generates and &lt;em&gt;runs&lt;/em&gt; test flows,&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;React frontend&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One of the headline features is a &lt;strong&gt;"Run Enrollment Flow"&lt;/strong&gt; button: the backend fires a real sequence of requests at the mock API and reports back "enrollment created, status ACTIVE." Locally, it worked every single time.&lt;/p&gt;

&lt;p&gt;Then I added a one-command Docker setup so anyone could try it without juggling three terminals. &lt;code&gt;docker compose up --build&lt;/code&gt;, open the app, click around… analyze works, generate works, and then — &lt;strong&gt;"Run Enrollment Flow" fails.&lt;/strong&gt; Only that button. The one that talks between services.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hunt
&lt;/h2&gt;

&lt;p&gt;The frontend calls the backend fine (both ports are published to the host). Generation is pure computation — fine. The only thing different about "Run Flow" is that &lt;strong&gt;the backend itself makes an outbound HTTP call&lt;/strong&gt; to the mock API.&lt;/p&gt;

&lt;p&gt;I opened the backend and there it was:&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;baseUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:4000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// 👈&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Natively, &lt;code&gt;localhost:4000&lt;/code&gt; &lt;em&gt;is&lt;/em&gt; the mock API — same machine. But inside the backend &lt;strong&gt;container&lt;/strong&gt;, &lt;code&gt;localhost&lt;/code&gt; means &lt;strong&gt;the container itself&lt;/strong&gt;. There's no mock API on port 4000 in there. The backend was cheerfully calling itself and getting nothing.&lt;/p&gt;

&lt;p&gt;Classic "works on my machine," except the machine changed out from under it the moment it went into a container.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;In Docker Compose, services reach each other by &lt;strong&gt;service name&lt;/strong&gt;, not &lt;code&gt;localhost&lt;/code&gt;. So I made the target configurable and pointed it at the service name inside the compose network — while keeping &lt;code&gt;localhost&lt;/code&gt; as the default so native runs don't change at all:&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/analysisRoutes.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;baseUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MOCK_API_URL&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:4000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.yml&lt;/span&gt;
&lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;MOCK_API_URL=http://mock-api:4000&lt;/span&gt;   &lt;span class="c1"&gt;# reach the mock by its service name&lt;/span&gt;
  &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mock-api&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🔗 &lt;strong&gt;PR:&lt;/strong&gt; &lt;a href="https://github.com/sshankar07/test-flow-agent/pull/3" rel="noopener noreferrer"&gt;https://github.com/sshankar07/test-flow-agent/pull/3&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Before / after
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Before:&lt;/strong&gt; &lt;code&gt;Run Flow&lt;/code&gt; in Docker → backend calls &lt;code&gt;localhost:4000&lt;/code&gt; → nothing there → failure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;After:&lt;/strong&gt; &lt;code&gt;Run Flow&lt;/code&gt; in Docker → backend calls &lt;code&gt;mock-api:4000&lt;/code&gt; → cross-container SUCCESS. I watched it create an enrollment (&lt;code&gt;ENR-4C554E55&lt;/code&gt;, status &lt;code&gt;ACTIVE&lt;/code&gt;) from inside the compose stack.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I took away
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hardcoded &lt;code&gt;localhost&lt;/code&gt; is a time bomb the day you containerize.&lt;/strong&gt; It's invisible until the network boundary moves.&lt;/li&gt;
&lt;li&gt;The fix isn't "hardcode the service name" either — it's &lt;strong&gt;make it configurable with a sane default&lt;/strong&gt;, so both native and Docker runs work with zero edits.&lt;/li&gt;
&lt;li&gt;Container-to-container = &lt;strong&gt;service-name DNS&lt;/strong&gt;; host-to-container = &lt;strong&gt;published ports&lt;/strong&gt;. Mixing them up is the root of a huge share of "it works locally but not in Docker" bugs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Small diff, satisfying fix, and a reminder that the oldest bugs are the ones that keep showing up in new outfits.&lt;/p&gt;

&lt;p&gt;Repo (open source, MIT): &lt;strong&gt;&lt;a href="https://github.com/sshankar07/test-flow-agent" rel="noopener noreferrer"&gt;https://github.com/sshankar07/test-flow-agent&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>bugsmash</category>
      <category>devchallenge</category>
      <category>docker</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
