Elysia.js is a Bun-native HTTP framework that handles over 500,000 requests per second in benchmarks. That number is real — it comes from Bun's native HTTP server combined with Elysia's zero-overhead routing. But raw speed isn't the reason to use it. The reason is Eden Treaty: end-to-end type safety between your server and client with zero code generation.
Read the full article with all code examples at stacknotice.com
Setup
bun create elysia my-api
cd my-api
bun run dev
Minimum working server:
// src/index.ts
import { Elysia } from 'elysia'
const app = new Elysia()
.get('/', () => 'Hello Elysia')
.listen(3000)
// This export is what enables Eden Treaty
export type App = typeof app
Routing with TypeBox Validation
import { Elysia, t } from 'elysia'
const app = new Elysia()
.get('/users', () => getUsers())
.get('/users/:id', ({ params }) => getUserById(params.id))
.post('/users', ({ body }) => createUser(body), {
body: t.Object({
name: t.String({ minLength: 1 }),
email: t.String({ format: 'email' }),
role: t.Union([t.Literal('admin'), t.Literal('user')]),
}),
})
.delete('/users/:id', ({ params }) => deleteUser(params.id))
The t namespace is TypeBox — one schema for both runtime validation and TypeScript types.
Route Groups as Plugins
// src/routes/users.ts
export const usersRouter = new Elysia({ prefix: '/users' })
.get('/', () => UserService.getAll())
.get('/:id', ({ params }) => UserService.getById(params.id))
.post('/', ({ body }) => UserService.create(body), {
body: t.Object({
name: t.String(),
email: t.String({ format: 'email' }),
}),
})
// src/index.ts
const app = new Elysia()
.use(usersRouter)
.use(productsRouter)
.listen(3000)
export type App = typeof app
Auth Middleware with derive
export const authPlugin = new Elysia({ name: 'auth' })
.derive({ as: 'scoped' }, async ({ headers, error }) => {
const token = headers['authorization']?.slice(7)
if (!token) throw error(401, 'Missing token')
const payload = await verifyJWT(token)
if (!payload) throw error(401, 'Invalid token')
return {
user: {
id: payload.sub as string,
email: payload.email as string,
role: payload.role as 'admin' | 'user',
},
}
})
// Apply to protected routes
export const protectedRouter = new Elysia({ prefix: '/api' })
.use(authPlugin)
.get('/me', ({ user }) => user) // `user` is fully typed
{ as: 'scoped' } means the derived context only applies to this instance — not to parent or sibling routes.
Eden Treaty — The Killer Feature
On your client, import the App type and create a typed client:
// client/api.ts
import { treaty } from '@elysiajs/eden'
import type { App } from '../server/src/index'
export const api = treaty<App>('http://localhost:3000')
Now you have a typed client that mirrors your API structure:
// Fully typed — no codegen, no schema files
const { data: users } = await api.users.get()
const { data: user } = await api.users({ id: '123' }).get()
const { data: newUser } = await api.users.post({
name: 'Alice',
email: 'alice@example.com',
})
// TypeScript error — body doesn't match schema
// @ts-expect-error
await api.users.post({ name: 123 }) // must be string
When you change a route's input shape on the server, TypeScript errors appear at every client call site that's now wrong. Zero manual type updates.
Drizzle ORM Integration
// src/plugins/database.ts
import { Elysia } from 'elysia'
import { db } from '../db'
export const databasePlugin = new Elysia({ name: 'database' })
.decorate('db', db)
// src/routes/posts.ts
export const postsRouter = new Elysia({ prefix: '/posts' })
.use(databasePlugin)
.use(authPlugin)
.get('/', async ({ db }) => {
return db.select().from(posts).orderBy(desc(posts.publishedAt))
})
.post('/', async ({ db, body, user }) => {
const [post] = await db.insert(posts)
.values({ ...body, authorId: user.id })
.returning()
return post
}, {
body: t.Object({
title: t.String({ minLength: 1 }),
content: t.String({ minLength: 1 }),
}),
})
Common Plugins
bun add @elysiajs/cors @elysiajs/swagger @elysiajs/bearer
const app = new Elysia()
.use(cors({ origin: ['http://localhost:3000'] }))
.use(swagger()) // Auto-generated Swagger UI at /swagger
.use(bearer()) // Extracts Bearer token to context.bearer
The swagger() plugin reads your TypeBox schemas and generates OpenAPI docs automatically.
Elysia vs Alternatives
| Use case | Best choice |
|---|---|
| Bun-only, need Eden Treaty | Elysia |
| Multi-runtime (Workers, Deno, Node) | Hono |
| Full Next.js stack | Next.js Route Handlers |
| tRPC-style type safety | tRPC + Next.js |
| Microservices in Bun | Elysia |
Elysia makes the most sense when you're fully committed to Bun and want end-to-end type safety without tRPC's JSON-RPC overhead. If you need Cloudflare Workers or Deno Deploy, use Hono instead.
For the full guide with lifecycle hooks, global error handling, performance details, and deployment config: stacknotice.com/blog/elysiajs-bun-complete-guide-2026
Top comments (0)