Engineering

Typed, tested backends on Fastify

A backend is a set of promises about what goes in and what comes out. We build ours on Fastify, in TypeScript, and the whole point of the way we do it is that those promises are typed, validated at the edge, and tested from the start — so the service behaves the way its contract says it does.

TypeBox schemas at every boundary

Every route defines its request and response shape as a TypeBox schema. That schema does two jobs at once: it validates incoming data at the edge, rejecting a malformed request before it reaches business logic, and it generates the static types the handler is written against, so the compiler and the runtime agree on what a payload contains.

One definition, two guarantees — that is why we prefer it to hand-written validation sitting beside hand-written types, where the two drift apart the first time someone edits one and forgets the other. A boundary that validates and types itself from a single source is a boundary that stays honest.

One error shape, everywhere

Errors leave the service in one consistent shape, through a single error handler, rather than each route inventing its own. A caller — including a client's own future team — can then handle failures uniformly instead of special-casing the response format of every endpoint. Rate limiting lives at the edge in nginx rather than scattered through route config, so the app has one job and the proxy has another.

Consistency here is not tidiness for its own sake. An unpredictable error surface is where integrations quietly break, because the caller handled the three failures it saw in testing and not the fourth it met in production. One shape means the fourth failure looks like the first three.

Coverage grown, not retrofitted

Tests are written alongside the code, not sprinted at before a release. The failing test comes first, then the code that satisfies it, and coverage is built up as the service grows rather than reconstructed from memory at the end — which never actually catches the cases that matter, only the ones easy to remember.

Our own products run on this pattern. Docusift, Memoria and Lekha share a Fastify backbone, and each reads its runtime configuration from an encrypted per-tenant store through a config module, so where a customer's data lives is set by architecture rather than by a setting. If you need a service built this way, see custom API development.

Questions

People also ask

    Why TypeBox schemas instead of separate types and validation?

    Because one TypeBox schema both validates a request at the edge and generates the static types the handler uses, so runtime and compiler agree. Separate hand-written types and validators drift apart the first time one is edited and the other is not.

    Where do rate limits live in your Fastify services?

    At the edge, in nginx, keyed by IP — not in per-route app config. Per-user or per-tenant business limits that need to inspect a token or the database stay in the app; general IP rate limiting does not.