express vs @nestjs/common vs koa vs hapi
Backend Frameworks and Architectural Patterns in Node.js
express@nestjs/commonkoahapiSimilar Packages:

Backend Frameworks and Architectural Patterns in Node.js

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.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
express129,453,71469,39675.4 kB2249 months agoMIT
@nestjs/common15,311,55976,449474 kB2410 days agoMIT
koa8,143,07435,69065 kB393 months agoMIT
hapi71,88714,793-568 years agoBSD-3-Clause

Backend Frameworks and Architectural Patterns in Node.js

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.

🏗️ Architecture & Bootstrapping

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();

🛣️ Routing & Handlers

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 };
  }
}

🧩 Middleware & Logic Flow

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
  }
}

⚠️ Error Handling

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
  }
}

📊 Summary Table

Featureexpresskoahapi@nestjs/common
StyleMinimalistModern AsyncConfigurationOpinionated Framework
RoutingBuilt-inExternal PluginBuilt-inDecorators
MiddlewareCallback ChainAsync StackExtension PointsGuards/Interceptors
TypeScriptCommunity TypesCommunity TypesCommunity TypesFirst-Class Support
Learning CurveLowMediumMediumHigh

💡 The Big Picture

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.

How to Choose: express vs @nestjs/common vs koa vs hapi

  • express:

    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.

  • @nestjs/common:

    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.

  • koa:

    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.

  • hapi:

    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.

README for express

Express Logo

Fast, unopinionated, minimalist web framework for Node.js.

This project has a Code of Conduct.

Table of contents

NPM Version NPM Downloads Linux Build Test Coverage OpenSSF Scorecard Badge

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')
})

Installation

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.

Features

  • Robust routing
  • Focus on high performance
  • Super-high test coverage
  • HTTP helpers (redirection, caching, etc)
  • View system supporting 14+ template engines
  • Content negotiation
  • Executable for generating applications quickly

Docs & Community

PROTIP Be sure to read the migration guide to v5

Quick Start

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

Philosophy

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.

Examples

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

Contributing

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.

Security Issues

If you discover a security vulnerability in Express, please see Security Policies and Procedures.

Running Tests

To run the test suite, first install the dependencies:

npm install

Then run npm test:

npm test

Current project team members

For information about the governance of the express.js project, see GOVERNANCE.md.

The original author of Express is TJ Holowaychuk

List of all contributors

TC (Technical Committee)

TC emeriti members

TC emeriti members

Triagers

Triagers emeriti members

Emeritus Triagers

License

MIT