Elysia
2,454,631 reqs/sFastify
415,600
Measured in requests/second. Result from TechEmpower Benchmark Round 22 (2023-10-17) in PlainText
本指南面向希望看到 Fastify 之间差异的用户,包括语法,以及如何通过示例将应用程序从 Fastify 迁移到 Elysia。
Fastify 是一个快速且低开销的 Node.js 网络框架,旨在简单易用。它基于 HTTP 模块构建,提供了一组易于构建 Web 应用程序的功能。
Elysia 是一个符合人体工程学的 Web 框架,支持 Bun、Node.js 和 Web 标准 API 的运行时。旨在使开发人员友好,并专注于 良好的类型安全 和性能。
得益于本地 Bun 实现和静态代码分析,Elysia 相比 Fastify 有了显著的性能提升。
415,600
Measured in requests/second. Result from TechEmpower Benchmark Round 22 (2023-10-17) in PlainText
Fastify 和 Elysia 具有类似的路由语法,使用 app.get()
和 app.post()
方法定义路由,并使用类似的路径参数语法。
import fastify from 'fastify'
const app = fastify()
app.get('/', (request, reply) => {
res.send('Hello World')
})
app.post('/id/:id', (request, reply) => {
reply.status(201).send(req.params.id)
})
app.listen({ port: 3000 })
Fastify 使用
request
和reply
作为请求和响应对象
import { Elysia } from 'elysia'
const app = new Elysia()
.get('/', 'Hello World')
.post(
'/id/:id',
({ status, params: { id } }) => {
return status(201, id)
}
)
.listen(3000)
Elysia 使用单一的
context
并直接返回响应
在风格指南上有一点不同,Elysia 推荐使用方法链和对象解构。
如果您不需要使用上下文,Elysia 还支持响应的内联值。
两者都具有类似的属性,以访问输入参数,如 headers
、query
、params
和 body
,并自动将请求主体解析为 JSON、URL 编码数据和表单数据。
import fastify from 'fastify'
const app = fastify()
app.post('/user', (request, reply) => {
const limit = request.query.limit
const name = request.body.name
const auth = request.headers.authorization
reply.send({ limit, name, auth })
})
Fastify 解析数据并将其放入
request
对象中
import { Elysia } from 'elysia'
const app = new Elysia()
.post('/user', (ctx) => {
const limit = ctx.query.limit
const name = ctx.body.name
const auth = ctx.headers.authorization
return { limit, name, auth }
})
Elysia 解析数据并将其放入
context
对象中
Fastify 使用函数回调来定义子路由,而 Elysia 则将每个实例视为可以即插即用的组件。
import fastify, { FastifyPluginCallback } from 'fastify'
const subRouter: FastifyPluginCallback = (app, opts, done) => {
app.get('/user', (request, reply) => {
reply.send('Hello User')
})
}
const app = fastify()
app.register(subRouter, {
prefix: '/api'
})
Fastify 使用函数回调声明子路由
import { Elysia } from 'elysia'
const subRouter = new Elysia({ prefix: '/api' })
.get('/user', 'Hello User')
const app = new Elysia()
.use(subRouter)
Elysia 将每个实例视为一个组件
虽然 Elysia 在构造函数中设置前缀,但 Fastify 要求您在选项中设置前缀。
Elysia 内置支持请求验证,具有良好的类型安全,默认情况下使用 TypeBox,而 Fastify 使用 JSON Schema 声明模式,并使用 ajv 进行验证。
但是,不能自动推导类型,您需要使用类型提供程序,如 @fastify/type-provider-json-schema-to-ts
来推导类型。
import fastify from 'fastify'
import { JsonSchemaToTsProvider } from '@fastify/type-provider-json-schema-to-ts'
const app = fastify().withTypeProvider<JsonSchemaToTsProvider>()
app.patch(
'/user/:id',
{
schema: {
params: {
type: 'object',
properties: {
id: {
type: 'string',
pattern: '^[0-9]+$'
}
},
required: ['id']
},
body: {
type: 'object',
properties: {
name: { type: 'string' }
},
required: ['name']
},
}
},
(request, reply) => {
// 将字符串映射到数字
request.params.id = +request.params.id
reply.send({
params: request.params,
body: request.body
})
}
})
Fastify 使用 JSON Schema 进行验证
import { Elysia, t } from 'elysia'
const app = new Elysia()
.patch('/user/:id', ({ params, body }) => ({
params,
body
}),
{
params: t.Object({
id: t.Number()
}),
body: t.Object({
name: t.String()
})
})
Elysia 使用 TypeBox 进行验证,并自动强制转化类型
此外,Fastify 还可以使用 TypeBox 或 Zod 进行验证,使用 @fastify/type-provider-typebox
自动推导类型。
而 Elysia 更倾向于使用 TypeBox 进行验证,Elysia 还支持通过 TypeMap 的 Zod 和 Valibot。
Fastify 使用 fastify-multipart
处理文件上传,底层使用 Busboy
,而 Elysia 使用 Web 标准 API 处理表单数据,使用声明性 API 进行 mimetype 验证。
然而,Fastify 并没有提供一种简单的方法进行文件验证,例如,文件大小和 mimetype,需要一些变通方法来验证文件。
import fastify from 'fastify'
import multipart from '@fastify/multipart'
import { fileTypeFromBuffer } from 'file-type'
const app = fastify()
app.register(multipart, {
attachFieldsToBody: 'keyValues'
})
app.post(
'/upload',
{
schema: {
body: {
type: 'object',
properties: {
file: { type: 'object' }
},
required: ['file']
}
}
},
async (req, res) => {
const file = req.body.file
if (!file) return res.status(422).send('未上传文件')
const type = await fileTypeFromBuffer(file)
if (!type || !type.mime.startsWith('image/'))
return res.status(422).send('文件不是有效的图像')
res.header('Content-Type', type.mime)
res.send(file)
}
)
Fastify 使用
fastify-multipart
处理文件上传,假装type: object
以允许 Buffer
import { Elysia, t } from 'elysia'
const app = new Elysia()
.post('/upload', ({ body }) => body.file, {
body: t.Object({
file: t.File({
type: 'image'
})
})
})
Elysia 通过
t.File
处理文件和 mimetype 验证
由于 multer 不验证 mimetype,您需要使用 file-type 或类似库手动验证 mimetype。
而 Elysia 自动验证文件上传,使用 file-type 自动验证 mimetype。
Fastify 和 Elysia 都有一些类似的生命周期事件,采用基于事件的方法。
Elysia 的生命周期事件可以如下所示。
点击图片放大
Fastify 的生命周期事件可以如下所示。
输入请求
│
└─▶ 路由
│
└─▶ 实例记录器
│
4**/5** ◀─┴─▶ onRequest 钩子
│
4**/5** ◀─┴─▶ preParsing 钩子
│
4**/5** ◀─┴─▶ 解析
│
4**/5** ◀─┴─▶ preValidation 钩子
│
400 ◀─┴─▶ 验证
│
4**/5** ◀─┴─▶ preHandler 钩子
│
4**/5** ◀─┴─▶ 用户处理程序
│
└─▶ 回复
│
4**/5** ◀─┴─▶ preSerialization 钩子
│
└─▶ onSend 钩子
│
4**/5** ◀─┴─▶ 输出响应
│
└─▶ onResponse 钩子
两者在拦截请求和响应生命周期事件的语法上也相似,然而 Elysia 不需要您调用 done
来继续生命周期事件。
import fastify from 'fastify'
const app = fastify()
// 全局中间件
app.addHook('onRequest', (request, reply, done) => {
console.log(`${request.method} ${request.url}`)
done()
})
app.get(
'/protected',
{
// 路由特定中间件
preHandler(request, reply, done) {
const token = request.headers.authorization
if (!token) reply.status(401).send('未授权')
done()
}
},
(request, reply) => {
reply.send('受保护的路由')
}
)
Fastify 使用
addHook
注册中间件,并要求您调用done
继续生命周期事件
import { Elysia } from 'elysia'
const app = new Elysia()
// 全局中间件
.onRequest('/user', ({ method, path }) => {
console.log(`${method} ${path}`)
})
// 路由特定中间件
.get('/protected', () => 'protected', {
beforeHandle({ status, headers }) {
if (!headers.authorizaton)
return status(401)
}
})
Elysia 自动检测生命周期事件,并不需要您调用
done
来继续生命周期事件
Elysia 确保良好的类型安全。
例如,您可以使用 derive 和 resolve 以 安全的类型 方式自定义上下文,而 Fastify 则无法做到这一点。
import fastify from 'fastify'
const app = fastify()
app.decorateRequest('version', 2)
app.get('/version', (req, res) => {
res.send(req.version)Property 'version' does not exist on type 'FastifyRequest<RouteGenericInterface, Server<typeof IncomingMessage, typeof ServerResponse>, IncomingMessage, ... 4 more ..., ResolveFastifyRequestType<...>>'.})
app.get(
'/token',
{
preHandler(req, res, done) {
const token = req.headers.authorization
if (!token) return res.status(401).send('未授权')
// @ts-ignore
req.token = token.split(' ')[1]
done()
}
},
(req, res) => {
req.versionProperty 'version' does not exist on type 'FastifyRequest<RouteGenericInterface, Server<typeof IncomingMessage, typeof ServerResponse>, IncomingMessage, ... 4 more ..., ResolveFastifyRequestType<...>>'.
res.send(req.token)Property 'token' does not exist on type 'FastifyRequest<RouteGenericInterface, Server<typeof IncomingMessage, typeof ServerResponse>, IncomingMessage, ... 4 more ..., ResolveFastifyRequestType<...>>'. }
)
app.listen({
port: 3000
})
Fastify 使用
decorateRequest
,但没有提供良好的类型安全
import { Elysia } from 'elysia'
const app = new Elysia()
.decorate('version', 2)
.get('/version', ({ version }) => version)
.resolve(({ status, headers: { authorization } }) => {
if(!authorization?.startsWith('Bearer '))
return status(401)
return {
token: authorization.split(' ')[1]
}
})
.get('/token', ({ token, version }) => {
version
return token
})
Elysia 使用
decorate
扩展上下文,并使用resolve
将自定义属性添加到上下文中
虽然 Fastify 可以使用 declare module
扩展 FastifyRequest
接口,但它是全局可用的,并且没有良好的类型安全,也无法保证该属性在所有请求处理程序中都可用。
declare module 'fastify' {
interface FastifyRequest {
version: number
token: string
}
}
这是以上 Fastify 示例正常工作的必要条件,但并没有提供良好的类型安全
Fastify 使用函数返回 Fastify 插件定义命名中间件,而 Elysia 使用 macro 定义自定义钩子。
import fastify from 'fastify'
import type { FastifyRequest, FastifyReply } from 'fastify'
const app = fastify()
const role =
(role: 'user' | 'admin') =>
(request: FastifyRequest, reply: FastifyReply, next: Function) => {
const user = findUser(request.headers.authorization)
if (user.role !== role) return reply.status(401).send('未授权')
// @ts-ignore
request.user = user
next()
}
app.get(
'/token',
{
preHandler: role('admin')
},
(request, reply) => {
reply.send(request.user)Property 'user' does not exist on type 'FastifyRequest<RouteGenericInterface, Server<typeof IncomingMessage, typeof ServerResponse>, IncomingMessage, ... 4 more ..., ResolveFastifyRequestType<...>>'. }
)
Fastify 使用函数回调接受中间件的自定义参数
import { Elysia } from 'elysia'
const app = new Elysia()
.macro({
role: (role: 'user' | 'admin') => ({
resolve({ status, headers: { authorization } }) {
const user = findUser(authorization)
if(user.role !== role)
return status(401)
return {
user
}
}
})
})
.get('/token', ({ user }) => user, {
role: 'admin'
})
Elysia 使用宏传递自定义参数给自定义中间件
虽然 Fastify 使用函数回调,但它需要返回一个放置在事件处理程序中的函数或表示钩子的对象,这在需要多个自定义函数时可能很难处理,因为您需要将它们合并到一个对象中。
Fastify 和 Elysia 都提供生命周期事件来处理错误。
import fastify from 'fastify'
const app = fastify()
class CustomError extends Error {
constructor(message: string) {
super(message)
this.name = 'CustomError'
}
}
// 全局错误处理程序
app.setErrorHandler((error, request, reply) => {
if (error instanceof CustomError)
reply.status(500).send({
message: '出现问题!',
error
})
})
app.get(
'/error',
{
// 路由特定错误处理程序
errorHandler(error, request, reply) {
reply.send({
message: '仅此路由适用!',
error
})
}
},
(request, reply) => {
throw new CustomError('哦 uh')
}
)
Fastify 使用
setErrorHandler
作为全局错误处理程序,以及使用errorHandler
作为路由特定错误处理程序
import { Elysia } from 'elysia'
class CustomError extends Error {
// 可选:自定义 HTTP 状态码
status = 500
constructor(message: string) {
super(message)
this.name = 'CustomError'
}
// 可选:应将什么发送给客户端
toResponse() {
return {
message: "如果您看到此消息,我们的开发人员忘记处理此错误",
error: this
}
}
}
const app = new Elysia()
// 可选:注册自定义错误类
.error({
CUSTOM: CustomError,
})
// 全局错误处理程序
.onError(({ error, code }) => {
if(code === 'CUSTOM')
return {
message: '出现问题!',
error
}
})
.get('/error', () => {
throw new CustomError('哦 uh')
}, {
// 可选:路由特定错误处理程序
error({ error }) {
return {
message: '仅此路由适用!',
error
}
}
})
Elysia 提供自定义错误代码,简写 HTTP 状态和
toResponse
用于将错误映射到响应。
虽然两者都在生命周期事件中提供错误处理,但 Elysia 还提供:
toResponse
用于将错误映射到响应错误代码对日志记录和调试非常有用,并且在区分扩展相同类的不同错误类型时很重要。
Fastify 封装插件的副作用,而 Elysia 通过显式作用域机制和代码顺序控制插件的副作用。
import fastify from 'fastify'
import type { FastifyPluginCallback } from 'fastify'
const subRouter: FastifyPluginCallback = (app, opts, done) => {
app.addHook('preHandler', (request, reply) => {
if (!request.headers.authorization?.startsWith('Bearer '))
reply.code(401).send({ error: '未授权' })
})
done()
}
const app = fastify()
.get('/', (request, reply) => {
reply.send('Hello World')
})
.register(subRouter)
// 没有来自 subRouter 的副作用
.get('/side-effect', () => 'hi')
Fastify 封装插件的副作用
import { Elysia } from 'elysia'
const subRouter = new Elysia()
.onBeforeHandle(({ status, headers: { authorization } }) => {
if(!authorization?.startsWith('Bearer '))
return status(401)
})
const app = new Elysia()
.get('/', 'Hello World')
.use(subRouter)
// 没有来自 subRouter 的副作用
.get('/side-effect', () => 'hi')
Elysia 除非显式声明,否则不会有插件的副作用
两者都有插件的封装机制,以防止副作用。
然而,Elysia 可以显式声明哪个插件应该有副作用,通过声明一个作用域,而 Fastify 始终对此进行封装。
import { Elysia } from 'elysia'
const subRouter = new Elysia()
.onBeforeHandle(({ status, headers: { authorization } }) => {
if(!authorization?.startsWith('Bearer '))
return status(401)
})
// 仅限于父实例的作用域,但不超出
.as('scoped')
const app = new Elysia()
.get('/', 'Hello World')
.use(subRouter)
// 现在有来自 subRouter 的副作用
.get('/side-effect', () => 'hi')
Elysia 提供 3 种类型的作用域机制:
由于 Fastify 不提供作用域机制,我们需要:
然而,这可能导致在处理不当的情况下产生重复的副作用。
import fastify from 'fastify'
import type {
FastifyRequest,
FastifyReply,
FastifyPluginCallback
} from 'fastify'
const log = (request: FastifyRequest, reply: FastifyReply, done: Function) => {
console.log('中间件已执行')
done()
}
const app = fastify()
app.addHook('onRequest', log)
app.get('/main', (request, reply) => {
reply.send('来自主路由的问候!')
})
const subRouter: FastifyPluginCallback = (app, opts, done) => {
app.addHook('onRequest', log)
// 这将记录两次
app.get('/sub', (request, reply) => {
return reply.send('来自子路由的问候!')
})
done()
}
app.register(subRouter, {
prefix: '/sub'
})
app.listen({
port: 3000
})
在这个情况下,Elysia 提供了一个插件去重机制,以防止重复的副作用。
import { Elysia } from 'elysia'
const subRouter = new Elysia({ name: 'subRouter' })
.onBeforeHandle(({ status, headers: { authorization } }) => {
if(!authorization?.startsWith('Bearer '))
return status(401)
})
.as('scoped')
const app = new Elysia()
.get('/', 'Hello World')
.use(subRouter)
.use(subRouter)
.use(subRouter)
.use(subRouter)
// 副作用只调用一次
.get('/side-effect', () => 'hi')
通过使用唯一的 name
,Elysia 将插件仅应用一次,而不会导致重复的副作用。
Fastify 使用 @fastify/cookie
来解析 cookies,而 Elysia 则内置支持 cookies,使用基于信号的方法处理 cookies。
import fastify from 'fastify'
import cookie from '@fastify/cookie'
const app = fastify()
app.use(cookie, {
secret: 'secret',
hook: 'onRequest'
})
app.get('/', function (request, reply) {
request.unsignCookie(request.cookies.name)
reply.setCookie('name', 'value', {
path: '/',
signed: true
})
})
Fastify 使用
unsignCookie
验证 cookie 签名,使用setCookie
设置 cookie
import { Elysia } from 'elysia'
const app = new Elysia({
cookie: {
secret: 'secret'
}
})
.get('/', ({ cookie: { name } }) => {
// 签名验证自动处理
name.value
// cookie 签名自动签名
name.value = 'value'
name.maxAge = 1000 * 60 * 60 * 24
})
Elysia 使用基于信号的方法处理 cookies,签名验证自动处理
两者都提供基于 Swagger 的 OpenAPI 文档,但 Elysia 默认采用 Scalar UI,这是一种更现代且用户友好的 OpenAPI 文档界面。
import fastify from 'fastify'
import swagger from '@fastify/swagger'
const app = fastify()
app.register(swagger, {
openapi: '3.0.0',
info: {
title: '我的 API',
version: '1.0.0'
}
})
app.addSchema({
$id: 'user',
type: 'object',
properties: {
name: {
type: 'string',
description: '仅限名字'
},
age: { type: 'integer' }
},
required: ['name', 'age']
})
app.post(
'/users',
{
schema: {
summary: '创建用户',
body: {
$ref: 'user#'
},
response: {
'201': {
$ref: 'user#'
}
}
}
},
(req, res) => {
res.status(201).send(req.body)
}
)
await fastify.ready()
fastify.swagger()
Fastify 使用
@fastify/swagger
进行 OpenAPI 文档的生成
import { Elysia, t } from 'elysia'
import { swagger } from '@elysiajs/swagger'
const app = new Elysia()
.use(swagger())
.model({
user: t.Object({
name: t.String(),
age: t.Number()
})
})
.post('/users', ({ body }) => body, {
body: 'user[]',
response: {
201: 'user[]'
},
detail: {
summary: '创建用户'
}
})
Elysia 使用
@elysiajs/swagger
进行 OpenAPI 文档生成,默认使用 Scalar,也可以选择使用 Swagger
两者都提供使用 $ref
的模型引用以生成 OpenAPI 文档,然而 Fastify 不提供类型安全和为模型名称指定时的自动补全,而 Elysia 提供。
Fastify 内置支持测试,使用 fastify.inject()
模拟 网络请求,而 Elysia 使用 Web 标准 API 进行 实际 请求。
import fastify from 'fastify'
import request from 'supertest'
import { describe, it, expect } from 'vitest'
function build(opts = {}) {
const app = fastify(opts)
app.get('/', async function (request, reply) {
reply.send({ hello: 'world' })
})
return app
}
describe('GET /', () => {
it('应该返回 Hello World', async () => {
const app = build()
const response = await app.inject({
url: '/',
method: 'GET',
})
expect(res.status).toBe(200)
expect(res.text).toBe('Hello World')
})
})
Fastify 使用
fastify.inject()
模拟网络请求
import { Elysia } from 'elysia'
import { describe, it, expect } from 'vitest'
const app = new Elysia()
.get('/', 'Hello World')
describe('GET /', () => {
it('应该返回 Hello World', async () => {
const res = await app.handle(
new Request('http://localhost')
)
expect(res.status).toBe(200)
expect(await res.text()).toBe('Hello World')
})
})
Elysia 使用 Web 标准 API 处理 实际 请求
此外,Elysia 还提供了一个名为 Eden 的帮助库,提供端到端类型安全,允许我们在测试时获得自动补全和完全的类型安全。
import { Elysia } from 'elysia'
import { treaty } from '@elysiajs/eden'
import { describe, expect, it } from 'bun:test'
const app = new Elysia().get('/hello', 'Hello World')
const api = treaty(app)
describe('GET /', () => {
it('应该返回 Hello World', async () => {
const { data, error, status } = await api.hello.get()
expect(status).toBe(200)
expect(data).toBe('Hello World')
})
})
Elysia 提供内置支持 端到端类型安全,无需代码生成,Fastify 则没有此功能。
import { Elysia, t } from 'elysia'
import { treaty } from '@elysiajs/eden'
const app = new Elysia()
.post('/mirror', ({ body }) => body, {
body: t.Object({
message: t.String()
})
})
const api = treaty(app)
const { data, error } = await api.mirror.post({
message: 'Hello World'
})
if(error)
throw error
console.log(data)
如果端到端类型安全对您而言很重要,则 Elysia 是您的正确选择。
Elysia 提供了更符合人体工程学和开发人员友好的体验,专注于性能、类型安全和简单性,而 Fastify 是一个成熟的 Node.js 框架,但没有提供 良好的类型安全 和 端到端类型安全 。
如果您正在寻找一个易于使用、具有良好开发体验,并建立在 Web 标准 API 之上的框架,Elysia 是您的理想选择。
另外,如果您来自其他框架,您可以查看: