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 的 schema 构建器,提供运行时、编译时和 OpenAPI schema 生成的类型安全性。
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)
您可以在同一个处理器中使用任何验证器,而不会出现问题。
Elysia 支持以下类型的声明性 schema:
这些属性应作为路由处理器的第三个参数提供,以验证传入的请求。
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)
响应应如下所示:
URL | Query | Params |
---|---|---|
/id/a | ❌ | ❌ |
/id/1?name=Elysia | ✅ | ✅ |
/id/1?alias=Elysia | ❌ | ✅ |
/id/a?name=Elysia | ✅ | ❌ |
/id/a?alias=Elysia | ❌ | ❌ |
当提供 schema 时,类型将自动从 schema 推断,并为 API 文档生成 OpenAPI 类型,从而消除手动提供类型的冗余任务。
Guard 可用于将 schema 应用于多个处理器。
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
响应应列出如下:
Path | Response |
---|---|
/none | hi |
/none?name=a | hi |
/query | error |
/query?name=a | a |
如果为同一属性定义了多个全局 schema,则最新的一个将优先。如果同时定义了本地和全局 schema,则本地 schema 将优先。
Guard 支持两种类型来定义验证。
如果 schema 之间发生冲突,则覆盖 schema。
分离冲突的 schema,并独立运行两者,从而实现两者的验证。
要使用 schema
定义 guard 的 schema 类型:
import { Elysia } from 'elysia'
new Elysia()
.guard({
schema: 'standalone',
response: t.Object({
title: t.String()
})
})
传入的 HTTP 消息 是发送到服务器的数据。它可以是 JSON、form-data 或任何其他格式。
import { Elysia, t } from 'elysia'
new Elysia()
.post('/body', ({ body }) => body, {
body: t.Object({
name: t.String()
})
})
.listen(3000)
验证应如下所示:
Body | Validation |
---|---|
{ name: 'Elysia' } | ✅ |
{ name: 1 } | ❌ |
{ alias: 'Elysia' } | ❌ |
undefined | ❌ |
Elysia 默认禁用 GET 和 HEAD 消息的 body-parser,遵循 HTTP/1.1 RFC2616 的规范
如果请求方法没有为实体体定义明确的语义,则在处理请求时应忽略消息体。
大多数浏览器默认禁用 GET 和 HEAD 方法的 body 附件。
验证传入的 HTTP 消息(或 body)。
这些消息是 Web 服务器处理的其他附加消息。
body 以与 fetch
API 中的 body
相同的方式提供。内容类型应根据定义的 body 相应设置。
fetch('https://elysiajs.com', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'Elysia'
})
})
文件是一种特殊的 body 类型,可用于上传文件。
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 将自动假设 content-type 为 multipart/form-data
。
如果您使用 Standard Schema,请注意 Elysia 将无法像 t.File
那样自动验证内容类型。
但是 Elysia 导出了一个 fileType
,可以使用魔术数字来验证文件类型。
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)
查询必须以对象形式提供。
验证应如下所示:
Query | Validation |
---|---|
/?name=Elysia | ✅ |
/?name=1 | ✅ |
/?alias=Elysia | ❌ |
/?name=ElysiaJS&alias=Elysia | ✅ |
/ | ❌ |
查询字符串是 URL 的一个部分,以 ? 开头,可以包含一个或多个查询参数,这些是键值对,用于向服务器传递额外信息,通常用于自定义行为,如过滤或搜索。
查询在 Fetch API 中在 ? 之后提供。
fetch('https://elysiajs.com/?name=Elysia')
在指定查询参数时,重要的是要理解所有查询参数值必须表示为字符串。这是由于它们如何被编码并附加到 URL。
Elysia 将自动将 query
上的适用 schema 强制转换为相应类型。
有关更多信息,请参阅 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 将将其强制转换为指定类型的数组。
默认情况下,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 | Validation |
---|---|
/id/1 | ✅ |
/id/a | ❌ |
路径参数 (不要与查询字符串或查询参数混淆)。
此字段通常不需要,因为 Elysia 可以自动从路径参数推断类型,除非需要特定值模式,例如数字值或模板字面量模式。
fetch('https://elysiajs.com/id/1')
如果未提供 params schema,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
。
这意味着标头可以具有任何键值对,但值必须匹配 schema。
HTTP 标头允许客户端和服务器在 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 是服务器发送给客户端的一小段数据。它是与每次访问同一 Web 服务器一起发送的数据,让服务器记住客户端信息。
简单来说,它是一个与每个请求一起发送的字符串化状态。
此字段通常用于强制执行某些特定 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: 'Something went wrong'
})
return {
name: 'Jane Doe'
}
}, {
response: {
200: t.Object({
name: t.String()
}),
400: t.Object({
error: t.String()
})
}
})
这是 Elysia 特定的功能,允许我们将字段设置为可选。
验证失败时,提供自定义错误消息有两种方式:
status
属性Elysia 提供额外的 error 属性,允许我们在字段无效时返回自定义错误消息。
import { Elysia, t } from 'elysia'
new Elysia()
.post('/', () => 'Hello World!', {
body: t.Object({
x: t.Number({
error: 'x must be a number'
})
})
})
.listen(3000)
以下是使用错误属性在各种类型上的示例:
TypeBox | Error |
typescript
|
|
typescript
|
|
typescript
|
|
typescript
|
|
TypeBox 提供额外的 "error" 属性,允许我们在字段无效时返回自定义错误消息。
TypeBox | Error |
typescript
|
|
typescript
|
|
除了字符串之外,Elysia 类型的 error 还可以接受一个函数,以编程方式为每个属性返回自定义错误。
错误函数接受与 ValidationError
相同的参数
import { Elysia, t } from 'elysia'
new Elysia()
.post('/', () => 'Hello World!', {
body: t.Object({
x: t.Number({
error() {
return 'Expected x to be a number'
}
})
})
})
.listen(3000)
TIP
将鼠标悬停在 error
上以查看类型。
请注意,错误函数仅在字段无效时才会被调用。
请考虑以下表格:
Code | Body | Error |
typescript
| json
| Expected x to be a number |
typescript
| json
| (default error, `t.Number.error` is not called) |
typescript
| json
| Expected value to be an object |
我们可以通过将错误代码缩小到 "VALIDATION" 来基于 onError 事件自定义验证行为。
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)
// Find a specific error name (path is OpenAPI Schema compliance)
const name = error.all.find(
(x) => x.summary && x.path === '/name'
)
// If there is a validation error, then log it
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'
// Maybe in a different file eg. 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
})
这种关注点分离的方法是一种有效的方法,但随着应用程序变得更加复杂,我们可能会发现自己重用多个模型与不同的控制器。
我们可以通过创建“引用模型”来解决这个问题,允许我们命名模型并通过使用 model
注册模型来直接在 schema
中使用自动完成引用它。
import { Elysia, t } from 'elysia'
const app = new Elysia()
.model({
sign: t.Object({
username: t.String(),
password: t.String()
})
})
.post('/sign-in', ({ body }) => body, {
// with auto-completion for existing model name
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()
})
})
然后在实例文件中:
// index.ts
import { Elysia } from 'elysia'
import { authModel } from './auth.model'
const app = new Elysia()
.use(authModel)
.post('/sign-in', ({ body }) => body, {
// with auto-completion for existing model name
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 提供了一个有主见的选项来帮助防止决策疲劳。
我们可以通过访问 static
属性来获取每个 Elysia/TypeBox 类型的类型定义,如下所示:
import { t } from 'elysia'
const MyType = t.Object({
hello: t.Literal('Elysia')
})
type MyType = typeof MyType.static
这允许 Elysia 自动推断并提供类型,从而减少声明重复 schema 的需求
单个 Elysia/TypeBox schema 可以用于:
这允许我们将 schema 作为 单一真相来源。