Claude Code × ElysiaJS:Eden型安全クライアントで作るBunネイティブAPI

Claude Code × ElysiaJS:Eden型安全クライアントで作るBunネイティブAPI

はじめに

HonoでAPIを書いていると、ある壁にぶつかる瞬間がある。フロントエンドにHono RPCで型を共有しようとすると、レスポンスの成功ケースは型がつくが、エラーのステータスコード別の型推論が効かない。「400の場合と500の場合でレスポンスの型が変わる」というユースケースで、型安全性に妥協しなければならなくなる。

ElysiaJSはその問題に正面から向き合うBunネイティブのWebフレームワークだ。Eden Treatyというクライアントを使うと、ステータスコード別のエラー型まで含めたEnd-to-End型安全性が、コード生成なしで実現できる。Claude CodeとCLAUDE.mdを組み合わせると、このパターンを一貫して生成する環境が作れる。


ElysiaJSとは——Bunネイティブの型安全フレームワーク

ElysiaJSは「Ergonomic Framework for Humans」を掲げるBun専用のWebフレームワークだ。Bunのネイティブ機能(HTTPサーバーAPI・ファイルシステム・ホットリロード)を直接活用するため、他のランタイムを抽象化したフレームワークとは異なる密な統合が特徴になっている。

@elysiajs/nodeを使えばNode.jsでも動作するが、本来の設計はBun環境での最大パフォーマンスを前提にしている。

バリデーションはZodではなくTypeBoxを使う。TypeBoxはBun上でAOT(Ahead-of-Time)コンパイルされるため、Zodと比較して大幅に高速なバリデーションが実現されている(実際の数値はElysiaJS公式ドキュメントのベンチマーク記述で確認してほしい)。


HonoやtRPCとの違い——Edenが実現するEnd-to-End型安全性

Hono RPCとの比較

どちらもコード生成なしの型安全クライアントを持つが、型安全性の範囲が違う:

観点ElysiaJS EdenHono RPC
型安全の範囲ステータスコード別のエラー型まで網羅成功ケースのみ(エラー型は未対応)
クエリ/ボディのパース静的解析で自動パース手動でのパース関数呼び出しが必要
型推論の深さ大規模ルートでも安定多数のルートで型エラーが出やすい
ランタイムBun-native(Node.jsも可)マルチランタイム

Honoが「エッジランタイム全対応」という汎用性を取るのに対し、ElysiaJSは「Bunに特化した型安全性の深さ」を取る設計思想だ。

tRPCとの比較

ElysiaJS公式に「tRPCからの移行ガイド」があるほど、tRPCに近い開発体験を提供している。

tRPCはRESTful APIではなく独自の抽象レイヤーを強いるが、ElysiaJSはRESTful標準を守りながらtRPCと同等の型安全性を実現する。既存のREST APIからの移行が容易な点が実践上の大きな違いだ。


Claude Code × ElysiaJS——CLAUDE.mdに何を書くか

Claude CodeがElysiaJSのパターンに沿ったコードを生成するために、CLAUDE.mdに以下を記述する:

## Tech Stack
- Runtime: Bun
- Framework: ElysiaJS 1.x
- Validation: ElysiaJS built-in TypeBox (t.Object, t.String, t.Number...)
- Type-safe client: Eden Treaty (@elysiajs/eden)

## ElysiaJS Conventions
- Use t.Object() for body/query/params/response validation (do NOT use Zod)
- Always export typeof app from server entry for Eden Treaty
- Use new Elysia().group() for route grouping instead of separate router files
- Prefer .onError() for centralized error handling
- Plugin pattern: new Elysia().use(myPlugin) for modular code

## Commands
- Start: bun run src/index.ts
- Dev: bun --hot run src/index.ts
- Test: bun test

「Zodを使わずTypeBoxを使う」と明示することが重要だ。指定がなければClaude Codeは慣れ親しんだZodでバリデーションを書く可能性がある。do NOT use Zodという否定形の記述がClaude Codeに正確に伝わる。

また、export type App = typeof appをサーバーのエントリーポイントに必ず書くルールを定義しておくと、Eden Treatyのクライアント実装を依頼したときにClaude Codeが正確なパターンを生成できる。


実装ステップ——ルーティング・バリデーション・Edenクライアント

基本ルーティングとバリデーション

// src/index.ts
import { Elysia, t } from 'elysia'

const app = new Elysia()
  .post('/sign-up', ({ body }) => {
    return { message: Welcome, ${body.username} }
  }, {
    body: t.Object({
      username: t.String({ minLength: 3 }),
      password: t.String({ minLength: 8 }),
      email: t.String({ format: 'email' })
    }),
    response: {
      200: t.Object({ message: t.String() }),
      400: t.Object({ error: t.String() })
    }
  })
  .listen(3000)

export type App = typeof app

responseオブジェクトにステータスコード別の型を定義できる。これがEden Treatyのクライアント側でエラーハンドリングの型を確定させる仕組みの根拠になる。

Eden Treaty——コード生成なしのEnd-to-End型安全

サーバー側はtypeof appをexportするだけだ:

// src/index.ts(サーバー)
const app = new Elysia()
  .get('/hi', () => 'Hi Elysia')
  .post('/mirror', ({ body }) => body, {
    body: t.Object({
      id: t.Number(),
      name: t.String()
    })
  })
  .listen(3000)

export type App = typeof app

クライアント側はその型を受け取るだけで、すべての型が自動推論される:

// src/client.ts(クライアント)
import { treaty } from '@elysiajs/eden'
import type { App } from './index'

const api = treaty<App>('localhost:3000')

// POSTリクエスト(body型もレスポンス型も推論済み)
const { data, error } = await api.mirror.post({
  id: 1,
  name: 'Taro'
})

// エラーハンドリングもステータスコード別に型安全
if (error) {
  switch (error.status) {
    case 400:
      console.error('Bad Request:', error.value)
      break
    default:
      throw error.value
  }
}

console.log(data) // { id: number, name: string } として型確定

error.statusで分岐するとき、各ケースでerror.valueの型がステータスコードに対応した型として解決される。HonoやtRPCでは実現が難しかった「エラー型のステータスコード別推論」がコード生成なしで動く。

インストール:

bun add @elysiajs/eden

Claude Codeへの具体的な指示パターン

新しいエンドポイントを追加する際の依頼例:

src/index.ts のアプリに、以下のエンドポイントを追加してください。

POST /posts
- body: t.Object({ title: t.String({ minLength: 1 }), content: t.String() })
- response: { 201: t.Object({ id: t.String(), title: t.String() }) }
- 既存のPatternと同じ構造でEden Treatyが使えるように typeof app もexportしてください。

CLAUDE.mdにElysiaJSの規約が書かれていれば、Claude Codeはt.Objectでバリデーションを書き、responseオブジェクトにステータスコード別の型を定義し、export type App = typeof appを末尾に追加する一連の流れを実行する。


エンジニアリングマネジメントの視点

ElysiaJSの採用判断は、主にチームのランタイム選択と型安全性への要求度によって変わる。

Bunをすでにプロジェクトのランタイムとしているチームや、Claude Code × Bunの組み合わせで開発している場合は、ElysiaJSの型安全性は自然な恩恵として受け取れる。既存のHono + Node.jsプロジェクトをElysiaJSに移行するコストは、Honoとの記法の類似性から比較的低い(公式の移行ガイドも整備されている)。

一方、マルチランタイム対応(Cloudflare Workers + Node.jsなど)が必要な場合はHonoの選択肢が依然として有力だ。ElysiaJSはBun環境で真価を発揮するため、チームのインフラ戦略と合わせて判断したい。


まとめ

ElysiaJSのEden Treatyは、「コード生成なしでステータスコード別のエラー型まで推論する」という点でHono RPCやtRPCと異なる立ち位置にある。

BunでAPIを作る環境があるなら、まずnew Elysia()で基本ルーティングを書き、export type App = typeof appをエントリーポイントに追加してからtreaty()でクライアントを繋いでみてほしい。エラーハンドリングのswitch文で各ケースの型が確定する体験が、型安全性の深さを実感する最短の入口になる。

コメント

タイトルとURLをコピーしました