---
title: "10 — Building APIs — From the http Module to Frameworks"
uid: building-apis
tags: ["hono", "nodejs", "roadmap:nodejs", "express", "fastify", "api", "http", "nestjs"]
excerpt: "The http module is the raw metal every framework is forged from; frameworks differ in how opinionated they are about structure, validation, and performance."
date: 2026-08-13T03:27:56+0000
source: https://www.aveshina.my.id/en/blog/building-apis
---

"Just use Express and move on" was my API-building strategy, and it made framework choice look like religion. The model that finally stuck is a stack: **the http module is the raw metal every framework is forged from, and the frameworks differ mainly in how opinionated they are about structure, validation, and performance.** Once I saw the metal underneath, the framework choices stopped feeling like separate religions and started looking like points on an opinionation curve [1].

The framing that landed for me is to start at the metal, then watch each framework add opinions on top.

## The http module: the raw metal

Node.js ships an http module that can create a server in a handful of lines [1]. This is the layer every framework builds on.

```
import { createServer } from 'http';

const server = createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({ hello: 'world' }));
});

server.listen(3000);
```

req is the incoming request (method, URL, headers, body stream); res is the response I write to. There is no routing, no body parsing, no middleware — just "a request arrived, here is the response." Working at this layer teaches what the frameworks actually do for me: they parse the URL into route params, stream the body into a parsed object, attach helpers (res.json), and run middleware chains. Whenever a framework does something mysterious, the answer is usually "it is wrapping this raw req/res pair."

Most production code does not live at this layer — but knowing it exists is what makes the frameworks legible.

## Express: the minimal, ubiquitous default

**Express.js** is the most widely used Node.js web framework, and its defining trait is **minimalism** [2]. It gives me routing, middleware, and a handful of response helpers, and then gets out of the way. There is no prescribed project structure, no built-in schema validation, no ORM. That minimalism is the source of both its popularity (it fits any shape of project) and its pitfalls (every team rolls its own structure).

```
import express from 'express';

const app = express();
app.use(express.json());                 // middleware: parse JSON bodies

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

app.listen(3000);
```

The **middleware pattern** is Express's main idea: a request flows through a chain of functions, each receiving req, res, and next. Middleware can log, parse, authenticate, attach data, or short-circuit with a response. Composing middleware is how an Express app grows — and the discipline is keeping that chain ordered, because middleware runs in declaration order and the wrong order is a common source of bugs.

## Fastify: performance and schemas

**Fastify** is the performance-focused alternative, and its two distinguishing ideas are **schema-based validation/serialization** and a plugin architecture designed for low overhead [3]. Where Express parses every body optimistically, Fastify lets me declare a JSON schema for each route; incoming requests are validated against it, and outgoing responses are serialized using the schema for speed. The throughput numbers are consistently among the highest of any Node framework.

```
import Fastify from 'fastify';

const app = Fastify();

app.get('/users/:id', {
  schema: {
    params: { type: 'object', properties: { id: { type: 'string' } } },
    response: { 200: { type: 'object', properties: { id: { type: 'string' } } } }
  }
}, async (req, reply) => {
  return { id: req.params.id };
});

app.listen({ port: 3000 });
```

The schema is the contract — invalid requests are rejected before the handler runs, and the response is serialized faster than JSON.stringify. For a high-throughput API, that combination of safety and speed is the draw.

## NestJS: the opinionated, enterprise-shaped option

**NestJS** sits at the opposite end of the opinionation curve from Express. It is heavily inspired by Angular — **modules, decorators, dependency injection (a way to hand each part the things it needs), a prescribed project layout** — and defaults to TypeScript [4]. Where Express leaves structure to me, NestJS mandates it: controllers handle routes, services hold business logic, modules group them, and everything is wired through its DI container.

```
@Controller('users')
export class UsersController {
  constructor(private readonly users: UsersService) {}

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.users.findOne(id);
  }
}
```

The payoff is consistency at scale — a large team working on a NestJS codebase converges on the same shape, because there is only one place each kind of code goes. The cost is ceremony: a simple endpoint takes more files and more decorators than the Express equivalent. NestJS is the choice when "hundreds of endpoints maintained by dozens of engineers" is the actual problem.

## Hono: the edge-first framework

**Hono** is the newest of the four and reflects a different deployment reality — it is built to run not just on Node.js but on **edge runtimes** (Cloudflare Workers, Deno, Bun) [5]. It is lightweight, TypeScript-first, and its router is designed for the cold-start constraints of edge platforms, where every millisecond of startup matters. For an API that ships close to users across many regions, Hono is the modern pick.

## Choosing between them

The four are not ranked; they are points on axes:

- **Express** — minimal, mature, every tutorial assumes it. Good default for small-to-medium APIs and for teams that want full structural freedom.
- **Fastify** — performance and schema-first. Good when throughput matters and when I want validation baked in rather than bolted on.
- **NestJS** — opinionated, Angular-shaped, TypeScript-native. Good for large teams and complex domains where enforced structure pays off.
- **Hono** — edge-first, lightweight, multi-runtime. Good when the API deploys to the edge or when I want the same code on Node, Deno, and Bun.

They all sit on the same http module underneath, which is why the way of thinking ports cleanly between them — a request arrives, middleware runs, a handler responds.

```figure
<svg viewBox="0 0 740 200" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="Node.js API frameworks on an opinionation axis. Left to right: Hono (edge-first, minimal), Express (minimal, mature), Fastify (schema, performance), NestJS (opinionated, Angular-shaped, DI). At the bottom, the shared http module they all build on.">
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">
    <!-- axis -->
    <line x1="40" y1="120" x2="700" y2="120" stroke="#cbd5e1" stroke-width="1.5"/>
    <polygon points="700,120 694,117 694,123" fill="#cbd5e1"/>
    <text x="40" y="148" font-size="10" fill="#64748b">minimal / less opinionated</text>
    <text x="700" y="148" font-size="10" fill="#64748b" text-anchor="end">opinionated / structured</text>

    <!-- framework nodes -->
    <rect x="60" y="60" width="120" height="44" rx="8" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
    <text x="120" y="80" font-size="12" font-weight="700" fill="#1e1b4b" text-anchor="middle">Hono</text>
    <text x="120" y="96" font-size="9" fill="#475569" text-anchor="middle">edge-first, multi-runtime</text>

    <rect x="230" y="60" width="120" height="44" rx="8" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <text x="290" y="80" font-size="12" font-weight="700" fill="#052e16" text-anchor="middle">Express</text>
    <text x="290" y="96" font-size="9" fill="#475569" text-anchor="middle">minimal, mature</text>

    <rect x="400" y="60" width="120" height="44" rx="8" fill="#fef9c3" stroke="#ca8a04" stroke-width="1.5"/>
    <text x="460" y="80" font-size="12" font-weight="700" fill="#422006" text-anchor="middle">Fastify</text>
    <text x="460" y="96" font-size="9" fill="#475569" text-anchor="middle">schema, performance</text>

    <rect x="570" y="60" width="120" height="44" rx="8" fill="#fce7f3" stroke="#db2777" stroke-width="1.5"/>
    <text x="630" y="80" font-size="12" font-weight="700" fill="#500724" text-anchor="middle">NestJS</text>
    <text x="630" y="96" font-size="9" fill="#475569" text-anchor="middle">Angular-shaped, DI</text>

    <!-- shared http module bar -->
    <rect x="40" y="168" width="660" height="24" rx="6" fill="#f8fafc" stroke="#cbd5e1" stroke-width="1"/>
    <text x="370" y="184" font-size="10" font-weight="700" fill="#334155" text-anchor="middle">the http module — the shared metal underneath them all</text>
  </g>
</svg>
```

## How I use this

The model I keep is "pick the opinionation level that matches the project." For a small API or a prototype, I default to Express, because the ecosystem is deepest and the structure is mine to choose. For an API where throughput is a stated requirement, Fastify's schema-driven validation pays for itself in both speed and safety. For a large codebase with many contributors, NestJS's enforced structure keeps everyone convergent. For edge deployment, Hono. And I keep one habit regardless of framework: I read what the framework is doing to the raw req/res underneath, because that is where the leaks and the debugging actually happen. The metal under the framework is the same; only the opinions differ.

## References

[1] OpenJS Foundation, "HTTP," Node.js API Docs. [Online]. Available: [https://nodejs.org/docs/latest-v16.x/api/http.html](https://nodejs.org/docs/latest-v16.x/api/http.html)

[2] "Express.js," expressjs.com. [Online]. Available: [https://expressjs.com/](https://expressjs.com/)

[3] "Fastify Documentation," fastify.io. [Online]. Available: [https://www.fastify.io/docs/latest/](https://www.fastify.io/docs/latest/)

[4] "NestJS Documentation," docs.nestjs.com. [Online]. Available: [https://docs.nestjs.com](https://docs.nestjs.com)

[5] "Hono Documentation," hono.dev. [Online]. Available: [https://hono.dev/docs/](https://hono.dev/docs/)

```quiz
Q: What does the raw http module provide that every framework builds on?
- A router and middleware system
- A createServer function giving you the raw req and res objects — no routing, no body parsing
correct: 1
explain: The http module hands you the incoming request stream and the outgoing response stream. Frameworks add routing, body parsing, helpers, and middleware on top of that raw pair.

Q: Express's central organizing idea is…
- a dependency injection container
- middleware — a chain of functions that each see req, res, and next, running in declaration order
correct: 1
explain: An Express app is a middleware chain. Logging, parsing, auth, and handlers are all middleware composed in order. The framework itself stays minimal and leaves structure to the team.

Q: Fastify's main differentiator versus Express is…
- it ships with an ORM
- schema-based request validation and response serialization, optimized for high throughput
correct: 1
explain: Fastify lets you declare a JSON schema per route. Requests are validated and responses serialized against the schema, which is both safer and faster than optimistic parsing.

Q: NestJS is best described as…
- a minimal router like Express, but in TypeScript
- an opinionated, Angular-inspired framework with modules, decorators, and dependency injection
correct: 1
explain: NestJS mandates structure — controllers, services, modules, dependency injection — and defaults to TypeScript. It suits large teams and complex domains where enforced consistency pays off.

Q: Which framework is designed first for edge runtimes (Cloudflare Workers, Deno, Bun)?
- Express
- Hono
correct: 1
explain: Hono is built to run on edge and multi-runtime environments with fast cold starts. Express and Fastify are Node-first; NestJS is Node-and-TypeScript-first.
```
