Fastify vs Express — TypeScript Performance Comparison 2024

Sanjeev SharmaSanjeev Sharma
6 min read

Advertisement

Introduction

Why This Matters

Choosing between Fastify and Express shapes your entire backend architecture. Express has a decade of production history and the largest middleware ecosystem. Fastify is purpose-built for performance and has first-class TypeScript and JSON schema support baked in from the start.

For high-throughput microservices, API gateways, or latency-sensitive applications, Fastify's ~3x throughput advantage is meaningful. For teams that value ecosystem maturity, gentle learning curves, and abundant documentation, Express wins on those dimensions. Neither is universally correct — understanding both lets you make the right choice per project.

Performance Benchmarks

Fastify uses a radix tree router and optimized JSON serialization that outperforms Express significantly.

MetricExpressFastify
Requests/sec (simple route)~12,000~35,000
P99 latency~8ms~3ms
Memory (idle)~45 MB~40 MB
JSON parse/stringifyNativefast-json-stringify
Router algorithmLinear scanRadix tree

Benchmarks are approximations from wrk on a standard EC2 instance. Real numbers vary by workload.

Hello World Comparison

// Express
import express from 'express';
const app = express();
app.use(express.json());
 
app.get('/hello', (req, res) => {
  res.json({ hello: 'world' });
});
 
app.listen(3000, () => console.log('Express on :3000'));
 
// Fastify
import Fastify from 'fastify';
const fastify = Fastify({ logger: true });
 
fastify.get('/hello', async (_request, _reply) => {
  return { hello: 'world' }; // return value becomes the JSON response
});
 
fastify.listen({ port: 3000 }, (err) => {
  if (err) throw err;
  console.log('Fastify on :3000');
});

TypeScript Support

Fastify has significantly better TypeScript integration out of the box.

// Express — manual type annotation required
import { Request, Response } from 'express';
 
interface CreateUserBody {
  name: string;
  email: string;
}
 
app.post('/users', (req: Request<{}, {}, CreateUserBody>, res: Response) => {
  const { name, email } = req.body; // types come from manual annotation
  res.status(201).json({ name, email });
});
 
// Fastify — types derived from JSON schema via generics
import { FastifyRequest, FastifyReply } from 'fastify';
 
interface CreateUserBody {
  name: string;
  email: string;
}
 
fastify.post<{ Body: CreateUserBody }>(
  '/users',
  {
    schema: {
      body: {
        type: 'object',
        required: ['name', 'email'],
        properties: {
          name: { type: 'string', minLength: 2 },
          email: { type: 'string', format: 'email' },
        },
      },
    },
  },
  async (request, reply) => {
    const { name, email } = request.body; // fully typed
    reply.status(201).send({ name, email });
  }
);

Built-in Schema Validation

Fastify validates and serializes using JSON Schema, with no external library required.

import Fastify from 'fastify';
 
const fastify = Fastify();
 
// Schema-driven route — validates input and optimizes output serialization
fastify.post('/orders', {
  schema: {
    body: {
      type: 'object',
      required: ['userId', 'items'],
      properties: {
        userId: { type: 'string' },
        items: {
          type: 'array',
          items: {
            type: 'object',
            required: ['productId', 'quantity'],
            properties: {
              productId: { type: 'string' },
              quantity: { type: 'integer', minimum: 1 },
            },
          },
        },
      },
    },
    response: {
      201: {
        type: 'object',
        properties: {
          orderId: { type: 'string' },
          status: { type: 'string' },
        },
      },
    },
  },
  handler: async (request, reply) => {
    reply.status(201).send({ orderId: 'ord_123', status: 'pending' });
  },
});

Plugin Architecture

Fastify's plugin system uses encapsulation — plugins register decorators and hooks that are scoped to their subtree.

import Fastify, { FastifyPluginAsync } from 'fastify';
import fp from 'fastify-plugin';
 
// Plugin with type-safe decorators
const databasePlugin: FastifyPluginAsync = fp(async (fastify) => {
  const db = { query: async (sql: string) => [] }; // simplified
  fastify.decorate('db', db);
});
 
declare module 'fastify' {
  interface FastifyInstance {
    db: { query: (sql: string) => Promise<unknown[]> };
  }
}
 
// Route plugin — encapsulated
const userRoutes: FastifyPluginAsync = async (fastify) => {
  fastify.get('/users', async (request, reply) => {
    const users = await fastify.db.query('SELECT * FROM users');
    return users;
  });
};
 
const app = Fastify();
app.register(databasePlugin);
app.register(userRoutes, { prefix: '/api/v1' });

Middleware vs Hooks

Express uses middleware; Fastify uses lifecycle hooks that are more predictable.

// Express middleware (affects all subsequent routes)
app.use((req, res, next) => {
  req.requestId = crypto.randomUUID();
  next();
});
 
// Fastify hook (lifecycle-aware, encapsulated)
fastify.addHook('onRequest', async (request, reply) => {
  request.requestId = crypto.randomUUID();
});
 
// Available Fastify lifecycle hooks:
// onRequest → preParsing → preValidation → preHandler → handler
// → preSerialization → onSend → onResponse

When to Choose Each Framework

Choose Express when:

  • The team is already familiar with Express patterns
  • You need a specific middleware that has no Fastify equivalent
  • You are building an internal tool where 10k vs 30k req/sec does not matter
  • The project has many contributors who need a gentle learning curve

Choose Fastify when:

  • You are building a public-facing API where latency matters
  • You want built-in JSON schema validation without a separate library
  • TypeScript is a first-class requirement
  • You are building microservices that benefit from the plugin encapsulation model

Common Mistakes

  • Using Express middleware directly in Fastify — it requires the @fastify/middie adapter
  • Returning values from Express route handlers — only res.send() or res.json() works
  • Forgetting await fastify.ready() in tests before sending requests
  • Mixing Fastify reply methods with return statements — use one approach per handler
  • Adding global Express middleware that should be scoped — Fastify's plugin encapsulation prevents this

Best Practices

  • Use Fastify's JSON schema validation instead of adding a separate Zod/Joi integration for maximum performance
  • Register all Fastify plugins with fastify-plugin if they need to share state with sibling plugins
  • Use Fastify's fastify.inject() for integration testing without starting a real HTTP server
  • For Express, always use asyncHandler wrappers — Fastify async handlers propagate errors automatically
  • Benchmark your specific workload before switching frameworks — synthetic benchmarks differ from production traffic

Key Takeaways

  • Fastify processes ~35,000 req/sec vs Express ~12,000 on equivalent hardware — a 3x difference
  • Fastify has built-in JSON Schema validation and fast-json-stringify serialization with no extra libraries
  • Express has a larger ecosystem and more available middleware packages than Fastify
  • Fastify's plugin system uses encapsulation — a plugin's decorators are only visible in its subtree
  • TypeScript support in Fastify is first-class; Express requires more manual type annotations
  • Fastify's lifecycle hooks are more granular and predictable than Express middleware ordering
  • Express 5.x (released in 2024) adds async route handler support natively
  • Both frameworks can handle millions of requests per day — the performance gap matters most at high scale

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading