Skip to content
Our Sponsors
Open in Anthropic

title: 从 tRPC 迁移 - ElysiaJS prev: text: '快速开始' link: '/quick-start' next: text: '教程' link: '/tutorial' head: - - meta - property: 'og:title' content: 从 tRPC 迁移 - ElysiaJS

- - meta
  - name: 'description'
    content: 本指南面向希望了解 Elysia 与 tRPC 差异(包括语法)以及如何通过示例将应用程序从 tRPC 迁移到 Elysia 的 tRPC 用户。

- - meta
  - property: 'og:description'
    content: 本指南面向希望了解 Elysia 与 tRPC 差异(包括语法)以及如何通过示例将应用程序从 tRPC 迁移到 Elysia 的 tRPC 用户。

从 tRPC 迁移到 Elysia

本指南面向希望了解 Elysia 与 tRPC 差异(包括语法)以及如何通过示例将应用程序从 tRPC 迁移到 Elysia 的 tRPC 用户。

tRPC 是一个类型安全的 RPC 框架,用于使用 TypeScript 构建 API。它提供了一种创建端到端类型安全 API 的方法,具有前端和后端之间的类型安全契约。

Elysia 是一个符合人体工程学的 Web 框架。设计上注重人体工程学和开发者友好性,专注于可靠的类型安全性和性能。

概述

tRPC 主要设计为带有 RESTful API 专有抽象的 RPC 通信,而 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 使用嵌套路由器和过程来定义路由

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 使用过程和路由器在 RESTful API 上的专有抽象,但 Elysia 使用类似 Express 和 Hono 的语法,如 app.get()app.post() 方法定义路由,以及类似的路径参数语法。

处理器

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

tRPC 没有像 query、headers、status code 等 HTTP 属性的概念,只有 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() 方法定义子路由。

验证

两者都支持标准模式进行验证。允许你使用各种验证库,如 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 定义验证模式

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 使用特定属性定义验证模式

两者都提供从模式到上下文的自动类型推断。

文件上传

tRPC 不支持开箱即用的文件上传,需要你使用 base64 字符串作为输入,这效率低下,并且不支持 mimetype 验证。

而 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 以声明方式处理文件和 mimetype 验证

由于 tRPC 不支持开箱即用的 mimetype 验证,你需要使用像 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 使用定义为过程的单一中间件队列

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
: "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 提供对错误处理和作用域机制更细粒度的控制

虽然 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 可以通过声明作用域来明确说明哪些插件应该有副作用,而 Fastify 总是封装它。

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 - 影响所有实例

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
: 'Create user'
} })

Elysia 将规范无缝集成到模式中

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

而 Elysia 使用你提供的模式生成 OpenAPI 规范,并验证请求/响应,从单一真实来源自动推断类型。

Elysia 还将在 model 中注册的模式附加到 OpenAPI 规范,允许你在 Swagger 或 Scalar UI 的专用部分引用模型,而 tRPC 缺少此功能,将模式内联到路由中。

测试

Elysia 使用 Web 标准 API 处理请求和响应,而 tRPC 需要大量仪式来使用 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 和 WinterTC 标准,使其更适合构建通用 API。

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