Back to blog

Hono vs Express in 2026: Which Should You Use for a New API?

We built the same TypeScript API in Express 5 and Hono 4, benchmarked both on Node.js 22, and compared routing speed, TypeScript DX, middleware ecosystem, dependencies, and runtime portability. The results are not what the benchmark headlines suggest.

You are starting a new TypeScript API. Express 5 shipped as stable in early 2025 and the project is actively maintained again. Hono has grown from a Cloudflare Workers-only router to a full web framework with official adapters for Cloudflare Workers, Node.js, Deno, Bun, AWS Lambda, Vercel, and more. The comparison is genuinely harder than it was two years ago.

To make a data-backed recommendation, we built identical APIs in both frameworks, benchmarked them under the same conditions on Node.js 22, and looked at TypeScript experience, middleware ecosystem, and runtime portability. The results reveal something that the usual "Hono is faster" headlines omit.

TL;DR

| Scenario | Choose | |---|---| | New TypeScript API, runtime portability matters | Hono | | Cloudflare Workers, Deno, or Bun | Hono | | New API with minimal dependencies | Hono | | Existing Express codebase | Stay on Express | | Node.js only, middleware-heavy project | Express | | Migrating from Express 4 with no runtime change | Express 5, not Hono |

For a new TypeScript API targeting Node.js in 2026, Hono is the stronger default when runtime portability or TypeScript DX are priorities. Express 5 remains the better choice when you depend on its mature middleware ecosystem or are already running a large Express codebase.

Why this comparison looks different in 2026

Until March 2025, Express 4 had been the npm default for years with minimal active development. The narrative that "Express is dead" had become common enough to be accepted without checking.

Express 5 changed that. It became the default latest on npm in March 2025. The release fixed long-standing issues: async errors are now forwarded to error handlers automatically (no more try/catch wrapping in every async route), path-to-regexp was updated to v8, and the project has resumed active maintenance with a published LTS plan.

At the same time, Hono moved from a Cloudflare Workers-optimized router to a framework with official adapters for Cloudflare Workers, Cloudflare Pages, Deno, Bun, Fastly Compute, Vercel, AWS Lambda, and Node.js. The framework design is based on the Web Fetch API rather than Node.js-native http.IncomingMessage.

Both frameworks are actively developed in 2026. The choice is a real architectural decision, not a dead vs. alive judgment.

The design difference: Node.js native vs Web Standards

Express and Hono handle HTTP at different abstraction layers.

Express wraps Node.js's native http.IncomingMessage and http.ServerResponse. Every request handler receives the raw Node.js objects. This makes Express tightly coupled to the Node.js runtime, but it also means zero translation overhead on Node.js itself.

Hono is built on the Web Fetch API: Request and Response from the WHATWG specification. These are the same objects available in browsers, Cloudflare Workers, Deno, and modern Node.js. On Node.js specifically, Hono needs an adapter (@hono/node-server) that translates between the Node.js http module and the Fetch API.

This design choice is the root cause of every meaningful difference between the two frameworks — performance characteristics on Node.js, memory usage, TypeScript ergonomics, and runtime portability.

Building the same API

To compare fairly, we built the same API in both frameworks with these endpoints:

GET  /health           — health check, no middleware
GET  /users/:id        — authentication middleware + mock async DB (5ms)
POST /users            — authentication + Zod validation + mock async DB (5ms)

Both implementations used CORS middleware, the same Zod schemas, and an identical 5ms setTimeout to simulate a database round-trip.

Express 5:

import express, { Request, Response, NextFunction } from 'express'
import cors from 'cors'
import { z } from 'zod'

const app = express()
app.use(cors())
app.use(express.json())

interface AuthRequest extends Request {
  user?: { id: string }
}

function auth(req: AuthRequest, res: Response, next: NextFunction) {
  const token = req.headers['authorization']
  if (!token) return res.status(401).json({ error: 'Unauthorized' })
  req.user = { id: '1' }
  next()
}

const CreateUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(1),
})

app.get('/health', (req, res) => res.json({ status: 'ok' }))

app.get('/users/:id', auth, async (req: AuthRequest, res) => {
  await new Promise(r => setTimeout(r, 5))
  res.json({ id: req.params.id, name: 'Test User' })
})

app.post('/users', auth, async (req: AuthRequest, res) => {
  const result = CreateUserSchema.safeParse(req.body)
  if (!result.success) return res.status(400).json({ error: 'Invalid' })
  await new Promise(r => setTimeout(r, 5))
  res.status(201).json({ id: '123', ...result.data })
})

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

Hono 4:

import { Hono, type MiddlewareHandler } from 'hono'
import { cors } from 'hono/cors'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'

type Variables = { user: { id: string } }
const app = new Hono<{ Variables: Variables }>()

app.use('*', cors())

const auth: MiddlewareHandler<{ Variables: Variables }> = async (c, next) => {
  const token = c.req.header('authorization')
  if (!token) return c.json({ error: 'Unauthorized' }, 401)
  c.set('user', { id: '1' })
  await next()
}

const CreateUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(1),
})

app.get('/health', (c) => c.json({ status: 'ok' }))

app.get('/users/:id', auth, async (c) => {
  await new Promise(r => setTimeout(r, 5))
  return c.json({ id: c.req.param('id'), name: 'Test User' })
})

app.post('/users', auth, zValidator('json', CreateUserSchema), async (c) => {
  const data = c.req.valid('json')
  await new Promise(r => setTimeout(r, 5))
  return c.json({ id: '123', ...data }, 201)
})

The implementation surface is similar. The meaningful differences appear in TypeScript ergonomics.

TypeScript experience

Express uses Request and Response types from @types/express. Adding custom properties to the request object (like a user after authentication) requires you to extend the interface manually, either via declaration merging or a local type assertion:

// You define this everywhere or in a .d.ts file
interface AuthRequest extends Request {
  user?: { id: string }
}

// Then assert in each route that needs it
app.get('/users/:id', auth, async (req: AuthRequest, res) => {
  // TypeScript does not verify that auth middleware actually set req.user
  const userId = req.user?.id
})

The middleware chain does not carry type information. To get a typed value out of req.user, you extend the Request interface manually. TypeScript cannot verify that auth actually ran before the handler accesses that property — this limitation applies to both frameworks.

The difference in Hono is ergonomics, not compile-time guarantees. Context types flow through the app's generic without any interface extension:

type Variables = { user: { id: string } }
const app = new Hono<{ Variables: Variables }>()

// No interface extension needed — c.get('user') returns { id: string }
app.get('/users/:id', auth, async (c) => {
  const user = c.get('user') // { id: string }
  return c.json({ id: c.req.param('id') })
})

Hono's zValidator integrates directly with the Zod schema and makes c.req.valid('json') return the inferred type:

app.post('/users', zValidator('json', CreateUserSchema), (c) => {
  const { email, name } = c.req.valid('json') // z.infer<typeof CreateUserSchema>
})

In Express, req.body is typed as any regardless of whether validation ran. You either assert the type or accept the unsafe cast.

For teams working heavily in TypeScript, Hono's type propagation reduces the surface for type-and-compile-pass-but-runtime-fails errors.

Middleware ecosystem

This is Express's clearest advantage.

Express's middleware ecosystem is deep and battle-tested. Packages like helmet, express-rate-limit, passport, multer, express-validator, express-session, and morgan have been used in production for years. Configuration options are well-documented, edge cases are handled, and third-party integrations (databases, auth providers, monitoring) often ship Express middleware as their primary integration path.

Hono has built-in middleware for the most common cases — CORS, logging, JWT, basic auth, cache control, compression, rate limiting. For the standard use case, the built-ins are often sufficient. But for anything outside the core — passport strategies, upload handling, complex session management, legacy auth integrations — you are more likely to find an Express package than a Hono equivalent, or you build it yourself using the underlying platform.

If your project's requirements map to Hono's built-ins, the ecosystem gap is small. If you need a specific Express middleware that has no Hono equivalent, factor that into the decision.

Benchmark: what we measured

Environment:

| | | |---|---| | Node.js | v22.17.1 | | Express | 5.2.1 | | Hono | 4.13.1 | | @hono/node-server | 1.19.17 | | Tool | autocannon, 10 concurrent connections | | Duration per scenario | 10 seconds | | Platform | macOS, Apple Silicon (arm64) | | Date | 2026-08-08 |

Both servers ran in separate processes. No logging middleware was active during benchmarks. Each scenario was measured once (a single 10-second continuous run); results on Linux x86 may differ in absolute numbers, though the relative ordering should hold.

Results:

| Scenario | Framework | req/s | p50 (ms) | p99 (ms) | |---|---|---|---|---| | A: GET /health (pure routing) | Express 5 | 4,705 | 2 | 6 | | A: GET /health (pure routing) | Hono 4 | 10,579 | 0 | 2 | | B: GET /users/:id (auth + 5ms async) | Express 5 | 1,502 | 6 | 10 | | B: GET /users/:id (auth + 5ms async) | Hono 4 | 1,575 | 6 | 12 | | C: POST /users (auth + Zod + 5ms async) | Express 5 | 1,511 | 6 | 17 | | C: POST /users (auth + Zod + 5ms async) | Hono 4 | 1,550 | 6 | 14 |

What the numbers mean:

Scenario A is the framework's routing overhead in isolation. Hono handles 2.25× more requests per second than Express when there is nothing else to do.

Scenarios B and C tell a different story. The moment a 5ms async operation is in the path — representing a single database call — the difference is 4.9% and 2.6% respectively. At 1,500 req/s, that is approximately 73 requests per second. Whether that difference is meaningful depends on whether your bottleneck is the framework router or the operations the route performs.

Most API endpoints do work: they query databases, call external services, serialize responses, apply business logic. In those cases, the framework's routing overhead is not the constraint. The 2.25× difference in pure routing becomes a rounding error when a 5ms operation is added.

Memory RSS after load:

| Framework | RSS | |---|---| | Express 5 | 51 MB | | Hono 4 (Node.js) | 133 MB |

This result is counterintuitive. Hono is commonly described as lightweight, and it is on edge runtimes like Cloudflare Workers. On Node.js, however, @hono/node-server needs to bridge the Web Fetch API (Request, Response, Headers) to Node.js's http.IncomingMessage and http.ServerResponse. That translation layer carries overhead. Express, using Node.js native APIs directly, has a smaller RSS footprint during load.

Caveats: RSS varies with GC state and was measured at a single point after benchmark completion. These numbers are directionally meaningful but not precise capacity-planning figures. On Cloudflare Workers, Hono runs without any adapter and its footprint is dramatically smaller.

Dependencies and install footprint

| Framework (with cors + zod) | Total packages | node_modules size | |---|---|---| | Express 5 | 68 | 8.8 MB | | Hono 4 | 4 | 8.1 MB |

Express pulls in 28 direct dependencies — body-parser, accepts, type-is, path-to-regexp, and others — each with their own transitive dependencies. The total reaches 68 packages.

Hono's core package has zero external dependencies. @hono/node-server, @hono/zod-validator, and zod together bring the total to 4 packages.

The practical difference matters most in serverless and edge environments, where cold start time correlates with module load. For a long-running Node.js server, the difference in startup time is negligible in practice.

Runtime portability

The dependency difference also explains Hono's main architectural advantage: the same application code runs on multiple runtimes with minimal changes.

An Express application is written against Node.js APIs. Moving to Deno, Bun, or Cloudflare Workers requires rewriting the server entry point and replacing Express with a compatible framework. The business logic may port, but the framework layer does not.

A Hono application is written against Web Standard APIs. The Request object in a Hono handler is the same Request object available in Cloudflare Workers. Switching the deployment target means changing the adapter in index.ts — not rewriting route handlers.

Node.js entry:

import { serve } from '@hono/node-server'
import { app } from './app'
serve({ fetch: app.fetch, port: 3000 })

Cloudflare Workers entry:

import { app } from './app'
export default { fetch: app.fetch }

The route handlers in app.ts do not change. For teams that want the option to move between Node.js and edge runtimes without rewriting business logic, this is a real advantage — not a theoretical one.

Should existing Express apps migrate?

Performance alone is not a reason to migrate.

In the real-world scenarios in our benchmark, Express and Hono perform within 5% of each other. A migration to Hono will not make a slow endpoint fast. If your bottleneck is database queries, caching misses, or external API latency, the framework layer is not the constraint.

There are legitimate reasons to migrate:

  • You want to deploy to Cloudflare Workers or another edge runtime and need runtime portability
  • TypeScript DX across middleware chains matters significantly to your team
  • You want to reduce dependency surface (from 68 to 4 packages)

There are also reasons to stay on Express:

  • You depend on Express middleware with no Hono equivalent
  • Your team has deep Express knowledge and the migration cost is not offset by the benefits
  • You are already upgrading from Express 4 to Express 5, which fixes the async error handling issues that motivated some migrations

Express 5 has an official migration guide. If the improvements in v5 solve your current pain points, upgrading within Express is a lower-risk path than switching frameworks entirely.

What about Fastify?

Fastify is the third serious contender for new Node.js APIs. It is a schema-first framework that uses JSON Schema for validation and serialization, has a mature plugin ecosystem, and is consistently benchmarked significantly faster than Express on Node.js for raw routing throughput. We did not benchmark Fastify in this article.

Fastify is worth evaluating if you need maximum throughput on Node.js specifically, are comfortable with JSON Schema over Zod, and do not need multi-runtime deployment.

The key difference from Hono: Fastify is Node.js native like Express. It is not portable to Cloudflare Workers without significant changes. If runtime portability is part of your evaluation, Fastify and Express belong in the same category.

A detailed Hono vs Fastify comparison is outside the scope of this article.

How to choose

Choose Hono if:

  • You are deploying to Cloudflare Workers, Deno, Bun, or want the option to do so without rewriting routes
  • TypeScript type propagation through middleware chains matters to your team
  • You want minimal dependencies (4 packages vs 68)
  • You are starting a new API with no existing framework investment

Choose Express if:

  • You depend on specific Express middleware packages (passport strategies, multer, complex session management) that have no Hono equivalent
  • You are upgrading an existing Express 4 codebase — Express 5 is a lower-risk path than switching frameworks
  • Your team has deep Express knowledge and the benefits of Hono do not offset the migration cost
  • You are running Node.js exclusively with no plans to move to edge runtimes

Do not migrate if:

  • You are migrating for performance reasons only — the real-world throughput difference is under 5% in most scenarios
  • You have no clear benefit from Hono's runtime portability or TypeScript DX improvements
  • Your current Express middleware stack has no Hono equivalent and building replacements would be significant work

If you choose Hono and need to verify Supabase JWTs in your API, Hono Supabase Auth: 4 Ways to Verify JWTs and Which One to Use covers the current options including @supabase/server, getClaims(), Hono's JWK middleware, and jose.

For deploying a Hono application to Cloudflare Workers specifically, How to Migrate from Cloudflare Pages to Workers Without Downtime covers the wrangler configuration and SPA routing considerations.


Tested on Node.js v22.17.1, Express 5.2.1, Hono 4.13.1, @hono/node-server 1.19.17, macOS arm64. Benchmark tool: autocannon, 10 concurrent connections, 10s per scenario. Date: 2026-08-08.