10 — Building APIs — From the http Module to Frameworks
"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.
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
[2] "Express.js," expressjs.com. [Online]. Available: https://expressjs.com/
[3] "Fastify Documentation," fastify.io. [Online]. Available: https://www.fastify.io/docs/latest/
[4] "NestJS Documentation," docs.nestjs.com. [Online]. Available: https://docs.nestjs.com
[5] "Hono Documentation," hono.dev. [Online]. Available: https://hono.dev/docs/
Knowledge check · Question 1 of 5
What does the raw http module provide that every framework builds on?
Comments
Leave a Comment
You must be signed in to comment
0 Comments
No comments yet. Be the first to comment!