Skip to content
Node.js nd libraries 5 min read

Fastify: High-Performance Web Framework

Fastify is a web framework for Node.js built around two ideas: serve requests as fast as possible, and make the developer experience pleasant while doing it. It reaches its speed through a highly optimized router, schema-driven JSON serialization, and a lightweight plugin architecture that encapsulates state instead of mutating shared globals. If you are building JSON APIs and want low overhead per request plus first-class validation, Fastify is a strong default in 2026.

Installing and a first server

Fastify ships as an ES module-friendly package and works with Node.js 20/22 LTS. Install it alongside nothing else — the core is self-contained.

npm install fastify

A minimal server registers routes and then calls listen. Fastify’s listen returns a promise, so you can await it and handle startup errors cleanly.

import Fastify from 'fastify';

const app = Fastify({ logger: true });

app.get('/health', async (request, reply) => {
  return { status: 'ok', uptime: process.uptime() };
});

try {
  await app.listen({ port: 3000, host: '0.0.0.0' });
} catch (err) {
  app.log.error(err);
  process.exit(1);
}

Output:

{"level":30,"time":1718323200000,"msg":"Server listening at http://0.0.0.0:3000"}

Returning a value (or a resolved promise) from a handler sends it as the response. You only call reply.send() explicitly when you need to set status codes or headers first. Never mix return and reply.send() in the same handler.

CommonJS users can swap the import for const Fastify = require('fastify'); the rest of the API is identical.

Routing

Fastify uses find-my-way, a radix-tree router that matches routes in roughly constant time regardless of how many you register. You can declare routes with shorthand methods (app.get, app.post, …) or the full app.route() form, which accepts schema, hooks, and the handler together.

app.route({
  method: 'GET',
  url: '/users/:id',
  handler: async (request, reply) => {
    const { id } = request.params;
    return { id, name: 'Ada Lovelace' };
  },
});

Route parameters live on request.params, query strings on request.query, and the parsed body on request.body. Fastify parses application/json and application/x-www-form-urlencoded out of the box.

JSON schema validation and serialization

This is Fastify’s signature feature. You attach a JSON Schema to a route, and Fastify uses it for two things: validating incoming requests (via Ajv) and serializing outgoing responses (via fast-json-stringify). Schema-based serialization is dramatically faster than JSON.stringify because Fastify compiles a purpose-built function for the exact shape you declared.

const createUserSchema = {
  body: {
    type: 'object',
    required: ['email', 'age'],
    properties: {
      email: { type: 'string', format: 'email' },
      age: { type: 'integer', minimum: 18 },
    },
  },
  response: {
    201: {
      type: 'object',
      properties: {
        id: { type: 'string' },
        email: { type: 'string' },
      },
    },
  },
};

app.post('/users', { schema: createUserSchema }, async (request, reply) => {
  const { email } = request.body;
  reply.code(201);
  return { id: crypto.randomUUID(), email };
});

If a request fails validation, Fastify returns a 400 automatically — your handler never runs.

Output:

$ curl -s -XPOST localhost:3000/users -H 'content-type: application/json' -d '{"email":"x","age":12}'
{"statusCode":400,"error":"Bad Request","message":"body/email must match format \"email\""}

Note that the response schema also acts as a filter: any property not declared is stripped before sending, which prevents accidental leakage of sensitive fields.

The plugin system

Everything in Fastify is a plugin, including your own application. A plugin is an async function that receives the instance and an options object. Plugins create an encapsulated context — decorators, hooks, and routes registered inside a plugin do not leak to its siblings, only to its children. This is what makes large Fastify apps stay organized.

async function userRoutes(instance, opts) {
  instance.get('/users', async () => [{ id: '1' }]);
  instance.post('/users', async (req) => req.body);
}

app.register(userRoutes, { prefix: '/api/v1' });

You extend instances, requests, and replies with decorate, decorateRequest, and decorateReply — a cheaper, monomorphic alternative to attaching properties at runtime.

app.decorate('db', await connectToDatabase());

app.get('/count', async (request) => {
  return { total: await request.server.db.countUsers() };
});

Use fastify-plugin to break encapsulation deliberately when a plugin (like a database connection) should be shared with the whole tree rather than scoped to its registration point.

Hooks

Hooks let you run logic at defined points in the request/response lifecycle — authentication, logging, response shaping. They run in registration order and respect plugin encapsulation.

HookFires
onRequestEarliest, before body parsing — ideal for auth
preValidationBefore schema validation
preHandlerAfter validation, before the handler
onSendBefore the payload is sent, can mutate it
onResponseAfter the response is fully sent
onErrorWhen a handler or hook throws
app.addHook('onRequest', async (request, reply) => {
  if (!request.headers.authorization) {
    reply.code(401).send({ error: 'Unauthorized' });
  }
});

Why Fastify is faster than Express

Express works fine, but its design predates many V8 optimizations. Fastify’s edge comes from concrete engineering choices rather than micro-tweaks.

ConcernExpressFastify
RouterLinear array of layersRadix tree (find-my-way)
SerializationJSON.stringifyCompiled schema serializer
ValidationManual / middlewareBuilt-in Ajv per route
Async errorsNeeds wrappersNative promise support
LoggingAdd-on (e.g. morgan)Built-in Pino, structured

In typical JSON benchmarks Fastify handles two to three times the requests per second of Express for the same payload, mostly thanks to schema serialization and the faster router.

Best practices

  • Always attach a response schema to hot routes — serialization speed is where Fastify’s biggest wins come from.
  • Structure features as encapsulated plugins, and reach for fastify-plugin only when something genuinely needs to be shared globally.
  • Put authentication and rate-limiting in onRequest hooks so unauthorized requests are rejected before any parsing work.
  • Enable the built-in Pino logger and pass request.id through to downstream calls for traceable, structured logs.
  • Use app.decorate for shared resources (database pools, clients) instead of importing singletons across files.
  • Validate environment configuration at boot and await app.ready() in tests to ensure every plugin has loaded.
Last updated June 14, 2026
Was this helpful?