webdev.complete
🛜 Building a REST API
🟢The Backend
Lesson 68 of 117
30 min

Express & Hono

Routes, middleware, body parsing, static files.

Node ships http.createServer. Why use a framework at all? Because parsing JSON bodies, matching /users/:id routes, chaining auth middleware, and serving static files in 6 lines is hard; in 60, it's annoying. Frameworks give you the ergonomics. The two you'll meet today are Express (the OG since 2010) and Hono (the rising star, born 2022). They look similar. Their futures are not.

The mental model both share

Every web framework boils down to three concepts:

  • Router - match incoming requests by method + path to a handler function.
  • Handler - a function that reads the request and writes a response.
  • Middleware - functions that run before (and sometimes after) the handler. Auth checks, logging, body parsing.

A request flows through middleware like an assembly line. Each piece can mutate the request, attach data, short-circuit with a response, or call next() to pass control along.

Express: the 15-year-old workhorse

Express is the most-installed npm package of all time. It runs on top of Node's http module and shaped how Node servers are written. Express 5.0 finally shipped in September 2024 after a decade in limbo, and 5.1 took over the npm latest tag in March 2025. The headline fix: a rejected promise in an async handler now reaches your error middleware instead of vanishing.

bash
npm init -y
npm install express
npm install -D @types/express tsx typescript
server.ts
import express from "express";

const app = express();

// Middleware: parse JSON bodies into req.body
app.use(express.json());

// Middleware: log every request
app.use((req, _res, next) => {
  console.log(req.method + " " + req.url);
  next();
});

// In-memory store (use a DB in real life)
const users: { id: string; name: string }[] = [];

// GET /users
app.get("/users", (_req, res) => {
  res.json(users);
});

// GET /users/:id
app.get("/users/:id", (req, res) => {
  const user = users.find(u => u.id === req.params.id);
  if (!user) return res.status(404).json({ error: "not found" });
  res.json(user);
});

// POST /users
app.post("/users", (req, res) => {
  const { name } = req.body;
  if (typeof name !== "string") {
    return res.status(400).json({ error: "name is required" });
  }
  const user = { id: crypto.randomUUID(), name };
  users.push(user);
  res.status(201).json(user);
});

// Static files (e.g. ./public/index.html served at /)
app.use(express.static("public"));

app.listen(3000, () => {
  console.log("listening on http://localhost:3000");
});
Express conventions to memorize
req.params = URL path parameters (:id). req.query = query string (?limit=10). req.body = parsed body (only after express.json() middleware). res.json = send JSON + set Content-Type. res.status(201).json(...) = chain status codes.

Hono: built on Web Standards

Hono is the "modern" one. It uses the standard Request and Response objects (the same ones Service Workers and fetch use), which means the same codebase runs unchanged on Node, Bun, Deno, Cloudflare Workers, AWS Lambda, and Vercel Edge. It is also far smaller than Express in bundle size. Hono has been on the 4.x line since February 2024 and is still there, which is a good sign for a framework this young: the API you learn now has held for over two years.

If you are still on Express 4, plan the upgrade
Express 4 went into maintenance in April 2025, meaning security and high-priority fixes only, and the project set its end-of-life at no sooner than October 2026. It is genuinely still being patched, so this is not a fire. It is also not a version to start a new service on.
bash
npm install hono
# For Node, you also need the adapter:
npm install @hono/node-server
server.ts
import { Hono } from "hono";
import { logger } from "hono/logger";
import { serveStatic } from "@hono/node-server/serve-static";
import { serve } from "@hono/node-server";

const app = new Hono();

// Middleware: built-in logger
app.use("*", logger());

const users: { id: string; name: string }[] = [];

// GET /users
app.get("/users", (c) => c.json(users));

// GET /users/:id
app.get("/users/:id", (c) => {
  const id = c.req.param("id");
  const user = users.find(u => u.id === id);
  if (!user) return c.json({ error: "not found" }, 404);
  return c.json(user);
});

// POST /users
app.post("/users", async (c) => {
  const body = await c.req.json();
  if (typeof body.name !== "string") {
    return c.json({ error: "name is required" }, 400);
  }
  const user = { id: crypto.randomUUID(), name: body.name };
  users.push(user);
  return c.json(user, 201);
});

// Static files
app.use("/*", serveStatic({ root: "./public" }));

serve({ fetch: app.fetch, port: 3000 }, (info) => {
  console.log("listening on http://localhost:" + info.port);
});
Hono conventions to memorize
Every handler gets a c (context) instead of separate req/res. c.req.param("id") for path params. c.req.query("limit") for query. await c.req.json() for parsed body (no middleware needed). c.json(data, status) returns a Response.

Why Hono's Web Standards angle matters

Look closer at the Hono server. It builds a Request and returns a Response. That's the same fetch API your browser uses. The Node adapter (@hono/node-server) is the only thing that bridges to Node's legacy req/res objects. Drop the adapter and the same code runs on Cloudflare Workers without changes:

ts
// app.ts (shared)
import { Hono } from "hono";
const app = new Hono();
app.get("/", (c) => c.text("hello"));
export default app;

// Cloudflare Worker entrypoint - that's it
export default app;

// Bun
Bun.serve({ fetch: app.fetch });

// Node
serve({ fetch: app.fetch });

One library, every runtime. Express can't do that - it's Node-only.

Middleware chains: the assembly line

Both frameworks let you mount middleware globally, per-path, or per-method. Order matters - middleware runs top to bottom.

ts
// Express
app.use(express.json());                      // for every route
app.use("/admin", requireAdmin);              // only under /admin
app.get("/users", logUsers, fetchUsers);      // multiple per route

// Hono
app.use("*", logger());                        // for every route
app.use("/admin/*", requireAdmin);             // only under /admin
app.get("/users", logUsers, fetchUsers);       // chainable too
The dreaded missing next()
In Express, if a middleware doesn't call next() and doesn't send a response, the request hangs forever. Hono uses async functions that resolve, so it's harder to forget.

Body parsing

Express: nothing parses the body until you opt in with express.json(), express.urlencoded(), or multer for file uploads.

Hono: the body is a Web Request, so methods are built in. await c.req.json(), await c.req.formData(), await c.req.text(), await c.req.arrayBuffer().

Static files

Both let you serve a folder as static assets. Production-grade apps usually offload static to a CDN, but for local dev or small services it's plenty.

  • Express: app.use(express.static("public"))
  • Hono: app.use("/*", serveStatic({ root: "./public" }))

Which should you pick?

  • Express - more tutorials, more StackOverflow, easier to hire for. Pick it for: a team that already knows it, a legacy codebase, or when you want maximum middleware ecosystem.
  • Hono - tiny, type-safe, runs everywhere. Pick it for: new projects, edge deployment, TS-heavy teams, or when you want the same code in dev/prod/edge.

In 2026 my default for a new Node API is Hono. Express is still rock-solid, but it's designed for one runtime in a multi-runtime world.

Quiz

Quiz1 / 4

What does middleware do in Express/Hono?

Recap

  • A framework is a router + handlers + a middleware chain.
  • Express = oldest, biggest ecosystem, Node-only. Express 5 finally has async middleware.
  • Hono = built on Web Standards, runs on every modern runtime, tiny bundle, end-to-end type-safe.
  • Both support routes, params, queries, JSON bodies, middleware, and static files. Hono's ergonomics use a unified c (context).
  • Default pick in 2026: Hono for new projects, Express when the team already lives in it.
Built with Next.js, Tailwind & Sandpack.
Learn. Build. Ship.