Elysia
2,454,631 reqs/sExpress
113,117
以请求/秒为单位测量。结果来自 TechEmpower Benchmark 第 22 轮(2023-10-17)在 PlainText 中
本指南面向 Express 用户,旨在展示 Express 与 Elysia 的语法差异,并通过示例说明如何将应用从 Express 迁移至 Elysia。
Express 是 Node.js 中流行的 Web 框架,广泛用于构建 Web 应用和 API。它以简洁性和灵活性著称。
Elysia 是为 Bun、Node.js 及支持 Web 标准 API 的运行时而设计的人体工学 Web 框架。注重健全的类型安全和性能,旨在提供符合人体工学的开发者友好体验。
得益于原生 Bun 实现和静态代码分析,Elysia 相比 Express 有显著的性能提升。
113,117
以请求/秒为单位测量。结果来自 TechEmpower Benchmark 第 22 轮(2023-10-17)在 PlainText 中
Express 和 Elysia 拥有相似的路由语法,使用 app.get()
和 app.post()
方法定义路由,以及相似的路径参数语法。
import express from 'express'
const app = express()
app.get('/', (req, res) => {
res.send('Hello World')
})
app.post('/id/:id', (req, res) => {
res.status(201).send(req.params.id)
})
app.listen(3000)
Express 使用
req
和res
作为请求和响应对象
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
。
import express from 'express'
const app = express()
app.use(express.json())
app.post('/user', (req, res) => {
const limit = req.query.limit
const name = req.body.name
const auth = req.headers.authorization
res.json({ limit, name, auth })
})
Express 需要
express.json()
中间件来解析 JSON 请求体
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 默认解析 JSON、URL 编码数据和表单数据
Express 使用专用的 express.Router()
来声明子路由,而 Elysia 将每个实例视为可即插即用的组件。
import express from 'express'
const subRouter = express.Router()
subRouter.get('/user', (req, res) => {
res.send('Hello User')
})
const app = express()
app.use('/api', subRouter)
Express 使用
express.Router()
创建子路由
import { Elysia } from 'elysia'
const subRouter = new Elysia({ prefix: '/api' })
.get('/user', 'Hello User')
const app = new Elysia()
.use(subRouter)
Elysia 将每个实例视为组件
Elysia 内置了对使用 TypeBox 进行请求验证的支持,提供健全的类型安全,并支持开箱即用的标准模式,允许你使用喜欢的库如 Zod、Valibot、ArkType、Effect Schema 等。而 Express 不提供内置验证,需要根据每个验证库手动声明类型。
import express from 'express'
import { z } from 'zod'
const app = express()
app.use(express.json())
const paramSchema = z.object({
id: z.coerce.number()
})
const bodySchema = z.object({
name: z.string()
})
app.patch('/user/:id', (req, res) => {
const params = paramSchema.safeParse(req.params)
if (!params.success)
return res.status(422).json(result.error)
const body = bodySchema.safeParse(req.body)
if (!body.success)
return res.status(422).json(result.error)
res.json({
params: params.id.data,
body: body.data
})
})
Express 需要外部验证库如
zod
或joi
来验证请求体
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 'valibot'
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,语法相同。
Express 使用外部库 multer
处理文件上传,而 Elysia 内置了对文件和表单数据的支持,使用声明式 API 进行 MIME 类型验证。
import express from 'express'
import multer from 'multer'
import { fileTypeFromFile } from 'file-type'
import path from 'path'
const app = express()
const upload = multer({ dest: 'uploads/' })
app.post('/upload', upload.single('image'), async (req, res) => {
const file = req.file
if (!file)
return res
.status(422)
.send('No file uploaded')
const type = await fileTypeFromFile(file.path)
if (!type || !type.mime.startsWith('image/'))
return res
.status(422)
.send('File is not a valid image')
const filePath = path.resolve(file.path)
res.sendFile(filePath)
})
Express 需要
express.json()
中间件来解析 JSON 请求体
import { Elysia, t } from 'elysia'
const app = new Elysia()
.post('/upload', ({ body }) => body.file, {
body: t.Object({
file: t.File({
type: 'image'
})
})
})
Elysia 以声明式方式处理文件和 MIME 类型验证
由于 multer 不验证 MIME 类型,你需要使用 file-type 或类似库手动验证 MIME 类型。
Elysia 验证文件上传,并使用 file-type 自动验证 MIME 类型。
Express 中间件使用单一基于队列的顺序,而 Elysia 通过基于事件的生命周期提供更细粒度的控制。
Elysia 的生命周期事件可图示如下:
点击图片放大
虽然 Express 拥有单一顺序的请求管道流,但 Elysia 可以拦截请求管道中的每个事件。
import express from 'express'
const app = express()
// 全局中间件
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`)
next()
})
app.get(
'/protected',
// 路由特定中间件
(req, res, next) => {
const token = req.headers.authorization
if (!token)
return res.status(401).send('Unauthorized')
next()
},
(req, res) => {
res.send('Protected route')
}
)
Express 使用单一基于队列的顺序执行中间件
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 为请求管道中的每个点使用特定事件拦截器
虽然 Hono 有 next
函数来调用下一个中间件,但 Elysia 没有。
Elysia 设计为具备健全的类型安全。
例如,你可以使用 derive 和 resolve 以类型安全的方式自定义上下文,而 Express 则不行。
import express from 'express'
import type { Request, Response } from 'express'
const app = express()
const getVersion = (req: Request, res: Response, next: Function) => {
// @ts-ignore
req.version = 2
next()
}
app.get('/version', getVersion, (req, res) => {
res.send(req.version)Property 'version' does not exist on type 'Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>'.})
const authenticate = (req: Request, res: Response, next: Function) => {
const token = req.headers.authorization
if (!token)
return res.status(401).send('Unauthorized')
// @ts-ignore
req.token = token.split(' ')[1]
next()
}
app.get('/token', getVersion, authenticate, (req, res) => {
req.versionProperty 'version' does not exist on type 'Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>'.
res.send(req.token)Property 'token' does not exist on type 'Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>'.})
Express 使用单一基于队列的顺序执行中间件
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 为请求管道中的每个点使用特定事件拦截器
虽然 Express 可以使用 declare module
扩展 Request
接口,但它是全局可用的,不具备健全的类型安全,并且不保证该属性在所有请求处理程序中可用。
declare module 'express' {
interface Request {
version: number
token: string
}
}
这是上述 Express 示例工作所必需的,但它不提供健全的类型安全
Express 使用函数返回插件来定义可重用的路由特定中间件,而 Elysia 使用 macro 定义自定义钩子。
import express from 'express'
import type { Request, Response } from 'express'
const app = express()
const role = (role: 'user' | 'admin') =>
(req: Request, res: Response, next: Function) => {
const user = findUser(req.headers.authorization)
if (user.role !== role)
return res.status(401).send('Unauthorized')
// @ts-ignore
req.user = user
next()
}
app.get('/token', role('admin'), (req, res) => {
res.send(req.user)Property 'user' does not exist on type 'Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>'.})
Express 使用函数回调接受中间件的自定义参数
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 使用宏将自定义参数传递给自定义中间件
Express 对所有路由使用单一错误处理程序,而 Elysia 提供更细粒度的错误控制。
import express from 'express'
const app = express()
class CustomError extends Error {
constructor(message: string) {
super(message)
this.name = 'CustomError'
}
}
// 全局错误处理程序
app.use((error, req, res, next) => {
if(error instanceof CustomError) {
res.status(500).json({
message: 'Something went wrong!',
error
})
}
})
// 路由特定错误处理程序
app.get('/error', (req, res) => {
throw new CustomError('oh uh')
})
Express 使用中间件处理错误,对所有路由使用单一错误处理程序
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 提供更细粒度的错误控制和作用域机制
虽然 Express 提供使用中间件的错误处理,但 Elysia 提供:
toResponse
的简写方式,用于将错误映射到响应错误代码对日志记录和调试非常有用,并且在区分扩展自同一类的不同错误类型时非常重要。
Express 中间件是全局注册的,而 Elysia 通过显式的作用域机制和代码顺序控制插件的副作用。
import express from 'express'
const app = express()
app.get('/', (req, res) => {
res.send('Hello World')
})
const subRouter = express.Router()
subRouter.use((req, res, next) => {
const token = req.headers.authorization
if (!token)
return res.status(401).send('Unauthorized')
next()
})
app.use(subRouter)
// 受 subRouter 的副作用影响
app.get('/side-effect', (req, res) => {
res.send('hi')
})
Express 不处理中间件的副作用,需要前缀来分隔副作用
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 会将生命周期事件和上下文封装到所使用的实例中,因此插件的副作用不会影响父实例,除非明确声明。
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 种作用域机制:
虽然 Express 可以通过添加前缀来限定中间件的副作用范围,但这并不是真正的封装。副作用仍然存在,但被分隔到任何以该前缀开头的路由中,增加了开发者记忆哪些前缀有副作用的心智负担。
你可以执行以下操作:
这可能导致调试噩梦,因为 Express 不提供真正的封装。
Express 使用外部库 cookie-parser
解析 cookie,而 Elysia 内置了对 cookie 的支持,并使用基于信号的方法处理 cookie。
import express from 'express'
import cookieParser from 'cookie-parser'
const app = express()
app.use(cookieParser('secret'))
app.get('/', function (req, res) {
req.cookies.name
req.signedCookies.name
res.cookie('name', 'value', {
signed: true,
maxAge: 1000 * 60 * 60 * 24
})
})
Express 使用
cookie-parser
解析 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
Express 需要单独的配置来处理 OpenAPI、验证和类型安全,而 Elysia 内置了对 OpenAPI 的支持,使用模式作为单一事实来源。
import express from 'express'
import swaggerUi from 'swagger-ui-express'
const app = express()
app.use(express.json())
app.post('/users', (req, res) => {
// TODO: 验证请求体
res.status(201).json(req.body)
})
const swaggerSpec = {
openapi: '3.0.0',
info: {
title: 'My API',
version: '1.0.0'
},
paths: {
'/users': {
post: {
summary: 'Create user',
requestBody: {
content: {
'application/json': {
schema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'First name only'
},
age: { type: 'integer' }
},
required: ['name', 'age']
}
}
}
},
responses: {
'201': {
description: 'User created'
}
}
}
}
}
}
app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec))
Express 需要单独的配置来处理 OpenAPI、验证和类型安全
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 使用模式作为单一事实来源
Elysia 将根据你提供的模式生成 OpenAPI 规范,并根据模式验证请求和响应,并自动推断类型。
Elysia 还将 model
中注册的模式附加到 OpenAPI 规范中,允许你在 Swagger 或 Scalar UI 的专用部分引用该模型。
Express 使用单一的 supertest
库测试应用,而 Elysia 构建在 Web 标准 API 之上,允许与任何测试库一起使用。
import express from 'express'
import request from 'supertest'
import { describe, it, expect } from 'vitest'
const app = express()
app.get('/', (req, res) => {
res.send('Hello World')
})
describe('GET /', () => {
it('should return Hello World', async () => {
const res = await request(app).get('/')
expect(res.status).toBe(200)
expect(res.text).toBe('Hello World')
})
})
Express 使用
supertest
库测试应用
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,而 Express 不提供。
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 提供了更符合人体工学和开发者友好的体验,注重性能、类型安全和简洁性,而 Express 是 Node.js 中流行的 Web 框架,但在性能和简洁性方面存在一些限制。
如果你正在寻找一个易于使用、具有出色开发者体验并构建在 Web 标准 API 之上的框架,Elysia 是你的正确选择。
或者,如果你来自不同的框架,可以查看: