Elysia.js Complete Guide | Fast Web Framework for Bun
이 글의 핵심
Elysia is a TypeScript-first web framework for Bun — faster than Hono and Express with end-to-end type safety via TypeBox. This guide covers building production APIs with routing, validation, middleware, plugins, and WebSocket.
Why Elysia?
Elysia is a TypeScript-first web framework built for Bun:
- Speed: Benchmarks faster than Hono, Fastify, and Express
- Type safety: End-to-end types with TypeBox — no runtime type errors
- Eden Treaty: Generate type-safe frontend client from your API definition
- Batteries included: Swagger, CORS, JWT, WebSocket — all plugins
Setup
# Create project (Bun required)
bun create elysia my-api
cd my-api
bun install
bun run dev
Or from scratch:
mkdir my-api && cd my-api
bun init
bun add elysia
1. Basic Routing
import { Elysia } from 'elysia'
const app = new Elysia()
.get('/', () => 'Hello Elysia!')
.get('/hello/:name', ({ params }) => `Hello, ${params.name}!`)
.post('/echo', ({ body }) => body)
.listen(3000)
console.log(`Server running at http://localhost:${app.server?.port}`)
2. Type-Safe Validation
Elysia uses TypeBox (JSON Schema) for request/response validation — faster than Zod at runtime:
import { Elysia, t } from 'elysia'
const app = new Elysia()
// GET with query params validation
.get('/users', ({ query }) => {
return { page: query.page, limit: query.limit }
}, {
query: t.Object({
page: t.Number({ minimum: 1, default: 1 }),
limit: t.Number({ minimum: 1, maximum: 100, default: 20 }),
search: t.Optional(t.String()),
}),
})
// POST with body validation
.post('/users', async ({ body }) => {
const user = await createUser(body)
return user
}, {
body: t.Object({
name: t.String({ minLength: 2, maxLength: 100 }),
email: t.String({ format: 'email' }),
role: t.Union([t.Literal('admin'), t.Literal('user')], {
default: 'user',
}),
}),
// Response type validation + docs generation
response: t.Object({
id: t.Number(),
name: t.String(),
email: t.String(),
createdAt: t.String(),
}),
})
.listen(3000)
3. Middleware and Lifecycle Hooks
import { Elysia } from 'elysia'
const app = new Elysia()
// Global hooks
.onRequest(({ request }) => {
console.log(`${request.method} ${request.url}`)
})
.onBeforeHandle(({ request, set }) => {
// Runs before route handler
const token = request.headers.get('Authorization')
if (!token) {
set.status = 401
return { error: 'Unauthorized' }
}
})
.onAfterHandle(({ response, set }) => {
// Runs after route handler
set.headers['X-Powered-By'] = 'Elysia'
})
.onError(({ error, code, set }) => {
if (code === 'NOT_FOUND') {
set.status = 404
return { error: 'Route not found' }
}
if (code === 'VALIDATION') {
set.status = 400
return { error: 'Validation failed', details: error.message }
}
set.status = 500
return { error: 'Internal server error' }
})
.get('/', () => 'Hello!')
.listen(3000)
4. Plugins — Modular Architecture
import { Elysia } from 'elysia'
// Define a plugin
const userPlugin = new Elysia({ prefix: '/users' })
.get('/', async () => {
return await db.users.findAll()
})
.get('/:id', async ({ params, set }) => {
const user = await db.users.findById(params.id)
if (!user) { set.status = 404; return { error: 'Not found' } }
return user
})
.post('/', async ({ body }) => {
return await db.users.create(body)
})
const postPlugin = new Elysia({ prefix: '/posts' })
.get('/', () => db.posts.findAll())
// Compose plugins
const app = new Elysia()
.use(userPlugin)
.use(postPlugin)
.listen(3000)
// Routes: GET /users, GET /users/:id, POST /users, GET /posts
Built-in plugins
bun add @elysiajs/cors @elysiajs/jwt @elysiajs/swagger @elysiajs/bearer
import { Elysia } from 'elysia'
import { cors } from '@elysiajs/cors'
import { jwt } from '@elysiajs/jwt'
import { swagger } from '@elysiajs/swagger'
import { bearer } from '@elysiajs/bearer'
const app = new Elysia()
.use(cors({
origin: ['https://myapp.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
}))
.use(swagger({
documentation: {
info: { title: 'My API', version: '1.0.0' },
},
}))
// Auto-generates Swagger UI at /swagger
.use(jwt({
name: 'jwt',
secret: process.env.JWT_SECRET!,
}))
.use(bearer())
// Extracts Bearer token from Authorization header
.listen(3000)
5. JWT Authentication
import { Elysia, t } from 'elysia'
import { jwt } from '@elysiajs/jwt'
const app = new Elysia()
.use(jwt({ name: 'jwt', secret: process.env.JWT_SECRET! }))
.post('/auth/login', async ({ body, jwt, set }) => {
const user = await db.users.findByEmail(body.email)
if (!user || !await verifyPassword(body.password, user.passwordHash)) {
set.status = 401
return { error: 'Invalid credentials' }
}
const token = await jwt.sign({
sub: user.id,
role: user.role,
exp: Math.floor(Date.now() / 1000) + 3600,
})
return { token, user: { id: user.id, email: user.email } }
}, {
body: t.Object({
email: t.String({ format: 'email' }),
password: t.String({ minLength: 8 }),
}),
})
// Protected routes
.guard(
{
beforeHandle: async ({ jwt, bearer, set }) => {
const payload = await jwt.verify(bearer)
if (!payload) {
set.status = 401
return { error: 'Unauthorized' }
}
},
},
(app) => app
.get('/api/me', async ({ jwt, bearer }) => {
const payload = await jwt.verify(bearer)
return await db.users.findById(payload!.sub as string)
})
.get('/api/dashboard', () => ({ data: 'protected' }))
)
.listen(3000)
6. WebSocket
import { Elysia, t } from 'elysia'
const clients = new Set<{ send: Function }>()
const app = new Elysia()
.ws('/ws/chat', {
// Validate incoming messages
body: t.Object({
type: t.Union([t.Literal('message'), t.Literal('join')]),
text: t.Optional(t.String()),
room: t.Optional(t.String()),
}),
open(ws) {
clients.add(ws)
ws.send({ type: 'connected', clientCount: clients.size })
},
message(ws, data) {
if (data.type === 'message') {
// Broadcast to all connected clients
for (const client of clients) {
client.send({ type: 'message', text: data.text, from: ws.id })
}
}
},
close(ws) {
clients.delete(ws)
},
})
.listen(3000)
7. Eden Treaty — End-to-End Type Safety
Eden Treaty generates a type-safe client from your Elysia API:
bun add elysia @elysiajs/eden
// Server: export the app type
const app = new Elysia()
.get('/users', () => [{ id: 1, name: 'Alice' }])
.post('/users', ({ body }: { body: { name: string; email: string } }) => ({
id: 2,
...body,
}), {
body: t.Object({ name: t.String(), email: t.String() }),
})
.listen(3000)
export type App = typeof app
// Client: fully typed
import { treaty } from '@elysiajs/eden'
import type { App } from '../server'
const api = treaty<App>('http://localhost:3000')
// Autocomplete + type checking
const { data: users } = await api.users.get()
// data: { id: number; name: string }[]
const { data: newUser } = await api.users.post({
name: 'Bob',
email: '[email protected]',
})
// newUser: { id: number; name: string; email: string }
// Type error at compile time if wrong:
// await api.users.post({ name: 'Bob' }) // ❌ missing email
8. Context — Shared State
import { Elysia } from 'elysia'
// Decorate context with shared values
const app = new Elysia()
.decorate('db', new Database())
.decorate('logger', new Logger())
.derive(({ request }) => ({
requestId: crypto.randomUUID(),
ip: request.headers.get('x-forwarded-for') ?? 'unknown',
}))
// db, logger, requestId, ip available in all handlers
.get('/users', ({ db, logger, requestId }) => {
logger.info(`[${requestId}] Fetching users`)
return db.users.findAll()
})
.listen(3000)
Elysia vs Hono vs Fastify
| Elysia | Hono | Fastify | |
|---|---|---|---|
| Runtime | Bun (+ adapters) | Any | Node.js |
| Speed | Fastest (Bun) | Very fast | Fast |
| Type safety | End-to-end (Eden) | Routes only | Routes only |
| Validation | TypeBox (built-in) | Zod (plugin) | Ajv (built-in) |
| WebSocket | Built-in | Via adapter | Via plugin |
| Swagger | Plugin | Plugin | Plugin |
| Ecosystem | Growing | Large | Mature |
Key Takeaways
- Elysia = Bun-native, TypeScript-first, fastest REST framework in benchmarks
- TypeBox validation — schema validation with zero extra config, generates Swagger docs automatically
- Plugins —
cors,jwt,swagger,bearerall built by the Elysia team - Eden Treaty — end-to-end type safety like tRPC but for REST APIs
- WebSocket — first-class, with typed messages
- Lifecycle hooks —
onRequest,onBeforeHandle,onAfterHandle,onErrorfor middleware