express, hapi, and koa are standalone web frameworks that handle HTTP requests directly, while @nestjs/common is the core library for the NestJS framework. NestJS provides a structured, decorator-based architecture that runs on top of platforms like Express or Fastify, offering a full application framework rather than just a server library.
When building server-side applications in Node.js, the choice of framework shapes everything from folder structure to error handling. express, hapi, and koa are standalone servers, while @nestjs/common represents the NestJS framework which adds a heavy architectural layer on top of HTTP platforms. Let's compare how they handle real-world engineering tasks.
express is minimal. You create an app and listen on a port. There is no enforced structure.
// express: Minimal setup
const express = require('express');
const app = express();
app.listen(3000, () => {
console.log('Server running on port 3000');
});
koa is similar but uses ES6 classes and async functions by default. It does not include routing or request handling in the core.
// koa: Class-based setup
const Koa = require('koa');
const app = new Koa();
app.listen(3000, () => {
console.log('Server running on port 3000');
});
hapi requires a server object and explicit connection configuration. It is more verbose but enforces configuration early.
// hapi: Configuration-driven setup
const Hapi = require('@hapi/hapi');
const init = async () => {
const server = Hapi.server({ port: 3000, host: 'localhost' });
await server.start();
console.log('Server running on port 3000');
};
@nestjs/common is part of NestJS. You do not start a server with this package alone. You use it to define modules and controllers, then bootstrap via @nestjs/core.
// nestjs: Module-based bootstrap
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();
express attaches routes directly to the app instance using HTTP verbs.
// express: Direct routing
app.get('/users/:id', (req, res) => {
res.json({ id: req.params.id });
});
koa does not have built-in routing. You must install @koa/router or similar.
// koa: External router required
const Router = require('@koa/router');
const router = new Router();
router.get('/users/:id', (ctx) => {
ctx.body = { id: ctx.params.id };
});
app.use(router.routes());
hapi defines routes in a configuration object with a clear separation of method, path, and handler.
// hapi: Config-based routing
server.route({
method: 'GET',
path: '/users/{id}',
handler: (request, h) => {
return { id: request.params.id };
}
});
@nestjs/common uses decorators to define controllers and routes. This keeps routing logic close to the class methods.
// nestjs: Decorator-based routing
import { Controller, Get, Param } from '@nestjs/common';
@Controller('users')
export class UserController {
@Get(':id')
findOne(@Param('id') id: string) {
return { id };
}
}
express uses a request-response middleware chain. You call next() to pass control.
// express: Middleware chain
app.use((req, res, next) => {
console.log('Time:', Date.now());
next();
});
koa uses a stack-like middleware flow with async/await. You call await next() to go down the stack.
// koa: Async middleware stack
app.use(async (ctx, next) => {
console.log('Time:', Date.now());
await next();
});
hapi uses extension points (onRequest, onPreHandler, etc.) rather than a free-form middleware chain. This is more structured.
// hapi: Extension points
server.ext('onRequest', (request, h) => {
console.log('Time:', Date.now());
return h.continue;
});
@nestjs/common uses Guards, Interceptors, and Pipes. These are dependency-injected classes rather than functions.
// nestjs: Class-based guards
import { Injectable, CanActivate } from '@nestjs/common';
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(context): boolean {
return true; // Logic here
}
}
express relies on middleware with four arguments (err, req, res, next). It is easy to miss passing errors.
// express: Error middleware
app.use((err, req, res, next) => {
res.status(500).send('Something broke!');
});
koa uses try/catch blocks within async middleware. It handles errors more naturally with async flows.
// koa: Try/catch handling
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.status = 500;
ctx.body = 'Something broke!';
}
});
hapi has built-in error handling via the onPreResponse extension or try/catch in handlers. It standardizes error responses.
// hapi: Standardized errors
server.ext('onPreResponse', (request, h) => {
if (request.response.isBoom) {
// Handle Boom error
}
return h.continue;
});
@nestjs/common uses Exception Filters. You can create custom filters that catch specific error types globally.
// nestjs: Exception filters
import { ExceptionFilter, Catch } from '@nestjs/common';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception, host) {
// Handle exception globally
}
}
| Feature | express | koa | hapi | @nestjs/common |
|---|---|---|---|---|
| Style | Minimalist | Modern Async | Configuration | Opinionated Framework |
| Routing | Built-in | External Plugin | Built-in | Decorators |
| Middleware | Callback Chain | Async Stack | Extension Points | Guards/Interceptors |
| TypeScript | Community Types | Community Types | Community Types | First-Class Support |
| Learning Curve | Low | Medium | Medium | High |
express remains the standard for simplicity. If you need to build a quick API or microservice without strict rules, it is the fastest path. However, you must enforce your own structure as the project grows.
koa is the spiritual successor to Express for those who want modern JavaScript features without the baggage. It is excellent if you want to build your own framework on top of a solid core.
hapi is the enterprise choice for configuration-driven development. It reduces the risk of security mistakes by baking validation and security into the core, but it requires more initial setup.
@nestjs/common (NestJS) is the choice for large teams. It forces a modular structure that scales well. The learning curve is steep, but it pays off in maintainability for complex domains.
Final Thought: If you are a frontend team dipping into backend, express is familiar. If you are building a long-term product with a dedicated backend team, @nestjs/common provides the guardrails you need.
Choose express if you need a minimal, flexible foundation for APIs or web servers with a massive ecosystem of middleware. It is best for small to medium projects, microservices, or when you want full control over the architecture without framework-imposed constraints.
Choose this if you are building a large-scale enterprise application that requires strict structure, dependency injection, and deep TypeScript integration. It is ideal for teams that prefer an Angular-like modular architecture and need built-in support for testing, validation, and microservices.
Choose koa if you want a modern, lightweight core built by the Express team that fully embraces async/await. It is perfect for developers who find Express callback patterns outdated and want a clean slate to build their own middleware structure.
Choose hapi if you prioritize configuration over code and need robust built-in security, validation, and logging without relying on third-party plugins. It suits enterprise environments where strict governance and stability are more important than minimal setup time.
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