← 返回文章列表
Elysia 全指南:从入门到精通
基于费曼学习法、西蒙学习法、SQ3R 阅读法和康奈尔笔记法,系统掌握 Elysia 的核心概念与实战技巧。
一、概览与提问(SQ3R · Survey & Question)
SQ3R 第一步:快速浏览全貌,提出关键问题。
什么是 Elysia?
Elysia 是一个基于 Bun 运行时的 TypeScript Web 框架,以"为人类设计的人体工学框架"(Ergonomic Framework for Humans)为核心理念。它将端到端类型安全(End-to-End Type Safety)、极致性能和卓越的开发者体验融为一体,是当前 Bun 生态中最受欢迎的生产级框架。
Elysia 由 SaltyAom 自 2022 年起持续开发和维护,被 X(原 Twitter)、CS.Money、泰国农业和农业合作社银行等公司和超过 10,000 个开源项目在生产环境中使用。
Elysia 的核心设计哲学:
- 单一数据源(Single Source of Truth)——Schema 同时服务于运行时验证、TypeScript 类型推断、OpenAPI 文档生成和客户端-服务端通信
- 写更少的 TypeScript——框架自动推断类型,让你专注于业务逻辑
- Web 标准——基于
Request/Response构建,可以在 Bun、Node.js、Deno、Cloudflare Worker 等多种运行时上运行 - 链式调用——所有 API 通过方法链调用,确保类型安全
核心问题
- Elysia 适合什么场景?——构建高性能 RESTful API、全栈 TypeScript 项目、需要端到端类型安全的微服务、实时通信(WebSocket)场景。
- Elysia 与 Express、Fastify、Hono 相比有什么优势?——比 Express 快约 21 倍、比 Fastify 快约 6 倍;拥有比任何其他框架都更先进的类型系统;原生支持 OpenAPI 和 End-to-End Type Safety。
- Elysia 与 tRPC 相比有什么优势?——基于 RESTful 标准,同时提供 tRPC 级别的端到端类型安全;支持 OpenAPI 自动文档生成;遵循 HTTP 标准,更容易与其他系统集成。
- 学习 Elysia 需要什么基础?——TypeScript 基础(推荐但非必须)、HTTP 协议的基本理解、Node.js/Bun 运行时基础。
技术全景图
Elysia 的核心架构可以理解为五个层次:
基础层:路由系统——定义 HTTP 路由、路径参数、查询参数、请求体处理。这是构建 Web 服务的基础。
验证层:Elysia.t(TypeBox)——基于 TypeBox 的 Schema 构建器,同时支持 Standard Schema(Zod、Valibot 等),提供运行时验证和编译时类型推断的单一数据源。
中间层:生命周期(Lifecycle)——事件驱动的请求处理管道,包括 onRequest、onParse、onTransform、onBeforeHandle、onAfterHandle、onError 等事件,取代传统中间件模式。
高级层:插件与 Macro——通过 use() 组合插件、guard 应用共享 Schema、macro 定义可复用的路由选项、group 组织路由分组。
生态层:Eden Treaty——端到端类型安全的客户端库,无需代码生成即可在前后端之间同步类型。
二、用最简单的话说清楚(费曼学习法)
费曼学习法核心理念:如果你不能用简单的语言解释一件事,说明你还没有真正理解它。
核心概念讲解
1. 路由(Routing)
路由就是"告诉服务器:当有人访问某个网址时,该做什么事"。就像餐厅的菜单——你点"1号餐",服务员就知道要上什么菜。
import { Elysia } from "elysia";
new Elysia()
.get("/", "Hello Elysia") // 访问首页,返回文字
.get("/user/:id", ({ params }) => params.id) // 动态路径
.post("/form", ({ body }) => body) // 接收表单数据
.listen(3000);2. 验证(Validation)
验证就是"检查客人点的东西是否合理"。比如,年龄必须是数字而不是文字。Elysia 用 Elysia.t(简称 t)来定义规则,同时这些规则会自动变成 TypeScript 类型。
import { Elysia, t } from "elysia";
new Elysia()
.get("/user/:id", ({ params: { id } }) => id, {
params: t.Object({
id: t.Number(), // id 必须是数字
}),
})
.listen(3000);3. 生命周期(Lifecycle)
生命周期就像一条流水线上的不同工位。请求进来后,依次经过:接收 → 解析 → 转换 → 验证 → 前置处理 → 执行 → 后置处理 → 响应映射 → 发送响应。每个工位你都可以插入自己的逻辑。
new Elysia()
.onRequest(() => console.log("收到请求"))
.onBeforeHandle(({ headers, status }) => {
// 在处理之前检查权限
if (!headers.authorization) return status(401);
})
.get("/", () => "Hello")
.listen(3000);4. 插件(Plugin)
插件就是"把一组功能打包,随时插到任意服务器上使用"。每个 Elysia 实例都可以独立运行,也可以通过 use() 组合到其他实例中。
const logger = new Elysia().onRequest(({ request }) => console.log(request.url));
const app = new Elysia()
.use(logger) // 一行代码添加日志功能
.get("/", "Hello")
.listen(3000);5. Eden Treaty(端到端类型安全)
想象你在 A 城市的办公室打电话给 B 城市的同事。如果你们用同一种语言交流,沟通就很顺畅。Eden Treaty 就是让前后端"说同一种语言"的工具——服务端定义的类型,客户端自动获得,不需要手动同步。
// 服务端
export const app = new Elysia().get("/hi", () => "Hi Elysia").listen(3000);
export type App = typeof app;
// 客户端
import { treaty } from "@elysiajs/eden";
import type { App } from "./server";
const api = treaty<App>("localhost:3000");
const { data } = await api.hi.get(); // data 的类型自动推断为 string类比与比喻
- Elysia vs Express:Express 像一辆手动挡汽车——灵活但需要自己操作很多。Elysia 像一辆自动驾驶汽车——你告诉它目的地(定义 Schema),它自动处理验证、类型推断、文档生成。
- 生命周期 vs 中间件:传统中间件像排队——每个人都要经过同一个队列。生命周期像分拣中心——每个包裹根据不同阶段进入不同的处理通道。
- Elysia.t vs TypeScript 类型:TypeScript 类型只在编译时存在,运行时就消失了。Elysia.t 同时在编译时和运行时工作——它是真正的"一位一体"。
- Eden Treaty vs tRPC:tRPC 发明了一套专用通信协议,你需要学习新的 API。Eden Treaty 让你继续使用标准的 HTTP 方法,但自动获得类型安全。
常见误解澄清
误解 1:Elysia 只能在 Bun 上运行。
事实上,Elysia 基于 Web 标准(Request/Response),通过适配器可以运行在 Node.js、Deno、Cloudflare Worker、Vercel Edge Function 等多种运行时上。Bun 只是提供最佳性能的首选运行时。
误解 2:Elysia 必须使用 TypeScript。
TypeScript 不是必须的,但强烈推荐。Elysia 的类型系统是它的核心优势,不用 TypeScript就失去了最大的卖点。
误解 3:Elysia 的性能优势来自 Bun。
虽然 Bun 提供了高性能基础,但 Elysia 还通过静态代码分析(Static Code Analysis)和 Ahead-of-Time(AoT)编译进一步优化——在服务器启动时就生成优化后的路由处理代码。
三、锥形深入(西蒙学习法)
集中精力、目标导向、锥形深入——从核心开始,逐步扩展到周边。
第一层:核心基础
路由定义
Elysia 使用方法链定义路由。每个路由由三个部分组成:HTTP 方法、路径、处理函数。
import { Elysia } from "elysia";
new Elysia()
.get("/", "Hello World") // 返回字符串
.get("/json", () => ({ hello: "Elysia" })) // 自动转 JSON
.post("/data", ({ body }) => body) // 接收并返回 body
.put("/update", ({ body }) => body)
.delete("/remove", () => "Deleted")
.all("/any", () => "Any method") // 匹配所有 HTTP 方法
.listen(3000);路径参数(Path Parameters)
使用 :name 语法定义动态路径段,通过 params 访问。
// 动态路径参数
.get('/user/:id', ({ params: { id } }) => `User ${id}`)
// 多个路径参数
.get('/post/:postId/comment/:commentId', ({ params }) => params)
// 可选路径参数(加 ? 后缀)
.get('/page/:page?', ({ params: { page } }) => `Page ${page ?? 1}`)
// 通配符路径
.get('/files/*', ({ params }) => params['*'])路径优先级:静态路径 > 动态路径 > 通配符路径。
查询参数(Query Parameters)
查询参数自动解析为对象,通过 query 访问。
.get('/search', ({ query }) => query)
// GET /search?keyword=elysia&page=1 → { keyword: "elysia", page: "1" }注意:查询参数的值始终是字符串。如果需要数字类型,使用 t.Number() 配合 Schema 验证,Elysia 会自动转换。
请求体(Body)
通过 body 访问请求体。Elysia 自动解析 JSON、FormData 和 URL 编码格式。
import { Elysia, t } from "elysia";
new Elysia()
.post("/user", ({ body }) => body, {
body: t.Object({
name: t.String(),
age: t.Number(),
email: t.String({ format: "email" }),
}),
})
.listen(3000);响应处理
Elysia 自动将返回值转换为适当的 HTTP 响应:字符串返回 text/plain,对象返回 application/json。
// 字符串响应
.get('/', () => 'Hello')
// JSON 响应(自动)
.get('/json', () => ({ message: 'Hello' }))
// 自定义状态码
.get('/teapot', ({ status }) => status(418, "I'm a teapot"))
// 自定义响应头
.get('/', ({ set }) => {
set.headers['x-powered-by'] = 'Elysia'
return 'Hello'
})
// 重定向
.get('/redirect', ({ redirect }) => redirect('https://elysiajs.com'))文件处理
import { Elysia, file } from 'elysia'
// 返回静态文件
.get('/image', file('public/photo.webp'))
// 文件上传
.post('/upload', ({ body }) => body.file, {
body: t.Object({
file: t.File({ type: 'image' })
})
})
// 多文件上传
.post('/uploads', ({ body }) => body.files, {
body: t.Object({
files: t.Files()
})
})流式响应与 SSE
import { Elysia, sse } from 'elysia'
// 生成器流
.get('/stream', function* () {
yield 'Hello'
yield 'World'
})
// Server-Sent Events
.get('/sse', function* () {
yield sse({ event: 'message', data: 'Hello' })
yield sse({ event: 'message', data: 'World' })
yield sse({ event: 'done' })
})第二层:进阶用法
生命周期钩子(Lifecycle Hooks)
Elysia 的生命周期是一系列事件阶段,每个阶段你都可以插入自定义逻辑。
import { Elysia } from "elysia";
new Elysia()
// 1. 请求到达时(最早阶段,用于限流、缓存等)
.onRequest(({ request }) => {
console.log(`Request: ${request.url}`);
})
// 2. 解析请求体(可自定义 body parser)
.onParse(({ request, contentType }) => {
if (contentType === "application/custom") return request.text();
})
// 3. 转换阶段(在验证之前,用于修改 context)
.onTransform(({ params }) => {
// 将字符串 ID 转为数字
if (params.id) params.id = +params.id;
})
// 4. 验证后、处理前(用于权限检查等)
.onBeforeHandle(({ headers, status }) => {
if (!headers.authorization) return status(401);
})
// 5. 处理后(用于修改响应)
.onAfterHandle(({ set }) => {
set.headers["content-type"] = "application/json";
})
// 6. 错误处理
.onError(({ code, error }) => {
if (code === "NOT_FOUND") return "Not Found :(";
return new Response(error.toString());
})
// 7. 响应发送后(用于日志、清理等)
.onAfterResponse(({ set }) => {
console.log(`Response: ${set.status}`);
})
.get("/", () => "Hello")
.listen(3000);钩子分为两种类型:
- 本地钩子(Local Hook):通过路由的第三个参数传入,只对该路由生效
- 拦截钩子(Interceptor Hook):通过
onXxx方法注册,对注册之后的所有路由生效
// 本地钩子
.get('/protected', () => 'Secret', {
beforeHandle({ headers, status }) {
if (!headers.authorization) return status(401)
}
})
// 拦截钩子
.onBeforeHandle(({ headers, status }) => {
if (!headers.authorization) return status(401)
})
.get('/protected', () => 'Secret') // 自动应用上面的 beforeHandle插件系统
每个 Elysia 实例都是独立的,可以通过 use() 组合。这是 Elysia 的核心组合模式。
// 创建一个带配置的插件
const auth = (secret: string) =>
new Elysia({ name: "auth" })
.derive({ as: "global" }, ({ headers }) => {
const token = headers.authorization?.replace("Bearer ", "");
return { token };
})
.onBeforeHandle(({ token, status }) => {
if (!token) return status(401);
});
// 使用插件
const app = new Elysia()
.use(auth("my-secret"))
.get("/profile", ({ token }) => `Token: ${token}`)
.listen(3000);插件去重:通过设置 name 属性,Elysia 会自动去重重复注册的插件。
const ip = new Elysia({ name: "ip" }).derive({ as: "global" }, ({ server, request }) => ({
ip: server?.requestIP(request),
}));
// 即使 use 多次,ip 只会注册一次
const app = new Elysia().use(ip).use(ip); // 不会重复执行封装与作用域(Encapsulation & Scope)
默认情况下,Elysia 的生命周期是封装的——插件中定义的钩子不会影响父实例。这是与 Express 中间件的关键区别。
三种作用域级别:
- local(默认):仅当前实例及其后代生效
- scoped:扩展到直接父实例
- global:扩展到所有使用该插件的实例
const authPlugin = new Elysia().onBeforeHandle({ as: "scoped" }, ({ cookie, status }) => {
if (!cookie.session.value) return status(401);
});
const app = new Elysia()
.use(authPlugin)
.get("/profile", () => "Protected") // 受 authPlugin 保护
.listen(3000);Guard
Guard 用于将 Schema 和钩子批量应用到多个路由。
new Elysia()
.guard(
{
body: t.Object({
username: t.String(),
password: t.String(),
}),
},
(app) => app.post("/sign-in", ({ body }) => body).post("/sign-up", ({ body }) => body),
)
.get("/", () => "No validation needed")
.listen(3000);分组路由(Group)
使用 group 组织路由前缀:
new Elysia()
.group("/api", (app) => app.get("/users", () => "Users").post("/users", () => "Create User"))
.listen(3000);
// 等价于 /api/users GET 和 /api/users POST
// 也可以在构造函数中设置 prefix
const api = new Elysia({ prefix: "/api" }).get("/users", () => "Users");扩展 Context
使用 state、decorate、derive、resolve 扩展请求上下文:
// state:全局可变状态
new Elysia().state("counter", 0).get("/", ({ store }) => {
store.counter++;
return `Count: ${store.counter}`;
});
// decorate:全局不可变属性
new Elysia().decorate("logger", new Logger()).get("/", ({ logger }) => {
logger.log("Hello");
return "ok";
});
// derive:在验证之前派生新属性
new Elysia()
.derive(({ headers }) => ({
bearer: headers.authorization?.replace("Bearer ", ""),
}))
.get("/", ({ bearer }) => bearer);
// resolve:在验证之后派生新属性(更安全)
new Elysia()
.guard({
headers: t.Object({
authorization: t.String(),
}),
})
.resolve(({ headers: { authorization } }) => ({
token: authorization.split(" ")[1],
}))
.get("/", ({ token }) => token);Cookie 处理
Elysia 使用响应式信号(Signal)模式处理 Cookie:
import { Elysia, t } from "elysia";
new Elysia()
.get(
"/",
({ cookie: { visit } }) => {
visit.value ??= 0;
visit.value++;
visit.httpOnly = true;
visit.set({
sameSite: "lax",
secure: true,
maxAge: 60 * 60 * 24 * 7,
});
return `Visited ${visit.value} times`;
},
{
cookie: t.Cookie({
visit: t.Optional(t.Number()),
}),
},
)
.listen(3000);Cookie 签名验证:
new Elysia({
cookie: {
secret: "my-secret-key",
},
}).get(
"/",
({ cookie: { session } }) => {
session.value = "encrypted-value";
return "ok";
},
{
cookie: t.Cookie(
{
session: t.String(),
},
{
secrets: "my-secret-key",
sign: ["session"],
},
),
},
);错误处理
import { Elysia } from "elysia";
class AppError extends Error {
status = 400;
constructor(message: string) {
super(message);
}
}
new Elysia()
.error({ APP_ERROR: AppError })
.onError(({ code, error, status }) => {
switch (code) {
case "APP_ERROR":
return status(error.status, { message: error.message });
case "NOT_FOUND":
return status(404, "Not Found");
case "VALIDATION":
return status(422, error.message);
default:
return status(500, "Internal Server Error");
}
})
.get("/", () => {
throw new AppError("Something went wrong");
})
.listen(3000);WebSocket
Elysia 内置基于 µWebSocket 的 WebSocket 支持:
import { Elysia, t } from "elysia";
new Elysia()
.ws("/chat", {
body: t.String(),
response: t.String(),
open(ws) {
console.log("Client connected");
},
message(ws, message) {
ws.send(`Echo: ${message}`);
},
close(ws) {
console.log("Client disconnected");
},
})
.listen(3000);Macro 系统
Macro 是 Elysia 的独特功能,允许你定义可复用的路由选项——类似于函数,但是作为路由选项:
import { Elysia, t } from "elysia";
new Elysia()
.macro({
auth: {
cookie: t.Object({ session: t.String() }),
beforeHandle({ cookie: { session }, status }) {
if (!session.value) return status(401);
},
},
})
.get("/profile", () => "Profile", { auth: true })
.post("/settings", () => "Settings", { auth: true })
.listen(3000);OpenAPI 文档
一行代码生成 API 文档:
import { Elysia, t } from "elysia";
import { openapi } from "@elysiajs/openapi";
new Elysia()
.use(openapi())
.get("/user/:id", ({ params: { id } }) => id, {
params: t.Object({ id: t.Number() }),
detail: {
summary: "Get user by ID",
tags: ["User"],
},
})
.listen(3000);访问 /openapi 即可看到自动生成的 API 文档(默认使用 Scalar UI)。
第三层:深度解析
End-to-End Type Safety 原理
Elysia 的端到端类型安全核心原理:从 Elysia 实例导出 TypeScript 类型,客户端通过 Eden Treaty 消费该类型。
服务端:
// server.ts
import { Elysia, t } from "elysia";
export const app = new Elysia()
.get("/hi", () => "Hi Elysia")
.post("/user", ({ body }) => body, {
body: t.Object({
name: t.String(),
age: t.Number(),
}),
response: {
200: t.Object({ name: t.String(), age: t.Number() }),
400: t.Object({ error: t.String() }),
},
})
.listen(3000);
export type App = typeof app; // 导出类型客户端:
// client.ts
import { treaty } from "@elysiajs/eden";
import type { App } from "./server";
const api = treaty<App>("localhost:3000");
// 完全类型安全,带自动补全
const { data, error } = await api.user.post({
name: "Elysia",
age: 1,
});
// data 的类型会根据响应状态码自动推断
// error 也会精确类型化,包含所有可能的错误状态关键原理:
- Elysia 实例的类型包含了所有路由信息(路径、方法、请求 Schema、响应 Schema)
typeof app将运行时代码提升为编译时类型- Eden Treaty 解析这个类型,构建出类型安全的客户端 API
- 无需代码生成——纯粹的 TypeScript 类型推断
Type Soundness(类型健全性):Elysia 不仅推断"快乐路径"(200 OK),还能推断所有可能的错误状态码和对应的错误类型。这是其他端到端类型安全框架(如 tRPC)通常做不到的。
Eden Treaty 客户端详解
Eden Treaty 将 Elysia 服务端映射为一个树状结构的客户端对象:
const api = treaty<App>("localhost:3000");
// 路径 / → api.get()
const { data } = await api.get();
// 路径 /user/:id → api.user({ id: 123 }).get()
const { data } = await api.user({ id: 123 }).get();
// 路径 /api/deep/nested → api.api.deep.nested.post({ ... })
const { data } = await api.api.deep.nested.post({ body: "data" });Eden Treaty 还支持直接传入 Elysia 实例(用于测试或微服务通信,无需网络开销):
import { treaty } from "@elysiajs/eden";
const api = treaty(app); // 直接传入实例,不走网络
const { data } = await api.hi.get();性能优化原理
Elysia 的性能优势来自三个层面:
1. Bun 运行时——Bun 使用 JavaScriptCore(JSC)引擎而非 V8,启动速度更快,HTTP 服务器基于 µWebSocket 实现。
2. 静态代码分析——Elysia 在启动时分析你的代码,确定需要解析哪些属性(headers、query、body 等),只解析必要的部分。
3. Ahead-of-Time(AoT)编译——Elysia 的内置 JIT"编译器"在服务器启动前就将路由处理逻辑编译为优化后的代码:
// AoT 默认开启
new Elysia({ aot: true });性能基准(请求/秒):
| 框架 | 运行时 | 平均性能 |
|---|---|---|
| Elysia | Bun | 255,574 |
| Hono | Bun | 203,937 |
| Fastify | Node | 60,322 |
| Express | Node | 15,913 |
与其他框架对比
Elysia vs Express:
- 性能:Elysia 快约 21 倍
- 类型安全:Elysia 有完整的端到端类型安全,Express 没有
- 中间件模式:Express 使用队列式中间件,Elysia 使用事件驱动的生命周期
- 封装性:Express 中间件默认全局生效,Elysia 默认封装隔离
Elysia vs Hono:
- 性能:Elysia 比 Hono 快约 25%
- 类型安全:Elysia 提供更健全的类型系统(包括错误状态码的类型推断)
- OpenAPI:Elysia 内建支持,Hono 需要额外配置
- 目标平台:Hono 最初为 Cloudflare Workers 设计,Elysia 最初为 Bun 设计
Elysia vs tRPC:
- 协议:Elysia 基于 RESTful 标准,tRPC 使用专用 RPC 协议
- OpenAPI:Elysia 原生支持,tRPC 需要第三方库
- 学习曲线:Elysia 使用标准 HTTP 概念,tRPC 需要学习新的 API
- 类型安全:两者都支持,但 Elysia 还包含错误状态的类型推断
Standard Schema 支持
Elysia 支持多种验证库,你可以自由选择:
import { Elysia, t } from "elysia";
import { z } from "zod";
import * as v from "valibot";
new Elysia()
// TypeBox(内置)
.post("/typebox", ({ body }) => body, {
body: t.Object({ name: t.String() }),
})
// Zod
.post("/zod", ({ body }) => body, {
body: z.object({ name: z.string() }),
})
// Valibot
.post("/valibot", ({ body }) => body, {
body: v.object({ name: v.string() }),
});部署策略
编译为二进制(推荐):
bun build --compile --minify-whitespace --minify-syntax \
--target bun --outfile server src/index.ts编译后的二进制文件可以减少 2-3 倍的内存使用。
Docker 部署:
FROM oven/bun AS build
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install
COPY ./src ./src
RUN bun build --compile --minify-whitespace --minify-syntax \
--outfile server src/index.ts
FROM gcr.io/distroless/base
WORKDIR /app
COPY --from=build /app/server server
CMD ["./server"]
EXPOSE 3000Vercel 部署:
// src/index.ts
import { Elysia, t } from "elysia";
export default new Elysia().get("/", () => "Hello Vercel").listen(3000);vc deploy四、要点笔记(康奈尔笔记法)
关键概念速查表
| 线索/关键词 | 详细笔记 |
|---|---|
| 路由 | .get()、.post()、.put()、.delete()、.all()、.route() 定义路由;方法链调用 |
| 路径参数 | :name 动态路径、:name? 可选路径、* 通配符;通过 params 访问 |
| 查询参数 | URL 中 ?key=value 部分;通过 query 访问;值默认为字符串 |
| 请求体 | 通过 body 访问;自动解析 JSON、FormData、URL-encoded |
| 响应 | 直接 return 值;status() 设置状态码;set.headers 设置响应头;redirect() 重定向 |
| 验证 | t.Object()、t.String()、t.Number() 等;body、query、params、headers、cookie、response 都可验证 |
| 生命周期 | onRequest → onParse → onTransform → 验证 → onBeforeHandle → Handler → onAfterHandle → onMapResponse → onError → onAfterResponse |
| 插件 | .use() 组合实例;name 属性用于去重;默认封装隔离 |
| 作用域 | local(默认隔离)、scoped(扩展到父)、global(全局) |
| Guard | 批量应用 Schema 和钩子;guard(schema, callback) |
| Group | group(prefix, callback) 组织路由前缀 |
| Context 扩展 | state(可变全局)、decorate(不可变全局)、derive(验证前派生)、resolve(验证后派生) |
| Cookie | 响应式信号模式;cookie.name.value 读写;t.Cookie() 定义 Schema |
| WebSocket | .ws() 方法;message、open、close 回调;µWebSocket 底层 |
| Macro | .macro() 定义可复用路由选项;{ auth: true } 一行启用 |
| OpenAPI | @elysiajs/openapi 一行生成文档;fromTypes() 从类型生成 |
| Eden Treaty | treaty<App>(url) 创建类型安全客户端;无需代码生成 |
核心 API 速查
| API / 方法 | 用途 | 示例 |
|---|---|---|
new Elysia() | 创建实例 | new Elysia({ prefix: '/api' }) |
.get(path, handler, hook?) | 定义 GET 路由 | .get('/', () => 'Hello') |
.post(path, handler, hook?) | 定义 POST 路由 | .post('/user', ({ body }) => body) |
.ws(path, options) | 定义 WebSocket | .ws('/chat', { message(ws, msg) {} }) |
.use(plugin) | 使用插件 | .use(cors()) |
.guard(schema, callback) | 批量应用 Schema | .guard({ body: t.Object({...}) }, (app) => ...) |
.group(prefix, callback) | 路由分组 | .group('/api', (app) => ...) |
.model(models) | 注册引用模型 | .model({ user: t.Object({...}) }) |
.macro(definition) | 定义宏 | .macro({ auth: { beforeHandle() {} } }) |
.state(key, value) | 设置全局状态 | .state('version', 1) |
.decorate(key, value) | 添加上下文属性 | .decorate('logger', new Logger()) |
.derive(fn) | 验证前派生属性 | .derive(({ headers }) => ({ ... })) |
.resolve(fn) | 验证后派生属性 | .resolve(({ body }) => ({ ... })) |
.onRequest(fn) | 请求到达钩子 | .onRequest(({ request }) => ...) |
.onBeforeHandle(fn) | 处理前钩子 | .onBeforeHandle(({ status }) => ...) |
.onError(fn) | 错误处理钩子 | .onError(({ code }) => ...) |
.listen(port) | 启动服务器 | .listen(3000) |
t.Object({}) | 定义对象 Schema | t.Object({ name: t.String() }) |
t.String() | 字符串类型 | t.String({ format: 'email' }) |
t.Number() | 数字类型 | t.Number({ minimum: 0 }) |
t.File() | 文件类型 | t.File({ type: 'image' }) |
t.Cookie({}) | Cookie Schema | t.Cookie({ session: t.String() }) |
t.Optional() | 可选字段 | t.Optional(t.String()) |
t.Union([]) | 联合类型 | t.Union([t.String(), t.Number()]) |
file(path) | 返回静态文件 | .get('/img', file('photo.webp')) |
sse(data) | SSE 事件 | sse({ event: 'msg', data: 'hello' }) |
treaty<App>(url) | 创建 Eden 客户端 | treaty<App>('localhost:3000') |
本节总结
Elysia 的核心可以概括为一个公式:Schema 驱动的单一数据源 + 事件驱动的生命周期 + 组合式插件架构 = 端到端类型安全的 RESTful 框架。
掌握以下五个关键点,就能驾驭 Elysia 的 80% 场景:
- 路由与验证:用
t.Object()定义 Schema,自动获得类型推断和运行时验证 - 生命周期:用
onBeforeHandle等钩子在请求管道中插入自定义逻辑 - 插件组合:用
use()组合功能,用guard应用共享规则 - 封装与作用域:理解 local/scoped/global 三种作用域的区别
- Eden Treaty:用
treaty<App>()创建端到端类型安全的客户端
五、复习与实践(SQ3R · Recite & Review)
核心要点回顾
-
Elysia 是基于 Bun 的 TypeScript Web 框架,提供端到端类型安全、卓越性能和出色的开发者体验。
-
单一数据源是其核心设计理念——一个 Schema 同时用于运行时验证、TypeScript 类型推断、OpenAPI 文档和客户端类型同步。
-
生命周期系统取代了传统中间件,提供更细粒度的请求处理控制。钩子默认封装,通过作用域机制控制影响范围。
-
Eden Treaty 提供无需代码生成的端到端类型安全,支持错误状态码的精确类型推断。
-
性能优化来自 Bun 运行时 + 静态代码分析 + AoT 编译的三重加速。
-
跨平台——基于 Web 标准,可以在 Bun、Node.js、Deno、Cloudflare Worker 等多种运行时运行。
动手练习
练习 1:创建一个带验证的 CRUD API
import { Elysia, t } from "elysia";
interface User {
id: number;
name: string;
email: string;
}
let users: User[] = [];
let nextId = 1;
new Elysia()
.post(
"/users",
({ body }) => {
const user = { id: nextId++, ...body };
users.push(user);
return user;
},
{
body: t.Object({
name: t.String({ minLength: 1 }),
email: t.String({ format: "email" }),
}),
},
)
.get("/users", () => users)
.get(
"/users/:id",
({ params: { id }, status }) => {
const user = users.find((u) => u.id === id);
if (!user) return status(404, "User not found");
return user;
},
{
params: t.Object({ id: t.Number() }),
},
)
.delete(
"/users/:id",
({ params: { id }, status }) => {
const index = users.findIndex((u) => u.id === id);
if (index === -1) return status(404, "User not found");
users.splice(index, 1);
return status(200, "Deleted");
},
{
params: t.Object({ id: t.Number() }),
},
)
.listen(3000);练习 2:创建一个带认证中间件的 API
import { Elysia, t } from "elysia";
const auth = new Elysia({ name: "auth" }).macro({
auth: {
cookie: t.Object({ session: t.String() }),
beforeHandle({ cookie: { session }, status }) {
if (!session.value) return status(401);
},
},
});
new Elysia()
.use(auth)
.get("/public", () => "Anyone can see this")
.get("/private", () => "Only authenticated users", { auth: true })
.listen(3000);练习 3:使用 Eden Treaty 编写类型安全的测试
import { describe, expect, it } from "bun:test";
import { Elysia } from "elysia";
import { treaty } from "@elysiajs/eden";
const app = new Elysia().get("/hello", () => "Hello World").post("/echo", ({ body }) => body);
const api = treaty(app);
describe("Elysia API", () => {
it("GET /hello returns Hello World", async () => {
const { data } = await api.hello.get();
expect(data).toBe("Hello World");
});
it("POST /echo echoes body", async () => {
const { data } = await api.echo.post({ message: "test" });
expect(data).toEqual({ message: "test" });
});
});常见陷阱
- 不使用方法链——Elysia 的类型系统依赖方法链来追踪类型变化。如果不用方法链,类型推断会丢失。
// 错误:不使用方法链
const app = new Elysia();
app.state("version", 1);
app.get("/", ({ store }) => store.version); // 类型错误!
// 正确:使用方法链
new Elysia().state("version", 1).get("/", ({ store }) => store.version); // 类型正确- 钩子注册顺序错误——生命周期钩子只对注册之后的路由生效。注意把钩子放在路由之前。
// 错误:钩子在路由之后
.get('/', () => 'Hello')
.onBeforeHandle(() => console.log('log')) // 不会应用于上面的路由
// 正确:钩子在路由之前
.onBeforeHandle(() => console.log('log'))
.get('/', () => 'Hello')-
忽略封装——插件中的钩子默认不会影响父实例。如果需要跨实例生效,必须设置作用域。
-
在 Cloudflare Worker 上使用
file()或静态插件——Cloudflare Worker 没有fs模块,需要使用 Cloudflare 的内置静态文件服务。 -
Eden Treaty 版本不匹配——客户端和服务端必须使用相同版本的 Elysia,否则类型推断可能不正确。
延伸阅读
- Elysia 官方文档——最权威的参考
- Eden Treaty 文档——端到端类型安全客户端
- Elysia GitHub——源码和社区讨论
- Bun 文档——了解 Bun 运行时的特性
- TypeBox 文档——深入了解 Elysia.t 的底层类型系统
- Elysia 最佳实践——官方推荐的代码组织方式