Skip to content

验证

创建 API 服务器的目的在于接收输入并对其进行处理。

JavaScript 允许任何数据成为任何类型。Elysia 提供了一个工具,可以对数据进行验证,以确保数据的格式正确。

typescript
import { 
Elysia
,
t
} from 'elysia'
new
Elysia
()
.
get
('/id/:id', ({
params
: {
id
} }) =>
id
, {
params
:
t
.
Object
({
id
:
t
.
Number
()
}) }) .
listen
(3000)

TypeBox

Elysia.t 是基于 TypeBox 的模式构建器,提供了运行时、编译时和 OpenAPI 模式的类型安全,支持自动生成 OpenAPI 文档。

TypeBox 是一个极快、轻量且类型安全的 TypeScript 运行时验证库。Elysia 对 TypeBox 的默认行为进行了扩展和定制,以适应服务器端的验证需求。

我们相信验证至少应由框架原生处理,而不是依赖用户为每个项目设置自定义类型。

标准 Schema

Elysia 也支持 Standard Schema,允许您使用喜欢的验证库:

使用 Standard Schema,只需导入相应的 schema 并传递给路由处理器。

typescript
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)

您可以在同一处理器内无缝使用多种验证器。

TypeScript

我们可以通过访问 static 属性来获取每个 Elysia/TypeBox 类型的类型定义,如下所示:

ts
import { 
t
} from 'elysia'
const
MyType
=
t
.
Object
({
hello
:
t
.
Literal
('Elysia')
}) type
MyType
= typeof
MyType
.
static



这使得 Elysia 能够自动推断和提供类型,减少重复声明模式的需求。

一个单一的 Elysia/TypeBox 模式可以用于:

  • 运行时验证
  • 数据强制转换
  • TypeScript 类型
  • OpenAPI 模式

这使我们能够将模式作为 单一真实来源

模式类型

Elysia 支持以下类型的声明式模式:


这些属性应作为路由处理器的第三个参数提供,用以验证传入请求。

typescript
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)
localhost

GET

响应示例:

URL查询参数
/id/a
/id/1?name=Elysia
/id/1?alias=Elysia
/id/a?name=Elysia
/id/a?alias=Elysia

当提供了模式时,类型将自动从模式推断,并生成 OpenAPI 类型用于 API 文档,省去了手动提供类型的重复工作。

Guard

Guard 可用于将模式应用于多个处理器。

typescript
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 属性。响应示例如下:

localhost

GET

响应结果:

路径响应
/nonehi
/none?name=ahi
/queryerror
/query?name=aa

如果为同一属性定义了多个全局模式,则最后一个生效。如果同时定义本地与全局模式,则本地优先。

Guard Schema 类型

Guard 支持两种验证模式定义类型。

覆盖(默认)

模式之间冲突时,后者覆盖前者。

Elysia 默认覆盖模式运行示意

独立

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

Elysia 独立运行多个守护合并示意

通过使用 schema 属性定义守护的模式类型:

ts
import { Elysia } from 'elysia'

new Elysia()
	.guard({
		schema: 'standalone', 
		response: t.Object({
			title: t.String()
		})
	})

主体

传入的 HTTP 消息 是发送到服务器的数据,可以是 JSON、表单数据或其它任意格式。

typescript
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 默认禁用 GETHEAD 请求的 body 解析,遵循 HTTP/1.1 规范 RFC2616

如果请求方法不包含实体主体的定义语义,则应忽略消息主体。

大部分浏览器默认禁用在 GETHEAD 方法下附加主体。

规格

验证传入的 HTTP 消息(也就是主体)。

这些消息供 Web 服务器处理的附加信息。

主体对应于 fetch API 中的 body。内容类型应根据定义的主体类型相应设置。

typescript
fetch('https://elysiajs.com', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
        name: 'Elysia'
    })
})

文件

文件是特殊的主体类型,用于文件上传。

typescript
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

File(标准 Schema)

如果您使用标准 Schema,需注意 Elysia 无法像 t.File 那样自动验证内容类型。

但 Elysia 导出了一个 fileType 函数,可用来通过魔数(magic number)验证文件类型。

typescript
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

typescript
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 的一部分,以 ? 开头,由一个或多个键值对构成,用于向服务器传递额外信息,通常用于自定义行为,如过滤或搜索。

URL 对象

查询参数紧跟于 Fetch API 中请求的 ? 处。

typescript
fetch('https://elysiajs.com/?name=Elysia')

指定查询参数时,所有参数值必须表示为字符串,因为它们经过编码并附加到 URL。

强制转换

Elysia 会自动将查询中的值强制转换为模式所需的类型。

更多信息请参考 Elysia 行为

ts
import { 
Elysia
,
t
} from 'elysia'
new
Elysia
()
.
get
('/', ({
query
}) =>
query
, {
query
:
t
.
Object
({
name
:
t
.
Number
()
}) }) .
listen
(3000)
localhost

GET

1

数组

默认情况下,Elysia 将查询参数视为单个字符串,即使同一键被多次指定。

若要使用数组,需明确声明数组类型。

ts
import { 
Elysia
,
t
} from 'elysia'
new
Elysia
()
.
get
('/', ({
query
}) =>
query
, {
query
:
t
.
Object
({
name
:
t
.
Array
(
t
.
String
())
}) }) .
listen
(3000)
localhost

GET

{ "name": [ "rapi", "anis", "neon" ], "squad": "counter" }

一旦 Elysia 识别某属性为数组,将自动把它强制转换为指定类型的数组。

默认情况下,Elysia 支持下列查询数组格式:

nuqs

该格式由 nuqs 使用。

通过使用 , 作为分隔符,属性被解析为数组。

http://localhost?name=rapi,anis,neon&squad=counter
{
	name: ['rapi', 'anis', 'neon'],
	squad: 'counter'
}

HTML 表单格式

当同一个键被多次赋值时,该键被视为数组。

这与 HTML 表单格式相同,当相同名称的输入元素多次出现时。

http://localhost?name=rapi&name=anis&name=neon&squad=counter
// name: ['rapi', 'anis', 'neon']

参数

参数,或者说路径参数,是通过 URL 路径传递的数据,形式为 /key

typescript
import { 
Elysia
,
t
} from 'elysia'
new
Elysia
()
.
get
('/id/:id', ({
params
}) =>
params
, {
params
:
t
.
Object
({
id
:
t
.
Number
()
}) })
localhost

GET

参数必须以对象形式提供。

验证示例:

URL验证
/id/1
/id/a

规格

路径参数 (区别于查询字符串或查询参数)

通常无需额外声明此字段,Elysia 能自动推断路径参数类型,除非需要特定的值模式,例如数字或模板字面量。

typescript
fetch('https://elysiajs.com/id/1')

参数类型推断

如果未提供参数模式,Elysia 会自动将类型推断为字符串。

typescript
import { 
Elysia
,
t
} from 'elysia'
new
Elysia
()
.
get
('/id/:id', ({
params
}) =>
params
)

头部

头部是通过请求头部发送的额外数据。

typescript
import { 
Elysia
,
t
} from 'elysia'
new
Elysia
()
.
get
('/headers', ({
headers
}) =>
headers
, {
headers
:
t
.
Object
({
authorization
:
t
.
String
()
}) })

不同于其他类型,头部的 additionalProperties 默认允许为 true

这意味着头部可以包含任意键值对,但其值必须符合模式。

规格

HTTP 头部允许客户端和服务器传递附加信息,通常作为元数据处理。

此字段常用于强制某些特定头部字段,如 Authorization

头部的提供与 fetch API 中的 body 一致。

typescript
fetch('https://elysiajs.com/', {
    headers: {
        authorization: 'Bearer 12345'
    }
})

TIP

Elysia 仅以小写键名解析头部。

请确认在使用头部验证时,字段名称为小写。

Cookie 是通过请求的 Cookie 发送的数据。

typescript
import { 
Elysia
,
t
} from 'elysia'
new
Elysia
()
.
get
('/cookie', ({
cookie
}) =>
cookie
, {
cookie
:
t
.
Cookie
({
cookieName
:
t
.
String
()
}) })

Cookie 必须由 t.Cookiet.Object 形式定义。

headers类似,cookie 的 additionalProperties 默认设为 true

规格

HTTP Cookie 是服务器发送给客户端的小数据块,它会在每次访问相同网页服务器时自动发送,使服务器能记住客户端信息。

简单来说,Cookie 是每个请求中附带的字符串化状态。

此字段常用于强制某些特定 Cookie 字段。

Cookie 是特殊的请求头字段,Fetch API 不允许自定义值,需由浏览器管理。发送 Cookie 需设置 credentials 字段:

typescript
fetch('https://elysiajs.com/', {
    credentials: 'include'
})

t.Cookie 是特殊类型,类似于 t.Object,但支持设置 Cookie 特定选项。

typescript
import { 
Elysia
,
t
} from 'elysia'
new
Elysia
()
.
get
('/cookie', ({
cookie
}) =>
cookie
.
name
.
value
, {
cookie
:
t
.
Cookie
({
name
:
t
.
String
()
}, {
secure
: true,
httpOnly
: true
}) })

响应

响应是处理器返回的数据。

typescript
import { Elysia, t } from 'elysia'

new Elysia()
	.get('/response', () => {
		return {
			name: 'Jane Doe'
		}
	}, {
		response: t.Object({
			name: t.String()
		})
	})

按状态设置响应

响应可以按状态码定义。

typescript
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 特有功能,允许设置可选字段。

错误提供器

验证失败时,有两种方式提供自定义错误消息:

  1. 内联设置 error 属性
  2. 通过 onError 事件

错误属性

Elysia 提供额外的 error 属性,允许为字段无效时返回自定义错误消息。

typescript
import { Elysia, t } from 'elysia'

new Elysia()
    .post('/', () => 'Hello World!', {
        body: t.Object({
            x: t.Number({
                error: 'x 必须是一个数字'
            })
        })
    })
    .listen(3000)

以下示例展示不同类型上使用错误属性:

TypeBox 类型错误信息
typescript
t.String({
    format: 'email',
    error: '无效的电子邮件 :('
})
无效的电子邮件 :(
typescript
t.Array(
    t.String(),
    {
        error: '所有成员必须是一个字符串'
    }
)
所有成员必须是一个字符串
typescript
t.Object({
    x: t.Number()
}, {
    error: 'Invalid object UnU'
})
Invalid object UnU
typescript
t.Object({
    x: t.Number({
        error({ errors, type, validation, value }) {
            return '期望 x 为数字'
        }
    })
})
期望 x 为数字

自定义错误

TypeBox 提供额外的 "error" 属性,允许字段无效时返回自定义错误消息。

TypeBox 类型错误信息
typescript
t.String({
    format: 'email',
    error: '无效的电子邮箱 :( '
})
无效的电子邮箱 :(
typescript
t.Object({
    x: t.Number()
}, {
    error: 'Invalid object UnU'
})
Invalid object UnU

错误消息为函数

除了字符串外,Elysia 类型的 error 属性也可接收函数,为每个属性动态返回自定义错误信息。

错误函数接收参数与 ValidationError 相同。

typescript
import { Elysia, t } from 'elysia'

new Elysia()
    .post('/', () => 'Hello World!', {
        body: t.Object({
            x: t.Number({
                error() {
                    return '期望 x 为数字'
                }
            })
        })
    })
    .listen(3000)

TIP

鼠标悬停 error 可查看类型

错误按字段调用

注意,错误函数仅在对应字段无效时被调用。

参考下表:

代码主体错误消息
typescript
t.Object({
    x: t.Number({
        error() {
            return '期望 x 为数字'
        }
    })
})
json
{
    x: "hello"
}
期望 x 为数字
typescript
t.Object({
    x: t.Number({
        error() {
            return '期望 x 为数字'
        }
    })
})
json
"hello"
(默认错误,`t.Number.error` 不被调用)
typescript
t.Object(
    {
        x: t.Number({
            error() {
                return '期望 x 为数字'
            }
        })
    }, {
        error() {
            return '期望值为对象'
        }
    }
)
json
"hello"
期望值为对象

onError

我们可以通过 onError 事件自定义验证行为,捕获错误代码 "VALIDATION"。

typescript
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 功能直接交互。

typescript
import { Elysia, t } from 'elysia'

new Elysia()
    .onError(({ code, error }) => {
        if (code === 'VALIDATION')
            return error.all[0].message
    })
    .listen(3000)

错误列表

ValidationError 提供方法 ValidatorError.all,允许列出所有错误原因。

typescript
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

引用模型

有时我们会声明重复模型,或多次复用相同模型。

通过引用模型,我们可以为模型命名,并通过名称在不同处引用。

先看一个简单场景:

假设有用于登录的控制器,使用同一模型。

typescript
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
()
}) })

我们可以通过将模型提取成变量进行重构,并引用这些变量。

typescript
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 注册模型。

typescript
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 定义成插件,注册时提供一组模型,避免多次导入。

typescript
// auth.model.ts
import { Elysia, t } from 'elysia'

export const authModel = new Elysia()
    .model({
        sign: t.Object({
            username: t.String(),
            password: t.String()
        })
    })

然后在主程序中:

typescript
// @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 接受一个对象,键为模型名称,值为模型定义,支持多个模型。

typescript
// 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,并在模型名前加前缀作为命名空间。

typescript
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 也提供洞察选项,帮助您避免决策疲劳。