<?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: ToolboxMApp</title>
    <description>The latest articles on DEV Community by ToolboxMApp (@toolboxm_dc7e385f763e5d5).</description>
    <link>https://dev.to/toolboxm_dc7e385f763e5d5</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%2F4004796%2F5c3dee0e-3c81-4d3b-8002-efcfbaf405d7.png</url>
      <title>DEV Community: ToolboxMApp</title>
      <link>https://dev.to/toolboxm_dc7e385f763e5d5</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/toolboxm_dc7e385f763e5d5"/>
    <language>en</language>
    <item>
      <title>How to Test Retry Logic Without a Backend</title>
      <dc:creator>ToolboxMApp</dc:creator>
      <pubDate>Sun, 27 Sep 2026 10:20:28 +0000</pubDate>
      <link>https://dev.to/toolboxm_dc7e385f763e5d5/how-to-test-retry-logic-without-a-backend-bpe</link>
      <guid>https://dev.to/toolboxm_dc7e385f763e5d5/how-to-test-retry-logic-without-a-backend-bpe</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://scenariomock.com/blog/test-retry-logic-without-a-backend/" rel="noopener noreferrer"&gt;ScenarioMock blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You've written the retry logic. Exponential backoff, three attempts, then give up and show the user an error. It looks right.&lt;/p&gt;

&lt;p&gt;How do you know it works?&lt;/p&gt;

&lt;p&gt;Against a real staging backend, you'd have to break the thing on purpose. Against a mock, the endpoint cheerfully returns &lt;code&gt;200&lt;/code&gt; every single time, and the retry branch never executes. The code ships untested, and the first time it runs for real is in production, during an outage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why static mocks can't test retry logic
&lt;/h2&gt;

&lt;p&gt;A static mock maps one request shape to one response. Send the same request twice, get the same answer twice. That is exactly what makes it useless here.&lt;/p&gt;

&lt;p&gt;Retry logic isn't a property of a single request. It's a property of a &lt;em&gt;sequence&lt;/em&gt; of them. The behaviour you care about — does it back off, does it stop at the limit, does it recover cleanly when the endpoint comes back — only exists across multiple calls.&lt;/p&gt;

&lt;p&gt;You can, of course, hard-code the fixture to return &lt;code&gt;503&lt;/code&gt;. Now your retry loop runs, exhausts its attempts, and surfaces an error. That tests the failure path. What it doesn't test is the recovery path, and that's where the interesting bugs live: state that never gets cleared, a loading spinner that stays up after a successful retry, a duplicate request fired because the first attempt wasn't properly cancelled.&lt;/p&gt;

&lt;p&gt;To catch those, the endpoint has to fail and then succeed.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Sequence scenario, step by step
&lt;/h2&gt;

&lt;p&gt;A Sequence scenario in ScenarioMock keys off one thing: how many times it has been evaluated. You give it an ordered list of steps, each with a &lt;code&gt;match_count&lt;/code&gt; and a response. The first step whose count matches wins.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;match_count&lt;/code&gt; takes an exact number, or one of &lt;code&gt;&amp;gt;=N&lt;/code&gt;, &lt;code&gt;&amp;lt;=N&lt;/code&gt;, &lt;code&gt;&amp;gt;N&lt;/code&gt;, &lt;code&gt;&amp;lt;N&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So "fail twice, then succeed forever" is two steps:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;match_count&lt;/th&gt;
&lt;th&gt;Response&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;=2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;503 Service Unavailable&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;gt;=3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;200 Payment Success&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's the entire configuration. No code, no conditional branches, no fixture juggling.&lt;/p&gt;

&lt;p&gt;Each response can also carry a &lt;code&gt;delay_ms&lt;/code&gt;, which is what makes this useful for timeout testing too — a step that waits three seconds before returning &lt;code&gt;504&lt;/code&gt; will exercise your client's timeout handling in a way a fast error response never will.&lt;/p&gt;

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

&lt;p&gt;There's a public demo project you can call without signing up. Mock endpoints don't require authentication.&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="nv"&gt;HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://demo-mock--ecommerce-api.scenariomock.com

curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="nv"&gt;$HOST&lt;/span&gt;/api/payment &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"amount":9800,"currency":"JPY","card":"4242424242424242"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first two calls return &lt;code&gt;503&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="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Service temporarily unavailable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"retryAfter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"requestId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"req_f3a2b91c"&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;The third returns &lt;code&gt;200&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="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"transactionId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"txn_8f3a2b91"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;9800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"JPY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"paidAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2026-06-21T10:00:00Z"&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;Point your retry client at that URL instead of curl and you have a real test of the failure-then-recovery path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One caveat about this particular demo.&lt;/strong&gt; Its counter is set to &lt;code&gt;global&lt;/code&gt; scope and the counter clears 300 seconds after the &lt;em&gt;first&lt;/em&gt; request, so you're sharing it with everyone else reading this page. If your first call comes back &lt;code&gt;200&lt;/code&gt;, someone else already used up the two failing calls shortly before you did — wait a few minutes and start again.&lt;/p&gt;

&lt;p&gt;In your own project you'd use &lt;code&gt;per_ip&lt;/code&gt; scope instead, which gives each caller an independent counter. That's the setting you want for anything other than a public demo.&lt;/p&gt;

&lt;p&gt;There's a second Sequence endpoint on the same demo that models a flaky third-party API — first call times out after a three-second delay, second returns &lt;code&gt;500&lt;/code&gt;, third onward succeeds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="nv"&gt;$HOST&lt;/span&gt;/api/shipping/quote &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"zip":"100-0001","weight_kg":2.5}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Building the same thing in your own project
&lt;/h2&gt;

&lt;p&gt;Four steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a project. The slug becomes part of your mock's subdomain: &lt;code&gt;https://{your-domain}--{slug}.scenariomock.com&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Create an endpoint — method and path, for example &lt;code&gt;POST /api/payment&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Add a Sequence scenario. Define two steps: &lt;code&gt;&amp;lt;=2&lt;/code&gt; returning your error response, &lt;code&gt;&amp;gt;=3&lt;/code&gt; returning success.&lt;/li&gt;
&lt;li&gt;Set the scope to &lt;code&gt;per_ip&lt;/code&gt; so concurrent testers don't share a counter.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To run the sequence again, reset the counter from the endpoint screen. You can also set &lt;code&gt;reset_after_seconds&lt;/code&gt; so it clears on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two things that will trip you up
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A request that matches no scenario returns 200, not an error.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If a request matches an endpoint but no scenario produces a response, you get:&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="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"No scenario matched"&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;with status &lt;code&gt;200&lt;/code&gt;. This surprises people who expect a &lt;code&gt;404&lt;/code&gt; or a &lt;code&gt;500&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The usual cause is steps that don't cover every count. If you define &lt;code&gt;match_count: 1&lt;/code&gt; and &lt;code&gt;match_count: 2&lt;/code&gt; and nothing else, the third request falls through every step, and you get the message above instead of the success response you were expecting. Using &lt;code&gt;&amp;gt;=N&lt;/code&gt; on the final step avoids this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;reset_after_seconds&lt;/code&gt; is a window, not an idle timeout.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The timer starts on the first request and is not extended by subsequent ones. Setting it to 300 means the counter clears five minutes after the sequence began, regardless of how many calls happened in between. If you're expecting "clears after five minutes of inactivity", you'll get different behaviour than you planned.&lt;/p&gt;

&lt;p&gt;Worth knowing as well: counters are tracked per scenario, not per endpoint. Two Sequence scenarios on the same endpoint each count their own evaluations independently.&lt;/p&gt;




&lt;p&gt;The demo above is part of ScenarioMock, a hosted mock API server I built.&lt;br&gt;
Free tier is enough to try this out — &lt;a href="https://scenariomock.com" rel="noopener noreferrer"&gt;https://scenariomock.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Curious how other people handle this. Are you testing retry paths at all,&lt;br&gt;
or is that branch just shipping untested?&lt;/p&gt;

</description>
      <category>testing</category>
      <category>api</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>I built a Stateful Mock API server because existing tools only return fixed responses</title>
      <dc:creator>ToolboxMApp</dc:creator>
      <pubDate>Wed, 15 Jul 2026 07:27:26 +0000</pubDate>
      <link>https://dev.to/toolboxm_dc7e385f763e5d5/i-built-a-stateful-mock-api-server-because-existing-tools-only-return-fixed-responses-3ki1</link>
      <guid>https://dev.to/toolboxm_dc7e385f763e5d5/i-built-a-stateful-mock-api-server-because-existing-tools-only-return-fixed-responses-3ki1</guid>
      <description>&lt;h2&gt;
  
  
  The problem with existing mock tools
&lt;/h2&gt;

&lt;p&gt;Every time I needed to test an API client, I ran into the same wall.&lt;/p&gt;

&lt;p&gt;I'd set up Mockoon or WireMock, configure my endpoints, and then try to test a&lt;br&gt;
realistic flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;POST /users  &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt;: &lt;span class="s2"&gt;"Yoshi"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="c"&gt;# → 201 {"id": "abc123", "name": "Yoshi"}  ✅&lt;/span&gt;

GET /users/abc123
&lt;span class="c"&gt;# → 200 {"id": "abc123", "name": "Yoshi"}  ❌ returns the fixed mock, not the created user&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second request always returns the preconfigured static response — not the&lt;br&gt;
resource that was just created. Because that's how mock tools work: they replay&lt;br&gt;
fixed responses, they don't maintain state.&lt;/p&gt;

&lt;p&gt;To test a real CRUD flow, I had two options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write custom server code every time&lt;/li&gt;
&lt;li&gt;Stand up an actual backend&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Neither felt right for "I just want to test my API client."&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;ScenarioMock&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%2F25co0kcf8dfqhze1apao.jpg" 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%2F25co0kcf8dfqhze1apao.jpg" alt="ScenarioMock landing page: " width="800" height="852"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  What ScenarioMock does
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Stateful Mock
&lt;/h3&gt;

&lt;p&gt;Resources created via POST are stored in Redis and can be retrieved, updated,&lt;br&gt;
and deleted — just like a real database-backed API.&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;# Create&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://yourname--my-api.scenariomock.com/users &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"name": "Yoshi"}'&lt;/span&gt;
&lt;span class="c"&gt;# → {"id": "abc123", "name": "Yoshi"}&lt;/span&gt;

&lt;span class="c"&gt;# Retrieve — returns the created resource, not a fixed response&lt;/span&gt;
curl https://yourname--my-api.scenariomock.com/users/abc123
&lt;span class="c"&gt;# → {"id": "abc123", "name": "Yoshi"}&lt;/span&gt;

&lt;span class="c"&gt;# Update&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; PUT https://yourname--my-api.scenariomock.com/users/abc123 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"role": "admin"}'&lt;/span&gt;
&lt;span class="c"&gt;# → {"id": "abc123", "name": "Yoshi", "role": "admin"}&lt;/span&gt;

&lt;span class="c"&gt;# Delete&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; DELETE https://yourname--my-api.scenariomock.com/users/abc123
&lt;span class="c"&gt;# → 204&lt;/span&gt;

&lt;span class="c"&gt;# After delete → 404&lt;/span&gt;
curl https://yourname--my-api.scenariomock.com/users/abc123
&lt;span class="c"&gt;# → 404&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All of this is configured through a GUI — no code required.&lt;/p&gt;

&lt;p&gt;Each project gets its own subdomain in the form &lt;code&gt;{your-domain}--{project-slug}.scenariomock.com&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario Engine
&lt;/h3&gt;

&lt;p&gt;Three scenario types for dynamic behavior:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Static&lt;/strong&gt; — Always return the same response. The simplest case.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sequence&lt;/strong&gt; — Return different responses based on request count:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;Request #&lt;/span&gt;1 → 200 &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"ok"&lt;/span&gt;: &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="gp"&gt;Request #&lt;/span&gt;2 → 429 &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;: &lt;span class="s2"&gt;"rate limited"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="gp"&gt;Request #&lt;/span&gt;3+ → 200 &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"ok"&lt;/span&gt;: &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Useful for testing retry logic, rate limit handling, or transient error recovery.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conditional&lt;/strong&gt; — Branch based on request content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;If Authorization header is missing → 401
Otherwise → 200

If body.email matches .*@test\.com$ → 403
Otherwise → 200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Conditions can reference headers, query params, body fields (dot notation), or&lt;br&gt;
path parameters. Logic operators &lt;code&gt;all&lt;/code&gt; / &lt;code&gt;any&lt;/code&gt; / &lt;code&gt;not&lt;/code&gt; can be nested.&lt;/p&gt;
&lt;h3&gt;
  
  
  Shared URL
&lt;/h3&gt;

&lt;p&gt;Make a project public and share the URL. Anyone can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;View the endpoint list&lt;/li&gt;
&lt;li&gt;Send test requests directly in the browser&lt;/li&gt;
&lt;li&gt;See real-time request logs via WebSocket&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No account required to view or test. Perfect for sharing a mock with a client&lt;br&gt;
or a teammate who just needs to hit an endpoint.&lt;/p&gt;

&lt;p&gt;Here's a live example — a mock e-commerce API with stateful CRUD and a couple&lt;br&gt;
of scenarios wired up — you can try right now, no signup required:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://scenariomock.com/shared/demo-mock--ecommerce-api" rel="noopener noreferrer"&gt;https://scenariomock.com/shared/demo-mock--ecommerce-api&lt;/a&gt;&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%2F0ockkvkp33ozr7tcnze3.jpg" 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%2F0ockkvkp33ozr7tcnze3.jpg" alt="Endpoint list of the E-Commerce API demo project in ScenarioMock" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Technical design decisions
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Single Fastify process, hostname-based routing
&lt;/h3&gt;

&lt;p&gt;ScenarioMock runs as a single Node.js process. The host header determines&lt;br&gt;
which handler runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isAppHost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&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;hostname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;hostname&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;localhost&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;// yourname--my-api.scenariomock.com → { domain: 'yourname', slug: 'my-api' }&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;parseSubdomain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&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;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;([&lt;/span&gt;&lt;span class="sr"&gt;a-z0-9&lt;/span&gt;&lt;span class="se"&gt;][&lt;/span&gt;&lt;span class="sr"&gt;a-z0-9-&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;)\.(&lt;/span&gt;&lt;span class="sr"&gt;.+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;match&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="s1"&gt;app&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="s1"&gt;www&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;match&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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;domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--&lt;/span&gt;&lt;span class="dl"&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;domain&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&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;ul&gt;
&lt;li&gt;
&lt;code&gt;app.scenariomock.com&lt;/code&gt; → Management API (auth required)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;yourname--my-api.scenariomock.com&lt;/code&gt; → Mock handler (no auth, catch-all routes)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For local development, &lt;code&gt;*.lvh.me&lt;/code&gt; resolves to &lt;code&gt;127.0.0.1&lt;/code&gt; via a public DNS&lt;br&gt;
service, so subdomain routing works without editing &lt;code&gt;/etc/hosts&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Pluggable scenario handlers
&lt;/h3&gt;

&lt;p&gt;Each scenario type implements a &lt;code&gt;ScenarioHandler&lt;/code&gt; interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ScenarioHandler&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;unknown&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;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;configSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ZodType&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TConfig&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ScenarioContext&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="nx"&gt;StateStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;responseLoader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ResponseLoader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ScenarioResult&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// null = skip to next scenario&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The engine iterates through enabled scenarios in priority order. The first&lt;br&gt;
non-null result wins. Adding a new scenario type means implementing this&lt;br&gt;
interface — no changes to the engine itself.&lt;/p&gt;
&lt;h3&gt;
  
  
  Redis for scenario state
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;stateful_collection&lt;/code&gt; scenario stores data in Redis Hash:&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;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="s"&gt;collection:{projectId}:{stateKey}&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;Hash&lt;/span&gt;
&lt;span class="na"&gt;field&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;itemId&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;JSON string&lt;/span&gt;
&lt;span class="na"&gt;TTL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="s"&gt;configurable (default 24h)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using Redis Hash gives us O(1) HGET/HSET operations and native TTL support.&lt;br&gt;
The sequential ID counter lives alongside it:&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;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="s"&gt;collection:{projectId}:{stateKey}:seq&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;String (integer)&lt;/span&gt;
&lt;span class="na"&gt;ops&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="s"&gt;INCR&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Sequence scenarios, the request counter is:&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;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="s"&gt;seq:{endpointId}:{scenarioId}:{scopeKey}&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;String (integer)&lt;/span&gt;
&lt;span class="na"&gt;ops&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="s"&gt;INCR, EXPIRE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;scopeKey&lt;/code&gt; can be &lt;code&gt;global&lt;/code&gt;, the caller's IP, or a specific header value — so&lt;br&gt;
you can simulate per-user rate limits. Each Sequence scenario keeps its own&lt;br&gt;
counter.&lt;/p&gt;
&lt;h3&gt;
  
  
  Real-time log streaming
&lt;/h3&gt;

&lt;p&gt;Every Mock request is logged to PostgreSQL and also published to a Redis&lt;br&gt;
Pub/Sub channel:&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;channel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;project:{projectId}:logs&lt;/span&gt;
&lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;JSON (same shape as request_logs table)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The WebSocket handler subscribes and forwards messages to connected browsers.&lt;br&gt;
Authorization headers and cookies are redacted server-side before logging.&lt;/p&gt;


&lt;h2&gt;
  
  
  Bugs I hit along the way
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Zod union order matters
&lt;/h3&gt;

&lt;p&gt;I had this in the conditional scenario's predicate schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;predicateSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;union&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;equals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;   &lt;span class="c1"&gt;// ← first&lt;/span&gt;
  &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;literal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;z.unknown()&lt;/code&gt; accepts &lt;code&gt;undefined&lt;/code&gt;, so &lt;code&gt;{ missing: true }&lt;/code&gt; was parsed as&lt;br&gt;
&lt;code&gt;{ equals: undefined }&lt;/code&gt; — which &lt;code&gt;JSON.stringify&lt;/code&gt; turns into &lt;code&gt;{}&lt;/code&gt;. The evaluator&lt;br&gt;
saw an empty object, matched nothing, and the condition was always false.&lt;/p&gt;

&lt;p&gt;Fix: put the most constrained types first, &lt;code&gt;z.unknown()&lt;/code&gt; last.&lt;/p&gt;
&lt;h3&gt;
  
  
  Vite proxy + DELETE + Content-Type = 500
&lt;/h3&gt;

&lt;p&gt;My &lt;code&gt;apiRequest&lt;/code&gt; wrapper always set &lt;code&gt;Content-Type: application/json&lt;/code&gt;, even for&lt;br&gt;
requests without a body. Through Vite's dev proxy, this caused DELETE requests&lt;br&gt;
to be sent as chunked transfer encoding. Fastify saw &lt;code&gt;Content-Type: application/json&lt;/code&gt;&lt;br&gt;
and tried to parse the empty body — &lt;code&gt;SyntaxError&lt;/code&gt; → 500.&lt;/p&gt;

&lt;p&gt;Fix: only set &lt;code&gt;Content-Type&lt;/code&gt; when &lt;code&gt;options.body !== undefined&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  ZodError wasn't caught by the error handler
&lt;/h3&gt;

&lt;p&gt;Routes used &lt;code&gt;schema.parse(req.body)&lt;/code&gt; which throws &lt;code&gt;ZodError&lt;/code&gt; on invalid input.&lt;br&gt;
The global error handler only checked for &lt;code&gt;AppError&lt;/code&gt; and Fastify's built-in&lt;br&gt;
&lt;code&gt;error.validation&lt;/code&gt; — &lt;code&gt;ZodError&lt;/code&gt; fell through to the 500 handler.&lt;/p&gt;

&lt;p&gt;Fix: add &lt;code&gt;instanceof ZodError&lt;/code&gt; check before the generic 500 branch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;ZodError&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;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;code&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;send&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;validation_error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Validation failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;details&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;errors&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;h2&gt;
  
  
  Stack summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;td&gt;Node.js 20 + TypeScript strict&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP framework&lt;/td&gt;
&lt;td&gt;Fastify 5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;PostgreSQL 16 via Kysely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache / state&lt;/td&gt;
&lt;td&gt;Redis 7 via ioredis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Validation&lt;/td&gt;
&lt;td&gt;Zod&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frontend&lt;/td&gt;
&lt;td&gt;React 18 + Vite + Tailwind + shadcn/ui&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;JWT (HS256) via @fastify/jwt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payments&lt;/td&gt;
&lt;td&gt;Stripe Checkout + Webhooks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend hosting&lt;/td&gt;
&lt;td&gt;Fly.io (Singapore region)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frontend hosting&lt;/td&gt;
&lt;td&gt;Cloudflare Pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DB hosting&lt;/td&gt;
&lt;td&gt;Neon (serverless Postgres)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redis hosting&lt;/td&gt;
&lt;td&gt;Upstash (serverless Redis)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Error tracking&lt;/td&gt;
&lt;td&gt;Sentry&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Monthly cost at launch (July 2026): ~$3 (domain only — everything else was&lt;br&gt;
within free tiers).&lt;/p&gt;




&lt;h2&gt;
  
  
  Android companion app
&lt;/h2&gt;

&lt;p&gt;I also build Android apps, and I wanted a way to test ScenarioMock endpoints&lt;br&gt;
from my phone. My existing app fits perfectly:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://play.google.com/store/apps/details?id=com.toolbox.m.mobile_api_tester" rel="noopener noreferrer"&gt;API Tester – REST &amp;amp; JSON Client&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Build the mock on ScenarioMock, fire requests from the Android app. A complete&lt;br&gt;
API testing workflow without running any local server.&lt;/p&gt;




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

&lt;p&gt;&lt;strong&gt;&lt;a href="https://scenariomock.com" rel="noopener noreferrer"&gt;https://scenariomock.com&lt;/a&gt;&lt;/strong&gt; — Free plan, no credit card required.&lt;/p&gt;

&lt;p&gt;I'd love to hear what you think. Which scenario type would you use most?&lt;br&gt;
What's missing? Drop a comment below 👇&lt;/p&gt;

&lt;p&gt;If you want to see this in practice, I wrote up how to use it for&lt;br&gt;
&lt;a href="https://dev.to/toolboxm_dc7e385f763e5d5/how-to-test-retry-logic-without-a-backend-bpe"&gt;testing retry logic&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>api</category>
      <category>android</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
