High-Performance Node.js REST APIs: Why Fastify is 2x Faster than Express + Zod Validation
· 6 min read
For over a decade,Express.jswas the default web framework for Node.js. However, as modern web apps and multiplayer backends demand tens of thousands of requests per second with sub-millisecond latency, engineers increasingly turn toFastify— an ultra-fast, low-overhead web framework.
1. Performance Anatomy: Radix Tree vs Linear Routing
The core difference lies in how incoming URLs are matched against registered route handlers:
- Express.js:Uses a linear array of routes and regular expressions (
RegExp), iterating sequentially through handlers with O(n) complexity. - Fastify:Uses aRadix Tree(prefix tree powered by
find-my-way). Route lookup time is O(k) based solely on path length — whether you have 10 or 1,000 endpoints!
2. Precompiled JSON Serialization:fast-json-stringify
In standard Node.js servers, callingres.json(data)invokes V8's dynamicJSON.stringify()at runtime. Fastify compiles JSON schemas ahead of time usingfast-json-stringify, accelerating JSON serialization by2-3x.
// Fastify automatycznie kompiluje zoptymalizowaną funkcję serializującą
const responseSchema = {
200: {
type: 'object',
properties: {
status: { type: 'string' },
userId: { type: 'integer' },
timestamp: { type: 'number' }
}
}
};
3. Strict TypeScript Input Validation with Zod
Validating payloads at the API gateway prevents injection attacks, rejects malformed requests early, and guarantees end-to-end TypeScript type safety:
import Fastify from 'fastify';
import { serializerCompiler, validatorCompiler, ZodTypeProvider } from 'fastify-type-provider-zod';
import { z } from 'zod';
const app = Fastify({ logger: true }).withTypeProvider<ZodTypeProvider>();
// Konfiguracja kompilatorów Zod
app.setValidatorCompiler(validatorCompiler);
app.setSerializerCompiler(serializerCompiler);
// Definicja schematu rejestracji gracza
const RegisterSchema = z.object({
username: z.string().min(3).max(20).regex(/^[a-zA-Z0-9_]+$/),
email: z.string().email(),
coins: z.number().int().nonnegative().default(0)
});
// Rejestracja trasy z automatyczną walidacją
app.post('/api/players/register', {
schema: {
body: RegisterSchema,
response: {
201: z.object({
success: z.boolean(),
playerId: z.string(),
createdAt: z.string()
})
}
}
}, async (request, reply) => {
// request.body ma w pełni wydedukowany typ TypeScript!
const { username, email, coins } = request.body;
// Symulacja zapisu w bazie danych
const playerId = 'usr_' + Math.random().toString(36).substring(2, 9);
return reply.status(201).send({
success: true,
playerId,
createdAt: new Date().toISOString()
});
});
const start = async () => {
try {
await app.listen({ port: 3000, host: '0.0.0.0' });
console.log('🚀 Fastify API serwer działa na porcie 3000');
} catch (err) {
app.log.error(err);
process.exit(1);
}
};
start();
4. Load Testing Benchmarks (autocannon)
In benchmarks executed withautocannon(100 concurrent connections, 10 seconds sustained load on an 8-core server):
- Express 4.x:~18,400 req/sec, average latency 5.4 ms
- Fastify 4.x:~74,800 req/sec, average latency 1.3 ms