How to Migrate from Express to Fastify
·PkgPulse Team
TL;DR
Migrating from Express to Fastify gives you 2-3x better throughput and first-class TypeScript types. The route handler API is similar enough that most migrations are mechanical. Key differences: Fastify uses plugins (not middleware), request/response objects have different APIs, and you get built-in JSON schema validation. Most Express apps migrate in a day or two; complex middleware chains may take longer.
Key Takeaways
- 2-3x throughput improvement — Fastify handles ~77K req/sec vs Express's ~27K
- Plugin system replaces middleware —
fastify.register()instead ofapp.use() - Built-in JSON Schema validation — no separate validation library needed for basic cases
- TypeScript-first — Fastify generics type request body, params, query, response
fastify-expressfor gradual migration — run Express middleware inside Fastify
Step 1: Install Fastify
npm install fastify
npm install -D @types/node # If TypeScript
# Common Fastify plugins (replacing Express middleware):
npm install @fastify/cors # cors
npm install @fastify/helmet # helmet
npm install @fastify/rate-limit # express-rate-limit
npm install @fastify/jwt # jsonwebtoken wrapper
npm install @fastify/cookie # cookie-parser
npm install @fastify/static # express.static
npm install @fastify/multipart # multer (file uploads)
npm install @fastify/sensible # HTTP error helpers
Step 2: Route Handler Migration
// Express route syntax:
app.get('/users/:id', async (req, res) => {
const { id } = req.params;
const user = await db.user.findById(id);
if (!user) return res.status(404).json({ error: 'Not found' });
res.json(user);
});
app.post('/users', async (req, res) => {
const { name, email } = req.body;
const user = await db.user.create({ name, email });
res.status(201).json(user);
});
// Fastify equivalent:
fastify.get('/users/:id', async (request, reply) => {
const { id } = request.params as { id: string };
const user = await db.user.findById(id);
if (!user) return reply.code(404).send({ error: 'Not found' });
return user; // Just return — Fastify auto-serializes
});
fastify.post('/users', async (request, reply) => {
const { name, email } = request.body as { name: string; email: string };
const user = await db.user.create({ name, email });
return reply.code(201).send(user);
});
TypeScript-Typed Routes (Fastify advantage)
// Fastify generics give you full type safety:
interface CreateUserBody {
name: string;
email: string;
}
interface UserParams {
id: string;
}
interface UserResponse {
id: string;
name: string;
email: string;
createdAt: string;
}
fastify.post<{
Body: CreateUserBody;
Reply: UserResponse;
}>('/users', async (request, reply) => {
const { name, email } = request.body;
// ↑ TypeScript knows: { name: string; email: string }
const user = await db.user.create({ name, email });
return reply.code(201).send(user);
// ↑ TypeScript validates: must match UserResponse shape
});
Step 3: Middleware → Plugin Migration
// Express middleware pattern:
app.use(cors());
app.use(helmet());
app.use(express.json());
// Middleware order matters — app.use() wraps all routes below it
// Fastify plugin pattern:
// 1. Register global plugins before routes
// 2. Plugins are isolated by scope (can register per-route-group)
const fastify = Fastify({ logger: true });
// Register core plugins
await fastify.register(import('@fastify/cors'), {
origin: process.env.CORS_ORIGIN,
credentials: true,
});
await fastify.register(import('@fastify/helmet'));
await fastify.register(import('@fastify/sensible')); // Adds reply.notFound(), etc.
// Route-scoped plugins (only applies to routes in this scope)
await fastify.register(async (scope) => {
await scope.register(import('@fastify/jwt'), {
secret: process.env.JWT_SECRET!,
});
// These routes require JWT auth:
scope.addHook('preHandler', scope.authenticate);
scope.get('/me', async (request) => {
return request.user;
});
}, { prefix: '/api/v1' });
Step 4: Hooks (Replacing Middleware Logic)
// Express: use middleware for cross-cutting concerns
app.use((req, res, next) => {
req.requestId = uuid();
next();
});
// Fastify: use hooks
fastify.addHook('onRequest', async (request, reply) => {
request.requestId = crypto.randomUUID();
});
// All Fastify hooks (in order of execution):
// 1. onRequest — first, before parsing
// 2. preParsing — before body parsing
// 3. preValidation — before schema validation
// 4. preHandler — before route handler (for auth, etc.)
// 5. handler — your route function
// 6. preSerialization — before response serialization
// 7. onSend — before sending response
// 8. onResponse — after response sent
// 9. onError — on unhandled error
Step 5: Error Handling
// Express error middleware:
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
console.error(err.stack);
res.status(500).json({ error: 'Internal Server Error' });
});
// Fastify error handler:
fastify.setErrorHandler(async (error, request, reply) => {
fastify.log.error(error);
// Fastify HTTPError (from @fastify/sensible)
if (error.statusCode) {
return reply.code(error.statusCode).send({ error: error.message });
}
// Validation errors (from JSON Schema)
if (error.validation) {
return reply.code(400).send({
error: 'Validation Error',
details: error.validation,
});
}
return reply.code(500).send({ error: 'Internal Server Error' });
});
// In route handlers — throw to trigger the error handler:
fastify.get('/users/:id', async (request, reply) => {
const user = await db.user.findById(request.params.id);
if (!user) throw reply.notFound('User not found'); // @fastify/sensible
return user;
});
Step 6: JSON Schema Validation (Fastify Bonus)
// Fastify validates request/response automatically with JSON Schema
// No need for Joi, Zod middleware layer, or manual validation
fastify.post('/users', {
schema: {
body: {
type: 'object',
required: ['name', 'email'],
properties: {
name: { type: 'string', minLength: 1, maxLength: 50 },
email: { type: 'string', format: 'email' },
},
},
response: {
201: {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
email: { type: 'string' },
},
},
},
},
}, async (request, reply) => {
// request.body is validated — name and email guaranteed to exist
const user = await db.user.create(request.body);
return reply.code(201).send(user);
});
// Or use Zod with @fastify/type-provider-zod:
import { serializerCompiler, validatorCompiler } from 'fastify-type-provider-zod';
fastify.setValidatorCompiler(validatorCompiler);
fastify.setSerializerCompiler(serializerCompiler);
Gradual Migration: Run Both
// @fastify/express — run Express middleware inside Fastify
// Use during incremental migration
import fastifyExpress from '@fastify/express';
fastify.register(fastifyExpress);
// Now you can use Express middleware in Fastify:
fastify.use(expressMiddleware()); // Temporary bridge
Compare Express and Fastify on PkgPulse.
See the live comparison
View express vs. fastify on PkgPulse →