Blog
October 6, 20265 min readIbrahim Elkamali

Hono vs Express vs Fastify vs Elysia for a TypeScript API

Choose Hono, Express, Fastify, or Elysia by runtime, request model, validation style, and middleware ecosystem, with a Better Fullstack command for each.

honoexpressfastifyelysiabackend
Markdown

Stack decision map: choose an ecosystem, generate, inspect, and keep the code

Hono, Express, Fastify, and Elysia all route HTTP requests to TypeScript handlers. The useful comparison is not a feature count. It is where the server runs, which request model your code and middleware expect, and how much the framework should own validation and typing.

Settle one question first: does the project need a separate API server? If the web app is the only client, a framework-owned backend may be simpler. Self backend vs separate API covers that decision. The rest of this post assumes a separate server.

Runtime and deployment come first

Each framework describes a different home:

If the target is an edge or serverless platform that speaks the Fetch API, Hono and Elysia start closer to it. If the target is a long-running Node.js process, all four fit, and Express and Fastify are built for exactly that.

Request model

Hono and Elysia handlers work with Web Standard Request and Response objects. Express uses Node's request and response objects with the (req, res, next) middleware signature. Fastify has its own request and reply objects.

This affects every library that touches the request: auth handlers, RPC adapters, file uploads, and logging. A library written for the Fetch API plugs into Hono or Elysia directly and needs a bridge on Express or Fastify, and the reverse holds for Node-style middleware.

Validation and typing model

  • Hono pairs its validator with the hc client for RPC-style typed calls between server and client.
  • Express leaves validation to whatever middleware you add. Since Express 5, a rejected promise in middleware is passed to next, so async errors reach your error handler without manual wrapping.
  • Fastify uses a schema-based approach: JSON Schema with Ajv for request validation and fast-json-stringify for response serialization.
  • Elysia infers types from its own Elysia.t schema builder, accepts Standard Schema libraries such as Zod and Valibot, and provides the Eden client for end-to-end types. Its OpenAPI plugin, installed as @elysia/openapi, generates OpenAPI documentation from route schemas and, through its fromTypes generator, from TypeScript types. The Elysia server generated by the --api orpc command below does not include that plugin and serves oRPC's own OpenAPI reference at /api-reference instead. With --api openapi, Better Fullstack adds the plugin from the @elysiajs/openapi package and serves the documentation at /docs.

If an RPC layer such as oRPC or tRPC will own the typed contract, the framework's own typing features matter less. Better Fullstack generates that arrangement, described below.

Middleware and plugin model

Express's ecosystem is built on the middleware chain. Fastify organizes features as plugins inside encapsulation contexts: each context inherits from its parents, and parents cannot reach into their children unless encapsulation is deliberately broken. Hono registers middleware with app.use, and Elysia composes plugins with .use on the app instance.

Check the specific packages you depend on. A team with years of Express middleware has a concrete reason to stay on Express.

What Better Fullstack generates

The four commands below share every flag except backend and runtime:

bash
bun create better-fullstack@latest my-app \
  --ecosystem typescript \
  --frontend tanstack-router \
  --backend hono \
  --runtime bun \
  --database sqlite \
  --orm drizzle \
  --api orpc \
  --auth better-auth
bash
bun create better-fullstack@latest my-app \
  --ecosystem typescript \
  --frontend tanstack-router \
  --backend express \
  --runtime node \
  --database sqlite \
  --orm drizzle \
  --api orpc \
  --auth better-auth
bash
bun create better-fullstack@latest my-app \
  --ecosystem typescript \
  --frontend tanstack-router \
  --backend fastify \
  --runtime node \
  --database sqlite \
  --orm drizzle \
  --api orpc \
  --auth better-auth
bash
bun create better-fullstack@latest my-app \
  --ecosystem typescript \
  --frontend tanstack-router \
  --backend elysia \
  --runtime bun \
  --database sqlite \
  --orm drizzle \
  --api orpc \
  --auth better-auth

The oRPC procedures and router in packages/api are identical across all four. Only packages/api/src/context.ts adapts to the framework's request object, and the Express version converts Node headers to Web Headers before reading the Better Auth session. The server entry in apps/server/src/index.ts is where the frameworks differ:

ConcernHonoExpressFastifyElysia
oRPC adapter@orpc/server/fetch@orpc/server/node@orpc/server/fastify@orpc/server/fetch
Better Auth mountauth.handler(c.req.raw)toNodeHandler(auth)handler that builds a Web Requestauth.handler(request)
CORShono/corscors@fastify/cors@elysiajs/cors
Dev scriptbun run --hottsx watchtsx watchbun run --hot

The Fastify server registers the oRPC routes inside their own plugin with a content type parser that leaves body parsing to oRPC, and it enables Fastify's logger.

Hono is the only backend Better Fullstack accepts for the Cloudflare Workers runtime. This command keeps the same Hono server code and adds packages/infra/alchemy.run.ts, which declares a Worker and a D1 database with Alchemy:

bash
bun create better-fullstack@latest my-app \
  --ecosystem typescript \
  --frontend tanstack-router \
  --backend hono \
  --runtime workers \
  --database sqlite \
  --orm drizzle \
  --db-setup d1 \
  --server-deploy cloudflare \
  --api orpc \
  --auth better-auth

These commands produce generated projects. They do not prove that a given deployment target runs them; install, build, and deploy a small slice to the host you plan to use.

Decision table

If this is the stronger constraintStart with
Cloudflare Workers or several Fetch-based runtimesHono
Existing Express middleware and team experienceExpress
Node.js service with schema validation and plugin scopesFastify
Bun-first service using Elysia's types and EdenElysia
An RPC layer owns the typed contractAny of the four; choose by runtime
The web app is the only clientConsider a framework-owned backend

Compare real generated stacks

For the API layer on top of any of these servers, read tRPC vs oRPC.

GitHub Sponsors