Skip to content
Unverified — AI-generated content. Help verify this page

Express vs Fastify vs Hono vs Elysia

The backend JavaScript framework landscape has evolved from Express's dominance into a diverse ecosystem of performance-focused, type-safe alternatives. This page compares the four most relevant server frameworks across every dimension engineers care about.

Overview

Express

Express is the original Node.js web framework, created by TJ Holowaychuk in 2010. It defined the middleware pattern that every subsequent Node.js framework copied or improved upon. Express is minimalist by design — it provides routing, middleware chaining, and a thin wrapper over Node's HTTP module. Express 5 (finally released after years in beta) brings promise support and improved path matching. With 65,000+ GitHub stars and millions of weekly npm downloads, Express remains the most widely deployed Node.js framework.

Fastify

Fastify is a high-performance Node.js framework created by Matteo Collina and Tomas Della Vedova in 2017. It was built from the ground up for speed — using JSON Schema for request/response validation and serialization, a plugin system based on encapsulation, and an efficient radix-tree router. Fastify benchmarks 2-3x faster than Express for JSON serialization because it uses fast-json-stringify instead of JSON.stringify. It has excellent TypeScript support and a mature plugin ecosystem.

Hono

Hono (Japanese for "flame") is an ultralight web framework created by Yusuke Wada in 2022. It runs on every JavaScript runtime — Node.js, Deno, Bun, Cloudflare Workers, AWS Lambda, Vercel Edge, and Fastly Compute. Hono's key innovation is runtime portability: you write your API once and deploy it anywhere. At ~14 KB, it is the smallest framework here. Hono provides a middleware stack, routing, and first-class TypeScript support with RPC-style type-safe client generation.

Elysia

Elysia is a Bun-first web framework created by SaltyAom in 2023. It uses Bun's native HTTP server for maximum performance and provides end-to-end type safety through TypeScript inference — request validation, response types, and error types flow through the entire chain without manual annotation. Elysia consistently tops Bun benchmarks and provides features like Eden Treaty (a type-safe client similar to tRPC) and lifecycle hooks.

Architecture Comparison

Key Architectural Differences

Express processes requests through a linear middleware chain. Each middleware calls next() to pass control. There is no schema validation, no serialization optimization, and no encapsulation — all middleware shares the same req/res objects.

Fastify uses an encapsulated plugin system where each plugin gets its own scope. Request validation happens automatically via JSON Schema before the handler runs, and response serialization uses fast-json-stringify (pre-compiled from schema). This architecture enables both safety and speed.

Hono uses the Web Standards Request and Response objects, making it runtime-agnostic. Its middleware model is similar to Express but uses c.next() (context-based) rather than modifying a shared req/res object. This portability is the core design decision.

Elysia leverages Bun's HTTP server directly, avoiding the Node.js http module overhead. It uses TypeBox schemas for validation and infers the entire request/response type chain at compile time. The architecture is designed for Bun-first performance.

Feature Matrix

FeatureExpress 5Fastify 5Hono 4Elysia 1.x
RuntimeNode.jsNode.jsAny (Node, Bun, Deno, Edge)Bun (primary)
TypeScriptPartial (@types/express)Excellent (built-in)Excellent (built-in)Excellent (inferred)
ValidationManual (express-validator)JSON Schema (built-in)Zod/Valibot middlewareTypeBox (built-in)
SerializationJSON.stringifyfast-json-stringifyJSON.stringifyBun-optimized
RouterPath-to-regexpfind-my-way (radix tree)RegExpRouter / TrieRouterRadix tree
Middleware modelreq/res chainHooks + encapsulated pluginsContext-based chainLifecycle hooks
Plugin systemnpm packagesEncapsulated pluginsMiddleware + helpersPlugin chain
WebSocketws (third-party)@fastify/websocketBuilt-in (runtime-specific)Built-in (Bun native)
OpenAPI/Swaggerswagger-jsdoc@fastify/swagger (built-in)Zod OpenAPI middlewareBuilt-in (Eden)
Type-safe clientNoNohc (Hono Client)Eden Treaty
Streamingres.writereply.rawc.stream / SSE helpersBun streams
File uploadsmulter@fastify/multipartBuilt-in (parseBody)Built-in
Rate limitingexpress-rate-limit@fastify/rate-limitBuilt-in middlewarePlugin
CORScors package@fastify/corsBuilt-in middlewarePlugin
Static filesexpress.static@fastify/staticBuilt-in middlewarePlugin
Bundle size~200 KB~350 KB~14 KB~50 KB
Weekly npm downloads~30M~3M~500K~100K

Code Comparison

Basic CRUD API

ts
import express from 'express';

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

interface User {
  id: string;
  name: string;
  email: string;
}

const users: User[] = [];

app.get('/users', (req, res) => {
  res.json(users);
});

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);
});

app.post('/users', (req, res) => {
  // No built-in validation — must validate manually
  const { name, email } = req.body;
  if (!name || !email) {
    return res.status(400).json({ error: 'Name and email required' });
  }
  const user: User = { id: crypto.randomUUID(), name, email };
  users.push(user);
  res.status(201).json(user);
});

app.listen(3000);
ts
import Fastify from 'fastify';

const app = Fastify({ logger: true });

interface User {
  id: string;
  name: string;
  email: string;
}

const users: User[] = [];

app.get('/users', async () => {
  return users;
});

app.get<{ Params: { id: string } }>(
  '/users/:id',
  {
    schema: {
      params: {
        type: 'object',
        properties: { id: { type: 'string', format: 'uuid' } },
        required: ['id'],
      },
    },
  },
  async (request, reply) => {
    const user = users.find(u => u.id === request.params.id);
    if (!user) return reply.code(404).send({ error: 'Not found' });
    return user;
  }
);

app.post<{ Body: { name: string; email: string } }>(
  '/users',
  {
    schema: {
      body: {
        type: 'object',
        properties: {
          name: { type: 'string', minLength: 1 },
          email: { type: 'string', format: 'email' },
        },
        required: ['name', 'email'],
      },
    },
  },
  async (request, reply) => {
    const user: User = {
      id: crypto.randomUUID(),
      ...request.body,
    };
    users.push(user);
    return reply.code(201).send(user);
  }
);

app.listen({ port: 3000 });
ts
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';

const app = new Hono();

interface User {
  id: string;
  name: string;
  email: string;
}

const users: User[] = [];

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

app.get('/users', (c) => {
  return c.json(users);
});

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

app.post(
  '/users',
  zValidator('json', createUserSchema),
  (c) => {
    const body = c.req.valid('json');
    const user: User = { id: crypto.randomUUID(), ...body };
    users.push(user);
    return c.json(user, 201);
  }
);

export default app; // Works on any runtime
ts
import { Elysia, t } from 'elysia';

interface User {
  id: string;
  name: string;
  email: string;
}

const users: User[] = [];

const app = new Elysia()
  .get('/users', () => users)
  .get('/users/:id', ({ params: { id }, error }) => {
    const user = users.find(u => u.id === id);
    if (!user) return error(404, { error: 'Not found' });
    return user;
  })
  .post(
    '/users',
    ({ body }) => {
      const user: User = { id: crypto.randomUUID(), ...body };
      users.push(user);
      return user;
    },
    {
      body: t.Object({
        name: t.String({ minLength: 1 }),
        email: t.String({ format: 'email' }),
      }),
    }
  )
  .listen(3000);

Middleware

ts
// Timing middleware
app.use((req, res, next) => {
  const start = Date.now();
  res.on('finish', () => {
    const ms = Date.now() - start;
    console.log(`${req.method} ${req.url} ${res.statusCode} ${ms}ms`);
  });
  next();
});
ts
// Timing hook
app.addHook('onRequest', async (request) => {
  request.startTime = Date.now();
});

app.addHook('onResponse', async (request, reply) => {
  const ms = Date.now() - request.startTime;
  request.log.info(`${request.method} ${request.url} ${reply.statusCode} ${ms}ms`);
});
ts
// Timing middleware
app.use('*', async (c, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  console.log(`${c.req.method} ${c.req.url} ${c.res.status} ${ms}ms`);
});
ts
// Timing lifecycle
const app = new Elysia()
  .onBeforeHandle(({ request }) => {
    (request as any).startTime = Date.now();
  })
  .onAfterHandle(({ request, set }) => {
    const ms = Date.now() - (request as any).startTime;
    console.log(`${request.method} ${request.url} ${set.status} ${ms}ms`);
  });

Performance

Requests per Second (JSON serialization, 1 KB payload)

FrameworkRuntimeReq/sLatency (p50)Latency (p99)
ElysiaBun~120,0000.4ms2.1ms
HonoBun~105,0000.5ms2.5ms
FastifyNode.js~65,0000.8ms4.2ms
HonoNode.js~55,0000.9ms4.8ms
ExpressNode.js~22,0002.3ms12ms

Benchmark context

These numbers are from synthetic benchmarks (single endpoint, JSON serialization, no database). Real applications spend 95%+ of request time in database queries, external API calls, and business logic. The difference between 22K and 120K req/s is irrelevant when your database query takes 15ms. Choose based on DX and ecosystem, not raw throughput.

Memory Usage

FrameworkIdle MemoryUnder Load (1K concurrent)
Hono (Bun)22 MB58 MB
Elysia25 MB55 MB
Fastify40 MB95 MB
Express45 MB120 MB

Startup Time

FrameworkCold StartWith 50 Routes
Hono (Bun)12ms18ms
Elysia15ms22ms
Hono (Node)45ms65ms
Fastify80ms150ms
Express50ms85ms

Developer Experience

Learning Curve

AspectExpressFastifyHonoElysia
Time to first API5 min10 min5 min10 min
Concept countLow (req, res, next)Medium (schemas, hooks, plugins)Low (context, next)Medium (lifecycle, types, Eden)
DocumentationGood (but dated)ExcellentGoodGood
Error messagesPoor (generic)Good (schema-aware)GoodExcellent (type-inferred)
DebuggingEasy (simple stack)Moderate (plugin scoping)EasyModerate (Bun tooling)

Ecosystem Comparison

CategoryExpressFastifyHonoElysia
npm packages50,000+300+ official/community80+ middleware50+ plugins
AuthPassport.js@fastify/passportBuilt-in JWT/BearerBuilt-in JWT
DatabaseAny ORMAny ORMAny ORMAny ORM
LoggingMorgan, WinstonPino (built-in)Built-in loggerBuilt-in
TestingSupertestFastify injectapp.request()Elysia test utils
OpenAPIswagger-jsdoc@fastify/swagger@hono/zod-openapiBuilt-in
GraphQLapollo-server-expressmercurius@hono/graphql-server@elysiajs/graphql

Express migration note

If you have an existing Express app and want better performance, Fastify provides the most straightforward migration path. It has an Express compatibility layer (@fastify/express) that lets you use existing Express middleware while gradually migrating to Fastify's plugin system.

When to Use Which

Decision Summary

ScenarioBest ChoiceWhy
Cloudflare Workers / EdgeHonoBuilt for edge, 14 KB, Web Standards
Bun-first projectElysiaBun-native, fastest raw performance
Enterprise Node.js APIFastifySchema validation, logging, plugin system
Legacy project, quick prototypeExpressEveryone knows it, most npm packages
Multi-runtime deploymentHonoWrite once, deploy anywhere
Microservices with shared typesHono or ElysiaType-safe RPC clients
High-throughput Node.jsFastify3x faster than Express with validation
Serverless functionsHonoTiny bundle, fast cold start

Migration

Express to Fastify

  1. Install: npm install fastify @fastify/express
  2. Use compatibility layer: Register Express middleware via @fastify/express
  3. Migrate routes: Change (req, res) => {} to async (request, reply) => {}
  4. Add schemas: Define JSON Schema for request validation and response serialization
  5. Replace middleware: Swap Express middleware for Fastify plugins (cors, helmet, etc.)
  6. Remove compatibility layer once all routes are migrated
ts
// Before (Express)
app.post('/users', (req, res) => {
  const { name, email } = req.body;
  // manual validation...
  res.status(201).json(user);
});

// After (Fastify)
app.post('/users', {
  schema: {
    body: { type: 'object', properties: { name: { type: 'string' }, email: { type: 'string', format: 'email' } }, required: ['name', 'email'] },
  },
}, async (request, reply) => {
  // body is already validated
  const user = createUser(request.body);
  return reply.code(201).send(user);
});

Express to Hono

  1. Install: npm install hono
  2. Replace app creation: express() becomes new Hono()
  3. Migrate routes: (req, res) becomes (c) =>, res.json() becomes c.json()
  4. Migrate middleware: Most Express middleware has Hono equivalents or built-in alternatives
  5. Update deployment: Choose adapter for target runtime

Middleware compatibility

Express middleware is not compatible with Hono or Elysia. You will need to find equivalents or write custom middleware. Fastify has the best Express compatibility story through @fastify/express.

Verdict

Choose Express if you need maximum ecosystem compatibility, your team already knows it, or you are building a quick prototype that does not need to handle high traffic. Express is showing its age, but "boring technology" has real value — every Node.js developer knows Express, every tutorial uses it, and every middleware exists for it.

Choose Fastify if you are building a production Node.js API and care about performance, validation, and structured logging. Fastify is the natural successor to Express for server-side Node.js — it is faster, safer (schema validation by default), and better structured (plugin encapsulation). The migration path from Express is well-documented.

Choose Hono if you need to deploy across multiple runtimes (edge, serverless, Node, Bun, Deno) or if bundle size matters (serverless cold starts, edge workers). Hono is the universal framework — write once, deploy anywhere. Its middleware ecosystem is growing fast.

Choose Elysia if you are building on Bun and want maximum performance with end-to-end type safety. Elysia's Eden Treaty gives you tRPC-like type safety for REST APIs without code generation. The tradeoff is Bun lock-in and a smaller ecosystem.

Which Would You Choose?

Scenario 1: You are building a REST API that must deploy to Cloudflare Workers, AWS Lambda, and a traditional Node.js server depending on the client's infrastructure.

Recommendation: Hono

Hono is the only framework designed for runtime portability. Write your API once, deploy it to Workers (14 KB bundle, instant cold starts), Lambda, Deno, Bun, or Node.js. No other framework supports this many deployment targets with a single codebase.

Scenario 2: Your team of 20 backend engineers maintains a large Express monolith with 200+ routes, 50 custom middleware functions, and extensive Passport.js auth. Performance is becoming a bottleneck.

Recommendation: Fastify

Fastify's @fastify/express compatibility layer lets you run existing Express middleware while gradually migrating routes to Fastify's plugin system. You get 3x performance improvement incrementally without a big-bang rewrite. No other framework offers this migration path.

Scenario 3: You are a solo developer prototyping an API in a weekend hackathon. You need to ship something that works in 2 hours. Everyone on your team knows JavaScript basics.

Recommendation: Express

Express has the lowest barrier to entry. Every tutorial on the internet uses Express, every npm package has an Express example, and every developer you will ever hire knows it. For a prototype that may be thrown away, Express's ecosystem advantage outweighs any performance concern.

Common Misconceptions

  • "Express is too slow for production" — Express handles 22K req/s, which is more than enough for 99% of applications. Your database queries take 15ms; the framework overhead of 2ms vs 0.5ms is noise. Express is slow relative to Fastify, not slow in absolute terms.
  • "Hono is only for edge/serverless" — Hono runs perfectly on Node.js and Bun as a traditional long-running server. Its edge portability is a bonus, not a requirement.
  • "Elysia requires Bun for everything" — While Elysia is optimized for Bun, your application code (business logic, database queries) is standard TypeScript that can be adapted to other runtimes if needed.
  • "Fastify is hard to learn" — Fastify's core API is straightforward. The complexity comes from the plugin encapsulation model, which is a feature (enforced modularity), not a bug.

Real Migration Stories

Fastify team: From Express to Fastify at NearForm — NearForm, the consultancy behind Fastify, migrated several client projects from Express to Fastify. They reported 2-3x throughput improvements and found that JSON Schema validation eliminated entire categories of bugs that manual validation missed.

Discord: Custom framework — Discord famously moved from Express/Node.js to Elixir for their real-time gateway, demonstrating that sometimes the migration is not between Node.js frameworks but away from Node.js entirely when you have extreme concurrency requirements (millions of concurrent WebSocket connections).

Quiz

1. Why is Fastify faster than Express for JSON responses?

Fastify uses fast-json-stringify, which pre-compiles JSON Schema into an optimized serialization function. Express uses the generic JSON.stringify, which must inspect the object structure on every call.

2. What does Hono's "runtime portability" mean in practice?

A single Hono application can run on Node.js, Bun, Deno, Cloudflare Workers, AWS Lambda, Vercel Edge, and Fastly Compute without code changes. Hono uses the Web Standards Request/Response objects, which are supported on all these runtimes.

3. What is Elysia's Eden Treaty, and what problem does it solve?

Eden Treaty is a type-safe client generator for Elysia APIs. It gives you tRPC-like end-to-end type safety (autocompletion, type checking) between your Elysia backend and your frontend without code generation or schema files.

4. Why does Express middleware not work with Hono or Elysia?

Express middleware uses the (req, res, next) pattern with Node.js-specific IncomingMessage and ServerResponse objects. Hono uses Web Standards Request/Response, and Elysia uses Bun's native HTTP objects. The interfaces are fundamentally different.

5. In what scenario would Express's massive ecosystem outweigh Fastify's performance advantage?

When you need a specific npm package that only provides Express middleware (e.g., a niche auth strategy, a proprietary SDK integration). Fastify's @fastify/express compatibility layer can help, but native Express middleware availability is still the largest ecosystem.

One-Liner Summary

Express has the biggest ecosystem but slowest speed, Fastify is the natural Express successor for Node.js performance, Hono deploys anywhere in 14 KB, and Elysia squeezes max throughput from Bun.

"What I cannot create, I do not understand." — Richard Feynman