TechFeedTechFeed
Open Source

ElysiaJS 실전 튜토리얼 — Bun 기반 TypeScript API 서버, JWT 인증, Swagger 문서화, Docker 배포

ElysiaJS로 Bun 위에서 고성능 TypeScript REST API를 만드는 단계별 가이드. 라우팅, 플러그인 시스템, JWT 인증, Swagger 자동 문서화, Docker 배포까지 실전 코드 기반으로 완전 정리.

ElysiaJS는 Bun 런타임 위에서 동작하는 TypeScript 웹 프레임워크다. Express 대비 18배, Fastify 대비 3배 높은 처리량과 Bun의 내장 TypeScript 지원을 결합해, 별도 컴파일 없이 타입 안전한 API를 만들 수 있다. 이 글은 프로젝트 초기화부터 JWT 인증, Swagger 자동 문서화, Docker 배포까지 단계별로 실습한다.

ElysiaJS란 무엇인가

ElysiaJS는 SaltyAom이 만든 오픈소스 TypeScript 웹 프레임워크다. Bun의 저수준 HTTP API를 직접 사용하며, 런타임 타입 검증과 OpenAPI 자동 생성을 빌트인으로 제공한다.

핵심 특징은 세 가지다. 첫째, End-to-End 타입 안전성 — 서버 라우트 정의에서 클라이언트 요청까지 타입이 자동으로 흐른다. 둘째, 플러그인 기반 아키텍처 — 인증·캐싱·문서화가 모두 플러그인으로 분리돼 있어 필요한 것만 추가한다. 셋째, Bun 내장 TypeScript — tsc나 ts-node 없이 .ts 파일을 직접 실행한다.

Hono.js와 자주 비교되는데, Hono는 멀티 런타임(Node/Bun/Deno/Cloudflare)을 지원하는 반면 ElysiaJS는 Bun 전용으로 더 깊은 성능 최적화에 집중한다. Bun이 메인 런타임이라면 ElysiaJS가 유리하다.

프로젝트 초기 설정

Bun이 설치돼 있어야 한다. macOS·Linux는 curl -fsSL https://bun.sh/install | bash로 설치하고, Windows는 Bun 공식 문서에서 PowerShell 명령어를 확인하라.

프로젝트 초기화 및 ElysiaJS 설치
# Bun 설치 확인 bun --version # 새 프로젝트 생성 mkdir elysia-api && cd elysia-api bun init -y # ElysiaJS 및 핵심 플러그인 설치 bun add elysia @elysiajs/jwt @elysiajs/swagger @elysiajs/cors bun add drizzle-orm drizzle-kit @libsql/client bun add --dev @types/bun

설치 후 package.json의 scripts를 수정한다.

package.json scripts 설정
{ "scripts": { "dev": "bun run --watch src/index.ts", "start": "bun run src/index.ts", "db:generate": "drizzle-kit generate", "db:migrate": "bun run src/db/migrate.ts" } }
ElysiaJS 프로젝트 초기화 터미널 화면
bun init 이후 elysia 설치까지 10초도 걸리지 않는다

라우팅과 핸들러 구현

ElysiaJS의 라우팅은 Express와 유사하지만 타입 추론이 핵심 차이다. 경로 파라미터, 쿼리스트링, 요청 바디에 타입이 자동으로 붙는다.

src/index.ts — 기본 라우팅 구조
import { Elysia, t } from 'elysia' const app = new Elysia() .get('/', () => ({ message: 'ElysiaJS API' })) // 경로 파라미터 — :id는 string 타입으로 추론 .get('/users/:id', ({ params }) => { return { userId: params.id } }) // 요청 바디 스키마 정의 (t.Object) .post('/users', ({ body }) => { return { created: true, user: body } }, { body: t.Object({ name: t.String(), email: t.String({ format: 'email' }), age: t.Optional(t.Number()) }) }) // 쿼리스트링 검증 .get('/search', ({ query }) => { return { q: query.q, page: query.page } }, { query: t.Object({ q: t.String(), page: t.Optional(t.Number({ minimum: 1, default: 1 })) }) }) .listen(3000) console.log('Server running at http://localhost:3000')

t 객체는 ElysiaJS 빌트인 TypeBox 기반 스키마 빌더다. 스키마를 정의하면 런타임 검증과 TypeScript 타입 추론이 동시에 이뤄진다. 잘못된 바디가 들어오면 422 응답이 자동으로 반환된다.

타입 안전 유효성 검사: t.Object로 정의한 스키마는 런타임 유효성 검사와 TypeScript 타입을 동시에 처리한다. Zod 같은 별도 라이브러리 없이 빌트인 기능만으로 완전한 타입 안전성을 확보한다.

플러그인과 미들웨어 구조

ElysiaJS의 플러그인은 new Elysia() 인스턴스로 만든 모듈이다. 라우트·훅·상태를 캡슐화해 재사용할 수 있다. 라우트를 도메인별로 분리할 때 이 패턴을 쓴다.

src/plugins/logger.ts — 요청 로거 플러그인
import { Elysia } from 'elysia' export const loggerPlugin = new Elysia({ name: 'logger' }) .onRequest(({ request }) => { const url = new URL(request.url) console.log('[' + new Date().toISOString() + '] ' + request.method + ' ' + url.pathname) }) .onError(({ error, code }) => { console.error('[ERROR ' + code + ']', error.message) })
src/routes/users.ts — 라우트 모듈 분리
import { Elysia, t } from 'elysia' const users: { id: number; name: string; email: string }[] = [] let nextId = 1 export const usersRoute = new Elysia({ prefix: '/users' }) .get('/', () => users) .get('/:id', ({ params, error }) => { const user = users.find(u => u.id === Number(params.id)) if (!user) return error(404, { message: 'User not found' }) return user }) .post('/', ({ body }) => { const user = { id: nextId++, ...body } users.push(user) return { status: 201, user } }, { body: t.Object({ name: t.String({ minLength: 1 }), email: t.String({ format: 'email' }) }) }) .delete('/:id', ({ params, error }) => { const idx = users.findIndex(u => u.id === Number(params.id)) if (idx === -1) return error(404, { message: 'User not found' }) users.splice(idx, 1) return { deleted: true } })
ElysiaJS 플러그인 아키텍처 구조도
플러그인은 독립 Elysia 인스턴스로 만들어 메인 앱에 .use()로 주입한다

JWT 인증 구현

@elysiajs/jwt 플러그인이 JWT 서명·검증을 담당한다. 플러그인을 use하면 jwt 객체가 컨텍스트에 자동으로 주입된다.

src/routes/auth.ts — JWT 인증 라우트
import { Elysia, t } from 'elysia' import { jwt } from '@elysiajs/jwt' const JWT_SECRET = process.env.JWT_SECRET ?? 'change-me-in-production' export const authRoute = new Elysia({ prefix: '/auth' }) .use( jwt({ name: 'jwt', secret: JWT_SECRET, exp: '7d' }) ) // 로그인 — 토큰 발급 .post('/login', async ({ body, jwt: jwtCtx, error }) => { // 실제 환경에서는 DB 조회 + bcrypt 비교 if (body.email !== 'admin@example.com' || body.password !== 'password') { return error(401, { message: 'Invalid credentials' }) } const token = await jwtCtx.sign({ sub: '1', email: body.email, role: 'admin' }) return { token } }, { body: t.Object({ email: t.String({ format: 'email' }), password: t.String({ minLength: 6 }) }) }) // 내 정보 조회 — 토큰 검증 .get('/me', async ({ headers, jwt: jwtCtx, error }) => { const authHeader = headers['authorization'] if (!authHeader?.startsWith('Bearer ')) { return error(401, { message: 'Unauthorized' }) } const token = authHeader.slice(7) const payload = await jwtCtx.verify(token) if (!payload) return error(401, { message: 'Invalid token' }) return { user: payload } })

실제 프로젝트에서는 onBeforeHandle 훅으로 보호가 필요한 라우트에 인증 검사를 공통 적용할 수 있다. 훅 범위를 플러그인 단위로 제한하면 특정 라우트 그룹에만 인증을 강제할 수 있다.

JWT_SECRET 환경변수 필수: 프로덕션에서는 반드시 강력한 랜덤 문자열을 JWT_SECRET에 설정해야 한다. openssl rand -hex 32로 생성한 값을 .env에 저장하고 .gitignore에 추가하라.

Swagger 자동 문서화

@elysiajs/swagger 플러그인은 라우트 정의에서 OpenAPI 3.0 스펙을 자동으로 생성한다. t.Object로 정의한 스키마가 곧 API 문서가 된다.

src/index.ts — Swagger 플러그인 통합
import { Elysia } from 'elysia' import { swagger } from '@elysiajs/swagger' import { cors } from '@elysiajs/cors' import { loggerPlugin } from './plugins/logger' import { usersRoute } from './routes/users' import { authRoute } from './routes/auth' const app = new Elysia() .use(cors({ origin: process.env.ALLOWED_ORIGIN ?? '*', methods: ['GET', 'POST', 'PUT', 'DELETE'] })) // Swagger UI — /swagger 경로에서 확인 가능 .use(swagger({ documentation: { info: { title: 'ElysiaJS API', version: '1.0.0', description: 'Bun 기반 TypeScript REST API' }, tags: [ { name: 'auth', description: '인증 관련 엔드포인트' }, { name: 'users', description: '사용자 CRUD' } ] } })) .use(loggerPlugin) .use(authRoute) .use(usersRoute) .listen(3000) console.log('Server running at http://localhost:3000') console.log('Swagger UI at http://localhost:3000/swagger')

서버 실행 후 http://localhost:3000/swagger에 접속하면 등록된 모든 라우트가 Try it out 기능과 함께 시각화된다. 별도 API 문서 작성 없이 코드가 곧 문서가 된다.

ElysiaJS Swagger UI 자동 문서화 화면
@elysiajs/swagger 플러그인이 t.Object 스키마에서 OpenAPI 3.0 스펙을 자동 생성한다

Docker 컨테이너화 및 배포

Bun은 공식 Docker 이미지를 제공한다. 멀티스테이지 빌드로 프로덕션 이미지 크기를 최소화한다.

Dockerfile — 멀티스테이지 빌드
# 빌드 스테이지 FROM oven/bun:1 AS builder WORKDIR /app COPY package.json bun.lockb ./ RUN bun install --frozen-lockfile COPY . . # 프로덕션 스테이지 FROM oven/bun:1-slim AS runner WORKDIR /app ENV NODE_ENV=production COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/src ./src COPY --from=builder /app/package.json ./ EXPOSE 3000 CMD ["bun", "run", "src/index.ts"]
docker-compose.yml — 로컬 개발 환경
services: api: build: . ports: - '3000:3000' environment: - JWT_SECRET=your-secret-here - ALLOWED_ORIGIN=http://localhost:5173 volumes: - ./src:/app/src restart: unless-stopped

빌드 및 실행은 다음 명령어로 진행한다.

Docker 빌드 및 실행
# 이미지 빌드 docker build -t elysia-api . # 컨테이너 실행 docker run -p 3000:3000 -e JWT_SECRET=$(openssl rand -hex 32) elysia-api # docker-compose 사용 시 docker compose up -d

ElysiaJS vs Hono.js vs Fastify — 언제 어떤 걸 쓸까

세 프레임워크는 각자 다른 철학을 가진다.

  • ElysiaJS: Bun 전용, 최고 처리량, 빌트인 타입 안전성. Bun을 메인 런타임으로 쓰고 최대 성능이 필요할 때.
  • Hono.js: 멀티 런타임(Node/Bun/Deno/Cloudflare Workers). 엣지 배포나 런타임 교체 가능성이 있을 때.
  • Fastify: Node.js 생태계에서 검증된 안정성. 기존 Node.js 프로젝트와 통합하거나 팀이 Node.js에 익숙할 때.

신규 Bun 프로젝트에서는 ElysiaJS가 가장 생산적인 선택이다. 타입 추론, 플러그인, 문서화가 모두 빌트인이라 보일러플레이트가 적다.

Bun 런타임이 전제 조건: ElysiaJS는 Node.js에서 동작하지 않는다. 기존 Node.js 프로젝트에 점진적으로 도입하려면 Hono.js가 더 현실적인 선택이다.
ElysiaJSBunTypeScriptREST APIJWTSwaggerDockertutorial오픈소스백엔드

관련 도구

관련 포스트

Temporal.io 워크플로우 튜토리얼 — TypeScript로 분산 작업 큐와 장기 실행 프로세스 구현2026-04-192026년 주목할 오픈소스 프로젝트 10선2026-03-09Supabase vs Firebase 2026 비교 — 실무 선택 가이드2026-03-20오픈소스 기여 시작 가이드 — 첫 PR까지2026-02-26