DEV Community

Atlas Whoff
Atlas Whoff

Posted on

Elysia.js: The Bun-Native Web Framework That Benchmarks 10x Faster Than Express

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'
})
Enter fullscreen mode Exit fullscreen mode

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' }))
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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()
  }))
Enter fullscreen mode Exit fullscreen mode

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)
})
Enter fullscreen mode Exit fullscreen mode

Elysia equivalent:

app.get('/users/:id', async ({ params }) => {
  return db.users.findById(params.id)
})
Enter fullscreen mode Exit fullscreen mode

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)