Frameworks

Fastify: The High-Performance Node.js Framework That Outpaces Express

Fastify: The High-Performance Node.js Framework That Outpaces Express

Why Fastify Deserves Your Attention

Express has been the default Node.js web framework for over a decade. It works, it has middleware for everything, and every tutorial starts with it. But defaults age. Express was designed in 2010 for a different era of server-side JavaScript — before async/await, before TypeScript dominated, before JSON APIs became the standard workload for Node.js servers.

Fastify was built to address exactly these gaps. Created by Matteo Collina and Tomas Della Vedova in 2016, Fastify is a web framework engineered from the ground up for speed, developer experience, and production reliability. It handles tens of thousands of requests per second out of the box, provides built-in schema validation, and offers a plugin architecture that keeps large codebases organized.

This guide covers everything you need to build production-grade APIs with Fastify: from basic routing and plugins to validation, serialization, error handling, and deployment. If you are building RESTful APIs or microservices with Node.js, Fastify is the framework that respects both your users’ time and your server budget.

What Makes Fastify Fast

The name is not marketing. Fastify consistently benchmarks at 30,000-78,000 requests per second on a single core, compared to Express’s 15,000-25,000. That is not a marginal improvement — it is a 2-3x throughput advantage that translates directly to lower infrastructure costs and better response times under load.

Three architectural decisions drive this performance:

JSON serialization with fast-json-stringify. Most Node.js frameworks use JSON.stringify() for every response. Fastify generates optimized serialization functions from your JSON Schema definitions at startup. Instead of inspecting object types at runtime, these functions know the exact shape of your data and produce JSON output up to 5x faster than the native method.

Radix-tree routing with find-my-way. Express uses a linear array of route handlers, checking each pattern sequentially until it finds a match. Fastify uses a radix tree (compact prefix tree) that resolves routes in logarithmic time. With 50 routes, the difference is negligible. With 500 routes serving a complex API, it is substantial.

Schema-based request parsing. When you define schemas for request bodies and query parameters, Fastify compiles validation functions at startup using Ajv (Another JSON Schema Validator). These compiled validators run significantly faster than runtime type-checking approaches because the validation logic is generated as optimized JavaScript code once, then reused for every request.

The result is a framework where the overhead per request is measured in microseconds rather than milliseconds. For teams that care about application performance and observability, Fastify provides a foundation that leaves more headroom for your actual business logic.

Getting Started: Your First Fastify Server

Setting up a Fastify project takes less than a minute. Install the package, create a server file, and you are handling requests:

// Install: npm install fastify

import Fastify from 'fastify';

const app = Fastify({
  logger: true // Built-in Pino logger, structured JSON output
});

// Define a route with JSON Schema validation
app.get('/api/users/:id', {
  schema: {
    params: {
      type: 'object',
      properties: {
        id: { type: 'string', pattern: '^[0-9]+$' }
      },
      required: ['id']
    },
    response: {
      200: {
        type: 'object',
        properties: {
          id: { type: 'integer' },
          name: { type: 'string' },
          email: { type: 'string', format: 'email' },
          role: { type: 'string', enum: ['admin', 'editor', 'viewer'] }
        }
      }
    }
  }
}, async (request, reply) => {
  const { id } = request.params;

  // Your database call here
  const user = await findUserById(parseInt(id, 10));

  if (!user) {
    reply.code(404);
    return { error: 'User not found' };
  }

  return user; // Fastify serializes automatically using the response schema
});

// Start the server
app.listen({ port: 3000, host: '0.0.0.0' }, (err) => {
  if (err) {
    app.log.error(err);
    process.exit(1);
  }
});

Several things are worth noting in this example. The logger: true option activates Fastify’s built-in Pino logger, which produces structured JSON logs at roughly 5x the speed of Winston or Bunyan. The schema object serves triple duty: it validates incoming requests, generates optimized serialization for responses, and auto-documents your API. The route handler is async by default — no callback hell, no manual res.json() calls. Just return an object and Fastify handles the rest.

The Plugin System: Fastify’s Killer Feature

Performance gets the headlines, but Fastify’s plugin architecture is what makes it viable for large applications. Every plugin runs in its own encapsulated context. Decorators, hooks, and routes registered in a plugin are invisible to sibling plugins unless explicitly shared. This is fundamentally different from Express middleware, where everything lives in a single flat namespace.

Think of Fastify plugins like modules with clear boundaries. An authentication plugin can register a request.user decorator and an onRequest hook without any risk of colliding with a rate-limiting plugin that uses its own hooks and decorators. This encapsulation becomes critical as applications grow beyond a few dozen routes.

The plugin registration is also asynchronous by design, which means plugins can perform setup operations — connecting to databases, loading configuration, warming caches — before the server starts accepting requests. No more race conditions during startup.

Lifecycle Hooks

Fastify provides a rich set of lifecycle hooks that let you intercept requests at every stage of processing:

  • onRequest — fires immediately when a request arrives, before parsing. Ideal for authentication, request logging, or early rejection of unauthorized requests.
  • preParsing — runs before the request body is parsed. Useful for decompression or decryption of request payloads.
  • preValidation — executes after parsing but before schema validation. You can modify request data here, for example normalizing field names.
  • preHandler — runs after validation passes but before the route handler. This is the most common hook for authorization checks.
  • preSerialization — intercepts the response payload before serialization. Useful for adding computed fields or transforming response structure.
  • onSend — fires after serialization, just before sending the response. You can modify headers or the serialized payload here.
  • onResponse — executes after the response has been sent. Perfect for metrics, cleanup, or audit logging.
  • onError — triggered when an error occurs during the request lifecycle. Centralizes error reporting and transformation.

This granular control over the request lifecycle makes it straightforward to implement cross-cutting concerns like rate limiting and throttling without cluttering your route handlers.

Schema Validation and Serialization

Fastify’s schema-first approach is one of its strongest differentiators. Where Express leaves validation entirely to third-party middleware like Joi or express-validator, Fastify integrates JSON Schema validation into the framework core. Every route can define schemas for params, querystring, headers, body, and response.

The validation happens automatically. If a request body does not match the schema, Fastify returns a 400 error with a detailed validation message before your handler code ever runs. This eliminates an entire class of bugs — the kind where invalid data sneaks past your validation layer and causes cryptic errors downstream.

If you work with TypeScript, Fastify’s schema system pairs naturally with type inference. You can derive TypeScript types from your JSON Schema definitions, ensuring that your runtime validation and compile-time types stay synchronized. For projects that use Zod for TypeScript validation, the fastify-type-provider-zod package bridges the two approaches, letting you write Zod schemas and get Fastify’s compiled validation performance.

Response serialization is equally powerful. When you define a response schema, Fastify strips any properties not in the schema before sending the response. This acts as a security layer — even if your database query returns sensitive fields like password hashes, they will not appear in the API response unless explicitly declared in the schema.

Building a Production API: A Complete Example

Here is a more realistic example that demonstrates plugins, hooks, validation, and error handling working together:

import Fastify from 'fastify';
import fastifyCors from '@fastify/cors';
import fastifyRateLimit from '@fastify/rate-limit';
import fastifyJwt from '@fastify/jwt';

const app = Fastify({
  logger: {
    level: 'info',
    transport: {
      target: 'pino-pretty', // Human-readable logs in development
      options: { colorize: true }
    }
  }
});

// Register plugins
await app.register(fastifyCors, { origin: ['https://yourdomain.com'] });
await app.register(fastifyRateLimit, { max: 100, timeWindow: '1 minute' });
await app.register(fastifyJwt, { secret: process.env.JWT_SECRET });

// Authentication decorator
app.decorate('authenticate', async (request, reply) => {
  try {
    await request.jwtVerify();
  } catch (err) {
    reply.code(401).send({ error: 'Authentication required' });
  }
});

// Task routes as an encapsulated plugin
app.register(async function taskRoutes(fastify) {

  // Apply auth to all routes in this plugin
  fastify.addHook('onRequest', fastify.authenticate);

  // GET /api/tasks — list tasks with pagination
  fastify.get('/api/tasks', {
    schema: {
      querystring: {
        type: 'object',
        properties: {
          page: { type: 'integer', minimum: 1, default: 1 },
          limit: { type: 'integer', minimum: 1, maximum: 100, default: 20 },
          status: { type: 'string', enum: ['pending', 'active', 'done'] }
        }
      },
      response: {
        200: {
          type: 'object',
          properties: {
            tasks: {
              type: 'array',
              items: {
                type: 'object',
                properties: {
                  id: { type: 'integer' },
                  title: { type: 'string' },
                  status: { type: 'string' },
                  assignee: { type: 'string' },
                  createdAt: { type: 'string', format: 'date-time' }
                }
              }
            },
            total: { type: 'integer' },
            page: { type: 'integer' },
            pages: { type: 'integer' }
          }
        }
      }
    }
  }, async (request) => {
    const { page, limit, status } = request.query;
    const offset = (page - 1) * limit;
    const filters = status ? { status } : {};

    const [tasks, total] = await Promise.all([
      db.tasks.findMany({ where: filters, skip: offset, take: limit }),
      db.tasks.count({ where: filters })
    ]);

    return {
      tasks,
      total,
      page,
      pages: Math.ceil(total / limit)
    };
  });

  // POST /api/tasks — create a new task
  fastify.post('/api/tasks', {
    schema: {
      body: {
        type: 'object',
        required: ['title'],
        properties: {
          title: { type: 'string', minLength: 1, maxLength: 200 },
          description: { type: 'string', maxLength: 2000 },
          assignee: { type: 'string' },
          priority: { type: 'string', enum: ['low', 'medium', 'high'] }
        }
      },
      response: {
        201: {
          type: 'object',
          properties: {
            id: { type: 'integer' },
            title: { type: 'string' },
            status: { type: 'string' },
            createdAt: { type: 'string', format: 'date-time' }
          }
        }
      }
    }
  }, async (request, reply) => {
    const task = await db.tasks.create({
      data: {
        ...request.body,
        status: 'pending',
        createdBy: request.user.id
      }
    });

    reply.code(201);
    return task;
  });

}, { prefix: '' }); // No prefix — routes define full paths

// Global error handler
app.setErrorHandler((error, request, reply) => {
  request.log.error(error);

  if (error.validation) {
    reply.code(400).send({
      error: 'Validation Error',
      details: error.validation
    });
    return;
  }

  reply.code(error.statusCode || 500).send({
    error: error.message || 'Internal Server Error'
  });
});

// Graceful shutdown
const signals = ['SIGINT', 'SIGTERM'];
signals.forEach(signal => {
  process.on(signal, async () => {
    app.log.info(`Received ${signal}, shutting down gracefully`);
    await app.close();
    process.exit(0);
  });
});

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

This example demonstrates patterns you will need in any real-world Fastify application. CORS is configured to accept requests only from your domain. Rate limiting caps clients at 100 requests per minute. JWT authentication is implemented as a reusable decorator and applied to all routes in the task plugin via a hook. The error handler distinguishes validation errors from application errors, returning appropriate status codes and messages for each.

For teams managing complex projects, having this level of structured control over your API layer is essential. Tools like Taskee demonstrate how well-structured backend APIs translate into effective project management workflows.

Fastify vs Express: A Practical Comparison

The Express-to-Fastify comparison comes up in every discussion, so let us address it directly with specific technical differences:

Performance. Fastify is 2-3x faster than Express in benchmarks measuring raw throughput. The gap widens further when you use schema-based serialization, which Express does not support natively. In a production API serving JSON responses, the real-world difference is typically 40-80% more throughput per server instance.

Validation. Express relies on third-party middleware for validation. You install express-validator or Joi, configure it per route, and handle validation errors manually. Fastify validates requests automatically using JSON Schema declarations on the route definition. Invalid requests never reach your handler code.

Plugin isolation. Express middleware is global by default. If you call app.use(), every subsequent route sees that middleware. Fastify plugins are encapsulated — what happens in a plugin stays in a plugin unless explicitly exposed. This makes Fastify applications easier to reason about as they grow.

Logging. Express has no built-in logger. You install Morgan, Winston, or Pino separately. Fastify ships with Pino integrated, producing structured JSON logs with request IDs, response times, and status codes out of the box. Log performance is exceptional because Pino is designed for high-throughput production environments.

TypeScript support. Fastify was rewritten in TypeScript for version 4. Types are accurate, maintained as part of the core, and deeply integrated with the schema system. Express’s TypeScript support relies on community-maintained @types/express definitions, which can lag behind releases and do not cover middleware types comprehensively.

Async/await. Express was designed before async/await existed. Unhandled promise rejections in Express route handlers crash the process silently unless you wrap every handler in a try/catch. Fastify handles async errors natively — if your async handler throws, Fastify catches it and routes it to the error handler automatically.

Ecosystem and Plugins

Fastify’s ecosystem has matured significantly. The core team maintains official plugins for the most common needs, all published under the @fastify scope on npm:

  • @fastify/cors — Cross-Origin Resource Sharing configuration
  • @fastify/helmet — Security headers (Content-Security-Policy, X-Frame-Options, etc.)
  • @fastify/rate-limit — Request rate limiting with Redis or in-memory stores
  • @fastify/jwt — JSON Web Token authentication
  • @fastify/swagger — OpenAPI/Swagger documentation auto-generated from route schemas
  • @fastify/multipart — File upload handling
  • @fastify/websocket — WebSocket support built on the ws library
  • @fastify/static — Static file serving
  • @fastify/cookie — Cookie parsing and setting
  • @fastify/session — Server-side session management

The @fastify/swagger plugin deserves special attention. Because Fastify routes already define JSON Schema for request and response types, generating OpenAPI documentation is automatic. You register the plugin, and it produces a complete API specification from your existing route schemas — no additional annotations or JSDoc comments needed.

For teams exploring alternative JavaScript runtimes, Fastify also works with Bun and has experimental support for Deno, making it a portable choice across the evolving Node.js ecosystem.

Testing Fastify Applications

Fastify includes a built-in testing utility called inject that simulates HTTP requests without starting a server or opening network ports. This makes tests fast, isolated, and free of port-conflict issues:

const response = await app.inject({
  method: 'GET',
  url: '/api/tasks?page=1&limit=10',
  headers: {
    authorization: 'Bearer test-token'
  }
});

assert.equal(response.statusCode, 200);
const body = response.json();
assert.ok(Array.isArray(body.tasks));

The inject method returns a response object with the status code, headers, and body — everything you need to verify your API behavior. Because no actual HTTP server is started, these tests run in milliseconds rather than seconds. Combined with Fastify’s plugin encapsulation, you can test individual route groups in isolation without bootstrapping the entire application.

Performance Tuning and Production Tips

Fastify is fast by default, but several configuration options can push performance further in production environments:

Disable request logging for high-throughput routes. Pino is fast, but logging still costs CPU cycles. For health-check endpoints or high-frequency internal APIs, set logLevel: 'silent' on the route configuration.

Use connection pooling for databases. Fastify’s speed means it can saturate a database connection pool faster than Express. Size your pool according to your expected concurrency. A common starting point is 2x the number of CPU cores available to the Node.js process.

Enable HTTP/2. Fastify supports HTTP/2 natively with a single configuration flag. For APIs serving many concurrent requests from browser clients, HTTP/2 multiplexing reduces connection overhead significantly.

Pre-compile schemas. If you define shared schemas (referenced via $ref), add them to Fastify using app.addSchema() at startup. Fastify resolves references once during initialization rather than at request time.

Implement graceful shutdown. The code example above shows a basic graceful shutdown handler. In production, you should also drain active connections and notify load balancers before stopping. Fastify’s app.close() method handles cleanup of registered plugins, including closing database connections and flushing logs.

For comprehensive Node.js debugging strategies, pairing Fastify’s structured logging with tools like clinic.js provides deep visibility into performance bottlenecks.

When to Choose Fastify

Fastify is the right choice when you are building:

  • JSON APIs and microservices — Fastify’s schema-based serialization and validation make it the fastest option for JSON-heavy workloads.
  • High-throughput services — If your service handles thousands of requests per second, Fastify’s lower overhead translates to fewer servers and lower cloud bills.
  • TypeScript-first projects — Native TypeScript support with schema-derived types provides end-to-end type safety without extra tooling.
  • Projects migrating from Express — Fastify’s API is similar enough that migration is straightforward, and the @fastify/express compatibility plugin lets you use existing Express middleware during the transition.

Fastify is less ideal for server-rendered HTML applications (use Next.js, Nuxt, or Hono), simple static sites, or prototypes where Express’s larger collection of ready-made middleware would save time. It is also worth noting that if your API layer is straightforward enough to handle with a few routes, Express’s simplicity may outweigh Fastify’s performance advantages.

For agencies and development teams building client projects, the combination of Fastify’s performance with structured project delivery methodologies — like those supported by Toimi — creates a workflow where both the backend architecture and the development process are optimized for efficiency.

Migrating from Express to Fastify

Migration does not have to be all-or-nothing. Fastify provides the @fastify/express plugin that lets you mount existing Express middleware and routers inside a Fastify application. This enables a gradual migration strategy:

  1. Start with new routes. Write new endpoints in Fastify with full schema definitions. Keep existing Express routes running through the compatibility layer.
  2. Migrate middleware. Replace Express middleware with Fastify equivalents. Most common middleware has official Fastify plugins: CORS, helmet, compression, rate limiting.
  3. Convert routes incrementally. Move Express routes to Fastify format one group at a time. Add JSON Schema validation as you migrate each route — this is the main effort, but it pays off immediately in runtime safety.
  4. Remove the compatibility layer. Once all routes and middleware are native Fastify, drop @fastify/express and enjoy full performance benefits.

Most teams complete the migration within a few weeks of incremental work, running both frameworks simultaneously in production during the transition.

The Road Ahead

Fastify continues to evolve with the Node.js ecosystem. Version 5, currently in development, focuses on ESM-first architecture, improved TypeScript inference, and tighter integration with modern runtimes. The framework’s performance-conscious design philosophy extends beyond raw speed to encompass developer productivity, code maintainability, and operational reliability.

If you are starting a new Node.js API in 2025 or 2026, Fastify should be your default choice over Express. The performance gap is real, the developer experience is mature, and the ecosystem covers every common use case. Express earned its place in Node.js history, but Fastify represents the next generation of server-side JavaScript — faster, safer, and built for the way we write APIs today.

Frequently Asked Questions

Is Fastify really faster than Express?

Yes. In standardized benchmarks, Fastify handles 2-3x more requests per second than Express. The performance advantage comes from three main optimizations: schema-based JSON serialization using fast-json-stringify, radix-tree routing with find-my-way, and compiled validation functions via Ajv. In production environments serving JSON APIs, teams typically see 40-80% higher throughput per server instance after migrating from Express to Fastify.

Can I use Express middleware with Fastify?

Yes. The official @fastify/express plugin provides a compatibility layer that lets you use existing Express middleware inside a Fastify application. This makes gradual migration practical — you can run both frameworks simultaneously, migrating routes incrementally while keeping your Express middleware functional during the transition period.

Does Fastify support TypeScript?

Fastify has first-class TypeScript support. Starting from version 4, the framework core is written in TypeScript, so type definitions are always accurate and up to date. Fastify’s type system integrates with its schema validation — you can derive TypeScript types from JSON Schema definitions, ensuring that runtime validation and compile-time type checking stay synchronized. Type providers for Zod and TypeBox are also available for teams that prefer alternative schema libraries.

How does Fastify handle validation?

Fastify uses JSON Schema for request and response validation, powered by Ajv (Another JSON Schema Validator). You define schemas for route parameters, query strings, request bodies, headers, and responses as part of the route configuration. Fastify compiles these schemas into optimized validation functions at startup, so validation runs with minimal overhead at request time. Invalid requests are rejected with detailed error messages before reaching your handler code, preventing an entire class of bugs caused by unexpected input.

Is Fastify production-ready?

Fastify is thoroughly production-ready and used by major companies including Microsoft, Nearform, and numerous startups handling millions of daily requests. The framework has been in production use since 2017, has a well-defined LTS (Long Term Support) policy, and is maintained by a dedicated core team. Its built-in features — structured logging with Pino, graceful shutdown handling, comprehensive error management, and security-oriented response serialization — are specifically designed for production deployment scenarios.