express, fastify, hapi, koa, and restify are all popular Node.js frameworks used to build web servers and APIs, but they differ significantly in architecture, performance, and design philosophy. express is the most widely adopted, offering a minimal, unopinionated structure that relies heavily on middleware. fastify focuses on extreme speed and low overhead, featuring a built-in schema-based validation system. hapi provides a robust, configuration-driven approach with strong security defaults, often favored for large enterprise applications. koa is a modern, lightweight framework created by the Express team that uses async/await to eliminate callback hell, though it lacks built-in middleware. restify is specialized for building strict REST APIs, prioritizing performance and observability over general-purpose web features.
When building backend services in Node.js, choosing the right framework sets the tone for your entire project architecture. While express, fastify, hapi, koa, and restify all handle HTTP requests, they solve problems in very different ways. This comparison breaks down their core mechanics, helping you decide which tool fits your specific engineering needs.
The biggest difference lies in how much the framework decides for you. Some give you a blank canvas, while others provide a rigid blueprint.
express is unopinionated. It gives you a simple app instance and lets you stack middleware however you like. You decide the folder structure, error handling strategy, and validation logic.
// express: Minimal setup, you add everything
const express = require('express');
const app = express();
app.use(express.json()); // You must manually add body parser
app.get('/hello', (req, res) => {
res.send('Hello World');
});
fastify is also flexible but comes with more batteries included. It has a powerful plugin system and built-in validation, yet it keeps the core light.
// fastify: Built-in validation and serialization
const fastify = require('fastify')();
fastify.get('/hello', {
schema: {
response: { 200: { type: 'string' } }
}
}, async (request, reply) => {
return 'Hello World'; // Auto-serialized
});
hapi is highly opinionated. It forces a specific configuration style and separates concerns strictly. You define routes with detailed config objects, not just callbacks.
// hapi: Configuration-driven routes
const Hapi = require('@hapi/hapi');
const init = async () => {
const server = Hapi.server({ port: 3000 });
server.route({
method: 'GET',
path: '/hello',
handler: (request, h) => {
return 'Hello World';
}
});
await server.start();
};
koa is a minimal kernel. It doesn't include routing or request body parsing. You build the middleware stack yourself using async functions.
// koa: No built-in router or body parser
const Koa = require('koa');
const app = new Koa();
app.use(async ctx => {
ctx.body = 'Hello World';
});
restify is specialized for REST APIs. It skips HTML rendering features entirely and focuses on request handling pipelines.
// restify: Strictly for API endpoints
const restify = require('restify');
const server = restify.createServer();
server.get('/hello', (req, res, next) => {
res.send('Hello World');
return next();
});
Speed matters, especially for microservices. How a framework handles incoming data and validates it changes your code complexity.
express relies on third-party middleware for validation (like joi or zod). This adds flexibility but requires more setup code.
// express: Manual validation middleware
app.post('/user', (req, res, next) => {
if (!req.body.email) {
return res.status(400).send('Email required');
}
next();
});
fastify shines here. It uses JSON Schema to validate input and output automatically, which is faster than manual checks and reduces boilerplate.
// fastify: Schema-based validation
fastify.post('/user', {
schema: {
body: {
type: 'object',
required: ['email'],
properties: { email: { type: 'string' } }
}
}
}, async (request, reply) => {
return { status: 'created' }; // Validated automatically
});
hapi also has built-in validation (via joi integrated deeply). It validates params, query, payload, and headers before your handler even runs.
// hapi: Integrated validation
server.route({
method: 'POST',
path: '/user',
options: {
validate: {
payload: {
email: Joi.string().required()
}
}
},
handler: (request, h) => ({ status: 'created' })
});
koa leaves validation entirely to you. You pick your own library and wrap it in middleware.
// koa: Custom validation middleware
app.use(async (ctx, next) => {
if (ctx.request.body.email === undefined) {
ctx.status = 400;
ctx.body = 'Email required';
return;
}
await next();
});
restify includes plugins for parsing and validation but is less focused on schema enforcement than Fastify or Hapi. It prioritizes raw throughput.
// restify: Using plugins for parsing
server.use(restify.plugins.bodyParser());
server.post('/user', (req, res, next) => {
if (!req.params.email) {
return next(new restify.BadRequestError('Email required'));
}
res.send(201);
return next();
});
How the framework handles asynchronous logic affects code readability.
express traditionally uses the (req, res, next) callback pattern. While you can use async/await now, error handling still often relies on passing errors to next().
// express: Next() pattern for errors
app.get('/data', async (req, res, next) => {
try {
const data = await db.fetch();
res.json(data);
} catch (err) {
next(err); // Must pass to error handler
}
});
fastify embraces async/await fully. If you return a promise or throw an error, it handles the response automatically.
// fastify: Native async/await
fastify.get('/data', async (request, reply) => {
const data = await db.fetch(); // No try/catch needed for 500s
return data; // Auto-sent as JSON
});
hapi uses async functions for handlers. Errors thrown are automatically caught and converted to appropriate HTTP responses.
// hapi: Async handlers
handler: async (request, h) => {
const data = await db.fetch();
return data; // Errors caught automatically
}
koa was built for async/await from day one. It uses ctx (context) instead of separate req/res objects, making flow very clean.
// koa: Context-based async flow
app.use(async ctx => {
ctx.body = await db.fetch(); // Clean and linear
});
restify uses the next() chain extensively. It is designed around middleware chains where every step must call next() to proceed.
// restify: Explicit next() chain
server.get('/data', [
async (req, res, next) => {
req.data = await db.fetch();
return next();
},
(req, res, next) => {
res.send(req.data);
return next();
}
]);
Security defaults vary wildly between these tools.
express has no security defaults. You must install helmet for headers, cors for cross-origin rules, and rate limiters manually.
// express: Manual security setup
const helmet = require('helmet');
app.use(helmet());
app.use(require('cors')());
fastify includes some security headers by default and makes it easy to add others via plugins. Its error handling is consistent via hooks.
// fastify: Global error hook
fastify.setErrorHandler((error, request, reply) => {
reply.code(500).send({ error: 'Something went wrong' });
});
hapi is famous for security. It disables unnecessary features by default and requires explicit configuration for things like CORS and authentication.
// hapi: Explicit security config
await server.register(require('@hapi/inert')); // For static files
server.auth.strategy('jwt', 'jwt', { keys: 'secret' });
koa is barebones. Security is 100% your responsibility via middleware selection.
// koa: Import security middleware
app.use(require('koa-helmet')());
restify includes DDoS protection and rate limiting plugins out of the box, tailored for public APIs.
// restify: Built-in throttle
server.use(restify.plugins.throttle({
burst: 100,
rate: 50
}));
| Feature | express | fastify | hapi | koa | restify |
|---|---|---|---|---|---|
| Philosophy | Minimal, Unopinionated | High Performance, Schema-First | Configuration-Driven, Secure | Modern, Minimal Kernel | Strict REST API Focus |
| Validation | Manual (3rd party) | Built-in (JSON Schema) | Built-in (Joi) | Manual (3rd party) | Plugin-based |
| Async Style | Callback / Async | Async / Await | Async / Await | Async / Await | Callback Chain (next) |
| Routing | Built-in | Built-in | Built-in | External (e.g., koa-router) | Built-in |
| Best For | General Web Apps | Microservices / High Load | Enterprise Systems | Custom Middleware Stacks | Public REST APIs |
express remains the safe bet for general-purpose web apps due to its vast ecosystem. If you need a tutorial for it, one exists. If you need a plugin, it's already written.
fastify is the modern choice for performance-critical services. If you are tired of writing validation boilerplate and want speed, this is the upgrade path.
hapi suits large teams needing strict governance. Its verbosity prevents sloppy code, making it ideal for banking or healthcare systems where structure saves lives.
koa is for purists who want to construct their own framework. It offers the cleanest async syntax but requires you to make every architectural decision.
restify is a specialist tool. Use it if you are building a public API gateway where observability and strict REST compliance matter more than flexibility.
Final Thought: There is no single "best" framework. express wins on community, fastify on speed, hapi on structure, koa on elegance, and restify on API specificity. Match the tool to your team's constraints and your project's scale.
Choose express if you need a mature, flexible framework with a massive ecosystem of plugins and community support. It is ideal for projects where rapid prototyping is key, or when your team is already familiar with its middleware pattern. However, be prepared to manually assemble security and validation tools, as the core is minimal.
Choose fastify if performance is your top priority and you want built-in features like JSON schema validation and logging without extra dependencies. It is perfect for high-throughput microservices where reducing latency and boilerplate code is critical. Its plugin architecture is robust, but the learning curve can be slightly steeper due to its specific lifecycle hooks.
Choose koa if you want a modern, lightweight foundation that leverages async/await for cleaner control flow without the baggage of legacy callback patterns. It is best for developers who enjoy building their own middleware stack from scratch and value minimalism. Note that it does not include routing or body parsing by default, so you must select these tools yourself.
Choose restify if you are strictly building RESTful APIs and need built-in support for versioning, content negotiation, and detailed observability. It is designed for production-grade services where strict adherence to REST principles and performance monitoring are non-negotiable. Do not use it for rendering HTML views or general-purpose web applications, as it lacks those features.
Choose hapi if you are building large-scale enterprise applications that require strong configuration management and security out of the box. It is suitable for teams that prefer an opinionated structure with clear separation of concerns over raw flexibility. Avoid it for small projects where its verbose configuration might feel like over-engineering.
Fast, unopinionated, minimalist web framework for Node.js.
This project has a Code of Conduct.
import express from 'express'
const app = express()
app.get('/', (req, res) => {
res.send('Hello World')
})
app.listen(3000, () => {
console.log('Server is running on http://localhost:3000')
})
This is a Node.js module available through the npm registry.
Before installing, download and install Node.js. Node.js 18 or higher is required.
If this is a brand new project, make sure to create a package.json first with
the npm init command.
Installation is done using the
npm install command:
npm install express
Follow our installing guide for more information.
PROTIP Be sure to read the migration guide to v5
The quickest way to get started with express is to utilize the executable express(1) to generate an application as shown below:
Install the executable. The executable's major version will match Express's:
npm install -g express-generator@4
Create the app:
express /tmp/foo && cd /tmp/foo
Install dependencies:
npm install
Start the server:
npm start
View the website at: http://localhost:3000
The Express philosophy is to provide small, robust tooling for HTTP servers, making it a great solution for single page applications, websites, hybrids, or public HTTP APIs.
Express does not force you to use any specific ORM or template engine. With support for over 14 template engines via @ladjs/consolidate, you can quickly craft your perfect framework.
To view the examples, clone the Express repository:
git clone https://github.com/expressjs/express.git --depth 1 && cd express
Then install the dependencies:
npm install
Then run whichever example you want:
node examples/content-negotiation
The Express.js project welcomes all constructive contributions. Contributions take many forms, from code for bug fixes and enhancements, to additions and fixes to documentation, additional tests, triaging incoming pull requests and issues, and more!
See the Contributing Guide for more technical details on contributing.
If you discover a security vulnerability in Express, please see Security Policies and Procedures.
To run the test suite, first install the dependencies:
npm install
Then run npm test:
npm test
For information about the governance of the express.js project, see GOVERNANCE.md.
The original author of Express is TJ Holowaychuk