When you're building backend apps with Node.js, two frameworks really stand out: Express.js and Fastify. Both are solid choices, but they work differently and excel in different areas. Let me break down what makes each one special and help you figure out which one might work better for your next project.
Overview
Express.js: This has been around the block. It's the framework most developers learn first, and for good reason. Express keeps things simple and gives you tons of flexibility. The community is huge, which means you'll find middleware for pretty much anything you need. It's like the reliable old friend of Node.js frameworks.
Fastify: Think of this as the performance-focused younger sibling. Fastify was built from the ground up to be fast and handle lots of traffic without breaking a sweat. It comes with built-in validation using JSON schemas, which is pretty neat. If speed is your main concern, Fastify might be your answer.
Setting up Express and Fastify
Let's see how easy it is to get a basic server running with both frameworks.
Express.js
// express-setup.js
const express = require('express');
const app = express();
app.use(express.json()); // Parse JSON data
app.get('/', (req, res) => {
res.send('Hello from Express!');
});
app.listen(3000, () => {
console.log('Express server running on http://localhost:3000');
});
Fastify
// fastify-setup.js
const fastify = require('fastify')();
fastify.get('/', async (request, reply) => {
return { message: 'Hello from Fastify!' };
});
fastify.listen(3000, (err) => {
if (err) {
console.error(err);
process.exit(1);
}
console.log('Fastify server running on http://localhost:3000');
});
Both setups are pretty straightforward. Notice how Fastify just returns an object for JSON responses - it handles the conversion automatically, which is one of those little touches that makes it faster.
Request validation
Here's where things get interesting. Making sure the data coming into your API is valid is super important. Fastify has this built right in, while Express needs some help from external libraries like Joi.
Express.js (validation with Joi)
// express-validation.js
const express = require('express');
const Joi = require('joi');
const app = express();
app.use(express.json());
const userSchema = Joi.object({
name: Joi.string().required(),
age: Joi.number().integer().min(0).required(),
});
app.post('/user', (req, res) => {
const { error } = userSchema.validate(req.body);
if (error) return res.status(400).send(error.details[0].message);
res.send(`User ${req.body.name} validated successfully!`);
});
app.listen(3000, () => console.log('Express validation example running'));
Fastify (built-in JSON schema validation)
// fastify-validation.js
const fastify = require('fastify')();
fastify.post('/user', {
schema: {
body: {
type: 'object',
required: ['name', 'age'],
properties: {
name: { type: 'string' },
age: { type: 'integer', minimum: 0 },
},
},
},
handler: async (request, reply) => {
const { name, age } = request.body;
return { message: `User ${name} is ${age} years old.` };
},
});
fastify.listen(3000, (err) => {
if (err) throw err;
console.log('Fastify validation example running');
});
Middleware and plugins
Express uses middleware (functions that run between request and response), while Fastify uses a plugin system. Both work well, but they feel different when you're coding.
Express.js middleware example
// express-middleware.js
const express = require('express');
const app = express();
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
});
app.get('/', (req, res) => {
res.send('Middleware in Express!');
});
app.listen(3000, () => console.log('Express middleware example running'));
Fastify plugin example
// fastify-plugin.js
const fastify = require('fastify')();
fastify.register(async (instance) => {
instance.addHook('preHandler', async (request, reply) => {
console.log(`${request.method} ${request.url}`);
});
});
fastify.get('/', async (request, reply) => {
return { message: 'Plugins in Fastify!' };
});
fastify.listen(3000, (err) => {
if (err) throw err;
console.log('Fastify plugin example running');
});
Fastify's plugin system is pretty clever. It keeps things organized and can actually boost performance because of how it handles the lifecycle of requests.
Error handling
When things go wrong (and they will), both frameworks let you handle errors in a clean, centralized way.
Express.js error handling
// express-error-handling.js
const express = require('express');
const app = express();
app.get('/', (req, res) => {
throw new Error('Oops! Something went wrong.');
});
app.use((err, req, res, next) => {
console.error(err.message);
res.status(500).send('Internal Server Error');
});
app.listen(3000, () => console.log('Express error handling example running'));
Fastify error handling
// fastify-error-handling.js
const fastify = require('fastify')();
fastify.setErrorHandler((error, request, reply) => {
console.error(error.message);
reply.status(500).send({ error: 'Internal Server Error' });
});
fastify.get('/', async (request, reply) => {
throw new Error('Oops! Something went wrong in Fastify.');
});
fastify.listen(3000, (err) => {
if (err) throw err;
console.log('Fastify error handling example running');
});
Handling complex responses and optimization
Here's where Fastify really shines. It can optimize how it sends JSON responses by using schemas, which makes everything faster.
// fastify-optimized-response.js
const fastify = require('fastify')();
fastify.get('/data', {
schema: {
response: {
200: {
type: 'object',
properties: {
id: { type: 'number' },
name: { type: 'string' },
active: { type: 'boolean' },
},
},
},
},
handler: async (request, reply) => {
return { id: 1, name: 'John Doe', active: true };
},
});
fastify.listen(3000, (err) => {
if (err) throw err;
console.log('Fastify optimized response example running');
});
TypeScript support
If you're working on bigger projects, TypeScript can save you from a lot of headaches. Both frameworks work with TypeScript, but Fastify's approach feels more natural.
Fastify with TypeScript
import Fastify, { FastifyInstance } from 'fastify';
const server: FastifyInstance = Fastify();
server.get('/ping', async (request, reply) => {
return { pong: 'it worked!' };
});
server.listen(3000, (err) => {
if (err) {
console.error(err);
process.exit(1);
}
console.log('Fastify with TypeScript example running');
});
Summary
So which one should you pick? It depends on what you're building. Go with Express if you want something familiar and flexible - it's great for general apps and prototypes. Choose Fastify if you need speed and built-in validation for high-traffic applications. Both are solid choices.