DevLift
Back to Blog

Express vs Fastify vs Hono: Choosing Your Node.js Web Framework in 2026

Express has the ecosystem, Fastify has the schema validation, and Hono runs everywhere. Here's the practical breakdown of all three so you can actually make the call.

Admin
May 27, 202610 min read0 views

Express vs Fastify vs Hono: Choosing Your Node.js Web Framework in 2026

You're starting a new Node.js API. Someone on the team says "just use Express, everyone knows it." Another person pulls up a Fastify benchmark — several times the throughput with schema validation built in. Then someone mentions Hono: runs on Cloudflare Workers, Bun, and Deno, tiny bundle, TypeScript-first, and apparently competitive with Fastify in some tests.

All three are legitimate choices. The wrong pick isn't going to collapse your project — but it will shape your developer experience for months, affect how you handle types, and matter a lot if your workload is edge-deployed or latency-sensitive. Here's how to actually make the call.

Quick Decision Matrix

ExpressFastifyHono
Best forExisting codebases, tutorialsNode.js APIs needing performanceEdge/multi-runtime, greenfield TypeScript
TypeScriptCommunity @types (incomplete)First-class with genericsNative, route-level type inference
Throughput (measured, see below)~14K req/s~57K req/s~51K req/s
Install footprint4.3 MB, 65 packages14.2 MB, 42 packages3.6 MB, 1 package
RuntimesNode.js onlyNode.js onlyNode, Bun, Deno, CF Workers, Vercel Edge
MiddlewareLargest ecosystemPlugin system, scopedGrowing, WinterCG-compatible
Learning curveLowestMediumLow–medium
Use whenMigrating legacy code, max ecosystem coverageHigh-throughput Node.js microservicesMulti-runtime, serverless, new TypeScript APIs

The Performance Story (And When It Actually Matters)

You do not have to take my word for the ordering here — I ran it. Same machine, same handler, same load generator.

The setup: one route, GET /u/:id, returning a four-field JSON object. Express 5.2.1, Fastify 5.11.0 (with a response schema), and Hono 4.12.34 behind @hono/node-server 2.0.12. Load from autocannon 8.0.0, 50 connections, 8 seconds, over loopback, single process, Node 22.22.3 on 4 vCPU (arm64). A 3-second warm-up run was discarded before each measurement.

Frameworkreq/s (mean)p97.5 latencynon-2xx
Fastify 5.11.0 (response schema)57,0671 ms0
Fastify 5.11.0 (no schema)56,2641 ms0
Hono 4.12.34 (@hono/node-server)50,5841 ms0
Express 5.2.114,3375 ms0

Two things worth pulling out, because both cut against the usual framing.

The Express gap is bigger than the "2–3×" everyone repeats — about 4× here. Fastify and Hono land within ~11% of each other; Express is in a different class. Take the absolute numbers as shape, not truth: they are loopback numbers on a small VM with a trivial handler, and yours will differ. The ordering is the durable part, and it reproduces everywhere.

Fastify's compiled serializer did almost nothing on this payload — 57,067 vs 56,264 req/s with the response schema removed, a 1.4% difference. fast-json-stringify is real, but on a four-field object JSON.stringify is already native C++ and there is nothing to win. The schema earns its keep on large, deeply nested responses, and for input validation and field stripping. If you adopt Fastify expecting the serializer alone to double your throughput on small JSON, you will be disappointed.

The bigger caveat: real applications with database queries converge hard. Once your handler awaits a Postgres round trip, the framework stops being the bottleneck and the gap above compresses toward nothing. The performance difference between frameworks only matters if:

  • You're building latency-sensitive internal services (P99 targets <5ms)
  • You're running high-concurrency microservices under real load
  • You're optimizing for cloud cost at scale (more throughput per instance = fewer instances)

For a typical CRUD API used by a few thousand users, the framework choice is entirely about developer experience, not raw numbers.

Rendering diagram...

Deep Comparison

TypeScript: A Wider Gap Than You Expect

This is where the choice gets concrete fast.

Express was written before TypeScript was mainstream. The @types/express package fills the gap, but it's a shim — and a leaky one. Route params, query strings, and body types require manual casting:

// Express — manual typing everywhere
import { Request, Response } from "express";
 
app.get("/users/:id", async (req: Request, res: Response) => {
  const id = req.params.id; // string | undefined — no guarantee
  const page = req.query.page; // string | ParsedQs | string[] | ParsedQs[] | undefined
  // you cast or validate everything yourself
  const user = await db.user.findUnique({ where: { id } });
  res.json(user);
});

Fastify has proper generics. You define schemas and Fastify threads the types through:

// Fastify — typed via generics
import Fastify from "fastify";
import { Type, Static } from "@sinclair/typebox";
 
const server = Fastify();
 
const UserParams = Type.Object({ id: Type.String() });
const UserQuery = Type.Object({ include: Type.Optional(Type.String()) });
 
server.get<{
  Params: Static<typeof UserParams>;
  Querystring: Static<typeof UserQuery>;
}>("/users/:id", {
  schema: { params: UserParams, querystring: UserQuery },
}, async (request, reply) => {
  const { id } = request.params; // typed: { id: string }
  const { include } = request.query; // typed: { include?: string }
  return db.user.findUnique({ where: { id } });
});

Hono goes furthest — route-level type inference without any schema annotation ceremony:

// Hono — TypeScript-first, minimal boilerplate
import { Hono } from "hono";
 
const app = new Hono();
 
app.get("/users/:id", async (c) => {
  const id = c.req.param("id"); // string, typed from route definition
  const include = c.req.query("include"); // string | undefined
  const user = await db.user.findUnique({ where: { id } });
  return c.json(user);
});
 
export default app;

Hono also has hono/zod-validator for request body validation that integrates directly with Zod and types the validated output automatically. The ergonomics are meaningfully better if you care about end-to-end type safety.

Schema Validation & JSON Serialization

This is Fastify's killer feature that most comparisons undersell.

Express uses JSON.stringify() for responses — a generic, unoptimized serializer that has no idea what shape your response will be. Fastify uses fast-json-stringify, which compiles a specialized serializer from your output schema at startup:

// Fastify — define the response shape; get a compiled serializer and field stripping
server.get("/users/:id", {
  schema: {
    params: { type: "object", properties: { id: { type: "string" } } },
    response: {
      200: {
        type: "object",
        properties: {
          id: { type: "string" },
          email: { type: "string" },
          createdAt: { type: "string" },
        },
      },
    },
  },
}, async (request, reply) => {
  return db.user.findUnique({ where: { id: request.params.id } });
  // Fastify compiles a serializer from this schema at startup
  // AND strips any extra fields not in the schema (security win)
});

That field stripping is the underrated half. Anything your handler returns that isn't declared in the response schema never reaches the client — so a stray passwordHash on a model object can't leak through a careless return user. The schema also validates input before your handler runs, so malformed requests get rejected at the framework layer rather than inside your business logic.

What it is not is a guaranteed throughput multiplier. As measured above, removing the response schema cost 1.4% on a small payload. Treat the compiled serializer as a correctness and safety feature that sometimes also pays off in speed, not the other way round.

Hono takes a different approach — it doesn't pre-compile serializers. It's fast because it's tiny and its router is efficient, not because of serialization optimization. For pure throughput with schema validation, Fastify has the edge. For general speed with less configuration, Hono is competitive.

The Plugin/Middleware Architecture

Express middleware is a global, linear chain. Every request passes through every registered middleware in order:

// Express — all middleware runs for all routes unless you're careful
app.use(helmet());
app.use(cors());
app.use(express.json());
app.use(morgan("combined")); // logs every request
app.use(authMiddleware); // also every request...
 
app.get("/public/health", healthHandler); // also runs auth? oops.
app.get("/api/users", userHandler);

Fastify's plugin system uses encapsulation. Plugins, hooks, and decorators registered inside a scope stay inside that scope:

// Fastify — scoped plugins
fastify.register(async (instance) => {
  instance.addHook("preHandler", authHook); // only inside this scope
  instance.get("/api/users", userHandler);
  instance.get("/api/posts", postHandler);
});
 
fastify.get("/health", healthHandler); // no auth hook here

This scoping prevents the classic Express footgun where adding middleware to app.use() accidentally applies it to routes that shouldn't have it. It also means Fastify can skip hooks for routes that don't need them — less overhead per request.

Hono handles scoping through route grouping:

// Hono — scope via route groups
const api = new Hono();
api.use("/*", authMiddleware); // only applies to /api/* routes
 
api.get("/users", userHandler);
api.get("/posts", postHandler);
 
const app = new Hono();
app.route("/api", api);
app.get("/health", healthHandler); // no auth

Edge & Multi-Runtime Support

This is where Hono has no competition.

Express and Fastify are Node.js-specific. They use Node.js-native APIs (http.IncomingMessage, http.ServerResponse, Node streams) that don't exist on Cloudflare Workers, Deno Deploy, or Vercel Edge Functions.

Hono uses the WinterCG-compatible Fetch API — the same Request and Response objects you use in browsers and edge runtimes. The same Hono app runs on every supported runtime with just an adapter swap:

// The same app, different entry points
const app = new Hono();
 
app.get("/", (c) => c.text("Hello!"));
app.get("/users/:id", async (c) => {
  const user = await db.user.findUnique({ where: { id: c.req.param("id") } });
  return c.json(user);
});
 
// Node.js
import { serve } from "@hono/node-server";
serve({ fetch: app.fetch, port: 3000 });
 
// Cloudflare Workers
export default app; // that's it
 
// Bun
export default app; // same
 
// Vercel Edge
export const GET = app.fetch;

No code changes, just different entry points. This matters if you're deploying to multiple environments, running functions at the edge, or want to future-proof against a runtime migration.

Ecosystem & Middleware Availability

Express has an enormous ecosystem advantage. It pulled 128 million downloads in the week ending 2 August 2026 (npm registry) against Hono's 56 million and Fastify's 11 million, and it has 10+ years of history behind it. Practically every Node.js library has an Express integration: Passport.js (auth), Multer (file uploads), express-rate-limit, express-session, morgan, helmet, and hundreds more. Stack Overflow has answers for every Express problem imaginable.

Worth noting that Hono's download count is now several times Fastify's — the multi-runtime story has pulled it past the incumbent challenger faster than most people realise.

Fastify has a well-maintained official plugin ecosystem covering most common needs (fastify-jwt, fastify-multipart, fastify-static, fastify-swagger), and community plugins cover the rest.

Hono's ecosystem is smaller but growing fast. It has adapters for common patterns — JWT, CORS, rate limiting, serve-static — and WinterCG compatibility means many edge-native libraries work without modification.

⚠️
If you're migrating an existing Express app, don't underestimate middleware compatibility. Packages that use req.app, res.locals, or app.set() Express-specific APIs won't work in Fastify or Hono. Audit your dependencies first.

Install Footprint & Bundle Size

Two different numbers get conflated here constantly, so let me separate them. I measured both.

Install footprint — what npm install actually puts on disk (production deps only):

node_modules totalTop-level packages
Hono 4.12.343.6 MB1 (zero dependencies)
Express 5.2.14.3 MB65
Fastify 5.11.014.2 MB42

Hono's "zero dependencies" claim checks out literally — one directory in node_modules. That matters less for disk and more for supply-chain surface: there are 64 fewer packages that can be compromised or abandoned.

Bundle size is the number that actually drives edge cold starts, and it only applies to Hono, because Express and Fastify can't be bundled for a Workers-style runtime at all. Bundling a minimal Hono app with esbuild (--bundle --minify --format=esm):

MinifiedGzipped
hono/tiny preset11.4 KB4.9 KB
full hono19.1 KB7.9 KB

So Hono's documented "under 14kB" is real, and it refers to the hono/tiny preset — not to the default import, which is closer to 19 KB minified. Either way it is small enough that on a Workers-class runtime your own code dominates.

I have not measured Lambda cold starts, so I'm not going to give you millisecond figures for them. The directional claim is safe — smaller zip, faster cold start, and Hono's is dramatically smaller — but if cold start is a real constraint for you, measure it on your own account and runtime rather than trusting anyone's table, including mine.

When to Use Express

  • Your team already knows Express and the performance budget is comfortable
  • You're maintaining a large existing Express codebase — migrating to Fastify or Hono for a legacy app is risky work with limited upside
  • You need maximum middleware ecosystem coverage — if you're using Passport.js, complex session handling, or niche Express middleware that has no Fastify equivalent
  • Learning context: tutorials, education, onboarding junior developers. Express's simplicity and the volume of resources is genuinely valuable.

When to Use Fastify

  • You're building Node.js-specific APIs that won't run on edge runtimes
  • Your API is high-throughput or latency-sensitive and you want the framework to do real work (validation, serialization) rather than just routing
  • You like explicit schema contracts — defining input/output shapes at the route level rather than validating inside handlers
  • You care about the encapsulated plugin system for modular, scope-isolated application structure

When to Use Hono

  • You're building a new TypeScript-first API on any runtime — greenfield projects where you want the best ergonomics without ceremony
  • Your deployment target is Cloudflare Workers, Vercel Edge, or Bun (or you might move to one in the future)
  • Bundle size matters — serverless functions, Lambda, or edge deployments where cold start time is a real constraint
  • You want type-safe routing without defining separate schema objects alongside your route handlers

When to Mix Them

A common and perfectly reasonable pattern: Hono at the edge, Fastify on long-running Node.js services.

API gateway / edge functions → Hono (lightweight, fast cold starts, TypeScript-native)
Core business logic services → Fastify (schema validation, plugin scoping, high throughput)
Legacy internal services → Express (don't touch what works)

// Hono gateway that fans out to Fastify backend
const gateway = new Hono();
 
gateway.get("/api/users/:id", async (c) => {
  const res = await fetch(`http://users-service:3001/users/${c.req.param("id")}`);
  return c.json(await res.json());
});
 
export default gateway; // runs on CF Workers or Vercel Edge

This gets you Hono's edge advantages for the public-facing API (low cold start, multi-runtime) and Fastify's throughput for internal heavy-lifting.

Where This Lands

Express isn't going anywhere. The ecosystem is too large and the production deployment base too wide. But it's no longer the obvious default for new projects — not because it's bad, but because Fastify and Hono are better in meaningful ways when you have the choice.

Fastify is the right answer for teams building performance-sensitive Node.js backends who want schema validation and a better plugin architecture without giving up Node's native capabilities. The learning curve is real but pays off in structure.

Hono is the most interesting pick for 2026. The TypeScript ergonomics are the best of the three. The multi-runtime story is genuinely unique. And a single-package install that bundles to ~11 KB minified makes Express's "minimal" tag look generous by comparison.

If you're starting something new, use Hono unless you have a specific reason for Fastify (heavy schema validation needs) or Express (team familiarity, legacy ecosystem requirements). If you're maintaining Express code, stay on Express — the migration cost rarely justifies itself unless performance is an actual measured problem.

Comments (0)

No comments yet. Be the first to share your thoughts!

Related Articles

The EventEmitter pattern lets components in the same process react to the same event without being directly coupled — no message broker needed.
AdminAugust 3, 20266 min read
Reading a 2 GB file all at once doesn't run out of memory — it hits V8's 512 MB string limit first. Streams process it in flat memory instead. Here's the pipeline pattern, custom Transform streams, and backpressure, with the numbers measured on Node 22.
AdminAugust 3, 20266 min read
Node.js runs on one CPU core by default. The cluster module lets you fork N workers across all cores — here's the pattern, graceful shutdown, IPC, and when PM2 makes it unnecessary.
AdminAugust 3, 20266 min read