<?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: Bhabishya Bhatt</title>
    <description>The latest articles on DEV Community by Bhabishya Bhatt (@bhabishya).</description>
    <link>https://dev.to/bhabishya</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3372725%2F82c30145-1676-4c3d-916b-31e2e3553aaa.jpg</url>
      <title>DEV Community: Bhabishya Bhatt</title>
      <link>https://dev.to/bhabishya</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bhabishya"/>
    <language>en</language>
    <item>
      <title>I built a CLI that roasts HTTP status codes instead of just logging them</title>
      <dc:creator>Bhabishya Bhatt</dc:creator>
      <pubDate>Sat, 18 Apr 2026 15:00:22 +0000</pubDate>
      <link>https://dev.to/bhabishya/i-built-a-cli-that-roasts-http-status-codes-instead-of-just-logging-them-2eo7</link>
      <guid>https://dev.to/bhabishya/i-built-a-cli-that-roasts-http-status-codes-instead-of-just-logging-them-2eo7</guid>
      <description>&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.amazonaws.com%2Fuploads%2Farticles%2Fzqwrjq52agqovbpf0odx.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.amazonaws.com%2Fuploads%2Farticles%2Fzqwrjq52agqovbpf0odx.png" alt="INTERNAL SERVER ERROR"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Terminal output during local dev is boring. A 500 error scrolls by as a single dim line and you barely notice it. A 429 from a rate limiter looks identical to a successful 200. Your server is quietly screaming and your log file is a wall of near-identical text.&lt;/p&gt;

&lt;p&gt;I got fed up with this over a weekend and built &lt;code&gt;roastttp&lt;/code&gt; — a tiny CLI and Express middleware that reacts to HTTP status codes with dry, dev-sarcastic one-liners and ASCII art.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx roastttp https://httpbin.org/status/500
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /api/payment  → 500 INTERNAL SERVER ERROR · 2341ms

         (   .    )   (     )
          )           (       )
             .  '   .   '  .
         (    , )       (.   )
       ). , ( .   (  ) ( , ')
      (_,) . ), ) _) _,')  (, )
      ────────────────────────────
         SOMETHING IS ON FIRE

  🔥  INTERNAL SERVER ERROR (҂◡_◡)
  "somewhere, a try/except ate an error and pretended everything was fine"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's up on npm at 20KB with zero runtime dependencies: &lt;a href="https://www.npmjs.com/package/roastttp" rel="noopener noreferrer"&gt;&lt;code&gt;npm install roastttp&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The design rabbit hole
&lt;/h2&gt;

&lt;p&gt;The first version of this in my head was "GIFs for each status code." I spent a full evening researching how to render GIFs in a terminal before realizing three things:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One&lt;/strong&gt;, terminal image protocols (iTerm2, Kitty, Sixel) render &lt;em&gt;static images&lt;/em&gt;, not animated GIFs. Even in a modern terminal you'd be showing a single frame. So why fetch a GIF at all?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two&lt;/strong&gt;, Tenor's API is being sunset by Google in September 2026. Giphy's free tier is rate-limited to 100 requests per hour and requires a "Powered by GIPHY" attribution badge, which is weird to render in a terminal. Both platforms' ToS explicitly restrict commercial use of bundled content.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three&lt;/strong&gt;, even if I solved those, I'd still be shipping &lt;em&gt;other people's content&lt;/em&gt; and hoping nobody's lawyer notices. Indie project, fragile foundation.&lt;/p&gt;

&lt;p&gt;The conclusion was obvious in retrospect: go all-in on text. ASCII art, emojis, kaomoji, and hand-written one-liners. It's universal, it's legal, it's distinctive, and honestly it's &lt;em&gt;funnier&lt;/em&gt; than a Drake reaction GIF in 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  The voice
&lt;/h2&gt;

&lt;p&gt;This was the hardest part, and the most important. My first pass had Gen Z internet voice — "giving 500 energy", "and I don't know her", "bestie." I showed it to a dev friend and she said it didn't land. It felt like a marketing team impersonating a programmer.&lt;/p&gt;

&lt;p&gt;So I rewrote everything in a &lt;strong&gt;dev-sarcastic&lt;/strong&gt; register. Dry, weary, specific to real dev pain. Examples:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;404: "check the path. check it again. it's still wrong."&lt;/p&gt;

&lt;p&gt;500: "somewhere, a try/except ate an error and pretended everything was fine"&lt;/p&gt;

&lt;p&gt;429: "your retry logic does not include backoff. fix that."&lt;/p&gt;

&lt;p&gt;503: "503: the status page has not been updated. it never is."&lt;/p&gt;

&lt;p&gt;422: "your payload parsed. your payload is still wrong."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The voice rules I codified in CONTRIBUTING.md:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Punch up at abstractions (stack traces, JIRA, YAML, cache), not at people&lt;/li&gt;
&lt;li&gt;Under 80 characters when possible&lt;/li&gt;
&lt;li&gt;No proper nouns — no company names, no public figures&lt;/li&gt;
&lt;li&gt;No religion, politics, or identity humor&lt;/li&gt;
&lt;li&gt;Dev-specific beats generic — "Redis is tired. Redis needs a minute." beats "server busy, try later."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the kind of thing you can't A/B test; you just write a hundred lines, delete eighty, and trust your ear.&lt;/p&gt;

&lt;h2&gt;
  
  
  The technical decisions
&lt;/h2&gt;

&lt;p&gt;A handful of small choices that I think made this punch above its weight:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zero runtime dependencies.&lt;/strong&gt; The whole thing is TypeScript compiling to a single &lt;code&gt;dist/&lt;/code&gt; folder. ANSI colors are done by hand (it's ~30 lines). Arg parsing is done by hand. This felt excessive at first but it means &lt;code&gt;npm install roastttp&lt;/code&gt; is under 100ms and the package is 20KB instead of a quarter megabyte.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Catalog as JSON, not code.&lt;/strong&gt; The reactions live in &lt;code&gt;src/data/reactions.json&lt;/code&gt;, not in TypeScript. This way people who want to contribute roast lines never have to touch TypeScript — they submit a one-line diff to a JSON file. Turning contribution friction to near-zero matters a lot for humor projects where the community &lt;em&gt;is&lt;/em&gt; the product.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three-tier rendering.&lt;/strong&gt; Light codes (2xx, 3xx) get a single line. Medium codes (4xx) get a small ASCII box. Heavy codes (5xx) get a full dramatic scene. This scales visual weight to how catastrophic the error actually is, which matches how developers emotionally experience them anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structural typing for the Express adapter.&lt;/strong&gt; I didn't want &lt;code&gt;@types/express&lt;/code&gt; as a dependency. So the middleware's &lt;code&gt;ReqLike&lt;/code&gt; and &lt;code&gt;ResLike&lt;/code&gt; interfaces are structural — they match both Node's raw &lt;code&gt;http.IncomingMessage&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; Express's &lt;code&gt;Request&lt;/code&gt;. Zero install cost, works in both contexts.&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;ReqLike&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;method&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;originalUrl&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;url&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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ResLike&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;finish&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;close&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&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;Strict TypeScript everywhere.&lt;/strong&gt; &lt;code&gt;noUncheckedIndexedAccess&lt;/code&gt; caught two real bugs during development: accessing &lt;code&gt;.label&lt;/code&gt; on a fallback reaction that didn't have one, and indexing into the roasts array without null-checking. Strict mode earns its keep.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Express middleware
&lt;/h2&gt;

&lt;p&gt;This is where I think roastttp gets actually useful, not just funny. One line, your existing access logs are untouched, reactions only fire on 4xx and 5xx by default:&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;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;roastttp&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;roastttp/express&lt;/span&gt;&lt;span class="dl"&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;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&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;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;roastttp&lt;/span&gt;&lt;span class="p"&gt;());&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="s1"&gt;/ok&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="nx"&gt;res&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&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="s1"&gt;/boom&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="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;500&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bad day&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;p&gt;Options for when you want to tune it:&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="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;roastttp&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;reactOn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;    &lt;span class="c1"&gt;// specific codes, or a predicate&lt;/span&gt;
  &lt;span class="na"&gt;rarity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;// 30% chance — prevents habituation in busy servers&lt;/span&gt;
  &lt;span class="na"&gt;silent2xx&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="c1"&gt;// default: don't roast successes&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;rarity&lt;/code&gt; knob surprised me with how much it helped. If every 500 roasted, I'd mentally filter them out within an hour. At 30% rarity they stay novel, which is the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm not building (yet)
&lt;/h2&gt;

&lt;p&gt;I got the ship-lean discipline from a previous project. A week of polishing pre-launch is a week of not learning what people actually want. So roastttp v0.1 shipped deliberately incomplete:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No Next.js adapter yet.&lt;/strong&gt; Next.js's logger requires monkey-patching (see &lt;code&gt;next-logger&lt;/code&gt;), which is fragile across Next versions. Waiting for demand before taking on that maintenance burden.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No sound effects.&lt;/strong&gt; Terminal beeps are jarring, break SSH/CI, and would require a native dependency. The aesthetic is quiet, dry humor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No AI-generated dynamic roasts.&lt;/strong&gt; Tempting (I've built tools that call the Claude API for dynamic text) but ships complexity on day one. The hand-written catalog is fine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No theme/meme packs.&lt;/strong&gt; Planned for v0.2 as a paid tier — "pirate mode," "shakespeare mode," "Nepali-localized mode." But v0.1 ships with the one canonical voice.&lt;/li&gt;
&lt;/ul&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# zero-install try&lt;/span&gt;
npx roastttp https://your-api.com

&lt;span class="c"&gt;# preview the full gallery&lt;/span&gt;
npx roastttp &lt;span class="nt"&gt;--preview&lt;/span&gt;

&lt;span class="c"&gt;# render a specific code without a network call&lt;/span&gt;
npx roastttp &lt;span class="nt"&gt;--code&lt;/span&gt; 418
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repo: &lt;a href="https://github.com/clashrelated/roastttp" rel="noopener noreferrer"&gt;github.com/clashrelated/roastttp&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;PRs for new roast lines are welcome — it's a one-line JSON diff, and the CONTRIBUTING guide covers the voice rules. If you ship it in your own project, tag me. I want to see the wild places this ends up.&lt;/p&gt;

</description>
      <category>node</category>
      <category>typescript</category>
      <category>cli</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
