Skip to content
我们的赞助商

从 tRPC 迁移到 Elysia

本指南适用于 tRPC 用户,他们希望了解与 Elysia 的差异,包括语法,以及通过示例将应用程序从 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 使用嵌套路由器和过程来定义路由

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 没有查询、标头、状态码等 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 验证

由于开箱即用不验证 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()
	// 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 为请求管道中的每个点使用特定事件拦截器

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

虽然 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({
	// doesn't have side-effect from 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)
    // doesn't have side-effect from 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)
   	})
	// 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 提供 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,允许我们测试时具有自动完成和 tRPC 那样的完整类型安全性,而无需仪式。

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。

或者,如果您来自其他框架,可以查看: