关于 elysia
这项技能提供编写和重构 Elysia 处理器、响应架构及插件的约定,适用于需要类型化错误响应和统一路由行为的开发者,也涵盖 Eden Treaty 类型推断。
主要功能
- 01使用 status() 返回非 200 响应
- 02按状态码定义响应架构
- 03依据控制流选择 return 或 throw
- 04组合插件与共享认证守卫
- 05迁移 set.status 写法
适用场景
- 01重构 Elysia 错误处理器
- 02为 Eden 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 的类型安全字符串字面量(例如,使用
"Bad Request"而不是400) - 根据状态码自动推断响应类型
- 错误响应的 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 技能)。仅在 catch 块或深层嵌套代码中使用 throw status(...),尤其是在使用 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 的类型系统根据路由选项中的 response 架构推断响应类型,而非依据调用 status() 的方式。两种写法都能同样实现 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(),不要使用 new Elysia({ prefix }):由使用方控制挂载:
// 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()));
用于共享身份验证的 Guards
对于适用于多个路由的身份验证,请使用 .guard() 和 beforeHandle:
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 块中,使用
throw status(...)代替set.status = N; return { error: msg }; - 在路由选项中添加
response架构,以便 Eden Treaty 推断类型 - 仅当仍需使用
set.headers设置content-type等内容时,才在解构中保留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;
};