Fastify vs Express — TypeScript Performance Comparison 2024
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.
| Metric | Express | Fastify |
|---|---|---|
| Requests/sec (simple route) | ~12,000 | ~35,000 |
| P99 latency | ~8ms | ~3ms |
| Memory (idle) | ~45 MB | ~40 MB |
| JSON parse/stringify | Native | fast-json-stringify |
| Router algorithm | Linear scan | Radix 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 → onResponseWhen 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/middieadapter - Returning values from Express route handlers — only
res.send()orres.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-pluginif 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
asyncHandlerwrappers — 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-stringifyserialization 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