Skip to content

从 Fastify 到 Elysia

¥From Fastify to Elysia

本指南面向 Fastify 用户,帮助他们了解 Elysia 与 Fastify 之间的差异(包括语法),以及如何通过示例将应用从 Fastify 迁移到 Elysia。

¥This guide is for Fastify users who want to see a differences from Fastify including syntax, and how to migrate your application from Fastify to Elysia by example.

Fastify 是一款快速、低开销的 Node.js Web 框架,设计简洁易用。它建立在 HTTP 模块之上,并提供了一系列功能,使构建 Web 应用变得容易。

¥Fastify is a fast and low overhead web framework for Node.js, designed to be simple and easy to use. It is built on top of the HTTP module and provides a set of features that make it easy to build web applications.

Elysia 是一个符合人机工程学的 Web 框架,适用于 Bun、Node.js 和支持 Web 标准 API 的运行时。设计符合人机工程学且方便开发者使用,重点关注可靠的类型安全性和性能。

¥Elysia is an ergonomic web framework for Bun, Node.js, and runtime that supports Web Standard API. Designed to be ergonomic and developer-friendly with a focus on sound type safety and performance.

性能

¥Performance

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

¥Elysia has significant performance improvements over Fastify thanks to native Bun implementation, and static code analysis.

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

    415,600

Measured in requests/second. Result from TechEmpower Benchmark Round 22 (2023-10-17) in PlainText

路由

¥Routing

Fastify 和 Elysia 的路由语法相似,使用 app.get()app.post() 方法来定义路由,路径参数的语法也类似。

¥Fastify and Elysia has similar routing syntax, using app.get() and app.post() methods to define routes and similar path parameters syntax.

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

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 建议使用方法链和对象解构。

¥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

两者都具有类似的属性,用于访问输入参数,例如 headersqueryparamsbody,并自动将请求正文解析为 JSON、URL 编码数据和表单数据。

¥Both has a simliar property for accessing input parameters like headers, query, params, and body, and automatically parse the request body to JSON, URL-encoded data, and formdata.

ts
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 对象

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 解析数据并将其放入 context 对象

子路由

¥Subrouter

Fastify 使用函数回调函数定义子路由,而 Elysia 将每个实例视为可即插即用的组件。

¥Fastify use a function callback to define a subrouter while Elysia treats every instances as a component that can be plug and play together.

ts
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 使用函数回调函数声明子路由。

ts
import { Elysia } from 'elysia'

const subRouter = new Elysia({ prefix: '/api' })
	.get('/user', 'Hello User')

const app = new Elysia()
	.use(subRouter)

Elysia 将每个实例视为一个组件

Elysia 在构造函数中设置前缀,而 Fastify 要求你在选项中设置前缀。

¥While Elysia set the prefix in the constructor, Fastify requires you to set the prefix in the options.

验证

¥Validation

Elysia 内置了对请求验证的支持,使用 TypeBox 开箱即用地保证了声音类型安全,而 Fastify 使用 JSON Schema 声明模式,并使用 ajv 进行验证。

¥Elysia has a built-in support for request validation with sounds type safety out of the box using TypeBox while Fastify use JSON Schema for declaring schema, and ajv for validation.

然而,它不会自动推断类型,你需要使用像 @fastify/type-provider-json-schema-to-ts 这样的类型提供程序来推断类型。

¥However, doesn't infer type automatically, and you need to use a type provider like @fastify/type-provider-json-schema-to-ts to infer type.

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) => {
		// map string to number
		request.params.id = +request.params.id

		reply.send({
			params: request.params,
			body: request.body
		})
	}
})

Fastify 使用 JSON Schema 进行验证

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 '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 自动推断类型。

¥Alternatively, Fastify can also use TypeBox or Zod for validation using @fastify/type-provider-typebox to infer type automatically.

虽然 Elysia 更喜欢使用 TypeBox 进行验证,但 Elysia 也支持标准 Schema,允许你开箱即用地使用 Zod、Valibot、ArkType、Effect Schema 等库。

¥While Elysia prefers TypeBox for validation, Elysia also support for Standard Schema allowing you to use library like Zod, Valibot, ArkType, Effect Schema and so on out of the box.

文件上传

¥File upload

Fastify 使用 fastify-multipart 处理文件上传,并在底层使用 Busboy。而 Elysia 使用 Web 标准 API 处理表单数据,并使用声明式 API 进行 mimetype 验证。

¥Fastify use a fastify-multipart to handle file upload which use Busboy under the hood while Elysia use Web Standard API for handling formdata, mimetype valiation using declarative API.

然而,Fastify 不提供直接的文件验证方法,例如文件大小和 MIME 类型,并且需要一些变通方法来验证文件。

¥However, Fastify doesn't offers a straight forward way for file validation, eg. file size and mimetype, and required some workarounds to validate the file.

ts
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)
	}
)

Fastift 使用 fastify-multipart 处理文件上传,并使用伪造的 type: object 来允许使用 Buffer。

ts
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 进行 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。

¥While Elysia, validate file upload, and use file-type to validate mimetype automatically.

生命周期事件

¥Lifecycle Event

Fastify 和 Elysia 都使用基于事件的方法,生命周期事件有些相似。

¥Both Fastify and Elysia has some what similar lifecycle event using event-based approach.

Elysia 生命周期

¥Elysia Lifecycle

Elysia 的生命周期事件如下图所示。

¥Elysia's Life Cycle event can be illustrated as the following.

Elysia Life Cycle Graph

点击图片放大

Fastify 生命周期

¥Fastify Lifecycle

Fastify 的生命周期事件如下图所示。

¥Fastify's Life Cycle event can be illustrated as the following.

Incoming Request

  └─▶ Routing

        └─▶ Instance Logger

   4**/5** ◀─┴─▶ onRequest Hook

        4**/5** ◀─┴─▶ preParsing Hook

              4**/5** ◀─┴─▶ Parsing

                   4**/5** ◀─┴─▶ preValidation Hook

                            400 ◀─┴─▶ Validation

                              4**/5** ◀─┴─▶ preHandler Hook

                                    4**/5** ◀─┴─▶ User Handler

                                                    └─▶ Reply

                                                4**/5** ◀─┴─▶ preSerialization Hook

                                                                └─▶ onSend Hook

                                                            4**/5** ◀─┴─▶ Outgoing Response

                                                                            └─▶ onResponse Hook

两者在拦截请求和响应生命周期事件的语法也有些相似,但是 Elysia 不需要你调用 done 来继续生命周期事件。

¥Both also has somewhat similar syntax for intercepting the request and response lifecycle events, however Elysia doesn't require you to call done to continue the lifecycle event.

ts
import fastify from 'fastify'

const app = fastify()

// Global middleware
app.addHook('onRequest', (request, reply, done) => {
	console.log(`${request.method} ${request.url}`)

	done()
})

app.get(
	'/protected',
	{
		// Route-specific middleware
		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 来继续生命周期事件。

ts
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 会自动检测生命周期事件,无需调用 done 即可继续生命周期事件。

听起来类型安全

¥Sounds type safety

Elysia 的设计注重类型安全。

¥Elysia is designed to be sounds type safety.

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

¥For example, you can customize context in a type safe manner using derive and resolve while Fastify doesn't.

ts
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
.
version
Property '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,但不提供声音类型安全

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 使用 decorate 扩展上下文,使用 resolve 向上下文添加自定义属性

虽然 Fastify 可以使用 declare module 扩展 FastifyRequest 接口,但它是全局可用的,并且不具备良好的类型安全性,并且不保证该属性在所有请求处理程序中都可用。

¥While Fastify can, use declare module to extend the FastifyRequest 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.

ts
declare module 'fastify' {
  	interface FastifyRequest {
    	version: number
  		token: string
  	}
}

这是上述 Fastify 示例运行所必需的,它不提供声音类型安全。

中间件参数

¥Middleware parameter

Fastify 使用函数返回 Fastify 插件来定义命名中间件,而 Elysia 使用 macro 来定义自定义钩子。

¥Fastify use a function to return Fastify plugin to define a named middleware, while Elysia use macro to define a custom hook.

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

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 使用宏将自定义参数传递给自定义中间件

虽然 Fastify 使用函数回调,但它需要返回一个函数,该函数将被放置在事件处理程序中,或者返回一个以钩子形式表示的对象。当需要多个自定义函数时,处理起来会比较困难,因为你需要将它们合并到一个对象中。

¥While Fastify use a function callback, it needs to return a function to be placed in an event handler or an object represented as a hook which can be hard to handle when there are need for multiple custom functions as you need to reconcile them into a single object.

错误处理

¥Error handling

Fastify 和 Elysia 都提供了一个生命周期事件来处理错误。

¥Both Fastify and Elysia offers a lifecycle event to handle error.

ts
import fastify from 'fastify'

const app = fastify()

class CustomError extends Error {
	constructor(message: string) {
		super(message)
		this.name = 'CustomError'
	}
}

// global error handler
app.setErrorHandler((error, request, reply) => {
	if (error instanceof CustomError)
		reply.status(500).send({
			message: 'Something went wrong!',
			error
		})
})

app.get(
	'/error',
	{
		// route-specific error handler
		errorHandler(error, request, reply) {
			reply.send({
				message: 'Only for this route!',
				error
			})
		}
	},
	(request, reply) => {
		throw new CustomError('oh uh')
	}
)

Fastify 使用 setErrorHandler 作为全局错误处理程序,errorHandler 作为特定路由的错误处理程序

ts
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 提供自定义错误代码、状态的简写以及用于将错误映射到响应的 toResponse

虽然两者都提供使用生命周期事件的错误处理,但 Elysia 还提供:

¥While Both offers error handling using lifecycle event, Elysia also provide:

  1. 自定义错误代码
  2. 用于映射 HTTP 状态的简写,以及用于将错误映射到响应的 toResponse

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

¥The error code is useful for logging and debugging, and is important when differentiating between different error types extending the same class.

封装

¥Encapsulation

Fastify 封装了插件的副作用,而 Elysia 通过显式的作用域机制和代码顺序控制插件的副作用。

¥Fastify encapsulate plugin side-effect, while Elysia give you a control over side-effect of a plugin via explicit scoping mechanism, and order-of-code.

ts
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)
	// doesn't have side-effect from subRouter
	.get('/side-effect', () => 'hi')

Fastify 封装了插件的副作用。

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)
    // doesn't have side-effect from subRouter
    .get('/side-effect', () => 'hi')

除非明确说明,否则 Elysia 会封装插件的副作用。

两者都具有插件的封装机制,以防止副作用。

¥Both has a encapsulate mechanism of a plugin to prevent side-effect.

然而,Elysia 可以通过声明作用域来明确指定哪个插件应该具有副作用,而 Fastify 始终会对其进行封装。

¥However, Elysia can explicitly stated which plugin should have side-effect by declaring a scoped while Fastify always encapsulate it.

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

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

由于 Fastify 不提供作用域机制,我们需要:

¥As Fastify doesn't offers a scoping mechanism, we need to either:

  1. 为每个钩子创建一个函数并手动添加它们
  2. 使用高阶函数,并将其应用于需要该效果的实例

然而,如果处理不当,这可能会导致重复的副作用。

¥However, this can caused a duplicated side-effect if not handled carefully.

ts
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)

	// This would log twice
	app.get('/sub', (request, reply) => {
		return reply.send('Hello from sub router!')
	})

	done()
}

app.register(subRouter, {
	prefix: '/sub'
})

app.listen({
	port: 3000
})

在这种情况下,Elysia 提供了插件去重机制,以防止重复的副作用。

¥In this scenario, Elysia offers a plugin deduplication mechanism to prevent duplicated side-effect.

ts
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) 
	// side-effect only called once
	.get('/side-effect', () => 'hi')

通过使用唯一的 name,Elysia 只需应用一次插件,不会造成重复的副作用。

¥By using a unique name, Elysia will apply the plugin only once, and will not cause duplicated side-effect.

Fastify 使用 @fastify/cookie 解析 Cookie,而 Elysia 内置了对 Cookie 的支持,并使用基于信号的方法来处理 Cookie。

¥Fastify use @fastify/cookie to parse cookies, while Elysia has a built-in support for cookie and use a signal-based approach to handle cookies.

ts
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。

ts
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,并自动进行签名验证

OpenAPI

两者都使用 Swagger 提供 OpenAPI 文档,但 Elysia 默认使用 Scalar UI,这是一个更现代、更用户友好的 OpenAPI 文档界面。

¥Both offers OpenAPI documentation using Swagger, however Elysia default to Scalar UI which is a more modern and user-friendly interface for OpenAPI documentation.

ts
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 通过 Swagger 进行 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 默认使用 @elysiajs/swagger 进行 OpenAPI 文档处理,使用 Scalar 或 Swagger 进行文档处理

两者都使用 $ref 为 OpenAPI 文档提供模型引用,但 Fastify 不提供类型安全,也不提供用于指定模型名称的自动补齐功能,而 Elysia 则提供。

¥Both offers model reference using $ref for OpenAPI documentation, however Fastify doesn't offers type-safety, and auto-completion for specifying model name while Elysia does.

测试

¥Testing

Fastify 内置了使用 fastify.inject() 模拟网络请求进行测试的支持,而 Elysia 使用 Web 标准 API 来执行实际请求。

¥Fastify has a built-in support for testing using fastify.inject() to simulate network request while Elysia use a Web Standard API to do an actual request.

ts
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() 模拟网络请求。

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

¥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.

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')
}) })

端到端类型安全

¥End-to-end type safety

Elysia 内置了端到端类型安全支持,无需使用 Eden 进行代码生成,而 Fastify 不提供此功能。

¥Elysia offers a built-in support for end-to-end type safety without code generation using Eden, while Fastify doesn't offers one.

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

¥If end-to-end type safety is important for you then Elysia is the right choice.


Elysia 提供了更符合人机工程学且对开发者更友好的体验,注重性能、类型安全和简洁性。Fastify 是 Node.js 的成熟框架之一,但它不具备下一代框架提供的完善的类型安全和端到端类型安全。

¥Elysia offers a more ergonomic and developer-friendly experience with a focus on performance, type safety, and simplicity while Fastify is one of the established framework for Node.js, but doesn't has sounds type safety and end-to-end type safety offers by next generation framework.

如果你正在寻找一个易于使用、具有良好的开发体验并且基于 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: