Choosing a Node.js backend framework in 2026 means choosing between three tools with very different priorities. Express optimizes for familiarity. Fastify optimizes for Node.js performance. Hono optimizes for portability across runtimes.
Performance: The Numbers That Matter
| Framework | Throughput | Latency |
|---|---|---|
| Hono (Node.js) | ~85,000 req/s | ~0.5ms |
| Fastify | ~75,000 req/s | ~0.6ms |
| Express | ~30,000 req/s | ~1.4ms |
| Hono (Bun) | ~130,000+ req/s | ~0.3ms |
Express is ~2-3x slower than Fastify and Hono. The gap is real but only meaningful at scale (10k+ req/s). For most apps, database query time dominates.
Express — The Incumbent
import express from 'express'
import { z } from 'zod'
const app = express()
app.use(express.json())
app.post('/users', async (req, res) => {
const parsed = createUserSchema.safeParse(req.body)
if (!parsed.success) return res.status(400).json({ errors: parsed.error.flatten() })
const user = await db.user.create({ data: parsed.data })
res.status(201).json(user)
})
No built-in TypeScript. No built-in validation. No built-in serialization. You bring everything. That's also why there's an Express middleware for virtually every use case.
Choose Express when: legacy codebase, team already knows it, or you need a specific middleware with no alternative.
Fastify — Best Throughput on Node.js
Fastify compiles a serializer from your JSON Schema at startup — 2-3x faster than JSON.stringify. TypeScript is first-class.
import Fastify from 'fastify'
import { Type } from '@sinclair/typebox'
const fastify = Fastify({ logger: true })
fastify.post('/users', {
schema: {
body: Type.Object({
name: Type.String({ minLength: 1 }),
email: Type.String({ format: 'email' })
}),
response: {
201: Type.Object({ id: Type.String(), name: Type.String(), email: Type.String() })
}
}
}, async (request, reply) => {
// request.body is fully typed from schema
const user = await db.user.create({ data: request.body })
reply.status(201).send(user)
})
The schema does two things: validates input at runtime AND compiles the response serializer. One definition, both benefits.
Plugin system with fastify-plugin:
import fp from 'fastify-plugin'
export const authPlugin = fp(async (fastify) => {
fastify.decorate('authenticate', async (request, reply) => {
const token = request.headers.authorization?.replace('Bearer ', '')
if (!token) return reply.status(401).send({ error: 'Unauthorized' })
request.user = verifyJWT(token)
})
})
Choose Fastify when: staying on Node.js, schema-first development with TypeBox, need maximum throughput without leaving Node.
Hono — Runs Everywhere
Hono is built on web standards (Request, Response, Headers). The same code runs on Node.js, Bun, Cloudflare Workers, Deno, and AWS Lambda Edge.
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const app = new Hono()
app.post(
'/users',
zValidator('json', z.object({ name: z.string(), email: z.string().email() })),
async (c) => {
const body = c.req.valid('json') // fully typed
const user = await db.user.create({ data: body })
return c.json(user, 201)
}
)
export default app // same export on Node, Bun, or Cloudflare Workers
Built-in middleware that covers most APIs:
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
import { jwt } from 'hono/jwt'
import { rateLimiter } from 'hono-rate-limiter'
app.use('*', logger())
app.use('*', cors({ origin: 'https://myapp.com' }))
app.use('/api/*', jwt({ secret: process.env.JWT_SECRET! }))
RPC with end-to-end type safety (no code generation):
// server.ts
const routes = new Hono()
.get('/users/:id', async (c) => c.json(await getUser(c.req.param('id'))))
export type AppType = typeof routes
// client.ts
import { hc } from 'hono/client'
const client = hc<AppType>('http://localhost:3000')
const user = await client.users[':id'].$get({ param: { id: '123' } })
// Fully typed response — no API schema file needed
Choose Hono when: multi-runtime (Cloudflare Workers, Bun, Deno), edge deployments, you want tRPC-like type safety without the setup, or you're on Bun and want maximum performance.
The Same Endpoint in All Three
// Express
app.post('/posts', async (req, res) => {
const parsed = schema.safeParse(req.body)
if (!parsed.success) return res.status(400).json({ error: parsed.error })
const post = await db.post.create({ data: parsed.data })
res.status(201).json(post)
})
// Fastify
fastify.post('/posts', {
schema: { body: PostBody, response: { 201: PostResponse } }
}, async (request, reply) => {
const post = await db.post.create({ data: request.body })
reply.status(201).send(post)
})
// Hono
app.post('/posts', zValidator('json', schema), async (c) => {
const post = await db.post.create({ data: c.req.valid('json') })
return c.json(post, 201)
})
Fastify wins on compiled performance. Hono wins on portability and conciseness. Express requires the most boilerplate for equivalent safety.
Decision Framework
| Situation | Choose |
|---|---|
| Legacy Express codebase | Express |
| New project, Node.js only, max throughput | Fastify |
| New project, multi-runtime or edge | Hono |
| Using Bun | Hono |
| Need RPC-style type safety | Hono |
| Specific Express middleware with no alternative | Express |
Full article at stacknotice.com/blog/hono-vs-express-vs-fastify-2026
Top comments (0)