mrkeyoor.com_
Wed 05 Aug 05:02 UTC
npmWeb Backendupdated 05 Aug 2026

fastify

Fastify is a Node.js web framework built around one idea: JSON APIs get dramatically faster when you compile route schemas ahead of time. You attach JSON Schema to routes and Fastify compiles validation and serialization into generated functions, which is how it reaches its claimed 76k+ requests per second. Around that core sits a plugin architecture with encapsulation (plugins get isolated scopes for decorators and hooks), structured logging via Pino built in, and a large set of official @fastify/* plugins for CORS, JWT, rate limiting, Swagger, and more. It is inspired by Hapi and Express and is the most credible drop-in successor to Express for API servers.

Verdict

The best default for plain Node JSON APIs today: fast, actively maintained, with an official plugin suite that covers the usual production needs. The schema-first and plugin-encapsulation models are the price of admission; if your team will not pay it, Express or Hono will serve you with less friction.

API stability4/5Within a major, the API is dependable and v4 keeps a maintained branch while v5 is current; but majors land every couple of years with real breaking changes, and ecosystem plugins need matching major bumps each cycle.
Docs4/5Reference docs in the repo plus fastify.dev guides cover routes, hooks, plugins, validation, and TypeScript thoroughly, with an official demo app; the material is spread across many reference pages and the encapsulation docs take a couple of reads.
Maintenance5/5Very active project (pushed 2026-08-04, 36.9k stars), CII best-practices badge, a published security policy, a team of maintainers rather than one person, and long-term maintenance branches for previous majors.
Ecosystem4/510.7M weekly downloads and a broad official @fastify/* plugin collection plus community plugins; still well below the sheer volume of Express middleware and Stack Overflow answers, which is what the missing point reflects.

Use it if

  • You are building a JSON API where throughput and latency actually matter and you want validation and serialization compiled from JSON Schema rather than bolted on
  • You want structured logging from day one; Pino is wired in and enabled with a single logger: true option
  • You want an officially maintained plugin suite (@fastify/cors, @fastify/jwt, @fastify/swagger, @fastify/rate-limit) instead of assembling third-party middleware of varying quality
  • You are outgrowing Express and want a maintained framework with a similar mental model, first-class async/await, and real TypeScript support
Skip it if

Setup reality

npm i fastify installs clean with 15 direct dependencies (pino, find-my-way, fast-json-stringify, avvio, and friends), and npm init fastify scaffolds a project. The honest costs: the async plugin boot sequence (avvio) means registration order and missing awaits cause errors that surface far from their cause; getting real value out of Fastify means writing JSON Schema for routes, which is boilerplate until you adopt @sinclair/typebox or fastify-type-provider-zod; and .listen binds to 127.0.0.1 by default, so every first Docker deployment fails until you pass host: '0.0.0.0', a gotcha called out in the README itself. Major versions also arrive on a cadence (v5 is current, v4 maintained on a branch) with genuine migration work.

Patterns

Start a basic serverhello-server

import Fastify from 'fastify'

const fastify = Fastify({ logger: true })

fastify.get('/', async (request, reply) => {
  return { hello: 'world' }
})

try {
  await fastify.listen({ port: 3000 })
} catch (err) {
  fastify.log.error(err)
  process.exit(1)
}

Returning an object from an async handler sends JSON automatically; no reply.send needed.

Validate a request body with JSON Schemaroute-validation

fastify.post('/users', {
  schema: {
    body: {
      type: 'object',
      required: ['name', 'email'],
      properties: {
        name: { type: 'string', minLength: 1 },
        email: { type: 'string', format: 'email' }
      }
    }
  }
}, async (request) => {
  // request.body is already validated here
  return { created: request.body.name }
})

Invalid bodies get an automatic 400 before your handler runs; format: 'email' needs ajv-formats registered in some setups.

Speed up responses with a response schemaresponse-serialization

fastify.get('/users/:id', {
  schema: {
    response: {
      200: {
        type: 'object',
        properties: {
          id: { type: 'integer' },
          name: { type: 'string' }
        }
      }
    }
  }
}, async (request) => {
  return { id: 1, name: 'ada', passwordHash: 'x' }
})

Serialization strips fields not in the schema (passwordHash never leaves the server), which is both a speed and a safety feature; it also silently drops fields you forgot to declare.

Read path and query parametersurl-params

fastify.get('/items/:id', async (request) => {
  const { id } = request.params
  const { verbose } = request.query
  return { id, verbose: verbose === 'true' }
})

params and query values are strings unless a schema coerces them; declare types in schema.params to get numbers.

Share a decorator across the app with fastify-pluginplugin-encapsulation

import fp from 'fastify-plugin'

async function dbPlugin (fastify, opts) {
  const db = await connect(opts.url)
  fastify.decorate('db', db)
  fastify.addHook('onClose', () => db.close())
}

export default fp(dbPlugin)

// app.js
await fastify.register(import('./db-plugin.js'), { url: process.env.DB_URL })
fastify.get('/ping', async function () {
  return this.db.ping()
})

Without the fp() wrapper the decorator stays private to the plugin's scope; that is the single most common Fastify confusion.

Protect routes with an onRequest hookauth-hook

fastify.register(async (protectedScope) => {
  protectedScope.addHook('onRequest', async (request, reply) => {
    const token = request.headers.authorization
    if (!token || !(await verify(token))) {
      return reply.code(401).send({ error: 'unauthorized' })
    }
  })

  protectedScope.get('/admin', async () => ({ secret: true }))
})

Hooks added inside a registered plugin apply only to routes in that scope, which is how you keep public routes public.

Set a custom error handlererror-handler

fastify.setErrorHandler((error, request, reply) => {
  request.log.error(error)
  if (error.validation) {
    return reply.status(400).send({ error: 'bad request', details: error.validation })
  }
  reply.status(error.statusCode ?? 500).send({ error: 'internal error' })
})

Validation failures arrive here too (error.validation is set); without a handler they leak schema details to clients.

Test routes without opening a porttesting-inject

import { test } from 'node:test'
import assert from 'node:assert'
import { build } from './app.js' // exports a configured Fastify instance

test('GET / returns hello', async () => {
  const app = await build()
  const res = await app.inject({ method: 'GET', url: '/' })
  assert.strictEqual(res.statusCode, 200)
  assert.deepStrictEqual(res.json(), { hello: 'world' })
  await app.close()
})

inject (from light-my-request) simulates HTTP in-process, so tests run fast and never fight over ports.

Listen correctly inside a containerdocker-listen

// Fastify binds to 127.0.0.1 by default, invisible from outside a container
await fastify.listen({ port: 3000, host: '0.0.0.0' })

The README warns about this directly; bind 0.0.0.0 in containers, but never expose such a port on an untrusted network without a firewall in front.

Enable CORS with the official pluginregister-cors

import cors from '@fastify/cors'

await fastify.register(cors, {
  origin: ['https://app.example.com'],
  methods: ['GET', 'POST']
})

Match the plugin's major to your Fastify major; @fastify/* plugins release new majors alongside each Fastify major.

Close cleanly on SIGTERMgraceful-shutdown

process.on('SIGTERM', async () => {
  try {
    await fastify.close() // runs onClose hooks, stops accepting connections
    process.exit(0)
  } catch (err) {
    fastify.log.error(err)
    process.exit(1)
  }
})

fastify.close() triggers every plugin's onClose hook, so database pools registered via plugins shut down with the server.

Alternatives

PackageRegistryPick it when
expressnpmYou want the largest middleware ecosystem and tutorial coverage and throughput is not a concern
hononpmYou need one framework across Node, Bun, Deno, and edge runtimes like Cloudflare Workers
koanpmYou want a minimal middleware kernel and prefer picking every other piece yourself
@nestjs/corenpmYou want an opinionated application architecture with DI and decorators on top of the HTTP layer