Back to Blog
Backend

Fastify: What You Get for Leaving Express Behind

Schema-based serialization, a real plugin system with encapsulation, and first-class async — the Node framework that treats performance as a design constraint.

Published
Reading time
9 min read
Author
Yakhya

Express is fifteen years old, effectively feature-frozen, and still the default in most Node tutorials. Fastify is what a Node HTTP framework looks like when it is designed after promises, JSON Schema, and V8 optimization are all well understood. The headline is throughput, but the durable reasons to switch are architectural.

Schemas make it fast, and correct

Fastify's signature trick is compiling JSON Schema into specialized validation and serialization functions ahead of time. Validation uses Ajv; serialization uses fast-json-stringify, which generates a purpose-built function for that exact shape instead of calling generic `JSON.stringify`. The result is meaningfully faster responses — and, more importantly, a response body that physically cannot contain a field the schema does not declare. Accidental leakage of a password hash or internal flag stops being possible on that route.

javascript
import Fastify from 'fastify';

const app = Fastify({ logger: true });   // Pino, structured, on by default

app.post('/payments', {
  schema: {
    body: {
      type: 'object',
      required: ['accountId', 'amountMinor', 'currency'],
      properties: {
        accountId:   { type: 'string', format: 'uuid' },
        amountMinor: { type: 'integer', minimum: 1 },
        currency:    { type: 'string', minLength: 3, maxLength: 3 },
      },
      additionalProperties: false,
    },
    response: {
      201: {
        type: 'object',
        properties: {                    // anything else is stripped
          id:     { type: 'string' },
          status: { type: 'string' },
        },
      },
    },
  },
}, async (request, reply) => {
  const payment = await payments.create(request.body);
  reply.code(201);
  return payment;                        // just return it — no res.json()
});

await app.listen({ port: 3000, host: '0.0.0.0' });

Plugins with encapsulation

Express middleware is a flat, global chain: everything registered affects everything after it, and load order becomes tribal knowledge. Fastify plugins form a tree, and each one gets its own encapsulated context. A decorator, hook, or plugin registered inside a subtree is invisible outside it — so an authentication plugin can apply to `/admin` routes without touching the public ones, and two parts of the app can use different versions of the same dependency without a conflict. It is the closest thing Node has to a module system for runtime concerns, and it scales to large codebases in a way flat middleware does not.

  • Hooks (`onRequest`, `preHandler`, `onSend`, `onError`) give precise lifecycle points instead of one undifferentiated middleware slot.
  • Async handlers are native — return a value to send it, throw to trigger the error handler. No `next()`, no forgotten callback.
  • Errors thrown in async handlers are caught properly, which is the single most common Express footgun.
  • Pino logging is built in, structured as JSON, with a request id attached to every line by default.
  • `@fastify/swagger` derives OpenAPI from the schemas you already wrote, so documentation costs nothing extra.
  • First-party plugins cover the standard needs: cors, helmet, jwt, rate-limit, multipart, static, cookie, websocket.

TypeScript, honestly

The gap in the design is that JSON Schema and TypeScript types are two separate declarations of the same shape. Close it with a type provider — TypeBox is the common choice — so one definition produces both the runtime schema and the static type. Without that, you will eventually ship a handler whose types claim one thing while the schema validates another, and the compiler will be perfectly happy about it.

Benchmarks sell Fastify; encapsulation and schema-enforced responses are why teams keep it.

Choosing it

Take Fastify for new Node services, high-throughput APIs, and anywhere you want contracts enforced at the edges. Stay on Express when the project is small and the ecosystem's long tail of middleware matters more than throughput, or when your team's muscle memory is worth more than the migration. Reach past both for NestJS if you want an opinionated, DI-heavy architecture for a large team — and note that Nest can run on Fastify underneath, which is a reasonable way to get the structure and the speed together. Whatever you pick, the transferable lesson is the schema discipline: validate what comes in, declare what goes out, and let the framework enforce both.

Tags
FastifyNode.jsJSON SchemaPerformanceTypeScript
Keep readingAll Posts