express, hapi, koa, and sails are all server-side frameworks for Node.js that handle HTTP requests, routing, and middleware, but they differ significantly in architecture and philosophy. express is the minimal standard, offering unopinionated routing and middleware support. hapi (now @hapi/hapi) focuses on configuration over code with built-in validation and security. koa is built by the Express team to be more modern and lightweight, using async functions and context objects instead of request/response pairs. sails is a full MVC framework built on Express, providing an ORM, blueprints, and WebSocket support out of the box.
All four frameworks enable you to build web servers in Node.js, but they solve common problems in very different ways. express is the minimal standard, hapi is configuration-heavy, koa is modern and async-focused, and sails is a full MVC suite. Let's compare how they handle core engineering tasks.
express uses a middleware function chain where you pass req and res objects.
next() to pass control.// express: Middleware chain
const express = require('express');
const app = express();
app.use((req, res, next) => {
console.log('Time:', Date.now());
next();
});
app.get('/', (req, res) => {
res.send('Hello World');
});
hapi uses a configuration object to define routes and handlers.
// hapi: Configuration based
const Hapi = require('@hapi/hapi');
const init = async () => {
const server = Hapi.server({ port: 3000, host: 'localhost' });
server.route({
method: 'GET',
path: '/',
handler: (request, h) => {
return 'Hello World';
}
});
await server.start();
};
koa uses async functions and a single ctx (context) object.
req or res passed around; everything is on ctx.// koa: Async context
const Koa = require('koa');
const app = new Koa();
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
sails uses controllers and actions within an MVC structure.
api/controllers/HomeController.js.res.ok() simplify responses.// sails: Controller action
// api/controllers/HomeController.js
module.exports = {
index: async function (req, res) {
return res.ok('Hello World');
}
};
express relies on passing errors to next(err).
next with an error argument.// express: Error middleware
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('Something broke!');
});
hapi uses standard try/catch blocks within handlers.
// hapi: Try/catch in handler
server.route({
method: 'GET',
path: '/',
handler: async (request, h) => {
try {
const data = await getData();
return data;
} catch (err) {
throw err; // Handled by hapi
}
}
});
koa leverages native async/await try/catch.
// koa: Native try/catch
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.status = err.status || 500;
ctx.body = err.message;
}
});
sails provides built-in error responses and policies.
res.serverError() or similar helpers.config/responses.js.// sails: Built-in error response
module.exports = {
index: async function (req, res) {
try {
await SomeService.doWork();
return res.ok();
} catch (err) {
return res.serverError(err);
}
}
};
express defines routes directly on the app or router instance.
// express: Direct routing
app.get('/users/:id', (req, res) => {
res.json({ id: req.params.id });
});
hapi defines routes in a configuration array or object.
// hapi: Route config
server.route({
method: 'GET',
path: '/users/{id}',
handler: (request, h) => {
return { id: request.params.id };
}
});
koa does not include routing by default; you install koa-router.
// koa: External router
const Router = require('koa-router');
const router = new Router();
router.get('/users/:id', ctx => {
ctx.body = { id: ctx.params.id };
});
app.use(router.routes());
sails uses a config/routes.js file or automatic blueprints.
// sails: config/routes.js
'GET /users/:id': 'HomeController.index',
// Or automatic blueprint routing based on controller name
While the differences are clear, all four frameworks share core Node.js foundations. Here are key overlaps:
http module.// All frameworks ultimately listen on a port
// express: app.listen(3000)
// hapi: await server.start()
// koa: app.listen(3000)
// sails: sails.lift({ port: 3000 })
// Example: Logging middleware concept
// express: app.use(logger())
// koa: app.use(logger())
// hapi: await server.register(require('hapi-pino'))
// sails: config.http.middleware order includes logger
helmet.// Example: CORS setup
// express: app.use(cors())
// koa: app.use(cors())
// hapi: server.auth.strategy(...)
// sails: config/security.js
// Example: Sending JSON
// express: res.json({ data: 1 })
// hapi: return h.response({ data: 1 })
// koa: ctx.body = { data: 1 }
// sails: return res.json({ data: 1 })
| Feature | express | hapi | koa | sails |
|---|---|---|---|---|
| Philosophy | Minimal, unopinionated | Configuration over code | Modern, async-first | Full MVC framework |
| Routing | Built-in | Built-in config | External (koa-router) | Built-in + Blueprints |
| Error Handling | next(err) | Try/catch | Try/catch | Built-in resolvers |
| Request Object | req, res | request, h | ctx | req, res |
| Learning Curve | Low | Medium | Medium | High |
| Best For | APIs, Microservices | Enterprise, Validation | Performance, Custom | Rapid Prototyping, Real-time |
express is the safe choice π‘οΈ β it has the most resources, hires are easy to find, and it gets the job done without fuss. Ideal for most standard APIs and web services.
hapi is the structured choice π β great for large teams that need strict validation and configuration rules. Best when security and input validation are top priorities.
koa is the modern choice π β perfect for developers who want clean async code and don't mind assembling their own tools. Best for high-performance services where every millisecond counts.
sails is the complete choice ποΈ β suited for teams that want a Rails-like experience in Node.js. Best for startups needing real-time features and database models out of the box.
Final Thought: All four frameworks are capable production tools. Your choice should depend on how much structure you want imposed on your team versus how much freedom you need to build your own architecture.
Choose koa if you want a modern, lightweight foundation that leverages async/await without callback hell. It is best for teams that want to build their own middleware stack from scratch without the baggage of older Node.js patterns. It shines in high-performance scenarios where you need fine-grained control over the request context.
Choose sails if you need a full-featured MVC framework similar to Ruby on Rails or Django. It is perfect for rapid prototyping, real-time apps with WebSocket support, and projects that benefit from a built-in ORM and blueprints. Avoid it if you prefer minimal dependencies or want to pick each library yourself.
Choose express if you want the industry standard with the largest ecosystem of middleware and tutorials. It is ideal for teams that need flexibility without strict rules, allowing you to build your architecture from the ground up. It works well for APIs, microservices, and server-rendered views where you want control over every layer.
Choose hapi if you prefer configuration over code and need built-in input validation and security features. It is suitable for large enterprise applications where strict structure and plugin isolation are required. Note that the package scope changed to @hapi/hapi in recent versions, so ensure your team is aware of the migration path.
Expressive HTTP middleware framework for node.js to make web applications and APIs more enjoyable to write. Koa's middleware stack flows in a stack-like manner, allowing you to perform actions downstream then filter and manipulate the response upstream.
Only methods that are common to nearly all HTTP servers are integrated directly into Koa's small ~570 SLOC codebase. This includes things like content negotiation, normalization of node inconsistencies, redirection, and a few others.
Koa is not bundled with any middleware.
Koa requires node v18.0.0 or higher for ES2015 and async function support.
npm install koa
const Koa = require('koa');
const app = new Koa();
// response
app.use(ctx => {
ctx.body = 'Hello Koa';
});
app.listen(3000);
Koa is a middleware framework that can take two different kinds of functions as middleware:
Here is an example of logger middleware with each of the different functions:
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});
// Middleware normally takes two parameters (ctx, next), ctx is the context for one request,
// next is a function that is invoked to execute the downstream middleware. It returns a Promise with a then function for running code after completion.
app.use((ctx, next) => {
const start = Date.now();
return next().then(() => {
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});
});
The middleware signature changed between v1.x and v2.x. The older signature is deprecated.
Old signature middleware support has been removed in v3
Please see the Migration Guide from v2.x to v3.x for information on upgrading from v2.x to v3.x, and the Migration Guide from v1.x to v2.x for information on upgrading from v1.x to v2.x.
Each middleware receives a Koa Context object that encapsulates an incoming
http message and the corresponding response to that message. ctx is often used
as the parameter name for the context object.
app.use(async (ctx, next) => { await next(); });
Koa provides a Request object as the request property of the Context.
Koa's Request object provides helpful methods for working with
http requests which delegate to an IncomingMessage
from the node http module.
Here is an example of checking that a requesting client supports xml.
app.use(async (ctx, next) => {
ctx.assert(ctx.request.accepts('xml'), 406);
// equivalent to:
// if (!ctx.request.accepts('xml')) ctx.throw(406);
await next();
});
Koa provides a Response object as the response property of the Context.
Koa's Response object provides helpful methods for working with
http responses which delegate to a ServerResponse
.
Koa's pattern of delegating to Node's request and response objects rather than extending them
provides a cleaner interface and reduces conflicts between different middleware and with Node
itself as well as providing better support for stream handling. The IncomingMessage can still be
directly accessed as the req property on the Context and ServerResponse can be directly
accessed as the res property on the Context.
Here is an example using Koa's Response object to stream a file as the response body.
app.use(async (ctx, next) => {
await next();
ctx.response.type = 'xml';
ctx.response.body = fs.createReadStream('really_large.xml');
});
The Context object also provides shortcuts for methods on its request and response. In the prior
examples, ctx.type can be used instead of ctx.response.type and ctx.accepts can be used
instead of ctx.request.accepts.
For more information on Request, Response and Context, see the Request API Reference,
Response API Reference and Context API Reference.
The object created when executing new Koa() is known as the Koa application object.
The application object is Koa's interface with node's http server and handles the registration of middleware, dispatching to the middleware from http, default error handling, as well as configuration of the context, request and response objects.
Learn more about the application object in the Application API Reference.
Check the Troubleshooting Guide or Debugging Koa in the general Koa guide.
$ npm test
To report a security vulnerability, please do not open an issue, as this notifies attackers of the vulnerability. Instead, please email dead_horse, jonathanong, and niftylettuce to disclose.
See AUTHORS.
Looking for a career upgrade?
Support us with a monthly donation and help us continue our activities.
Become a sponsor and get your logo on our README on Github with a link to your site.