Elysia.js Complete Guide | Fast Web Framework for Bun

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: 'bob@example.com',
})
// 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

ElysiaHonoFastify
RuntimeBun (+ adapters)AnyNode.js
SpeedFastest (Bun)Very fastFast
Type safetyEnd-to-end (Eden)Routes onlyRoutes only
ValidationTypeBox (built-in)Zod (plugin)Ajv (built-in)
WebSocketBuilt-inVia adapterVia plugin
SwaggerPluginPluginPlugin
EcosystemGrowingLargeMature

Key Takeaways

  • Elysia = Bun-native, TypeScript-first, fastest REST framework in benchmarks
  • TypeBox validation — schema validation with zero extra config, generates Swagger docs automatically
  • Pluginscors, jwt, swagger, bearer all 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 hooksonRequest, onBeforeHandle, onAfterHandle, onError for middleware