Skip to content
我们的赞助商

从 Express 迁移到 Elysia

本指南面向 Express 用户,旨在展示 Express 与 Elysia 的语法差异,并通过示例说明如何将应用从 Express 迁移至 Elysia。

Express 是 Node.js 中流行的 Web 框架,广泛用于构建 Web 应用和 API。它以简洁性和灵活性著称。

Elysia 是为 Bun、Node.js 及支持 Web 标准 API 的运行时而设计的人体工学 Web 框架。注重健全的类型安全和性能,旨在提供符合人体工学的开发者友好体验。

性能

得益于原生 Bun 实现和静态代码分析,Elysia 相比 Express 有显著的性能提升。

  1. Elysia
    2,454,631 reqs/s
  2. Express

    113,117

以请求/秒为单位测量。结果来自 TechEmpower Benchmark 第 22 轮(2023-10-17)在 PlainText 中

路由

Express 和 Elysia 拥有相似的路由语法,使用 app.get()app.post() 方法定义路由,以及相似的路径参数语法。

ts
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 使用 reqres 作为请求和响应对象

ts
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 还支持内联值作为响应。

处理程序

两者都拥有相似的属性来访问输入参数,如 headersqueryparamsbody

ts
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 请求体

ts
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 将每个实例视为可即插即用的组件。

ts
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() 创建子路由

ts
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 不提供内置验证,需要根据每个验证库手动声明类型。

ts
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 需要外部验证库如 zodjoi 来验证请求体

ts
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
()
}) })
ts
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
()
}) })
ts
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 类型验证。

ts
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 请求体

ts
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 的生命周期事件可图示如下: Elysia 生命周期图

点击图片放大

虽然 Express 拥有单一顺序的请求管道流,但 Elysia 可以拦截请求管道中的每个事件。

ts
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 使用单一基于队列的顺序执行中间件

ts
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 设计为具备健全的类型安全。

例如,你可以使用 deriveresolve类型安全的方式自定义上下文,而 Express 则不行。

ts
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
.
version
Property '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 使用单一基于队列的顺序执行中间件

ts
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 接口,但它是全局可用的,不具备健全的类型安全,并且不保证该属性在所有请求处理程序中可用。

ts
declare module 'express' {
  	interface Request {
    	version: number
  		token: string
  	}
}

这是上述 Express 示例工作所必需的,但它不提供健全的类型安全

中间件参数

Express 使用函数返回插件来定义可重用的路由特定中间件,而 Elysia 使用 macro 定义自定义钩子。

ts
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 使用函数回调接受中间件的自定义参数

ts
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 提供更细粒度的错误控制。

ts
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 使用中间件处理错误,对所有路由使用单一错误处理程序

ts
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 提供:

  1. 全局和路由特定的错误处理程序
  2. 映射 HTTP 状态和 toResponse 的简写方式,用于将错误映射到响应
  3. 为每个错误提供自定义错误代码

错误代码对日志记录和调试非常有用,并且在区分扩展自同一类的不同错误类型时非常重要。

封装

Express 中间件是全局注册的,而 Elysia 通过显式的作用域机制和代码顺序控制插件的副作用。

ts
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 不处理中间件的副作用,需要前缀来分隔副作用

ts
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 会将生命周期事件和上下文封装到所使用的实例中,因此插件的副作用不会影响父实例,除非明确声明。

ts
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 种作用域机制:

  1. local - 仅应用于当前实例,无副作用(默认)
  2. scoped - 副作用作用域限定于父实例但不超出
  3. global - 影响所有实例

虽然 Express 可以通过添加前缀来限定中间件的副作用范围,但这并不是真正的封装。副作用仍然存在,但被分隔到任何以该前缀开头的路由中,增加了开发者记忆哪些前缀有副作用的心智负担。

你可以执行以下操作:

  1. 移动代码顺序,但前提是只有一个具有副作用的实例。
  2. 添加前缀,但副作用仍然存在。如果其他实例具有相同的前缀,那么它也会受到副作用的影响。

这可能导致调试噩梦,因为 Express 不提供真正的封装。

Express 使用外部库 cookie-parser 解析 cookie,而 Elysia 内置了对 cookie 的支持,并使用基于信号的方法处理 cookie。

ts
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

ts
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

OpenAPI

Express 需要单独的配置来处理 OpenAPI、验证和类型安全,而 Elysia 内置了对 OpenAPI 的支持,使用模式作为单一事实来源

ts
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、验证和类型安全

ts
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 之上,允许与任何测试库一起使用。

ts
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 库测试应用

ts
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 的辅助库,用于端到端类型安全,允许我们进行自动完成和完全类型安全的测试。

ts
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 不提供。

ts
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 是你的正确选择。

或者,如果你来自不同的框架,可以查看: