--- url: 'https://elysiajs.com/migrate/from-express.md' --- # 从 Express 到 Elysia 本指南适用于希望了解 Express 与 Elysia 之间差异的 Express 用户,包括语法,以及如何通过示例将应用程序从 Express 迁移到 Elysia。 **Express** 是一个流行的 Node.js 网络框架,广泛用于构建 Web 应用程序和 API。因其简单性和灵活性而闻名。 **Elysia** 是一个人性化的 Web 框架,适用于 Bun、Node.js 和支持 Web 标准 API 的运行时。设计时强调人体工学和开发者友好,专注于 **健全的类型安全** 和性能。 ## 性能 由于本机 Bun 实现和静态代码分析,Elysia 在性能上相比 Express 有显著提高。 ## 路由 Express 和 Elysia 有类似的路由语法,使用 `app.get()` 和 `app.post()` 方法来定义路由,并且有类似的路径参数语法。 ::: code-group ```ts [Express] 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` 作为请求和响应对象 ::: code-group ```ts [Elysia] 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`)时有类似的属性。 ::: code-group ```ts [Express] 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 主体 ::: code-group ```ts [Elysia] 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 将每个实例视为可以插件式组合的组件。 ::: code-group ```ts [Express] 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()` 创建子路由 ::: code-group ```ts [Elysia] import { Elysia } from 'elysia' const subRouter = new Elysia({ prefix: '/api' }) .get('/user', 'Hello User') const app = new Elysia() .use(subRouter) ``` ::: > Elysia 将每个实例视为一个组件 ## 验证 Elysia 内置支持使用 TypeBox 进行请求验证,提供类型安全,并开箱即用支持标准 Schema,使您可以使用喜欢的库如 Zod、Valibot、ArkType、Effect Schema 等。而 Express 不提供内置验证,需要基于各验证库手动声明类型。 ::: code-group ```ts [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` 来验证请求主体 ::: code-group ```ts twoslash [Elysia TypeBox] 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 twoslash [Elysia Zod] 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 twoslash [Elysia Valibot] 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 类型验证。 ::: code-group ```ts [Express] 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('没有上传文件') const type = await fileTypeFromFile(file.path) if (!type || !type.mime.startsWith('image/')) return res .status(422) .send('文件不是有效的图像') const filePath = path.resolve(file.path) res.sendFile(filePath) }) ``` ::: > Express 需要 `express.json()` 中间件来解析 JSON 主体 ::: code-group ```ts [Elysia] 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 生命周期图](/assets/lifecycle-chart.svg) > 点击图片放大 尽管 Express 对请求管道有单一流的处理顺序,Elysia 可以拦截请求管道中的每个事件。 ::: code-group ```ts [Express] 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('未授权') next() }, (req, res) => { res.send('受保护的路由') } ) ``` ::: > Express 使用单个基于队列的顺序来处理中间件,按顺序执行 ::: code-group ```ts [Elysia] 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 为请求管道中的每个点使用特定的事件拦截器 虽然 Express 具有调用下一个中间件的 `next` 函数,但 Elysia 并没有。 ## 健全的类型安全 Elysia 被设计为具有健全的类型安全。 例如,您可以使用 [derive](/essential/life-cycle.html#derive) 和 [resolve](/essential/life-cycle.html#resolve) 以 **类型安全** 的方式自定义上下文,而 Express 则无法做到。 ::: code-group ```ts twoslash [Express] // @errors: 2339 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) // ^? }) const authenticate = (req: Request, res: Response, next: Function) => { const token = req.headers.authorization if (!token) return res.status(401).send('未授权') // @ts-ignore req.token = token.split(' ')[1] next() } app.get('/token', getVersion, authenticate, (req, res) => { req.version // ^? res.send(req.token) // ^? }) ``` ::: > Express 使用单个基于队列的顺序来处理中间件,按顺序执行 ::: code-group ```ts twoslash [Elysia] 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](/patterns/macro) 定义自定义钩子。 ::: code-group ```ts twoslash [Express] const findUser = (authorization?: string) => { return { name: 'Jane Doe', role: 'admin' as const } } // ---cut--- // @errors: 2339 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('未授权') // @ts-ignore req.user = user next() } app.get('/token', role('admin'), (req, res) => { res.send(req.user) // ^? }) ``` ::: > Express 使用函数回调接受中间件的自定义参数 ::: code-group ```ts twoslash [Elysia] const findUser = (authorization?: string) => { return { name: 'Jane Doe', role: 'admin' as const } } // ---cut--- 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 提供了更精细的错误处理控制。 ::: code-group ```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: '发生了错误!', error }) } }) // 路由特定错误处理程序 app.get('/error', (req, res) => { throw new CustomError('哦,啊') }) ``` ::: > Express 使用中间件处理错误,所有路由共享一个错误处理程序 ::: code-group ```ts twoslash [Elysia] 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('哦,啊') }, { // 可选:路由特定错误处理程序 error({ error }) { return { message: '仅适用于此路由!', error } } }) ``` ::: > Elysia 提供更精细的错误处理控制和作用域机制 尽管 Express 使用中间件进行错误处理,Elysia 提供: 1. 全局和路由特定的错误处理器 2. 用于映射 HTTP 状态码和 `toResponse` 方法简写,将错误映射为响应 3. 为每种错误提供自定义错误代码 错误代码对日志记录和调试非常有用,并且在区分继承自同一类的不同类型错误时非常重要。 Elysia 在提供以上功能的同时确保类型安全,而 Express 则没有。 ## 封装 Express 中间件注册为全局,而 Elysia 通过显式作用域机制和代码顺序,控制插件的副作用。 ::: code-group ```ts [Express] 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('未授权') next() }) app.use(subRouter) // 从子路由产生副作用 app.get('/side-effect', (req, res) => { res.send('嗨') }) ``` ::: > Express 不会处理中间件的副作用,需要添加前缀以分开副作用 ::: code-group ```ts [Elysia] 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) // 不会有来自子路由的副作用 .get('/side-effect', () => '嗨') ``` ::: > Elysia 将副作用封装到插件中 默认情况下,Elysia 将生命事件和上下文封装到所使用的实例中,因此插件的副作用不会影响父实例,除非明确说明。 ```ts [Elysia] import { Elysia } from 'elysia' const subRouter = new Elysia() .onBeforeHandle(({ status, headers: { authorization } }) => { if(!authorization?.startsWith('Bearer ')) return status(401) }) // 作用域限定于父实例,但不再超过此范围 .as('scoped') // [!code ++] const app = new Elysia() .get('/', 'Hello World') .use(subRouter) // [!code ++] // 现在有来自子路由的副作用 .get('/side-effect', () => '嗨') ``` Elysia 提供三种类型的作用域机制: 1. **局部** - 只应用于当前实例,没有副作用(默认) 2. **受限** - 将副作用作用域限定于父实例,但不再超过此范围 3. **全局** - 影响所有实例 虽然 Express 可以通过添加前缀来限制中间件副作用,但这并不是真正的封装。副作用仍然存在,但被分隔到以所述前缀开头的任何路由,使得开发人员需要记住哪个前缀具有副作用。 这使得您可以执行以下操作: 1. 重新排列代码顺序,但仅在只有一个具有副作用的实例中有效。 2. 添加前缀,但副作用仍然存在。如果其他实例具有相同的前缀,则它也具有副作用。 这可能导致调试时出现噩梦场景,因为 Express 不提供真正的封装。 ## Cookie Express 使用外部库 `cookie-parser` 解析 cookies,而 Elysia 内置支持 cookie,并使用基于信号的方法处理 cookies。 ::: code-group ```ts [Express] 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` 解析 Cookies ::: code-group ```ts [Elysia] 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 ## OpenAPI Express 需要单独的配置来支持 OpenAPI、验证和类型安全,而 Elysia 使用架构作为 **单一真实来源** 内置支持 OpenAPI。 ::: code-group ```ts [Express] 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: '我的 API', version: '1.0.0' }, paths: { '/users': { post: { summary: '创建用户', requestBody: { content: { 'application/json': { schema: { type: 'object', properties: { name: { type: 'string', description: '仅为名' }, age: { type: 'integer' } }, required: ['name', 'age'] } } } }, responses: { '201': { description: '用户已创建' } } } } } } app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)) ``` ::: > Express 需要单独的配置来支持 OpenAPI、验证和类型安全 ::: code-group ```ts twoslash [Elysia] import { Elysia, t } from 'elysia' import { openapi } from '@elysiajs/openapi' // [!code ++] const app = new Elysia() .use(openapi()) // [!code ++] .model({ user: t.Array( t.Object({ name: t.String(), age: t.Number() }) ) }) .post('/users', ({ body }) => body, { // ^? body: 'user', response: { 201: 'user' }, detail: { summary: '创建用户' } }) ``` ::: > Elysia 将架构用作单一真实来源 Elysia 会根据您提供的架构生成 OpenAPI 规范,并根据该架构验证请求和响应,并自动推断类型。 Elysia 还将在 `model` 中注册的架构附加到 OpenAPI 规范,允许您在 Swagger 或 Scalar UI 的专用部分中引用该模型。 ## 测试 Express 使用单个 `supertest` 库测试应用程序,而 Elysia 构建在 Web 标准 API 之上,允许与任何测试库一起使用。 ::: code-group ```ts [Express] 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('应该返回 Hello World', async () => { const res = await request(app).get('/') expect(res.status).toBe(200) expect(res.text).toBe('Hello World') }) }) ``` ::: > Express 使用 `supertest` 库测试应用程序 ::: code-group ```ts [Elysia] 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](/eden/overview) 的助手库,用于端到端的类型安全,允许我们进行自动补全和完全类型安全的测试。 ```ts twoslash [Elysia] 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 内置支持 **端到端类型安全**,无需代码生成,Express 不提供此功能。 ::: code-group ```ts twoslash [Elysia] 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) // ^? // ---cut-after--- console.log('ok') ``` ::: 如果端到端类型安全对您来说很重要,那么 Elysia 是正确的选择。 *** Elysia 提供了更人性化和开发者友好的体验,专注于性能、类型安全和简易性,而 Express 是一个流行的 Node.js 网络框架,但在性能和简易性方面存在一些局限性。 如果您在寻找一个易于使用、具有出色开发者体验且基于 Web 标准 API 的框架,Elysia 是您正确的选择。 另外,如果您来自其他框架,可以查看: