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 的 schema 构建器,提供运行时、编译时和 OpenAPI schema 生成的类型安全性。

Elysia 扩展并自定义了 TypeBox 的默认行为,以匹配服务器端验证需求。

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

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

您可以在同一个处理器中使用任何验证器,而不会出现问题。

Schema 类型

Elysia 支持以下类型的声明性 schema:


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

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)

响应应如下所示:

URLQueryParams
/id/a
/id/1?name=Elysia
/id/1?alias=Elysia
/id/a?name=Elysia
/id/a?alias=Elysia

当提供 schema 时,类型将自动从 schema 推断,并为 API 文档生成 OpenAPI 类型,从而消除手动提供类型的冗余任务。

Guard

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

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

响应应列出如下:

PathResponse
/nonehi
/none?name=ahi
/queryerror
/query?name=aa

如果为同一属性定义了多个全局 schema,则最新的一个将优先。如果同时定义了本地和全局 schema,则本地 schema 将优先。

Guard Schema 类型

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

override (默认)

如果 schema 之间发生冲突,则覆盖 schema。

Elysia run with default override guard showing schema gets override

standalone

分离冲突的 schema,并独立运行两者,从而实现两者的验证。

Elysia run with standalone merging multiple guard together

要使用 schema 定义 guard 的 schema 类型:

ts
import { Elysia } from 'elysia'

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

Body

传入的 HTTP 消息 是发送到服务器的数据。它可以是 JSON、form-data 或任何其他格式。

typescript
import { 
Elysia
,
t
} from 'elysia'
new
Elysia
()
.
post
('/body', ({
body
}) =>
body
, {
body
:
t
.
Object
({
name
:
t
.
String
()
}) }) .
listen
(3000)

验证应如下所示:

BodyValidation
{ name: 'Elysia' }
{ name: 1 }
{ alias: 'Elysia' }
undefined

Elysia 默认禁用 GETHEAD 消息的 body-parser,遵循 HTTP/1.1 RFC2616 的规范

如果请求方法没有为实体体定义明确的语义,则在处理请求时应忽略消息体。

大多数浏览器默认禁用 GETHEAD 方法的 body 附件。

Specs

验证传入的 HTTP 消息(或 body)。

这些消息是 Web 服务器处理的其他附加消息。

body 以与 fetch API 中的 body 相同的方式提供。内容类型应根据定义的 body 相应设置。

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

File

文件是一种特殊的 body 类型,可用于上传文件。

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 将自动假设 content-type 为 multipart/form-data

File (Standard Schema)

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

但是 Elysia 导出了一个 fileType,可以使用魔术数字来验证文件类型。

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 来验证文件类型,这非常重要,因为大多数验证器实际上无法正确验证文件,例如检查内容类型及其值,这可能导致安全漏洞。

Query

查询是通过 URL 发送的数据。它可以是 ?key=value 的形式。

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

查询必须以对象形式提供。

验证应如下所示:

QueryValidation
/?name=Elysia
/?name=1
/?alias=Elysia
/?name=ElysiaJS&alias=Elysia
/

Specs

查询字符串是 URL 的一个部分,以 ? 开头,可以包含一个或多个查询参数,这些是键值对,用于向服务器传递额外信息,通常用于自定义行为,如过滤或搜索。

URL Object

查询在 Fetch API 中在 ? 之后提供。

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

在指定查询参数时,重要的是要理解所有查询参数值必须表示为字符串。这是由于它们如何被编码并附加到 URL。

Coercion

Elysia 将自动将 query 上的适用 schema 强制转换为相应类型。

有关更多信息,请参阅 Elysia 行为

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

GET

1

Array

默认情况下,即使指定多次,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 将将其强制转换为指定类型的数组。

默认情况下,Elysia 使用以下格式格式化查询数组:

nuqs

此格式由 nuqs 使用。

使用 , 作为分隔符,属性将被视为数组。

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

HTML form format

如果键被指定多次,则键将被视为数组。

这类似于 HTML 表单格式,当具有相同名称的输入被指定多次时。

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

Params

参数或路径参数是通过 URL 路径发送的数据。

它们可以是 /key 的形式。

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

GET

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

验证应如下所示:

URLValidation
/id/1
/id/a

Specs

路径参数 (不要与查询字符串或查询参数混淆)

此字段通常不需要,因为 Elysia 可以自动从路径参数推断类型,除非需要特定值模式,例如数字值或模板字面量模式。

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

Params type inference

如果未提供 params schema,Elysia 将自动推断类型为字符串。

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

Headers

标头是通过请求的标头发送的数据。

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

与其他类型不同,标头默认设置 additionalPropertiestrue

这意味着标头可以具有任何键值对,但值必须匹配 schema。

Specs

HTTP 标头允许客户端和服务器在 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 默认设置 additionalPropertiestrue

Specs

HTTP Cookie 是服务器发送给客户端的一小段数据。它是与每次访问同一 Web 服务器一起发送的数据,让服务器记住客户端信息。

简单来说,它是一个与每个请求一起发送的字符串化状态。

此字段通常用于强制执行某些特定 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
}) })

Response

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

typescript
import { Elysia, t } from 'elysia'

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

Response per status

响应可以按状态码设置。

typescript
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 特定的功能,允许我们将字段设置为可选。

Error Provider

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

  1. 内联 status 属性
  2. 使用 onError 事件

Error Property

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

typescript
import { Elysia, t } from 'elysia'

new Elysia()
    .post('/', () => 'Hello World!', {
        body: t.Object({
            x: t.Number({
               	error: 'x must be a number'
            })
        })
    })
    .listen(3000)

以下是使用错误属性在各种类型上的示例:

TypeBoxError
typescript
t.String({
    format: 'email',
    error: 'Invalid email :('
})
Invalid Email :(
typescript
t.Array(
    t.String(),
    {
        error: 'All members must be a string'
    }
)
All members must be a string
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 'Expected x to be a number'
        }
    })
})
Expected x to be a number

Custom Error

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

TypeBoxError
typescript
t.String({
    format: 'email',
    error: 'Invalid email :('
})
Invalid Email :(
typescript
t.Object({
    x: t.Number()
}, {
    error: 'Invalid object UnU'
})
Invalid object UnU

Error message as function

除了字符串之外,Elysia 类型的 error 还可以接受一个函数,以编程方式为每个属性返回自定义错误。

错误函数接受与 ValidationError 相同的参数

typescript
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 上以查看类型。

Error is Called Per Field

请注意,错误函数仅在字段无效时才会被调用。

请考虑以下表格:

CodeBodyError
typescript
t.Object({
    x: t.Number({
        error() {
            return 'Expected x to be a number'
        }
    })
})
json
{
    x: "hello"
}
Expected x to be a number
typescript
t.Object({
    x: t.Number({
        error() {
            return 'Expected x to be a number'
        }
    })
})
json
"hello"
(default error, `t.Number.error` is not called)
typescript
t.Object(
    {
        x: t.Number({
            error() {
                return 'Expected x to be a number'
            }
        })
    }, {
        error() {
            return 'Expected value to be an object'
        }
    }
)
json
"hello"
Expected value to be an object

onError

我们可以通过将错误代码缩小到 "VALIDATION" 来基于 onError 事件自定义验证行为。

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)

Error List

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)

                    // 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

Reference Model

有时您可能会发现自己声明重复的模型或多次重用相同的模型。

使用引用模型,我们可以命名我们的模型并通过引用名称重用它。

让我们从一个简单场景开始。

假设我们有一个使用相同模型处理登录的控制器。

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'
// 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 中使用自动完成引用它。

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
, {
// with auto-completion for existing model name
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
// 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 文档中。

Multiple Models

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

Naming Convention

重复的模型名称将导致 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 提供了一个有主见的选项来帮助防止决策疲劳。

TypeScript

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

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



这允许 Elysia 自动推断并提供类型,从而减少声明重复 schema 的需求

单个 Elysia/TypeBox schema 可以用于:

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

这允许我们将 schema 作为 单一真相来源