一、概览与提问(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)——事件驱动的请求处理管道,包括 onRequestonParseonTransformonBeforeHandleonAfterHandleonError 等事件,取代传统中间件模式。

高级层:插件与 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

使用 statedecoratederiveresolve 扩展请求上下文:

// 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 也会精确类型化,包含所有可能的错误状态

关键原理

  1. Elysia 实例的类型包含了所有路由信息(路径、方法、请求 Schema、响应 Schema)
  2. typeof app 将运行时代码提升为编译时类型
  3. Eden Treaty 解析这个类型,构建出类型安全的客户端 API
  4. 无需代码生成——纯粹的 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 });

性能基准(请求/秒):

框架运行时平均性能
ElysiaBun255,574
HonoBun203,937
FastifyNode60,322
ExpressNode15,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 3000

Vercel 部署

// 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() 等;bodyqueryparamsheaderscookieresponse 都可验证
生命周期onRequestonParseonTransform → 验证 → onBeforeHandle → Handler → onAfterHandleonMapResponseonErroronAfterResponse
插件.use() 组合实例;name 属性用于去重;默认封装隔离
作用域local(默认隔离)、scoped(扩展到父)、global(全局)
Guard批量应用 Schema 和钩子;guard(schema, callback)
Groupgroup(prefix, callback) 组织路由前缀
Context 扩展state(可变全局)、decorate(不可变全局)、derive(验证前派生)、resolve(验证后派生)
Cookie响应式信号模式;cookie.name.value 读写;t.Cookie() 定义 Schema
WebSocket.ws() 方法;messageopenclose 回调;µWebSocket 底层
Macro.macro() 定义可复用路由选项;{ auth: true } 一行启用
OpenAPI@elysiajs/openapi 一行生成文档;fromTypes() 从类型生成
Eden Treatytreaty<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({})定义对象 Schemat.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 Schemat.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% 场景:

  1. 路由与验证:用 t.Object() 定义 Schema,自动获得类型推断和运行时验证
  2. 生命周期:用 onBeforeHandle 等钩子在请求管道中插入自定义逻辑
  3. 插件组合:用 use() 组合功能,用 guard 应用共享规则
  4. 封装与作用域:理解 local/scoped/global 三种作用域的区别
  5. Eden Treaty:用 treaty<App>() 创建端到端类型安全的客户端

五、复习与实践(SQ3R · Recite & Review)

核心要点回顾

  1. Elysia 是基于 Bun 的 TypeScript Web 框架,提供端到端类型安全、卓越性能和出色的开发者体验。

  2. 单一数据源是其核心设计理念——一个 Schema 同时用于运行时验证、TypeScript 类型推断、OpenAPI 文档和客户端类型同步。

  3. 生命周期系统取代了传统中间件,提供更细粒度的请求处理控制。钩子默认封装,通过作用域机制控制影响范围。

  4. Eden Treaty 提供无需代码生成的端到端类型安全,支持错误状态码的精确类型推断。

  5. 性能优化来自 Bun 运行时 + 静态代码分析 + AoT 编译的三重加速。

  6. 跨平台——基于 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" });
  });
});

常见陷阱

  1. 不使用方法链——Elysia 的类型系统依赖方法链来追踪类型变化。如果不用方法链,类型推断会丢失。
// 错误:不使用方法链
const app = new Elysia();
app.state("version", 1);
app.get("/", ({ store }) => store.version); // 类型错误!
 
// 正确:使用方法链
new Elysia().state("version", 1).get("/", ({ store }) => store.version); // 类型正确
  1. 钩子注册顺序错误——生命周期钩子只对注册之后的路由生效。注意把钩子放在路由之前。
// 错误:钩子在路由之后
.get('/', () => 'Hello')
.onBeforeHandle(() => console.log('log'))  // 不会应用于上面的路由
 
// 正确:钩子在路由之前
.onBeforeHandle(() => console.log('log'))
.get('/', () => 'Hello')
  1. 忽略封装——插件中的钩子默认不会影响父实例。如果需要跨实例生效,必须设置作用域。

  2. 在 Cloudflare Worker 上使用 file() 或静态插件——Cloudflare Worker 没有 fs 模块,需要使用 Cloudflare 的内置静态文件服务。

  3. Eden Treaty 版本不匹配——客户端和服务端必须使用相同版本的 Elysia,否则类型推断可能不正确。

延伸阅读