Elysia の使い方 — Bun 向け高速 Web フレームワーク完全ガイド
一言でいうと
Elysia は Bun ランタイム上で動作する、エンドツーエンドの型安全性を備えた高速 Web フレームワークです。Express の約18倍のパフォーマンスを謳い、TypeScript のエルゴノミクス(開発者体験)を極限まで追求した設計が特徴です。
どんな時に使う?
- Bun で高速な REST API / GraphQL サーバーを構築したいとき — Bun のネイティブ HTTP サーバーを活かし、最小限のオーバーヘッドで API を構築できます
- フロントエンドとバックエンドで型を共有したいとき — tRPC のようなエンドツーエンド型安全を、REST スタイルの API で実現できます(Eden というクライアントコネクタを提供)
- Express / Fastify からの移行で、よりモダンかつ高速なフレームワークを探しているとき — プラグインエコシステムが充実しており、Swagger ドキュメント自動生成、JWT 認証、CORS などが公式プラグインとして揃っています
インストール
Elysia は Bun ランタイムが必須です。事前に Bun をインストールしてください。
# プロジェクトの雛形を作成(推奨)
bun create elysia app
# 既存プロジェクトに追加する場合
bun add elysia
# npm / yarn / pnpm でもインストール可能ですが、実行は Bun が必要です
npm install elysia
yarn add elysia
pnpm add elysia
注意: Elysia は Bun のネイティブ API に依存しているため、Node.js 単体では動作しません。
基本的な使い方
最もシンプルな Hello World サーバーです。
// src/index.ts
import { Elysia } from 'elysia'
const app = new Elysia()
.get('/', () => 'Hello, Elysia!')
.get('/user/:id', ({ params: { id } }) => `User: ${id}`)
.post('/user', ({ body }) => body, {
body: t.Object({
name: t.String(),
email: t.String()
})
})
.listen(3000)
console.log(`🦊 Elysia is running at http://${app.server?.hostname}:${app.server?.port}`)
bun run src/index.ts
上記の t はバリデーション用のスキーマビルダーです。次のセクションで詳しく解説します。
よく使う API
1. ルーティング(GET / POST / PUT / DELETE)
Elysia はメソッドチェーンでルートを定義します。コンテキストオブジェクトから params、query、body、headers などに型安全にアクセスできます。
import { Elysia } from 'elysia'
const app = new Elysia()
.get('/posts', () => {
return [{ id: 1, title: 'Hello' }]
})
.get('/posts/:id', ({ params: { id } }) => {
// id は自動的に string 型として推論される
return { id, title: 'Hello' }
})
.post('/posts', ({ body }) => {
return { message: 'Created', data: body }
})
.put('/posts/:id', ({ params: { id }, body }) => {
return { message: `Updated ${id}`, data: body }
})
.delete('/posts/:id', ({ params: { id } }) => {
return { message: `Deleted ${id}` }
})
.listen(3000)
2. バリデーション(t スキーマ)
Elysia は内部で TypeBox を採用しており、t オブジェクトでリクエスト / レスポンスのスキーマを宣言的に定義できます。定義したスキーマは ランタイムバリデーション と TypeScript の型推論 の両方に使われます。
import { Elysia, t } from 'elysia'
const app = new Elysia()
.post(
'/user',
({ body }) => {
// body は { name: string; age: number } として型推論される
return { message: `Hello, ${body.name}! You are ${body.age} years old.` }
},
{
body: t.Object({
name: t.String({ minLength: 1 }),
age: t.Number({ minimum: 0 })
}),
response: t.Object({
message: t.String()
})
}
)
.get(
'/search',
({ query }) => {
// query.keyword は string 型として推論される
return { results: [], keyword: query.keyword }
},
{
query: t.Object({
keyword: t.String(),
page: t.Optional(t.Numeric({ default: 1 }))
})
}
)
.listen(3000)
バリデーションエラーが発生すると、Elysia は自動的に 422 Unprocessable Entity を返します。
3. ライフサイクルフック(onBeforeHandle / onAfterHandle / onError)
リクエストのライフサイクルにフックを挿入して、認証チェックやロギングなどの横断的関心事を処理できます。
import { Elysia } from 'elysia'
const app = new Elysia()
// 全ルートの前に実行される
.onBeforeHandle(({ request }) => {
console.log(`[${new Date().toISOString()}] ${request.method} ${request.url}`)
})
// レスポンス後に実行される
.onAfterHandle(({ response }) => {
console.log('Response sent:', typeof response)
})
// エラーハンドリング
.onError(({ code, error }) => {
if (code === 'NOT_FOUND') {
return { message: 'Route not found' }
}
console.error(error)
return { message: 'Internal Server Error' }
})
.get('/', () => 'Hello!')
.listen(3000)
4. プラグインシステム(.use())
Elysia のプラグインは Elysia インスタンスそのもの を返す関数です。型情報が完全に伝播するため、プラグインが追加したデコレータやルートも型安全に利用できます。
import { Elysia } from 'elysia'
import { swagger } from '@elysiajs/swagger'
import { cors } from '@elysiajs/cors'
// カスタムプラグインの定義
const authPlugin = new Elysia({ name: 'auth' })
.derive(({ headers }) => {
const token = headers.authorization?.replace('Bearer ', '')
return {
userId: token ? decodeToken(token) : null
}
})
function decodeToken(token: string): string | null {
// 実際のトークンデコード処理
return 'user-123'
}
const app = new Elysia()
.use(swagger()) // /swagger に自動ドキュメント生成
.use(cors()) // CORS 設定
.use(authPlugin) // カスタム認証プラグイン
.get('/profile', ({ userId }) => {
// userId は authPlugin の derive により型推論される
if (!userId) return { error: 'Unauthorized' }
return { userId }
})
.listen(3000)
主要な公式プラグイン:
| プラグイン | パッケージ名 | 用途 |
|---|---|---|
| Swagger | @elysiajs/swagger | OpenAPI ドキュメント自動生成 |
| CORS | @elysiajs/cors | CORS ヘッダー設定 |
| JWT | @elysiajs/jwt | JWT 認証 |
| Static | @elysiajs/static | 静的ファイル配信 |
| Bearer | @elysiajs/bearer | Bearer トークン抽出 |
5. グループ化と Eden(エンドツーエンド型安全クライアント)
グループ化 でルートをモジュール分割し、Eden でフロントエンドからバックエンドの型を直接参照できます。
// server.ts
import { Elysia, t } from 'elysia'
const userRoutes = new Elysia({ prefix: '/user' })
.get('/', () => [{ id: 1, name: 'Alice' }])
.get('/:id', ({ params: { id } }) => ({ id, name: 'Alice' }), {
params: t.Object({ id: t.Numeric() })
})
.post('/', ({ body }) => ({ ...body, id: 2 }), {
body: t.Object({
name: t.String(),
email: t.String()
})
})
const app = new Elysia()
.use(userRoutes)
.listen(3000)
// 型をエクスポート
export type App = typeof app
// client.ts(フロントエンド側)
import { treaty } from '@elysiajs/eden'
import type { App } from './server'
const api = treaty<App>('http://localhost:3000')
// 完全に型安全な API 呼び出し
const { data, error } = await api.user.get()
// data は { id: number; name: string }[] として推論される
const { data: user } = await api.user({ id: 1 }).get()
// user は { id: number; name: string } として推論される
const { data: newUser } = await api.user.post({
name: 'Bob',
email: 'bob@example.com'
})
// newUser は { name: string; email: string; id: number } として推論される
類似パッケージとの比較
| 特徴 | Elysia | Hono | Express | Fastify |
|---|---|---|---|---|
| ランタイム | Bun(必須) | Bun / Node / Deno / Edge | Node | Node |
| 型安全性 | ◎ エンドツーエンド | ○ RPC モード対応 | △ 手動定義 | ○ スキーマ定義 |
| パフォーマンス | ◎ 非常に高速 | ◎ 高速 | △ 低速 | ○ 高速 |
| バリデーション | 組み込み(TypeBox) | Zod 等を別途利用 | 別途ライブラリ必要 | 組み込み(JSON Schema) |
| エコシステム | ○ 成長中 | ○ 成長中 | ◎ 非常に豊富 | ◎ 豊富 |
| クライアント連携 | Eden(公式) | hc(公式) | なし | なし |
| 学習コスト | 低い | 低い | 低い | 中程度 |
選定の目安:
- Bun を使うことが確定しており、最高のパフォーマンスと型安全性を求めるなら → Elysia
- マルチランタイム対応が必要なら → Hono
- 既存の Node.js エコシステムを活用したいなら → Fastify / Express
注意点・Tips
⚠️ Bun ランタイムが必須
Elysia は Bun のネイティブ HTTP サーバー(Bun.serve)に依存しています。Node.js や Deno では動作しません。CI/CD 環境やデプロイ先でも Bun が利用可能であることを確認してください。
⚠️ メソッドチェーンの順序が型推論に影響する
Elysia の型推論はメソッドチェーンの順序に依存します。derive や state で追加したコンテキストは、それ以降に定義されたルートでのみ型として認識されます。
// ❌ authPlugin の derive より前に定義されたルートでは userId が使えない
const app = new Elysia()
.get('/before', ({ userId }) => userId) // 型エラー
.use(authPlugin)
.get('/after', ({ userId }) => userId) // OK
💡 t.Numeric() と t.Number() の違い
URL パラメータやクエリ文字列は常に string として届きます。t.Numeric() を使うと、文字列を自動的に数値に変換してくれます。
// params.id は number 型になる
.get('/user/:id', ({ params }) => params.id, {
params: t.Object({ id: t.Numeric() })
})
💡 group でルートを整理する
大規模アプリケーションでは、ルートをファイル分割して use で結合するパターンが推奨されます。各ファイルで new Elysia({ prefix: '/xxx' }) を使うことで、自然にモジュール分割できます。
💡 Swagger ドキュメントを活用する
@elysiajs/swagger を追加するだけで、t スキーマから OpenAPI ドキュメントが自動生成されます。スキーマを丁寧に定義しておけば、フロントエンドチームとの API 仕様共有が格段に楽になります。
import { Elysia } from 'elysia'
import { swagger } from '@elysiajs/swagger'
const app = new Elysia()