アプローチの比較:Node.js + Express vs. Bun + ElysiaJS
10年以上にわたり、JavaScriptバックエンドの立ち上げといえばNode.jsにExpressまたはFastifyを組み合わせるのが定番でした。このスタックは今も数多くの本番システムを支えています。しかし現代のプロジェクトでは、TypeScriptのトランスパイル(tsx/esbuild)、環境変数の読み込み(dotenv)、テストランナー(jest/vitest)、スキーマバリデータ(zod)など、個別ツールの複雑な組み合わせに依存しがちです。
Bunは、JavaScriptCoreをベースとした単一のネイティブバイナリにこれらのツールを統合します。さらに、BunのHTTPプリミティブ専用に設計されたWebフレームワークであるElysiaJSと組み合わせることで、15ms未満のコールドスタート、ネイティブなTypeScript実行、静的型推論が実現します。合成ベンチマークでも実際のI/Oワークロードでも、Expressが約20,000 req/sにとどまるのに対し、Elysiaはベースラインメモリ60MB未満で毎秒150,000リクエスト以上を一貫して処理できます。
BunとElysiaJSを採用するメリットとデメリット
メリット
- 圧倒的なスループットと低レイテンシ: Bunは低レベルのC++バインディング経由でHTTP処理を実行し、Expressの4倍から8倍のリクエスト処理能力(req/s)を発揮します。
- 設定不要のTypeScriptサポート:
tsconfig.jsonのパス設定やビルド手順に頭を悩ませることなく、.tsや.tsxファイルを直接実行できます。 - ネイティブなTypeBoxバリデーション: TypeBoxのJITコンパイルによりスキーマがコンパイルされ、Joiなどの一般的なランタイムライブラリと比較して最大100倍高速にペイロードを検証します。
- Eden Treatyによるエンドツーエンドの型安全性: サーバーの型定義をエクスポートするだけで、フロントエンド側で自動補完とコンパイル時エラーチェックを備えた完全型安全なAPIエンドポイントを利用できます。
デメリット
- Node.jsのエッジケースにおける互換性: Bunは主要なnpmパッケージのほとんどをサポートしていますが、非公開のV8内部APIや古いC++ネイティブアドオンに依存するレガシーライブラリでは、互換性の問題が生じる場合があります。
- ミドルウェアエコシステム: Elysiaのプラグインエコシステムは認証、OpenAPI、CORS、レート制限などをカバーしているものの、10年以上の歴史を持つExpressに比べるとサードパーティ製プラグインの層はまだ発展途上です。
推奨環境と事前準備
チュートリアルを進めるには、ローカル環境にBunがインストールされている必要があります。BunはmacOS、Linux、Windows Subsystem for Linux(WSL)向けの公式バイナリを提供しています。
# Linux / macOSにBunをインストール
curl -fsSL https://bun.sh/install | bash
# インストールを確認(バージョン1.1以上を推奨)
bun --version
コードエディタには、公式のElysiaおよびTailwind/TypeScript拡張機能を導入したVisual Studio Codeを使用すると、シームレスな自動補完とインラインスキーマ診断が利用できるためおすすめです。
ステップ・バイ・ステップ実装ガイド
ステップ1:プロジェクトの初期化
新規プロジェクトディレクトリを作成し、コアパッケージをインストールします。Bunはグローバルなハードリンクを使用するため、約50〜200ミリ秒でパッケージのインストールが完了します。
mkdir bun-elysia-api
cd bun-elysia-api
bun init -y
bun add elysia @elysiajs/cors @elysiajs/swagger
ステップ2:コアサーバーとRESTエンドポイントの構築
index.tsファイルを作成します。ここでは、Elysiaのスキーマビルダー(t)による高速ペイロードバリデーションを活用し、型定義されたGET・POSTルートを持つインメモリ商品カタログを定義します。
import { Elysia, t } from 'elysia';
import { swagger } from '@elysiajs/swagger';
import { cors } from '@elysiajs/cors';
interface Product {
id: string;
name: string;
price: number;
inStock: boolean;
}
const products: Product[] = [
{ id: '1', name: 'メカニカルキーボード', price: 120, inStock: true },
{ id: '2', name: 'エルゴノミクスマウス', price: 65, inStock: false },
];
export const app = new Elysia()
.use(cors())
.use(swagger({
documentation: {
info: {
title: '商品カタログAPI',
version: '1.0.0',
},
},
}))
.get('/api/products', () => products)
.get(
'/api/products/:id',
({ params, error }) => {
const product = products.find((item) => item.id === params.id);
if (!product) {
return error(404, { message: '商品が見つかりません' });
}
return product;
},
{
params: t.Object({
id: t.String(),
}),
}
)
.post(
'/api/products',
({ body, set }) => {
const newProduct: Product = {
id: (products.length + 1).toString(),
...body,
};
products.push(newProduct);
set.status = 201;
return newProduct;
},
{
body: t.Object({
name: t.String({ minLength: 2 }),
price: t.Number({ minimum: 0 }),
inStock: t.Boolean(),
}),
}
)
.listen(3000);
console.log(`サーバーが起動しました: http://${app.server?.hostname}:${app.server?.port}`);
export type App = typeof app;
ホットリロードを有効にしてサーバーを起動します:
bun run --watch index.ts
ブラウザで http://localhost:3000/swagger を開きます。ElysiaはTypeBoxスキーマの制約から、完全なリクエスト・レスポンス定義を備えた対話型Swagger UIを自動生成します。
ステップ3:Eden Treatyを使ったフロントエンド接続
Eden Treatyを使うと、コード生成ツールやビルドスクリプト、手動でのOpenAPI同期を一切行わずに、バックエンドのルート定義をフロントエンドコードと同期できます。まず、クライアントのワークスペースにEdenをインストールします:
bun add @elysiajs/eden
エクスポートされた App 型定義をクライアントスクリプトにインポートし、Treatyクライアントを初期化します:
import { treaty } from '@elysiajs/eden';
import type { App } from './index';
const client = treaty<App>('http://localhost:3000');
async function fetchAndCreateProducts() {
// 1. URLパスの自動補完に対応した完全型安全なGET呼び出し
const { data: allProducts, error: listError } = await client.api.products.get();
if (listError) {
console.error('商品の取得に失敗しました:', listError.value);
return;
}
console.log(`${allProducts.length}件の商品を読み込みました:`, allProducts);
// 2. 型チェック付きPOST呼び出し:不正なフィールド型はTypeScriptにより即座に検出されます
const { data: created, error: createError } = await client.api.products.post({
name: 'USB-C ドッキングステーション',
price: 89,
inStock: true,
});
if (createError) {
console.error('バリデーションエラー:', createError.value);
return;
}
console.log(`商品を作成しました (ID: ${created.id})`);
}
fetchAndCreateProducts();
ステップ4:Dockerによる本番デプロイ
Bunアプリケーションをコンテナ化することで、極めて軽量なデプロイ成果物が完成します。標準的なAlpineベースのBunイメージは100MB未満で、約20ミリ秒で高速起動します。
プロジェクトのルートディレクトリに Dockerfile を作成します:
# マルチステージ本番ビルド
FROM oven/bun:1-alpine AS base
WORKDIR /app
# 依存関係のキャッシュとインストール
COPY package.json bun.lock* bun.lockb* ./
RUN bun install --frozen-lockfile --production
# ソースファイルのコピー
COPY . .
# セキュリティ確保のため非rootユーザーでコンテナを実行
USER bun
EXPOSE 3000
ENV NODE_ENV=production
CMD ["bun", "run", "index.ts"]
コンテナイメージをビルドし、ローカルでテストします:
docker build -t bun-elysia-api .
docker run -p 3000:3000 bun-elysia-api
これで、瞬時に起動し、ワイヤースピードで厳格なスキーマ検証を行い、クライアント側のボイラープレートを一切必要としないエンドツーエンド型安全なAPIが完成しました。

