Skip to content
Our Sponsors
Open in Anthropic

验证

创建 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 模式。

Elysia 为服务器端验证量身定制 TypeBox 以实现无缝体验。

标准模式

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

要使用 Standard 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)

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

模式类型

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

响应应如下所示:

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

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

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

响应应列出如下:

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

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

Guard 模式类型

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

override (默认)

如果模式相互冲突,则覆盖模式。

Elysia 运行默认 override guard 显示模式被覆盖

standalone

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

Elysia 运行 standalone 将多个 guard 合并在一起

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

ts
import { Elysia } from 'elysia'

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

Body

传入的 HTTP Message 是发送到服务器的数据。它可以是 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 方法的主体附件。

规范

验证传入的 HTTP Message(或 body)。

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

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

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

文件

文件是一种特殊的 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 将自动假设内容类型为 multipart/form-data

文件 (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
/

规范

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

URL Object

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

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

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

强制转换

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

有关更多信息,请参阅 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 将将其强制转换为指定类型的数组。

默认情况下,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']

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

规范

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

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

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

参数类型推断

如果未提供参数模式,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

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

规范

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

规范

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

每个状态的响应

响应可以按状态码设置。

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 的特定功能,允许我们使字段可选。

错误提供程序

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

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

错误属性

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

自定义错误

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

错误消息作为函数

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

错误函数接受与 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 上以查看类型。

每个字段调用错误

请注意,如果字段无效,则只会调用错误函数。

请考虑以下表格:

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)

错误列表

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

引用模型

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

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

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

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

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 文档中。

多个模型

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 提供了一个有主见的选项来帮助防止决策疲劳。

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 模式

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