본문으로 건너뛰기 Elysia.js Complete Guide | Fast Web Framework for Bun

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
  • Plugins ??cors, 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 hooks ??onRequest, onBeforeHandle, onAfterHandle, onError for middleware

Frequently Asked Questions (FAQ)

Q. When would I use this in practice?

A. 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.

Q. What should I read before this?

A. Follow the previous article or related articles links at the bottom of each post to learn in sequence.

Q. Where can I study this more deeply?

A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers Elysia, Bun, TypeScript, Web-API, Backend, Performance.