elysia について
Elysia のハンドラー、レスポンススキーマ、プラグインの作成とリファクタリングに関する規約を示すスキルです。型付きエラーレスポンスや一貫したルート動作を扱い、Eden Treaty の型推論にも対応します。
主な機能
- 01非 200 レスポンスに status() を使用
- 02ステータス別レスポンススキーマの定義
- 03制御フローに応じた return と throw の選択
- 04プラグインと共有認証ガードの構成
- 05set.status パターンからの移行
活用シーン
- 01Elysia のエラーハンドラーのリファクタリング
- 02Eden Treaty のエラーレスポンスの型付け
- 03複数ルートへの共有認証の追加
- 04プレフィックスに依存しないプラグインの作成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ epicenterhq/ epicenter --skill elysiaインストール後、Claude Code で/elysiaを入力して読み込まれたか確認します。
詳細
Elysia.js のパターン(v1.2+)
参考リポジトリ
- Hono :Cloudflare Workers 向けの超高速 Web フレームワーク
- Cloudflare Docs :Cloudflare Workers、Durable Objects、KV のドキュメント
上流情報に基づく確認
Worker のランタイム動作、Hono ミドルウェアのセマンティクス、リクエスト/レスポンスのストリーミング、Durable Objects、WebSockets、または Cloudflare のデプロイ制限が正確性に影響する場合は、記憶に頼る前に、honojs/hono または cloudflare/cloudflare-docs を対象に DeepWiki へ具体的な質問をしてください。まず参考情報として利用し、重要な詳細を変更する前に、ローカルにインストールされた型、ソース、または公式ドキュメントで確認してください。
以下にすでに記載されている Elysia 固有の規約については、DeepWiki を使う必要はありません。
このスキルを使う場面
次の場合にこのパターンを使ってください。
- Elysia のハンドラーを
status()レスポンスを使うように記述またはリファクタリングする場合。 - Eden Treaty の型安全性のために、ステータスごとのレスポンススキーマを定義する場合。
set.statusとエラーオブジェクトを返すハンドラーを移行する場合。- 認証やルートの動作を共有するために、Elysia のプラグイン/ガードを組み合わせる場合。
- 制御フローのコンテキストに応じて、
return status(...)とthrow status(...)のどちらを使うか選ぶ場合。
status() ヘルパー(必ず使う)
set.status を設定してオブジェクトを返す方法は使わないでください。 ハンドラーのコンテキストから必ず status を分割代入し、200 以外のすべてのレスポンスで使ってください。これにより、次のことが可能になります。
- IntelliSense を完全に利用できる、型安全な文字列リテラル(例:
400ではなく"Bad Request") - ステータスコードごとのレスポンスタイプの自動推論
- エラーレスポンスに対する Eden Treaty のエンドツーエンドの型安全性
基本的な使い方
import { Elysia, t } from 'elysia';
new Elysia().post(
'/chat',
async ({ body, headers, status }) => {
// ^^^^^^ destructure status from context
if (!isValid(body.provider)) {
// Use string literal for self-documenting, typesafe status codes
return status('Bad Request', 'Unsupported provider');
}
if (!apiKey) {
return status('Unauthorized', 'Missing API key');
}
return doWork(body);
},
{
// Define response schemas per status code for full type safety
response: {
200: t.Any(),
400: t.String(),
401: t.String(),
},
},
);
return status() と throw status() の比較
どちらも動作し、フレームワークは両方を処理します。違いは制御フローだけです。
| パターン | 動作 | 使用する場面 |
|---|---|---|
return status(...) | 通常のリターンとなり、レスポンスパイプラインに進みます | 自然なリターンポイント(バリデーションガード、ハンドラーの末尾)にいる場合 |
throw status(...) | 実行を直ちに中断します | ネストの深いロジック内や try/catch 内にいて、処理を抜けたい場合 |
このコードベースの規約では、return status(...) を優先します。 これは、ほかの箇所でも広く使われている、エラー時に早期リターンするパターン(error-handling スキルを参照)に合致します。throw status(...) は、catch ブロック内や、return を使うと不自然になるネストの深いコードで使ってください。
// GOOD: return for validation guards (matches codebase style)
async ({ body, status }) => {
if (!isValid(body.provider)) {
return status('Bad Request', `Unsupported provider: ${body.provider}`);
}
const apiKey = resolveApiKey(body.provider, headerApiKey);
if (!apiKey) {
return status('Unauthorized', 'Missing API key');
}
// happy path
return doWork(body);
};
// GOOD: throw inside catch blocks
async ({ body, status }) => {
try {
return await streamResponse(body);
} catch (error) {
if (isAbortError(error)) {
throw status(499, 'Client closed request');
}
throw status('Bad Gateway', `Provider error: ${error.message}`);
}
};
どちらも型推論は同じです
return status(...) と throw status(...) は、どちらも同じ ElysiaCustomStatusResponse オブジェクトを生成します。Elysia の型システムは、status() の呼び出し方ではなく、ルートオプションの response スキーマからレスポンスタイプを推論します。Eden Treaty の型安全性は、どちらの方法でも同様に機能します。
使用可能な文字列ステータスコード(StatusMap)
読みやすさを高めるため、数値コードの代わりに次の文字列リテラルを使用してください。
| 文字列リテラル | コード | 一般的な用途 |
|---|---|---|
'Bad Request' | 400 | 検証の失敗、不正な形式の入力 |
'Unauthorized' | 401 | 認証情報がない、または無効 |
'Forbidden' | 403 | 認証は有効だが、権限が不十分 |
'Not Found' | 404 | リソースが存在しない |
'Conflict' | 409 | 状態の競合(重複、すでに存在する) |
'Unprocessable Content' | 422 | 意味的に無効な入力 |
'Too Many Requests' | 429 | レート制限 |
'Internal Server Error' | 500 | 予期しないサーバー障害 |
'Bad Gateway' | 502 | アップストリームプロバイダーのエラー |
'Service Unavailable' | 503 | 一時的な過負荷/メンテナンス |
標準外のコード(nginx の 499 など)には、数値リテラルを直接使用します:status(499, 'Client closed request')。
Eden Treaty の型安全性のためのレスポンススキーマ
ルートオプションで、ステータスコードごとに response スキーマを定義します。これにより、Eden Treaty がクライアント側でエラー型を推論できるようになります。
new Elysia().post(
'/chat',
async ({ body, status }) => {
if (!isValid(body.provider)) {
return status('Bad Request', `Unsupported provider: ${body.provider}`);
}
return streamResult;
},
{
body: t.Object({
provider: t.String(),
model: t.String(),
}),
response: {
200: t.Any(), // Success type
400: t.String(), // Bad Request body type
401: t.String(), // Unauthorized body type
502: t.String(), // Bad Gateway body type
},
},
);
すると、Eden Treaty は次のように推論します。
const { data, error } = await api.chat.post({
provider: 'openai',
model: 'gpt-4',
});
if (error) {
// error.status is typed as 400 | 401 | 502
// error.value is typed per status code (string in this case)
switch (error.status) {
case 400: // error.value: string
case 401: // error.value: string
case 502: // error.value: string
}
}
エラーレスポンスの本文:文字列とオブジェクト
エラーの本文にはプレーンな文字列を推奨します。 ステータスコードでエラーの種類はすでに伝わります。説明的な文字列メッセージで十分であり、API をシンプルに保てます。
// GOOD: Plain string - status code provides the category
return status('Bad Request', `Unsupported provider: ${provider}`);
return status('Unauthorized', 'Missing API key: set x-provider-api-key header');
// AVOID: Wrapping in { error: "..." } object - redundant with status code
set.status = 400;
return { error: `Unsupported provider: ${provider}` };
構造化されたエラーの本文(複数のフィールド、エラーコード、検証の詳細など)が必要な場合は、TypeBox スキーマを定義します。
const ErrorBody = t.Object({
message: t.String(),
code: t.Optional(t.String()),
});
// In route options:
response: {
400: ErrorBody,
401: ErrorBody,
}
プラグインの構成
Elysia プラグインは、Elysia インスタンスを返す関数にすぎません。プラグイン内では new Elysia({ prefix }) ではなく new Elysia() を使用し、マウント方法は利用側で制御できるようにします。
// GOOD: Plugin is prefix-agnostic
export function createMyPlugin() {
return new Elysia().post('/endpoint', async ({ body, status }) => {
// ...
});
}
// Consumer controls the prefix
app.use(new Elysia({ prefix: '/api' }).use(createMyPlugin()));
認証を共有するためのガード
複数のルートに適用する認証には、beforeHandle とともに .guard() を使用します。
const authed = new Elysia().guard({
async beforeHandle({ headers, status }) {
const token = extractBearerToken(headers.authorization);
if (!isValid(token)) {
return status('Unauthorized', 'Invalid or missing token');
}
},
});
// All routes under this guard require auth
return authed
.get('/protected', () => 'secret')
.post('/admin', () => 'admin stuff');
移行チェックリスト:set.status から status() へ
既存のハンドラーを更新する場合:
- ハンドラーの分割代入で
setをstatusに置き換えます set.status = N; return { error: msg };をreturn status('String Literal', msg);に置き換えます- catch ブロックでは、
set.status = N; return { error: msg };の代わりにthrow status(...)を使用します - Eden Treaty の型推論のため、ルートオプションに
responseスキーマを追加します content-typeなどのためにset.headersが引き続き必要な場合に限り、分割代入でsetを残します
// BEFORE
async ({ body, headers, set }) => {
if (!valid) {
set.status = 400;
return { error: 'Bad input' };
}
};
// AFTER
async ({ body, headers, status }) => {
if (!valid) {
return status('Bad Request', 'Bad input');
}
};
// AFTER (when you also need set.headers)
async ({ body, headers, set, status }) => {
if (!valid) {
return status('Bad Request', 'Bad input');
}
set.headers['content-type'] = 'application/octet-stream';
return binaryData;
};