
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.
I got fed up with this over a weekend and built roastttp — a tiny CLI and Express middleware that reacts to HTTP status codes with dry, dev-sarcastic one-liners and ASCII art.
npx roastttp https://httpbin.org/status/500
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"
It's up on npm at 20KB with zero runtime dependencies: npm install roastttp.
The design rabbit hole
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:
One, terminal image protocols (iTerm2, Kitty, Sixel) render static images, not animated GIFs. Even in a modern terminal you'd be showing a single frame. So why fetch a GIF at all?
Two, 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.
Three, even if I solved those, I'd still be shipping other people's content and hoping nobody's lawyer notices. Indie project, fragile foundation.
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 funnier than a Drake reaction GIF in 2026.
The voice
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.
So I rewrote everything in a dev-sarcastic register. Dry, weary, specific to real dev pain. Examples:
404: "check the path. check it again. it's still wrong."
500: "somewhere, a try/except ate an error and pretended everything was fine"
429: "your retry logic does not include backoff. fix that."
503: "503: the status page has not been updated. it never is."
422: "your payload parsed. your payload is still wrong."
The voice rules I codified in CONTRIBUTING.md:
- Punch up at abstractions (stack traces, JIRA, YAML, cache), not at people
- Under 80 characters when possible
- No proper nouns — no company names, no public figures
- No religion, politics, or identity humor
- Dev-specific beats generic — "Redis is tired. Redis needs a minute." beats "server busy, try later."
This is the kind of thing you can't A/B test; you just write a hundred lines, delete eighty, and trust your ear.
The technical decisions
A handful of small choices that I think made this punch above its weight:
Zero runtime dependencies. The whole thing is TypeScript compiling to a single dist/ 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 npm install roastttp is under 100ms and the package is 20KB instead of a quarter megabyte.
Catalog as JSON, not code. The reactions live in src/data/reactions.json, 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 is the product.
Three-tier rendering. 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.
Structural typing for the Express adapter. I didn't want @types/express as a dependency. So the middleware's ReqLike and ResLike interfaces are structural — they match both Node's raw http.IncomingMessage and Express's Request. Zero install cost, works in both contexts.
interface ReqLike {
method?: string;
originalUrl?: string;
url?: string;
}
interface ResLike {
statusCode: number;
on(event: 'finish' | 'close', cb: () => void): void;
}
Strict TypeScript everywhere. noUncheckedIndexedAccess caught two real bugs during development: accessing .label on a fallback reaction that didn't have one, and indexing into the roasts array without null-checking. Strict mode earns its keep.
The Express middleware
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:
import express from 'express';
import { roastttp } from 'roastttp/express';
const app = express();
app.use(roastttp());
app.get('/ok', (_req, res) => res.send('ok'));
app.get('/boom', (_req, res) => res.status(500).send('bad day'));
Options for when you want to tune it:
app.use(roastttp({
reactOn: [404, 500], // specific codes, or a predicate
rarity: 0.3, // 30% chance — prevents habituation in busy servers
silent2xx: true, // default: don't roast successes
}));
The rarity 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.
What I'm not building (yet)
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:
-
No Next.js adapter yet. Next.js's logger requires monkey-patching (see
next-logger), which is fragile across Next versions. Waiting for demand before taking on that maintenance burden. - No sound effects. Terminal beeps are jarring, break SSH/CI, and would require a native dependency. The aesthetic is quiet, dry humor.
- No AI-generated dynamic roasts. 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.
- No theme/meme packs. Planned for v0.2 as a paid tier — "pirate mode," "shakespeare mode," "Nepali-localized mode." But v0.1 ships with the one canonical voice.
Try it
# zero-install try
npx roastttp https://your-api.com
# preview the full gallery
npx roastttp --preview
# render a specific code without a network call
npx roastttp --code 418
Repo: github.com/clashrelated/roastttp
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.
United States
NORTH AMERICA
Related News
UCP Variant Data: The #1 Reason Agent Checkouts Fail
7h ago
Amazon Employees Are 'Tokenmaxxing' Due To Pressure To Use AI Tools
21h ago
How Braze’s CTO is rethinking engineering for the agentic area
10h ago

Décryptage technique : Comment builder un téléchargeur de vidéos Reddit performant (DASH, HLS & WebAssembly)
17h ago
How AI Reduced Manual Driver Verification by 75% — Operations Case Study. Part 2
4h ago
