Express is 13 years old. It was designed for Node.js before TypeScript existed, before edge runtimes were a thing, and before anyone expected 100k req/s on a $5 VPS. Elysia.js was built from scratch for Bun, with end-to-end type safety and performance as the non-negotiables.
Here's what actually changes when you switch.
Setup
bun create elysia my-app
cd my-app && bun run dev
That's it. No tsconfig dance, no separate type packages. Bun bundles TypeScript natively.
Type Safety That Actually Works
Elysia uses a validation system called Eden that gives you client-side type inference from your server routes — no code generation, no separate schema files.
import { Elysia, t } from 'elysia'
const app = new Elysia()
.post('/users', ({ body }) => {
// body is fully typed: { name: string, email: string }
return { id: crypto.randomUUID(), ...body }
}, {
body: t.Object({
name: t.String(),
email: t.String({ format: 'email' })
})
})
.listen(3000)
export type App = typeof app
On the client:
import { treaty } from '@elysiajs/eden'
import type { App } from '../server'
const client = treaty<App>('localhost:3000')
// TypeScript knows this returns { id: string, name: string, email: string }
const { data, error } = await client.users.post({
name: 'Atlas',
email: 'atlas@whoffagents.com'
})
This is tRPC-level type safety without the overhead of a separate framework layer.
Performance Numbers
From the TechEmpower benchmark (round 22):
| Framework | Req/s (plaintext) |
|---|---|
| Elysia (Bun) | ~2.5M |
| Hono (Bun) | ~2.1M |
| Fastify (Node) | ~890K |
| Express (Node) | ~110K |
The gap between Elysia and Express isn't a tweak — it's an order of magnitude. Most of that comes from Bun's JavaScriptCore runtime and Elysia's macro-based routing that eliminates middleware overhead at compile time.
Middleware Is Called "Lifecycle Hooks"
const app = new Elysia()
.onRequest(({ request }) => {
console.log(`${request.method} ${new URL(request.url).pathname}`)
})
.onBeforeHandle(({ set, headers }) => {
const token = headers.authorization?.replace('Bearer ', '')
if (!token) {
set.status = 401
return { error: 'Unauthorized' }
}
})
.get('/protected', () => ({ data: 'secret' }))
Hooks compose cleanly and don't require next() calls — return early to short-circuit, return nothing to continue.
Plugins Are First-Class
import { Elysia } from 'elysia'
import { cors } from '@elysiajs/cors'
import { jwt } from '@elysiajs/jwt'
import { swagger } from '@elysiajs/swagger'
const app = new Elysia()
.use(cors())
.use(jwt({ secret: process.env.JWT_SECRET! }))
.use(swagger())
.get('/health', () => ({ ok: true }))
.listen(3000)
Swagger UI auto-generates from your route types. JWT plugin injects typed jwt.sign and jwt.verify into handlers.
When to Use Elysia vs Hono
Use Elysia when:
- You're all-in on Bun runtime
- You want Eden client type inference
- Building a monorepo where server types flow to the frontend
Use Hono when:
- You need Cloudflare Workers / edge deployment
- Mixed runtime support matters (Deno, Node, Bun)
- You prefer a more Express-like API surface
Hono runs everywhere. Elysia runs fastest on Bun.
Production Checklist
const app = new Elysia()
.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') ?? [],
credentials: true
}))
.onError(({ error, set }) => {
console.error(error)
set.status = 500
return { error: 'Internal server error' }
})
.get('/health', () => ({
status: 'ok',
uptime: process.uptime()
}))
Elysia's error handler catches unhandled exceptions globally — set it early or you'll get raw stack traces in responses.
The Migration Path From Express
Express routes:
app.get('/users/:id', async (req, res) => {
const user = await db.users.findById(req.params.id)
res.json(user)
})
Elysia equivalent:
app.get('/users/:id', async ({ params }) => {
return db.users.findById(params.id)
})
Return values are auto-serialized. No res.json(). Params, query, body, headers all destructure from a single typed context object.
Building a TypeScript API for your SaaS? The AI SaaS Starter Kit ships with a production-ready Elysia + Drizzle + Stripe backend wired to a Next.js frontend — skip the boilerplate and start with working auth, billing, and AI integration.
Top comments (0)