Why a sandbox at all
Recruiters skim. A line on the CV that says "built a Fastify API" reads the same whether you wrote one route or twenty. A page on the site that lets the visitor send a real 429 reads completely differently — they don't have to take my word for it.
The /sandbox page exposes three things:
- Auth (JWT) — login → token → tampered token. The whole round-trip, including the 401 you get when you flip a signature byte.
- Rate limit —
5 req / 10 s / IP, with the liveX-RateLimit-RemainingandRetry-Afterheaders surfaced in the UI. - Webhook tester — every visitor gets a private inbox URL. Anything POSTed to it streams back via SSE.
Each is implemented in <100 lines on the server. The whole sandbox cost me less code than the eslint config.
The patterns that made it cheap
Zod schemas as the route contract.
fastify-type-provider-zod lets you declare the body / params /
response schema once, get TypeScript types out of it on the handler, and
get OpenAPI docs out of it for free at /docs. The schemas double as
the test fixtures.
reply.hijack() plus a tiny SSE helper. Fastify's hijack flow takes
the response socket out of the request lifecycle. Wrap the boilerplate
(Content-Type: text/event-stream, CORS-on-hijack, keep-alive ping,
cleanup) in one helper and every SSE route is six lines.
In-memory stores with documented TTLs. Render's free tier has no persistent disk and the API doesn't try to fake one — the analytics store, the webhook tester and the (fallback) feedback adapter all wear their ephemerality on their sleeve.