Elysia
2,454,631 reqs/sExpress
113,117
Measured in requests/second. Result from TechEmpower Benchmark Round 22 (2023-10-17) in PlainText
¥From Express to Elysia
本指南面向 Express 用户,帮助他们了解 Elysia 与 Express 之间的差异(包括语法),以及如何通过示例将应用从 Express 迁移到 Elysia。
¥This guide is for Express users who want to see the differences from Express including syntax, and how to migrate your application from Express to Elysia by example.
Express 是一个流行的 Node.js Web 框架,广泛用于构建 Web 应用和 API。它以简洁灵活而闻名。
¥Express is a popular web framework for Node.js, and widely used for building web applications and APIs. It is known for its simplicity and flexibility.
Elysia 是一个符合人机工程学的 Web 框架,适用于 Bun、Node.js 和支持 Web 标准 API 的运行时。设计符合人机工程学且方便开发者使用,重点关注可靠的类型安全性和性能。
¥Elysia is an ergonomic web framework for Bun, Node.js, and runtimes that support Web Standard API. Designed to be ergonomic and developer-friendly with a focus on sound type safety and performance.
¥Performance
得益于原生的 Bun 实现和静态代码分析,Elysia 的性能相比 Express 有显著提升。
¥Elysia has significant performance improvements over Express thanks to native Bun implementation, and static code analysis.
113,117
Measured in requests/second. Result from TechEmpower Benchmark Round 22 (2023-10-17) in PlainText
¥Routing
Express 和 Elysia 具有相似的路由语法,使用 app.get()
和 app.post()
方法定义路由,并且路径参数语法也相似。
¥Express and Elysia have similar routing syntax, using app.get()
and app.post()
methods to define routes and similar path parameter syntax.
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 建议使用方法链和对象解构。
¥There is a slight different in style guide, Elysia recommends usage of method chaining and object destructuring.
如果你不需要使用上下文,Elysia 还支持在响应中使用内联值。
¥Elysia also supports an inline value for the response if you don't need to use the context.
¥Handler
两者都具有类似的属性,用于访问输入参数,例如 headers
、query
、params
和 body
。
¥Both has a simliar property for accessing input parameters like headers
, query
, params
, and 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 编码数据和表单数据
¥Subrouter
Express 使用专用的 express.Router()
来声明子路由,而 Elysia 将每个实例视为可即插即用的组件。
¥Express use a dedicated express.Router()
for declaring a sub router while Elysia treats every instances as a component that can be plug and play together.
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 将每个实例视为一个组件
¥Validation
Elysia 内置了使用 TypeBox 进行请求验证的支持,确保类型安全,并开箱即用地支持标准 Schema,允许你使用你喜欢的库,例如 Zod、Valibot、ArkType、Effect Schema 等等。虽然 Express 没有提供内置验证,并且需要根据每个验证库进行手动类型声明。
¥Elysia has a built-in support for request validation using TypeBox sounds type safety, and support for Standard Schema out of the box allowing you to use your favorite library like Zod, Valibot, ArkType, Effect Schema and so on. While Express doesn't offers a built-in validation, and require a manual type declaration based on each validation library.
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。
¥File upload
Express 使用外部库 multer
来处理文件上传,而 Elysia 内置了对文件和表单数据的支持,并使用声明式 API 进行 mimetype 验证。
¥Express use an external library multer
to handle file upload, while Elysia has a built-in support for file and formdata, mimetype valiation using declarative API.
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 处理文件,并以声明方式进行 mimetype 验证
由于 multer 不验证 mimetype,你需要使用 file-type 或类似的库手动验证 mimetype。
¥As multer doesn't validate mimetype, you need to validate the mimetype manually using file-type or similar library.
Elysia 验证文件上传,并使用 file-type 自动验证 mimetype。
¥Elysia validate file upload, and use file-type to validate mimetype automatically.
¥Middleware
Express 中间件使用基于队列的单一顺序,而 Elysia 使用基于事件的生命周期提供更精细的控制。
¥Express middleware use a single queue-based order while Elysia give you a more granular control using an event-based lifecycle.
Elysia 的生命周期事件如下图所示。
¥Elysia's Life Cycle event can be illustrated as the following.
点击图片放大
虽然 Express 按顺序为请求管道提供单一流程,但 Elysia 可以拦截请求管道中的每个事件。
¥While Express has a single flow for request pipeline in order, Elysia can intercept each event in a request pipeline.
import express from 'express'
const app = express()
// Global middleware
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`)
next()
})
app.get(
'/protected',
// Route-specific middleware
(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()
// Global middleware
.onRequest(({ method, path }) => {
console.log(`${method} ${path}`)
})
// Route-specific middleware
.get('/protected', () => 'protected', {
beforeHandle({ status, headers }) {
if (!headers.authorizaton)
return status(401)
}
})
Elysia 在请求管道中的每个点使用特定的事件拦截器
虽然 Hono 有一个 next
函数来调用下一个中间件,但 Elysia 没有。
¥While Hono has a next
function to call the next middleware, Elysia does not has one.
¥Sounds type safety
Elysia 的设计注重类型安全。
¥Elysia is designed to be sounds type safety.
例如,你可以使用 derive 和 resolve 以类型安全的方式自定义上下文,而 Express 则不行。
¥For example, you can customize context in a type safe manner using derive and resolve while Express doesn't.
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
接口,但它是全局可用的,并且不具备良好的类型安全性,并且不保证该属性在所有请求处理程序中都可用。
¥While Express can, use declare module
to extend the Request
interface, it is globally available and doesn't have sounds type safety, and doesn't garantee that the property is available in all request handlers.
declare module 'express' {
interface Request {
version: number
token: string
}
}
这是上述 Express 示例运行所必需的,它不提供声音类型安全。
¥Middleware parameter
Express 使用函数返回一个插件来定义可复用的特定路由中间件,而 Elysia 使用 macro 来定义自定义钩子。
¥Express use a function to return a plugin to define a reusable route-specific middleware, while Elysia use macro to define a custom hook.
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 使用宏将自定义参数传递给自定义中间件
¥Error handling
Express 对所有路由使用单个错误处理程序,而 Elysia 对错误处理提供更精细的控制。
¥Express use a single error handler for all routes, while Elysia provides a more granular control over error handling.
import express from 'express'
const app = express()
class CustomError extends Error {
constructor(message: string) {
super(message)
this.name = 'CustomError'
}
}
// global error handler
app.use((error, req, res, next) => {
if(error instanceof CustomError) {
res.status(500).json({
message: 'Something went wrong!',
error
})
}
})
// route-specific error handler
app.get('/error', (req, res) => {
throw new CustomError('oh uh')
})
Express 使用中间件处理错误,所有路由使用同一个错误处理程序。
import { Elysia } from 'elysia'
class CustomError extends Error {
// Optional: custom HTTP status code
status = 500
constructor(message: string) {
super(message)
this.name = 'CustomError'
}
// Optional: what should be sent to the client
toResponse() {
return {
message: "If you're seeing this, our dev forgot to handle this error",
error: this
}
}
}
const app = new Elysia()
// Optional: register custom error class
.error({
CUSTOM: CustomError,
})
// Global error handler
.onError(({ error, code }) => {
if(code === 'CUSTOM')
return {
message: 'Something went wrong!',
error
}
})
.get('/error', () => {
throw new CustomError('oh uh')
}, {
// Optional: route specific error handler
error({ error }) {
return {
message: 'Only for this route!',
error
}
}
})
Elysia 提供更精细的错误处理控制和作用域机制
虽然 Express 使用中间件提供错误处理,但 Elysia 提供:
¥While Express offers error handling using middleware, Elysia provide:
toResponse
错误代码对于日志记录和调试非常有用,并且在区分扩展同一类的不同错误类型时非常重要。
¥The error code is useful for logging and debugging, and is important when differentiating between different error types extending the same class.
¥Encapsulation
Express 中间件是全局注册的,而 Elysia 通过显式的作用域机制和代码顺序允许你控制插件的副作用。
¥Express middleware is registered globally, while Elysia give you a control over side-effect of a plugin via explicit scoping mechanism, and order-of-code.
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)
// has side-effect from 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)
// doesn't have side-effect from subRouter
.get('/side-effect', () => 'hi')
Elysia 会将副作用封装到插件中。
默认情况下,Elysia 会将生命周期事件和上下文封装到所使用的实例中,因此除非明确说明,否则插件的副作用不会影响父实例。
¥By default, Elysia will encapsulate lifecycle events and context to the instance that is used, so that the side-effect of a plugin will not affect parent instance unless explicitly stated.
import { Elysia } from 'elysia'
const subRouter = new Elysia()
.onBeforeHandle(({ status, headers: { authorization } }) => {
if(!authorization?.startsWith('Bearer '))
return status(401)
})
// Scoped to parent instance but not beyond
.as('scoped')
const app = new Elysia()
.get('/', 'Hello World')
.use(subRouter)
// now have side-effect from subRouter
.get('/side-effect', () => 'hi')
Elysia 提供三种作用域机制:
¥Elysia offers 3 type of scoping mechanism:
虽然 Express 可以通过添加前缀来限定中间件副作用的范围,但这并不是真正的封装。副作用仍然存在,但与任何以该前缀开头的路由分开,这给开发者增加了记住哪个前缀具有副作用的心理负担。
¥While Express can scope the middleware side-effect by adding a prefix, it isn't a true encapsulation. The side-effect is still there but separated to any routes starts with said prefix, adding a mental overhead to the developer to memorize which prefix has side-effect.
你可以执行以下操作:
¥Which you can do the following:
由于 Express 并未提供真正的封装,这会导致调试过程变得非常困难。
¥This can leads to a nightmarish scenario to debug as Express doesn't offers true encapsulation.
Express 使用外部库 cookie-parser
来解析 Cookie,而 Elysia 内置了对 Cookie 的支持,并使用基于信号的方法来处理 Cookie。
¥Express use an external library cookie-parser
to parse cookies, while Elysia has a built-in support for cookie and use a signal-based approach to handle cookies.
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 } }) => {
// signature verification is handle automatically
name.value
// cookie signature is signed automatically
name.value = 'value'
name.maxAge = 1000 * 60 * 60 * 24
})
Elysia 使用基于信号的方法来处理 Cookie
Express 需要单独的 OpenAPI、验证和类型安全配置,而 Elysia 内置了对 OpenAPI 的支持,并使用架构作为单一事实来源。
¥Express require a separate configuration for OpenAPI, validation, and type safety while Elysia has a built-in support for OpenAPI using schema as a single source of truth.
import express from 'express'
import swaggerUi from 'swagger-ui-express'
const app = express()
app.use(express.json())
app.post('/users', (req, res) => {
// TODO: validate request body
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 will generate OpenAPI specification based on the schema you provided, and validate the request and response based on the schema, and infer type automatically.
Elysia 还将在 model
中注册的模式附加到 OpenAPI 规范中,允许你在 Swagger 或 Scalar UI 的专用部分中引用该模型。
¥Elysia also appends the schema registered in model
to the OpenAPI spec, allowing you to reference the model in a dedicated section in Swagger or Scalar UI.
¥Testing
Express 使用单个 supertest
库来测试应用,而 Elysia 构建于 Web 标准 API 之上,因此可以与任何测试库一起使用。
¥Express use a single supertest
library to test the application, while Elysia is built on top of Web Standard API allowing it be used with any testing library.
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 的辅助库,用于端到端类型安全,允许我们使用自动补全和完全类型安全进行测试。
¥Alternatively, Elysia also offers a helper library called Eden for End-to-end type safety, allowing us to test with auto-completion, and full type safety.
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')
})
})
¥End-to-end type safety
Elysia 内置了端到端类型安全支持,无需使用 Eden 进行代码生成,而 Express 不提供此功能。
¥Elysia offers a built-in support for end-to-end type safety without code generation using Eden, Express doesn't offers one.
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 是正确的选择。
¥If end-to-end type safety is important for you then Elysia is the right choice.
Elysia 提供了更符合人机工程学且对开发者更友好的体验,注重性能、类型安全和简洁性。Express 是 Node.js 的流行 Web 框架,但在性能和简洁性方面存在一些局限性。
¥Elysia offers a more ergonomic and developer-friendly experience with a focus on performance, type safety, and simplicity while Express is a popular web framework for Node.js, but it has some limitations when it comes to performance and simplicity.
如果你正在寻找一个易于使用、具有良好的开发体验并且基于 Web 标准 API 构建的框架,那么 Elysia 是你的正确选择。
¥If you are looking for a framework that is easy to use, has a great developer experience, and is built on top of Web Standard API, Elysia is the right choice for you.
或者,如果你来自其他框架,你可以查看:
¥Alternatively, if you are coming from a different framework, you can check out: