← all notes

Shipping a public API sandbox to a portfolio site

Why JWT auth, a real rate-limited endpoint, and a webhook tester live next to a CV — and the small Fastify patterns that made it cheap.

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 limit5 req / 10 s / IP, with the live X-RateLimit-Remaining and Retry-After headers 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.