Elysia
2,454,631 reqs/sFastify
415,600
以请求/秒为单位测量。结果来自 TechEmpower 基准测试 第 22 轮 (2023-10-17) 的 PlainText 测试
本指南面向 Fastify 用户,旨在展示与 Fastify 的差异,包括语法差异,并通过示例说明如何将应用从 Fastify 迁移至 Elysia。
Fastify 是一个快速且低开销的 Node.js Web 框架,设计简洁易用。它构建在 HTTP 模块之上,提供了一系列功能,便于构建 Web 应用。
Elysia 是一个符合人体工程学的 Web 框架,适用于 Bun、Node.js 以及支持 Web 标准 API 的运行时。设计注重人体工程学和开发者友好性,重点关注健全的类型安全和性能。
得益于原生的 Bun 实现和静态代码分析,Elysia 相比 Fastify 有显著的性能提升。
415,600
以请求/秒为单位测量。结果来自 TechEmpower 基准测试 第 22 轮 (2023-10-17) 的 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 不会自动推断类型,你需要使用类型提供者如 @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()
})
})
import { Elysia } from 'elysia'
import { z } from 'zod'
const app = new Elysia()
.patch('/user/:id', ({ params, body }) => ({
params,
body
}),
{
params: z.object({
id: z.number()
}),
body: z.object({
name: z.string()
})
})
import { Elysia } from 'elysia'
import * as v from 'zod'
const app = new Elysia()
.patch('/user/:id', ({ params, body }) => ({
params,
body
}),
{
params: v.object({
id: v.number()
}),
body: v.object({
name: v.string()
})
})
Elysia 使用 TypeBox 进行验证,并自动强制类型转换。同时支持各种验证库,如 Zod、Valibot,语法相同。
或者,Fastify 也可以使用 TypeBox 或 Zod 进行验证,使用 @fastify/type-provider-typebox
自动推断类型。
虽然 Elysia 偏好 TypeBox 进行验证,但 Elysia 也支持标准模式,允许你开箱即用地使用 Zod、Valibot、ArkType、Effect Schema 等库。
Fastify 使用 fastify-multipart
处理文件上传,它在底层使用 Busboy
,而 Elysia 使用 Web 标准 API 处理表单数据,使用声明式 API 进行 MIME 类型验证。
然而,Fastify 没有提供直接的文件验证方式,例如文件大小和 MIME 类型,需要一些变通方法来验证文件。
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('No file uploaded')
const type = await fileTypeFromBuffer(file)
if (!type || !type.mime.startsWith('image/'))
return res.status(422).send('File is not a valid image')
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
处理文件和 MIME 类型验证
由于 multer 不验证 MIME 类型,你需要使用 file-type 或类似库手动验证 MIME 类型。
而 Elysia 验证文件上传,并使用 file-type 自动验证 MIME 类型。
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('Unauthorized')
done()
}
},
(request, reply) => {
reply.send('Protected route')
}
)
Fastify 使用
addHook
注册中间件,并要求调用done
以继续生命周期事件
import { Elysia } from 'elysia'
const app = new Elysia()
// 全局中间件
.onRequest(({ 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('Unauthorized')
// @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('Unauthorized')
// @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: 'Something went wrong!',
error
})
})
app.get(
'/error',
{
// 路由特定错误处理程序
errorHandler(error, request, reply) {
reply.send({
message: 'Only for this route!',
error
})
}
},
(request, reply) => {
throw new CustomError('oh 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: "If you're seeing this, our dev forgot to handle this error",
error: this
}
}
}
const app = new Elysia()
// 可选:注册自定义错误类
.error({
CUSTOM: CustomError,
})
// 全局错误处理程序
.onError(({ error, code }) => {
if(code === 'CUSTOM')
return {
message: 'Something went wrong!',
error
}
})
.get('/error', () => {
throw new CustomError('oh uh')
}, {
// 可选:路由特定错误处理程序
error({ error }) {
return {
message: 'Only for this route!',
error
}
}
})
Elysia 提供自定义错误代码、状态码的简写以及
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: 'Unauthorized' })
})
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('Middleware executed')
done()
}
const app = fastify()
app.addHook('onRequest', log)
app.get('/main', (request, reply) => {
reply.send('Hello from main!')
})
const subRouter: FastifyPluginCallback = (app, opts, done) => {
app.addHook('onRequest', log)
// 这会记录两次
app.get('/sub', (request, reply) => {
return reply.send('Hello from sub router!')
})
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
解析 cookie,而 Elysia 内置支持 cookie,并使用基于信号的方法处理 cookie。
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 使用基于信号的方法处理 cookie,签名验证自动处理
两者都提供使用 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: 'My API',
version: '1.0.0'
}
})
app.addSchema({
$id: 'user',
type: 'object',
properties: {
name: {
type: 'string',
description: 'First name only'
},
age: { type: 'integer' }
},
required: ['name', 'age']
})
app.post(
'/users',
{
schema: {
summary: 'Create user',
body: {
$ref: 'user#'
},
response: {
'201': {
$ref: 'user#'
}
}
}
},
(req, res) => {
res.status(201).send(req.body)
}
)
await fastify.ready()
fastify.swagger()
Fastify 使用
@fastify/swagger
进行 OpenAPI 文档,使用 Swagger
import { Elysia, t } from 'elysia'
import { openapi } from '@elysiajs/openapi'
const app = new Elysia()
.use(openapi())
.model({
user: t.Array(
t.Object({
name: t.String(),
age: t.Number()
})
)
})
.post('/users', ({ body }) => body, {
body: 'user',
response: {
201: 'user'
},
detail: {
summary: 'Create user'
}
})
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('should return 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('should return 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('should return Hello World', async () => {
const { data, error, status } = await api.hello.get()
expect(status).toBe(200)
expect(data).toBe('Hello World')
})
})
Elysia 内置支持端到端类型安全,无需代码生成,使用 Eden,而 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 是你的正确选择。
或者,如果你来自不同的框架,可以查看: