localhost
GET

创建 API 服务器的目的在于接收输入并对其进行处理。
JavaScript 允许任何数据成为任何类型。Elysia 提供了一个工具,可以对数据进行验证,以确保数据的格式正确。
import { Elysia, t } from 'elysia'
new Elysia()
.get('/id/:id', ({ params: { id } }) => id, {
params: t.Object({
id: t.Number()
})
})
.listen(3000)Elysia.t 是基于 TypeBox 的模式构建器,提供了运行时、编译时和 OpenAPI 模式的类型安全,支持自动生成 OpenAPI 文档。
TypeBox 是一个极快、轻量且类型安全的 TypeScript 运行时验证库。Elysia 对 TypeBox 的默认行为进行了扩展和定制,以适应服务器端的验证需求。
我们相信验证至少应由框架原生处理,而不是依赖用户为每个项目设置自定义类型。
Elysia 也支持 Standard Schema,允许您使用喜欢的验证库:
使用 Standard Schema,只需导入相应的 schema 并传递给路由处理器。
import { Elysia } from 'elysia'
import { z } from 'zod'
import * as v from 'valibot'
new Elysia()
.get('/id/:id', ({ params: { id }, query: { name } }) => id, {
params: z.object({
id: z.coerce.number()
}),
query: v.object({
name: v.literal('Lilith')
})
})
.listen(3000)您可以在同一处理器内无缝使用多种验证器。
我们可以通过访问 static 属性来获取每个 Elysia/TypeBox 类型的类型定义,如下所示:
import { t } from 'elysia'
const MyType = t.Object({
hello: t.Literal('Elysia')
})
type MyType = typeof MyType.static
这使得 Elysia 能够自动推断和提供类型,减少重复声明模式的需求。
一个单一的 Elysia/TypeBox 模式可以用于:
这使我们能够将模式作为 单一真实来源。
Elysia 支持以下类型的声明式模式:
这些属性应作为路由处理器的第三个参数提供,用以验证传入请求。
import { Elysia, t } from 'elysia'
new Elysia()
.get('/id/:id', () => 'Hello World!', {
query: t.Object({
name: t.String()
}),
params: t.Object({
id: t.Number()
})
})
.listen(3000)GET
响应示例:
| URL | 查询 | 参数 |
|---|---|---|
| /id/a | ❌ | ❌ |
| /id/1?name=Elysia | ✅ | ✅ |
| /id/1?alias=Elysia | ❌ | ✅ |
| /id/a?name=Elysia | ✅ | ❌ |
| /id/a?alias=Elysia | ❌ | ❌ |
当提供了模式时,类型将自动从模式推断,并生成 OpenAPI 类型用于 API 文档,省去了手动提供类型的重复工作。
Guard 可用于将模式应用于多个处理器。
import { Elysia, t } from 'elysia'
new Elysia()
.get('/none', ({ query }) => 'hi')
.guard({
query: t.Object({
name: t.String()
})
})
.get('/query', ({ query }) => query)
.listen(3000)这段代码确保查询中在其后每个处理器均必须包含字符串类型的 name 属性。响应示例如下:
GET
响应结果:
| 路径 | 响应 |
|---|---|
| /none | hi |
| /none?name=a | hi |
| /query | error |
| /query?name=a | a |
如果为同一属性定义了多个全局模式,则最后一个生效。如果同时定义本地与全局模式,则本地优先。
Guard 支持两种验证模式定义类型。
模式之间冲突时,后者覆盖前者。

分别处理碰撞的模式并独立运行,确保两个模式都被验证。

通过使用 schema 属性定义守护的模式类型:
import { Elysia } from 'elysia'
new Elysia()
.guard({
schema: 'standalone',
response: t.Object({
title: t.String()
})
})传入的 HTTP 消息 是发送到服务器的数据,可以是 JSON、表单数据或其它任意格式。
import { Elysia, t } from 'elysia'
new Elysia()
.post('/body', ({ body }) => body, {
body: t.Object({
name: t.String()
})
})
.listen(3000)验证示例:
| 主体 | 验证 |
|---|---|
| { name: 'Elysia' } | ✅ |
| { name: 1 } | ❌ |
| { alias: 'Elysia' } | ❌ |
undefined | ❌ |
Elysia 默认禁用 GET 和 HEAD 请求的 body 解析,遵循 HTTP/1.1 规范 RFC2616
如果请求方法不包含实体主体的定义语义,则应忽略消息主体。
大部分浏览器默认禁用在 GET 和 HEAD 方法下附加主体。
验证传入的 HTTP 消息(也就是主体)。
这些消息供 Web 服务器处理的附加信息。
主体对应于 fetch API 中的 body。内容类型应根据定义的主体类型相应设置。
fetch('https://elysiajs.com', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'Elysia'
})
})文件是特殊的主体类型,用于文件上传。
import { Elysia, t } from 'elysia'
new Elysia()
.post('/body', ({ body }) => body, {
body: t.Object({
file: t.File({ format: 'image/*' }),
multipleFiles: t.Files()
})
})
.listen(3000)通过提供文件类型,Elysia 会自动假设内容类型为 multipart/form-data。
如果您使用标准 Schema,需注意 Elysia 无法像 t.File 那样自动验证内容类型。
但 Elysia 导出了一个 fileType 函数,可用来通过魔数(magic number)验证文件类型。
import { Elysia, fileType } from 'elysia'
import { z } from 'zod'
new Elysia()
.post('/body', ({ body }) => body, {
body: z.object({
file: z.file().refine((file) => fileType(file, 'image/jpeg'))
})
})非常重要的是您应当使用 fileType 来验证文件类型,因为大多数验证器并不能正确验证文件,比如仅检查内容类型的值,这可能导致安全漏洞。
查询是通过 URL 发送的数据,形式为 ?key=value。
import { Elysia, t } from 'elysia'
new Elysia()
.get('/query', ({ query }) => query, {
query: t.Object({
name: t.String()
})
})
.listen(3000)查询参数必须以对象形式提供。
验证示例:
| 查询 | 验证 |
|---|---|
| /?name=Elysia | ✅ |
| /?name=1 | ✅ |
| /?alias=Elysia | ❌ |
| /?name=ElysiaJS&alias=Elysia | ✅ |
| / | ❌ |
查询字符串是 URL 的一部分,以 ? 开头,由一个或多个键值对构成,用于向服务器传递额外信息,通常用于自定义行为,如过滤或搜索。
查询参数紧跟于 Fetch API 中请求的 ? 处。
fetch('https://elysiajs.com/?name=Elysia')指定查询参数时,所有参数值必须表示为字符串,因为它们经过编码并附加到 URL。
Elysia 会自动将查询中的值强制转换为模式所需的类型。
更多信息请参考 Elysia 行为。
import { Elysia, t } from 'elysia'
new Elysia()
.get('/', ({ query }) => query, {
query: t.Object({
name: t.Number()
})
})
.listen(3000)GET
默认情况下,Elysia 将查询参数视为单个字符串,即使同一键被多次指定。
若要使用数组,需明确声明数组类型。
import { Elysia, t } from 'elysia'
new Elysia()
.get('/', ({ query }) => query, {
query: t.Object({
name: t.Array(t.String())
})
})
.listen(3000)GET
一旦 Elysia 识别某属性为数组,将自动把它强制转换为指定类型的数组。
默认情况下,Elysia 支持下列查询数组格式:
该格式由 nuqs 使用。
通过使用 , 作为分隔符,属性被解析为数组。
http://localhost?name=rapi,anis,neon&squad=counter
{
name: ['rapi', 'anis', 'neon'],
squad: 'counter'
}当同一个键被多次赋值时,该键被视为数组。
这与 HTML 表单格式相同,当相同名称的输入元素多次出现时。
http://localhost?name=rapi&name=anis&name=neon&squad=counter
// name: ['rapi', 'anis', 'neon']参数,或者说路径参数,是通过 URL 路径传递的数据,形式为 /key。
import { Elysia, t } from 'elysia'
new Elysia()
.get('/id/:id', ({ params }) => params, {
params: t.Object({
id: t.Number()
})
})GET
参数必须以对象形式提供。
验证示例:
| URL | 验证 |
|---|---|
| /id/1 | ✅ |
| /id/a | ❌ |
路径参数 (区别于查询字符串或查询参数)。
通常无需额外声明此字段,Elysia 能自动推断路径参数类型,除非需要特定的值模式,例如数字或模板字面量。
fetch('https://elysiajs.com/id/1')如果未提供参数模式,Elysia 会自动将类型推断为字符串。
import { Elysia, t } from 'elysia'
new Elysia()
.get('/id/:id', ({ params }) => params)
头部是通过请求头部发送的额外数据。
import { Elysia, t } from 'elysia'
new Elysia()
.get('/headers', ({ headers }) => headers, {
headers: t.Object({
authorization: t.String()
})
})不同于其他类型,头部的 additionalProperties 默认允许为 true。
这意味着头部可以包含任意键值对,但其值必须符合模式。
HTTP 头部允许客户端和服务器传递附加信息,通常作为元数据处理。
此字段常用于强制某些特定头部字段,如 Authorization。
头部的提供与 fetch API 中的 body 一致。
fetch('https://elysiajs.com/', {
headers: {
authorization: 'Bearer 12345'
}
})TIP
Elysia 仅以小写键名解析头部。
请确认在使用头部验证时,字段名称为小写。
Cookie 是通过请求的 Cookie 发送的数据。
import { Elysia, t } from 'elysia'
new Elysia()
.get('/cookie', ({ cookie }) => cookie, {
cookie: t.Cookie({
cookieName: t.String()
})
})Cookie 必须由 t.Cookie 或 t.Object 形式定义。
与headers类似,cookie 的 additionalProperties 默认设为 true。
HTTP Cookie 是服务器发送给客户端的小数据块,它会在每次访问相同网页服务器时自动发送,使服务器能记住客户端信息。
简单来说,Cookie 是每个请求中附带的字符串化状态。
此字段常用于强制某些特定 Cookie 字段。
Cookie 是特殊的请求头字段,Fetch API 不允许自定义值,需由浏览器管理。发送 Cookie 需设置 credentials 字段:
fetch('https://elysiajs.com/', {
credentials: 'include'
})t.Cookie 是特殊类型,类似于 t.Object,但支持设置 Cookie 特定选项。
import { Elysia, t } from 'elysia'
new Elysia()
.get('/cookie', ({ cookie }) => cookie.name.value, {
cookie: t.Cookie({
name: t.String()
}, {
secure: true,
httpOnly: true
})
})响应是处理器返回的数据。
import { Elysia, t } from 'elysia'
new Elysia()
.get('/response', () => {
return {
name: 'Jane Doe'
}
}, {
response: t.Object({
name: t.String()
})
})响应可以按状态码定义。
import { Elysia, t } from 'elysia'
new Elysia()
.get('/response', ({ status }) => {
if (Math.random() > 0.5)
return status(400, {
error: '出了点问题'
})
return {
name: 'Jane Doe'
}
}, {
response: {
200: t.Object({
name: t.String()
}),
400: t.Object({
error: t.String()
})
}
})这是 Elysia 特有功能,允许设置可选字段。
验证失败时,有两种方式提供自定义错误消息:
error 属性Elysia 提供额外的 error 属性,允许为字段无效时返回自定义错误消息。
import { Elysia, t } from 'elysia'
new Elysia()
.post('/', () => 'Hello World!', {
body: t.Object({
x: t.Number({
error: 'x 必须是一个数字'
})
})
})
.listen(3000)以下示例展示不同类型上使用错误属性:
| TypeBox 类型 | 错误信息 |
typescript | |
typescript | |
typescript | |
typescript | |
TypeBox 提供额外的 "error" 属性,允许字段无效时返回自定义错误消息。
| TypeBox 类型 | 错误信息 |
typescript | |
typescript | |
除了字符串外,Elysia 类型的 error 属性也可接收函数,为每个属性动态返回自定义错误信息。
错误函数接收参数与 ValidationError 相同。
import { Elysia, t } from 'elysia'
new Elysia()
.post('/', () => 'Hello World!', {
body: t.Object({
x: t.Number({
error() {
return '期望 x 为数字'
}
})
})
})
.listen(3000)TIP
鼠标悬停 error 可查看类型
注意,错误函数仅在对应字段无效时被调用。
参考下表:
| 代码 | 主体 | 错误消息 |
typescript | json | 期望 x 为数字 |
typescript | json | (默认错误,`t.Number.error` 不被调用) |
typescript | json | 期望值为对象 |
我们可以通过 onError 事件自定义验证行为,捕获错误代码 "VALIDATION"。
import { Elysia, t } from 'elysia'
new Elysia()
.onError(({ code, error }) => {
if (code === 'VALIDATION')
return error.message
})
.listen(3000)缩小后的错误类型表现为从 elysia/error 导入的 ValidationError。
ValidationError 暴露名为 validator 的属性,类型为 TypeCheck,允许与 TypeBox 功能直接交互。
import { Elysia, t } from 'elysia'
new Elysia()
.onError(({ code, error }) => {
if (code === 'VALIDATION')
return error.all[0].message
})
.listen(3000)ValidationError 提供方法 ValidatorError.all,允许列出所有错误原因。
import { Elysia, t } from 'elysia'
new Elysia()
.post('/', ({ body }) => body, {
body: t.Object({
name: t.String(),
age: t.Number()
}),
error({ code, error }) {
switch (code) {
case 'VALIDATION':
console.log(error.all)
// 查找特定错误,路径符合 OpenAPI 规范
const name = error.all.find(
(x) => x.summary && x.path === '/name'
)
// 如有验证错误则打印
if(name)
console.log(name)
}
}
})
.listen(3000)关于 TypeBox 验证器更多细节,请参阅 TypeCheck。
有时我们会声明重复模型,或多次复用相同模型。
通过引用模型,我们可以为模型命名,并通过名称在不同处引用。
先看一个简单场景:
假设有用于登录的控制器,使用同一模型。
import { Elysia, t } from 'elysia'
const app = new Elysia()
.post('/sign-in', ({ body }) => body, {
body: t.Object({
username: t.String(),
password: t.String()
}),
response: t.Object({
username: t.String(),
password: t.String()
})
})我们可以通过将模型提取成变量进行重构,并引用这些变量。
import { Elysia, t } from 'elysia'
// 也许在不同文件,如 models.ts
const SignDTO = t.Object({
username: t.String(),
password: t.String()
})
const app = new Elysia()
.post('/sign-in', ({ body }) => body, {
body: SignDTO,
response: SignDTO
})这种关注点分离的方式有效,但随着项目复杂性增加,我们可能在不同控制器中多次复用多个模型。
可以通过创建“引用模型”解决此问题,命名模型并在 schema 中直接以名称引用,同时通过 model 注册模型。
import { Elysia, t } from 'elysia'
const app = new Elysia()
.model({
sign: t.Object({
username: t.String(),
password: t.String()
})
})
.post('/sign-in', ({ body }) => body, {
// 使用已存在的模型名称,享受自动补全
body: 'sign',
response: 'sign'
})当想访问模型组时,可将 model 定义成插件,注册时提供一组模型,避免多次导入。
// auth.model.ts
import { Elysia, t } from 'elysia'
export const authModel = new Elysia()
.model({
sign: t.Object({
username: t.String(),
password: t.String()
})
})然后在主程序中:
// @filename: auth.model.ts
import { Elysia, t } from 'elysia'
export const authModel = new Elysia()
.model({
sign: t.Object({
username: t.String(),
password: t.String()
})
})
// @filename: index.ts
// ---省略---
// index.ts
import { Elysia } from 'elysia'
import { authModel } from './auth.model'
const app = new Elysia()
.use(authModel)
.post('/sign-in', ({ body }) => body, {
// 使用已存在模型名称,享受自动补全
body: 'sign',
response: 'sign'
})这种方法不仅实现关注点分离,还允许多处复用模型,并且将模型集成至 OpenAPI 文档。
model 接受一个对象,键为模型名称,值为模型定义,支持多个模型。
// auth.model.ts
import { Elysia, t } from 'elysia'
export const authModel = new Elysia()
.model({
number: t.Number(),
sign: t.Object({
username: t.String(),
password: t.String()
})
})重复模型名称会导致 Elysia 抛错。为避免声明重复命名的模型,我们可采用以下命名规范。
假设我们将所有模型存储于 models/<name>.ts,并在模型名前加前缀作为命名空间。
import { Elysia, t } from 'elysia'
// admin.model.ts
export const adminModels = new Elysia()
.model({
'admin.auth': t.Object({
username: t.String(),
password: t.String()
})
})
// user.model.ts
export const userModels = new Elysia()
.model({
'user.auth': t.Object({
username: t.String(),
password: t.String()
})
})一定程度上可以避免命名冲突,但最终最好团队达成一致的命名方案。
Elysia 也提供洞察选项,帮助您避免决策疲劳。