express vs hapi vs koa vs micro
Choosing the Right Node.js Web Framework for Backend Services
expresshapikoamicroSimilar Packages:

Choosing the Right Node.js Web Framework for Backend Services

express, hapi, koa, and micro are all popular Node.js frameworks used to build web servers and APIs, but they differ significantly in architecture, philosophy, and feature sets. express is the most widely adopted, offering a minimalist, unopinionated core with a massive ecosystem of middleware. hapi provides a rich, configuration-driven approach with built-in features for validation, caching, and security, favoring convention over code. koa, created by the Express team, modernizes the stack by leveraging async/await and removing callback-based middleware, offering a more elegant flow control. micro takes an extreme minimalist approach, focusing solely on building small, composable microservices with a functional programming style, relying heavily on native Node.js features.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
express069,26575.4 kB2238 months agoMIT
hapi014,796-567 years agoBSD-3-Clause
koa035,69665 kB422 months agoMIT
micro010,62242.1 kB8-MIT

Express vs Hapi vs Koa vs Micro: Architectural Deep Dive for Node.js Services

When building backend services in Node.js, the choice of framework dictates your application's structure, error handling strategy, and long-term maintainability. While express, hapi, koa, and micro all serve HTTP requests, their underlying architectures solve common problems in fundamentally different ways. Let's examine how they handle routing, middleware flow, error management, and extensibility.

πŸ›£οΈ Routing and Request Handling

How you define URL paths and handle incoming requests varies from implicit conventions to explicit configuration.

express uses imperative method chaining to define routes directly on the app instance. It is straightforward but can become unwieldy in large files without manual organization.

// express: Imperative route definition
const express = require('express');
const app = express();

app.get('/users/:id', (req, res) => {
  res.json({ id: req.params.id });
});

app.listen(3000);

hapi relies on a configuration object approach. Routes are defined in an array with explicit properties for method, path, and handler, promoting a clear separation of concerns.

// hapi: Configuration-based routes
const Hapi = require('@hapi/hapi');

const init = async () => {
  const server = Hapi.server({ port: 3000 });
  
  server.route({
    method: 'GET',
    path: '/users/{id}',
    handler: (request, h) => {
      return { id: request.params.id };
    }
  });

  await server.start();
};
init();

koa does not include a router in its core. You must install a separate router package (like @koa/router), keeping the core library small but requiring extra setup.

// koa: External router required
const Koa = require('koa');
const Router = require('@koa/router');

const app = new Koa();
const router = new Router();

router.get('/users/:id', (ctx) => {
  ctx.body = { id: ctx.params.id };
});

app.use(router.routes());
app.listen(3000);

micro has no built-in routing system. It expects you to parse the URL manually or use a helper like micro-fork or path-to-regexp to dispatch logic based on the request URL.

// micro: Manual URL parsing
const { parse } = require('url');
const micro = require('micro');

const server = micro(async (req, res) => {
  const { pathname } = parse(req.url);
  
  if (pathname.startsWith('/users/')) {
    const id = pathname.split('/')[2];
    return { id };
  }
  
  return { error: 'Not found' };
});

server.listen(3000);

πŸ”„ Middleware and Control Flow

The mechanism for executing logic before or after a request reaches the handler is where these frameworks diverge most sharply.

express uses a linear middleware stack based on callbacks. You must explicitly call next() to pass control to the next function; forgetting to do so hangs the request.

// express: Callback-based middleware with next()
app.use((req, res, next) => {
  console.log('Time:', Date.now());
  next(); // Must call next() to continue
});

app.get('/', (req, res) => {
  res.send('Hello');
});

hapi uses an extension point system (onRequest, pre, onResponse) rather than a free-form middleware stack. This enforces a structured lifecycle but offers less ad-hoc flexibility.

// hapi: Extension points in lifecycle
server.ext('onRequest', (request, h) => {
  request.log('info', 'Request received');
  return h.continue; // Explicitly continue
});

server.route({
  method: 'GET',
  path: '/',
  handler: (request, h) => 'Hello'
});

koa introduces a "downstream-upstream" flow using async/await. Middleware functions wrap around the next function, allowing logic to run both before and after the downstream middleware completes naturally.

// koa: Async/await composition
app.use(async (ctx, next) => {
  console.log('Start:', Date.now());
  await next(); // Waits for downstream to finish
  console.log('End:', Date.now());
});

app.use(async (ctx) => {
  ctx.body = 'Hello';
});

micro does not have a middleware stack. Instead, it encourages composing small, single-purpose functions. You wrap your main handler with higher-order functions to achieve similar effects.

// micro: Function composition
const withLogging = (handler) => async (req, res) => {
  console.log('Start:', Date.now());
  const result = await handler(req, res);
  console.log('End:', Date.now());
  return result;
};

const handler = withLogging(async (req, res) => {
  return { hello: 'world' };
});

⚠️ Error Handling Strategies

How errors are caught and returned to the client differs from automatic catches to manual try/catch blocks.

express requires a special error-handling middleware with four arguments (err, req, res, next). If you forget this signature, errors will not be caught.

// express: Special error middleware signature
app.get('/fail', (req, res, next) => {
  next(new Error('Something broke'));
});

app.use((err, req, res, next) => {
  res.status(500).json({ error: err.message });
});

hapi handles errors by returning an error object or throwing within the handler. The framework automatically catches these and formats the response based on configuration.

// hapi: Throw or return error objects
server.route({
  method: 'GET',
  path: '/fail',
  handler: (request, h) => {
    throw new Error('Something broke'); // Caught automatically
  }
});

koa leverages native try/catch blocks due to its async/await foundation. Any error thrown in downstream middleware bubbles up and can be caught in a central middleware.

// koa: Native try/catch in context
app.use(async (ctx, next) => {
  try {
    await next();
  } catch (err) {
    ctx.status = 500;
    ctx.body = { error: err.message };
  }
});

app.use(async (ctx) => {
  throw new Error('Something broke');
});

micro treats errors as rejected Promises. If your async handler throws or returns a rejected promise, micro automatically sends a 500 response with the error message.

// micro: Rejected promises handled automatically
const server = micro(async (req, res) => {
  throw new Error('Something broke'); // Returns 500 automatically
});

πŸ“¦ Built-in Features vs Ecosystem

The philosophy of "batteries included" versus "minimal core" determines how much you need to install separately.

express provides almost nothing out of the box besides routing. You must install separate packages for body parsing, validation, and security headers.

// express: Requires external middleware
const bodyParser = require('body-parser');
app.use(bodyParser.json()); // Not built-in

hapi includes input validation (via joi), caching, authentication schemes, and CORS handling as core features. This reduces dependency count but increases initial learning curve.

// hapi: Built-in validation
server.route({
  method: 'POST',
  path: '/user',
  options: {
    validate: {
      payload: {
        name: Joi.string().required()
      }
    }
  },
  handler: (request) => request.payload
});

koa strips away everything, including body parsing. You must explicitly add middleware for common tasks, giving you full control over what runs.

// koa: Body parser is external
const bodyParser = require('koa-bodyparser');
app.use(bodyParser()); // Must be added manually

micro assumes you are building a single endpoint. It automatically parses JSON bodies if the content-type matches, but offers no other utilities.

// micro: Auto JSON parsing for simple cases
const { send } = require('micro');
const micro = require('micro');

module.exports = micro(async (req, res) => {
  // req is already parsed if Content-Type is application/json
  const data = await req; 
  send(res, 200, data);
});

🌐 Real-World Architectural Fit

Scenario 1: Enterprise API with Strict Security

You need robust input validation, authentication strategies, and detailed logging out of the box.

  • βœ… Best choice: hapi
  • Why? Its configuration-driven approach enforces consistency, and built-in validation prevents common security oversights.

Scenario 2: Rapid Prototyping or Legacy Migration

You need to get a server running quickly with access to thousands of existing plugins and tutorials.

  • βœ… Best choice: express
  • Why? The massive ecosystem means a plugin exists for almost any requirement, speeding up development time.

Scenario 3: Modern Microservices with Complex Logic

You want clean, readable code for complex asynchronous flows without callback hell.

  • βœ… Best choice: koa
  • Why? The async/await middleware model makes complex control flow (like transaction management) much easier to read and debug.

Scenario 4: Serverless Functions or Single-Purpose Endpoints

You are deploying to AWS Lambda or Vercel and need the smallest possible footprint for a single task.

  • βœ… Best choice: micro
  • Why? It eliminates boilerplate, letting you focus purely on the business logic of a single function.

πŸ“Œ Summary Comparison

Featureexpresshapikoamicro
PhilosophyMinimalist, unopinionatedConfiguration-driven, robustModern, elegant coreFunctional, single-purpose
MiddlewareCallback-based (next())Lifecycle extensionsAsync/Await compositionFunction wrapping
Error HandlingSpecial 4-arg middlewareAutomatic catch & formatNative try/catchRejected promise handling
RoutingBuilt-inBuilt-in (config)External packageManual / External
Body ParsingExternal (body-parser)Built-inExternalAuto (JSON only)
Best ForGeneral purpose, legacyEnterprise, high securityModern apps, complex flowsMicroservices, serverless

πŸ’‘ Final Recommendation

Your choice should align with your team's experience and the project's complexity.

If you need stability and ecosystem breadth, express remains the safe default, though it requires discipline to avoid spaghetti code.

If you are building large-scale systems where configuration and security are paramount, hapi pays off in reduced technical debt.

If you value code clarity and modern JavaScript features, koa offers the best developer experience for custom architectures.

If you are building tiny, isolated services, micro removes all distractions, letting you ship faster with less code.

Avoid micro for anything beyond simple endpoints, and be wary of hapi if your team prefers code-over-configuration. For most new greenfield projects today, koa or a modern express setup with strict linting offers the best balance of flexibility and maintainability.

How to Choose: express vs hapi vs koa vs micro

  • express:

    Choose express if you need a battle-tested, flexible framework with the largest ecosystem of plugins and community support. It is ideal for teams that want to assemble their own architecture from proven components or need to integrate with legacy systems. However, be prepared to manually configure security, validation, and error handling, as the core is intentionally bare-bones.

  • hapi:

    Choose hapi if you prefer a framework that enforces structure and provides robust built-in capabilities for input validation, authentication, and caching. It is well-suited for large enterprise applications where configuration consistency and security defaults are more valuable than raw flexibility. Avoid it if you dislike heavy configuration objects or need a ultra-lightweight footprint.

  • koa:

    Choose koa if you want a modern, lightweight foundation that fully embraces async/await for cleaner middleware logic and better error handling. It is perfect for developers who find Express's callback-style middleware cumbersome but still want the freedom to pick their own routing and validation libraries. Note that it does not include a router or body parser out of the box.

  • micro:

    Choose micro only for building simple, stateless microservices or serverless functions where minimal boilerplate is critical. It forces a functional approach where every request is handled by a single async function. Do not use it for complex monolithic applications, as it lacks built-in routing, middleware stacks, and advanced request lifecycle management.

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