Skip to content
在 AI 工具中打开 Anthropic

从 tRPC 到 Elysia

本指南适用于想了解 Elysia 与 tRPC 语法差异,并通过示例了解如何将应用从 tRPC 迁移到 Elysia 的用户。

tRPC 是一个使用 TypeScript 构建 API 的类型安全 RPC 框架。它提供了创建端到端类型安全 API 的方式,实现前后端之间的类型安全契约。

Elysia 是一个符合人体工学的 Web 框架。设计旨在符合人体工学且对开发者友好,重点关注可靠的类型安全和性能。

概述

tRPC 主要设计为基于 RPC 通信,采用对 RESTful API 的专有抽象,而 Elysia 则专注于 RESTful API。

tRPC 的主要特点是提供前后端之间的端到端类型安全契约,Elysia 也通过 Eden 提供了类似功能。

这使得 Elysia 更适合构建使用开发者已经熟悉的 RESTful 标准的通用 API,而无需学习新的专有抽象,同时享有 tRPC 提供的端到端类型安全。

路由

Elysia 使用类似 Express 和 Hono 的语法,比如 app.get()app.post() 方法定义路由及路径参数语法。

而 tRPC 使用嵌套路由器的方式定义路由。

ts
import { initTRPC } from '@trpc/server'
import { createHTTPServer } from '@trpc/server/adapters/standalone'

const t = initTRPC.create()

const appRouter = t.router({
	hello: t.procedure.query(() => 'Hello World'),
	user: t.router({
		getById: t.procedure
			.input((id: string) => id)
			.query(({ input }) => {
				return { id: input }
			})
	})
})

const server = createHTTPServer({
  	router: appRouter
})

server.listen(3000)

tRPC 使用嵌套路由器和 procedure 来定义路由

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 使用 HTTP 方法和路径参数来定义路由

虽然 tRPC 使用专有的 procedure 和 router 抽象 RESTful API,但 Elysia 采用类似 Express 和 Hono 的 app.get()app.post() 方法及路径参数语法来定义路由。

处理器

tRPC 的处理器称为 procedure,可以是 querymutation,而 Elysia 直接使用 HTTP 方法如 getpostputdelete 等。

tRPC 没有 HTTP 属性的概念,如 query、headers、状态码等,只有 inputoutput

ts
import { initTRPC } from '@trpc/server'

const t = initTRPC.create()

const appRouter = t.router({
	user: t.procedure
		.input((val: { limit?: number; name: string; authorization?: string }) => val)
		.mutation(({ input }) => {
			const limit = input.limit
			const name = input.name
			const auth = input.authorization

			return { limit, name, auth }
		})
})

tRPC 使用单一的 input 表示所有属性

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 为每个 HTTP 属性使用对应的专属属性

Elysia 通过静态代码分析确定需要解析的内容,仅解析所需属性。

这对性能和类型安全很有帮助。

子路由器

tRPC 使用嵌套路由器定义子路由器,而 Elysia 使用 .use() 方法定义子路由器。

ts
import { initTRPC } from '@trpc/server'

const t = initTRPC.create()

const subRouter = t.router({
	user: t.procedure.query(() => 'Hello User')
})

const appRouter = t.router({
	api: subRouter
})

tRPC 使用嵌套路由器定义子路由器

ts
import { Elysia } from 'elysia'

const subRouter = new Elysia()
	.get('/user', 'Hello User')

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

Elysia 使用 .use() 方法定义子路由器

虽然 tRPC 可以内联子路由器,Elysia 使用 .use() 方法定义子路由器。

验证

两者均支持标准 schema 验证。允许使用各种验证库,如 Zod、Yup、Valibot 等。

ts
import { initTRPC } from '@trpc/server'
import { z } from 'zod'

const t = initTRPC.create()

const appRouter = t.router({
	user: t.procedure
		.input(
			z.object({
				id: z.number(),
				name: z.string()
			})
		)
		.mutation(({ input }) => input)
//                    ^?
})

tRPC 使用 input 定义验证 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 '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 使用对应专门属性定义验证 schema

两者都能自动根据 schema 推断上下文中的类型。

文件上传

tRPC 默认不支持文件上传,需要将文件作为 base64 字符串输入,效率低且不支持 mime 类型验证。

而 Elysia 内建支持基于 Web 标准 API 的文件上传。

ts
import { initTRPC } from '@trpc/server'
import { z } from 'zod'

import { fileTypeFromBuffer } from 'file-type'

const t = initTRPC.create()

export const uploadRouter = t.router({
	uploadImage: t.procedure
		.input(z.base64())
		.mutation(({ input }) => {
			const buffer = Buffer.from(input, 'base64')

			const type = await fileTypeFromBuffer(buffer)
			if (!type || !type.mime.startsWith('image/'))
				throw new TRPCError({
      				code: 'UNPROCESSABLE_CONTENT',
       				message: 'Invalid file type',
    			})

			return input
		})
})

tRPC

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 类型验证

由于 tRPC 默认不验证 mime 类型,需借助第三方库如 file-type 来验证实际文件类型。

中间件

tRPC 中间件采用基于队列的单序列,使用类似 Express 的 next 方法,而 Elysia 则通过基于事件的生命周期提供更细粒度的控制。

Elysia 的生命周期事件可用下图说明。 Elysia Life Cycle Graph

点击图片放大

tRPC 在请求管道中的流程为单一顺序,而 Elysia 可拦截请求管道中的每个事件。

ts
import { initTRPC } from '@trpc/server'

const t = initTRPC.create()

const log = t.middleware(async ({ ctx, next }) => {
	console.log('Request started')

	const result = await next()

	console.log('Request ended')

	return result
})

const appRouter = t.router({
	hello: log
		.procedure
		.query(() => 'Hello World')
})

tRPC 使用单个中间件队列定义为 procedure

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 为请求管道中的每个节点使用特定事件拦截器

虽然 tRPC 使用 next 函数调用队列中的下一个中间件,Elysia 则为请求管道中各节点使用特定事件拦截器。

类型安全

Elysia 设计上提供了类型安全。

例如,你可以使用 deriveresolve类型安全的方式自定义上下文,而 tRPC 则通过类型转换使用 context 来提供上下文,但这无法确保 100% 的类型安全,因此不够严谨。

ts
import { 
initTRPC
} from '@trpc/server'
const
t
=
initTRPC
.
context
<{
version
: number
token
: string
}>().
create
()
const
appRouter
=
t
.
router
({
version
:
t
.
procedure
.
query
(({
ctx
: {
version
} }) =>
version
),
token
:
t
.
procedure
.
query
(({
ctx
: {
token
,
version
} }) => {
version
return
token
}) })

tRPC 使用 context 来扩展上下文,但没有类型安全保障

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 在请求管道的每个点使用了特定的事件拦截器

中间件参数

两者都支持自定义中间件,但 Elysia 使用宏函数来传递自定义参数给中间件,而 tRPC 使用高阶函数传参,缺乏类型安全。

ts
import { 
initTRPC
,
TRPCError
} from '@trpc/server'
const
t
=
initTRPC
.
create
()
const
findUser
= (
authorization
?: string) => {
return {
name
: 'Jane Doe',
role
: 'admin' as
const
} } const
role
= (
role
: 'user' | 'admin') =>
t
.
middleware
(({
next
,
input
}) => {
const
user
=
findUser
(
input
as string)
if(
user
.
role
!==
role
)
throw new
TRPCError
({
code
: 'UNAUTHORIZED',
message
: 'Unauthorized',
}) return
next
({
ctx
: {
user
} }) }) const
appRouter
=
t
.
router
({
token
:
t
.
procedure
.
use
(
role
('admin'))
.
query
(({
ctx
: {
user
} }) =>
user
)
})

tRPC 使用高阶函数向自定义中间件传递自定义参数

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

错误处理

tRPC 使用类似中间件的方式处理错误,而 Elysia 提供了类型安全的自定义错误类,以及全局和路由特定的错误拦截器。

ts
import { initTRPC, TRPCError } from '@trpc/server'

const t = initTRPC.create()

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

const appRouter = t.router()
	.middleware(async ({ next }) => {
		try {
			return await next()
		} catch (error) {
			console.log(error)

			throw new TRPCError({
	  			code: 'INTERNAL_SERVER_ERROR',
	  			message: error.message
			})
		}
	})
	.query('error', () => {
		throw new CustomError('oh uh')
	})

tRPC 使用类似中间件的方式处理错误

ts
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
('oh uh')
}, { // 可选:路由特定错误处理器
error
({
error
}) {
return {
message
: '仅该路由生效!',
error
} } })

Elysia 提供了更细粒度的错误处理控制和作用域机制

虽然 tRPC 提供了类似中间件的错误处理,但 Elysia 额外提供了:

  1. 全局及路由特定的错误处理器
  2. HTTP 状态码与 toResponse 映射的简写用法,用于将错误映射到响应
  3. 为每种错误提供自定义错误码

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

Elysia 在这方面均保证类型安全,而 tRPC 则没有。

封装

tRPC 封装了过程或路由的副作用,保持其始终隔离;而 Elysia 通过显式的作用域机制和代码顺序,让你控制插件的副作用。

ts
import { initTRPC } from '@trpc/server'

const t = initTRPC.create()

const subRouter = t.router()
	.middleware(({ ctx, next }) => {
		if(!ctx.headers.authorization?.startsWith('Bearer '))
			throw new TRPCError({
	  			code: 'UNAUTHORIZED',
	  			message: 'Unauthorized',
			})

		return next()
	})

const appRouter = t.router({
	// 不会有 subRouter 的副作用
	hello: t.procedure.query(() => 'Hello World'),
	api: subRouter
		.mutation('side-effect', () => 'hi')
})

tRPC 将插件的副作用封装在过程或路由内

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 可以通过声明作用域显式指出哪个插件会有副作用,而 tRPC 始终封装副作用。

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 提供三种作用域机制:

  1. local - 只应用于当前实例,无副作用(默认)
  2. scoped - 作用域限定到父实例,不会传播
  3. global - 影响所有实例

OpenAPI

tRPC 不提供官方 OpenAPI 支持,依赖第三方库如 trpc-to-openapi,这并非一整合解决方案。

而 Elysia 内置了基于 @elysiajs/openapi 的 OpenAPI 支持,只需一行代码开启。

ts
import { initTRPC } from '@trpc/server'
import { createHTTPServer } from '@trpc/server/adapters/standalone'

import { OpenApiMeta } from 'trpc-to-openapi';

const t = initTRPC.meta<OpenApiMeta>().create()

const appRouter = t.router({
	user: t.procedure
		.meta({
			openapi: {
				method: 'post',
				path: '/users',
				tags: ['User'],
				summary: 'Create user',
			}
		})
		.input(
			t.array(
				t.object({
					name: t.string(),
					age: t.number()
				})
			)
		)
		.output(
			t.array(
				t.object({
					name: t.string(),
					age: t.number()
				})
			)
		)
		.mutation(({ input }) => input)
})

export const openApiDocument = generateOpenApiDocument(appRouter, {
  	title: 'tRPC OpenAPI',
  	version: '1.0.0',
  	baseUrl: 'http://localhost:3000'
})

tRPC 依赖第三方库生成 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
: '创建用户'
} })

Elysia 无缝地将规范集成进 Schema

tRPC 依赖第三方库生成 OpenAPI 规范,并且必须在元数据中定义正确的路径名和 HTTP 方法,这要求你持续关注路由和过程的放置方式。

而 Elysia 使用你提供的 Schema 生成 OpenAPI 规范,自动验证请求/响应并推断类型,覆盖了单一可信来源

Elysia 还会把在 model 中注册的 Schema 附加到 OpenAPI 规范中,允许你在 Swagger 或 Scalar UI 的专用部分引用模型,而 tRPC 则直接内联 Schema 到路由中,缺少这一功能。

测试

Elysia 使用 Web 标准 API 来处理请求和响应,而 tRPC 则需要大量的仪式(ceremony)来使用 createCallerFactory 运行请求。

ts
import { describe, it, expect } from 'vitest'

import { initTRPC } from '@trpc/server'
import { z } from 'zod'

const t = initTRPC.create()

const publicProcedure = t.procedure
const { createCallerFactory, router } = t

const appRouter = router({
	post: router({
		add: publicProcedure
			.input(
				z.object({
					title: z.string().min(2)
				})
			)
			.mutation(({ input }) => input)
	})
})

const createCaller = createCallerFactory(appRouter)

const caller = createCaller({})

describe('GET /', () => {
	it('should return Hello World', async () => {
		const newPost = await caller.post.add({
			title: '74 Itoki Hana'
		})

		expect(newPost).toEqual({
			title: '74 Itoki Hana'
		})
	})
})

tRPC 需要 createCallerFactory 并且运行请求需要大量仪式

ts
import { Elysia, t } from 'elysia'
import { describe, it, expect } from 'vitest'

const app = new Elysia()
	.post('/add', ({ body }) => body, {
		body: t.Object({
			title: t.String({ minLength: 2 })
		})
	})

describe('GET /', () => {
	it('should return Hello World', async () => {
		const res = await app.handle(
			new Request('http://localhost/add', {
				method: 'POST',
				body: JSON.stringify({ title: '74 Itoki Hana' }),
				headers: {
					'Content-Type': 'application/json'
				}
			})
		)

		expect(res.status).toBe(200)
		expect(await res.res()).toEqual({
			title: '74 Itoki Hana'
		})
	})
})

Elysia 使用 Web 标准 API 来处理请求和响应

另外,Elysia 还提供了一个名为 Eden 的辅助库,用于端到端的类型安全,类似于 tRPC.createCallerFactory,允许我们带有自动补全和完整类型安全地进行测试,且无需繁琐的仪式。

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

端到端类型安全

两者都提供了用于客户端与服务器通信的端到端类型安全。

ts
import { 
initTRPC
} from '@trpc/server'
import {
createHTTPServer
} from '@trpc/server/adapters/standalone'
import {
z
} from 'zod'
import {
createTRPCProxyClient
,
httpBatchLink
} from '@trpc/client'
const
t
=
initTRPC
.
create
()
const
appRouter
=
t
.
router
({
mirror
:
t
.
procedure
.
input
(
z
.
object
({
message
:
z
.
string
()
}) ) .
output
(
z
.
object
({
message
:
z
.
string
()
}) ) .
mutation
(({
input
}) =>
input
)
}) const
server
=
createHTTPServer
({
router
:
appRouter
})
server
.
listen
(3000)
const
client
=
createTRPCProxyClient
<typeof
appRouter
>({
links
: [
httpBatchLink
({
url
: 'http://localhost:3000'
}) ] }) const {
message
} = await
client
.
mirror
.
mutate
({
message
: 'Hello World'
})
message

tRPC 使用 createTRPCProxyClient 创建带有端到端类型安全的客户端

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 使用 treaty 来运行请求,并提供端到端类型安全

虽然两者都提供端到端类型安全,但 tRPC 只处理请求成功的 正常路径 ,并不具备错误处理的类型健全性,导致其不健全。

如果类型健全性对你来说很重要,那么 Elysia 是正确的选择。


虽然 tRPC 是构建类型安全 API 的优秀框架,但它在 RESTful 兼容性和类型健全性方面存在局限。

Elysia 专注于开发者体验与类型健全性,符合 RESTful、OpenAPI 和 WinterCG 标准,设计上更符合构建通用 API 的需求,且使用起来更符合人体工程学和开发者友好性。

另外,如果你来自其他框架,也可以查看: