从 v9 迁移到 v10
欢迎使用 tRPC v10!我们很高兴为你带来一个新的主要版本,以继续通过出色的 DX 实现完美的端到端类型安全性。
¥Welcome to tRPC v10! We're excited to bring you a new major version to continue the journey towards perfect end-to-end type safety with excellent DX.
在版本 10 的框架下,我们正在解锁性能改进,为你带来生活质量的增强,并为我们在未来构建新功能创造空间。
¥Under the hood of version 10, we are unlocking performance improvements, bringing you quality of life enhancements, and creating room for us to build new features in the future.
tRPC v10 为来自 v9 的用户提供了一个兼容层。.interop()
允许你逐步采用 v10,以便你可以继续构建项目的其余部分,同时仍然享受 v10 的新功能。
¥tRPC v10 features a compatibility layer for users coming from v9. .interop()
allows you to incrementally adopt v10 so that you can continue building the rest of your project while still enjoying v10's new features.
变更摘要
¥Summary of changes
Initializing your server
/src/server/trpc.tsts
/*** This is your entry point to setup the root configuration for tRPC on the server.* - `initTRPC` should only be used once per app.* - We export only the functionality that we use so we can enforce which base procedures should be used* * Learn how to create protected base procedures and other things below:* @see https://trpc.nodejs.cn/docs/v10/router* @see https://trpc.nodejs.cn/docs/v10/procedures*/import { initTRPC } from '@trpc/server';import superjson from 'superjson';import { Context } from './context';const t = initTRPC.context<Context>().create({/*** @see https://trpc.nodejs.cn/docs/v10/data-transformers*/transformer: superjson,/*** @see https://trpc.nodejs.cn/docs/v10/error-formatting*/errorFormatter(opts) {return opts.shape;},});/*** Create a router* @see https://trpc.nodejs.cn/docs/v10/router*/export const router = t.router;/*** Create an unprotected procedure* @see https://trpc.nodejs.cn/docs/v10/procedures**/export const publicProcedure = t.procedure;/*** @see https://trpc.nodejs.cn/docs/v10/merging-routers*/export const mergeRouters = t.mergeRouters;
/src/server/trpc.tsts
/*** This is your entry point to setup the root configuration for tRPC on the server.* - `initTRPC` should only be used once per app.* - We export only the functionality that we use so we can enforce which base procedures should be used* * Learn how to create protected base procedures and other things below:* @see https://trpc.nodejs.cn/docs/v10/router* @see https://trpc.nodejs.cn/docs/v10/procedures*/import { initTRPC } from '@trpc/server';import superjson from 'superjson';import { Context } from './context';const t = initTRPC.context<Context>().create({/*** @see https://trpc.nodejs.cn/docs/v10/data-transformers*/transformer: superjson,/*** @see https://trpc.nodejs.cn/docs/v10/error-formatting*/errorFormatter(opts) {return opts.shape;},});/*** Create a router* @see https://trpc.nodejs.cn/docs/v10/router*/export const router = t.router;/*** Create an unprotected procedure* @see https://trpc.nodejs.cn/docs/v10/procedures**/export const publicProcedure = t.procedure;/*** @see https://trpc.nodejs.cn/docs/v10/merging-routers*/export const mergeRouters = t.mergeRouters;
Defining routers & procedures
ts
// v9:const appRouter = trpc.router().query('greeting', {input: z.string(),resolve(opts) {return `hello ${opts.input}!`;},});// v10:const appRouter = router({greeting: publicProcedure.input(z.string()).query((opts) => `hello ${opts.input}!`),});
ts
// v9:const appRouter = trpc.router().query('greeting', {input: z.string(),resolve(opts) {return `hello ${opts.input}!`;},});// v10:const appRouter = router({greeting: publicProcedure.input(z.string()).query((opts) => `hello ${opts.input}!`),});
Calling procedures
ts
// v9client.query('greeting', 'KATT');trpc.useQuery(['greeting', 'KATT']);// v10// You can now CMD+click `greeting` to jump straight to your server code.client.greeting.query('KATT');trpc.greeting.useQuery('KATT');
ts
// v9client.query('greeting', 'KATT');trpc.useQuery(['greeting', 'KATT']);// v10// You can now CMD+click `greeting` to jump straight to your server code.client.greeting.query('KATT');trpc.greeting.useQuery('KATT');
Inferring types
v9
ts
// Building multiple complex helper types yourself. Yuck!export type TQuery = keyof AppRouter['_def']['queries'];export type InferQueryInput<TRouteKey extends TQuery> = inferProcedureInput<AppRouter['_def']['queries'][TRouteKey]>;type GreetingInput = InferQueryInput<'greeting'>;
ts
// Building multiple complex helper types yourself. Yuck!export type TQuery = keyof AppRouter['_def']['queries'];export type InferQueryInput<TRouteKey extends TQuery> = inferProcedureInput<AppRouter['_def']['queries'][TRouteKey]>;type GreetingInput = InferQueryInput<'greeting'>;
v10
Inference helpers
ts
// Inference helpers are now shipped out of the box.import type {inferRouterInputs ,inferRouterOutputs } from '@trpc/server';import type {AppRouter } from './server';typeRouterInput =inferRouterInputs <AppRouter >;typeRouterOutput =inferRouterOutputs <AppRouter >;typePostCreateInput =RouterInput ['post']['create'];typePostCreateOutput =RouterOutput ['post']['create'];
ts
// Inference helpers are now shipped out of the box.import type {inferRouterInputs ,inferRouterOutputs } from '@trpc/server';import type {AppRouter } from './server';typeRouterInput =inferRouterInputs <AppRouter >;typeRouterOutput =inferRouterOutputs <AppRouter >;typePostCreateInput =RouterInput ['post']['create'];typePostCreateOutput =RouterOutput ['post']['create'];
See Inferring types for more.
Middlewares
Middlewares are now reusable and can be chained, see the middleware docs for more.
ts
// v9const appRouter = trpc.router().middleware((opts) => {const { ctx } = opts;if (!ctx.user) {throw new TRPCError({ code: 'UNAUTHORIZED' });}return opts.next({ctx: {...ctx,user: ctx.user,},});}).query('greeting', {resolve(opts) {return `hello ${opts.ctx.user.name}!`;},});// v10const protectedProcedure = t.procedure.use((opts) => {const { ctx } = opts;if (!ctx.user) {throw new TRPCError({ code: 'UNAUTHORIZED' });}return opts.next({ctx: {// Old context will automatically be spread.// Only modify what's changed.user: ctx.user,},});});const appRouter = t.router({greeting: protectedProcedure.query((opts) => {return `Hello ${opts.ctx.user.name}!`}),});
ts
// v9const appRouter = trpc.router().middleware((opts) => {const { ctx } = opts;if (!ctx.user) {throw new TRPCError({ code: 'UNAUTHORIZED' });}return opts.next({ctx: {...ctx,user: ctx.user,},});}).query('greeting', {resolve(opts) {return `hello ${opts.ctx.user.name}!`;},});// v10const protectedProcedure = t.procedure.use((opts) => {const { ctx } = opts;if (!ctx.user) {throw new TRPCError({ code: 'UNAUTHORIZED' });}return opts.next({ctx: {// Old context will automatically be spread.// Only modify what's changed.user: ctx.user,},});});const appRouter = t.router({greeting: protectedProcedure.query((opts) => {return `Hello ${opts.ctx.user.name}!`}),});
Full example with data transformer, OpenAPI metadata, and error formatter
/src/server/trpc.tsts
import { initTRPC } from '@trpc/server';import superjson from 'superjson';// Context is usually inferred,// but we will need it here for this example.interface Context {user?: {id: string;name: string;};}interface Meta {openapi: {enabled: boolean;method: string;path: string;};}export const t = initTRPC.context<Context>().meta<Meta>().create({errorFormatter({ shape, error }) {return {...shape,data: {...shape.data,zodError:error.code === 'BAD_REQUEST' && error.cause instanceof ZodError? error.cause.flatten(): null,},};},transformer: superjson,});
/src/server/trpc.tsts
import { initTRPC } from '@trpc/server';import superjson from 'superjson';// Context is usually inferred,// but we will need it here for this example.interface Context {user?: {id: string;name: string;};}interface Meta {openapi: {enabled: boolean;method: string;path: string;};}export const t = initTRPC.context<Context>().meta<Meta>().create({errorFormatter({ shape, error }) {return {...shape,data: {...shape.data,zodError:error.code === 'BAD_REQUEST' && error.cause instanceof ZodError? error.cause.flatten(): null,},};},transformer: superjson,});
从 v9 迁移
¥Migrating from v9
我们推荐两种策略来立即开始(和完成!)升级你的代码库。
¥We recommend two strategies to start (and finish!) upgrading your codebase today.
使用代码模式
¥Using a codemod
@sachinraja 为此重大升级创建了 一个优秀的 codemod。运行脚本即可在短时间内完成 95% 的工作。
¥@sachinraja has created an excellent codemod for this major upgrade. Run the script to have 95% of the work done for you in a matter of moments.
如果你使用 codemod,则在进行完整迁移之前,你仍应执行下面的步骤 1-3,以确保它适合你。
¥If you use the codemod, you should still do steps 1-3 below to make sure that works for you before doing the full migration.
请注意,这个代码模块并不完美,但会为你完成很多繁重的工作。
¥Please note that this codemod isn't perfect but will do a lot of the heavy lifting for you.
使用 .interop()
¥Using .interop()
今天重写所有现有的 v9 路由对于你和你的团队来说可能负担太重。相反,让我们保留这些 v9 程序,并通过利用 v10 的 interop()
方法逐步采用 v10。
¥Rewriting all of your existing v9 routes today may be too heavy of a lift for you and your team. Instead, let's keep those v9 procedures in place and incrementally adopt v10 by leveraging v10's interop()
method.
1. 在 v9 路由上启用 interop()
¥ Enable interop()
on your v9 router
将你的 v9 路由变成 v10 路由只需要 10 个字符。将 .interop()
添加到 v9 路由的末尾...你的服务器代码就完成了!
¥Turning your v9 router into a v10 router only takes 10 characters. Add .interop()
to the end of your v9 router... and you're done with your server code!
src/server/routers/_app.tsdiff
const appRouter = trpc.router<Context>()/* ... */+ .interop();export type AppRouter = typeof appRouter;
src/server/routers/_app.tsdiff
const appRouter = trpc.router<Context>()/* ... */+ .interop();export type AppRouter = typeof appRouter;
有 .interop()
不支持的一些功能 个。我们期望几乎所有用户都能够使用 .interop()
在短短几分钟内迁移他们的服务器端代码。如果你发现 .interop()
无法正常工作,请务必执行 检查这里。
¥There are a few features that are not supported by .interop()
. We expect nearly all of our users to be able to use .interop()
to migrate their server side code in only a few minutes. If you are discovering that .interop()
is not working correctly for you, be sure to check here.
2. 创建 t
对象
¥ Create the t
-object
现在,让我们初始化一个 v10 路由,以便我们可以开始使用 v10 来编写我们将要编写的任何新路由。
¥Now, let's initialize a v10 router so we can start using v10 for any new routes we will write.
src/server/trpc.tsts
import { initTRPC } from '@trpc/server';import superjson from 'superjson';import { Context } from './context';const t = initTRPC.context<Context>().create({// Optional:transformer: superjson,// Optional:errorFormatter(opts) {const { shape } = opts;return {...shape,data: {...shape.data,},};},});/*** We recommend only exporting the functionality that we* use so we can enforce which base procedures should be used**/export const router = t.router;export const mergeRouters = t.mergeRouters;export const publicProcedure = t.procedure;
src/server/trpc.tsts
import { initTRPC } from '@trpc/server';import superjson from 'superjson';import { Context } from './context';const t = initTRPC.context<Context>().create({// Optional:transformer: superjson,// Optional:errorFormatter(opts) {const { shape } = opts;return {...shape,data: {...shape.data,},};},});/*** We recommend only exporting the functionality that we* use so we can enforce which base procedures should be used**/export const router = t.router;export const mergeRouters = t.mergeRouters;export const publicProcedure = t.procedure;
3. 创建一个新的 appRouter
¥ Create a new appRouter
将旧的
appRouter
重命名为legacyRouter
¥Rename your old
appRouter
tolegacyRouter
创建一个新的应用路由:
¥Create a new app router:
src/server/routers/_app.tsts
import {mergeRouters ,publicProcedure ,router } from './trpc';// Renamed from `appRouter`constlegacyRouter =trpc .router ()/* ... */.interop ();constmainRouter =router ({greeting :publicProcedure .query (() => 'hello from tRPC v10!'),});// Merge v9 router with v10 routerexport constappRouter =mergeRouters (legacyRouter ,mainRouter );export typeAppRouter = typeofappRouter ;
src/server/routers/_app.tsts
import {mergeRouters ,publicProcedure ,router } from './trpc';// Renamed from `appRouter`constlegacyRouter =trpc .router ()/* ... */.interop ();constmainRouter =router ({greeting :publicProcedure .query (() => 'hello from tRPC v10!'),});// Merge v9 router with v10 routerexport constappRouter =mergeRouters (legacyRouter ,mainRouter );export typeAppRouter = typeofappRouter ;
请小心使用最终会具有相同调用者名称的过程!如果旧路由中的路径与新路由中的路径匹配,你将遇到问题。
¥Be careful of using procedures that will end up having the same caller name! You will run into issues if a path in your legacy router matches a path in your new router.
4. 在你的客户端中使用它
¥ Use it in your client
现在,你的客户端(作为 v10 调用者)将可以使用这两组过程。你现在需要 访问你的客户端代码以将调用者更新为 v10 语法。
¥Both sets of procedures will now be available for your client as v10 callers. You will now need to visit your client code to update your callers to the v10 syntax.
ts
// Vanilla JS v10 client caller:client.proxy.greeting.query();// React v10 client caller:trpc.proxy.greeting.useQuery();
ts
// Vanilla JS v10 client caller:client.proxy.greeting.query();// React v10 client caller:trpc.proxy.greeting.useQuery();
互操作的局限性
¥Limitations of interop
订阅
¥Subscriptions
我们更改了订阅的 API,其中订阅需要返回 observable
实例。参见 订阅文档。
¥We have changed the API of Subscriptions where subscriptions need to return an observable
-instance. See subscriptions docs.
🚧 请随时为改进此部分做出贡献
¥🚧 Feel free to contribute to improve this section
自定义 HTTP 选项
¥Custom HTTP options
参见 HTTP 特定选项从 TRPCClient
移至链接。
¥See HTTP-specific options moved from TRPCClient
to links.
自定义链接
¥Custom Links
在 v10 中,Links 架构已被彻底改造。因此,为 v9 制作的自定义链接不适用于 v10 或在互操作时。如果你想了解有关如何为 v10 创建自定义链接的更多信息,请查看 链接文档。
¥In v10, the Links architecture has been completely revamped. Therefore, custom links made for v9 will not work for v10 or while on interop. If you want more information about how to create a custom link for v10, checkout the Links documentation.
客户端包变更
¥Client Package Changes
v10 还对应用的客户端进行了更改。进行一些关键改变后,你将解锁一些关键的生活质量变化:
¥v10 also brings changes to the client side of your application. After making a few key changes, you'll unlock a few key quality of life changes:
直接从客户端跳转到服务器定义
¥Jump to server definitions straight from your client
直接从客户端重命名路由或程序
¥Rename routers or procedures straight from the client
@trpc/react-query
将 @trpc/react 重命名为 @trpc/react-query
¥Renaming of @trpc/react to @trpc/react-query
@trpc/react
软件包已重命名为 @trpc/react-query
。这是为了反映它是 react-query
的薄封装器这一事实,并允许在没有 @trpc/react-query
包的情况下在 React 中使用 trpc 的情况,例如与即将推出的 React 服务器组件(RSC)或其他数据获取库一起使用 适配器。如果你使用的是 @trpc/react
,则需要将其删除并安装 @trpc/react-query
,并更新导入:
¥The @trpc/react
package has been renamed to @trpc/react-query
. This is to reflect the fact that it is a thin wrapper around react-query
, as well as to allow for situations where trpc may be used in react without the @trpc/react-query
package, such as with upcoming React Server Components (RSCs) or with other data fetching library adapters. If you're using @trpc/react
, you'll need to remove it and install @trpc/react-query
instead, as well as update your imports:
diff
- import { createReactQueryHooks } from '@trpc/react';+ import { createReactQueryHooks } from '@trpc/react-query';
diff
- import { createReactQueryHooks } from '@trpc/react';+ import { createReactQueryHooks } from '@trpc/react-query';
react-query
大版本升级
¥Major version upgrade of react-query
我们已将 peerDependencies
从 react-query@^3
升级到 @tanstack/react-query@^4
。因为我们的客户端钩子只是反应查询的一个薄封装,所以我们鼓励你 访问他们的迁移指南 了解有关新的 React 钩子实现的更多详细信息。
¥We've upgraded peerDependencies
from react-query@^3
to @tanstack/react-query@^4
. Because our client hooks are only a thin wrapper around react-query, we encourage you to visit their migration guide for more details about your new React hooks implementation.
钩子上的 tRPC 特定选项已移至 trpc
¥tRPC-specific options on hooks moved to trpc
为了避免与任何内置 react-query
属性发生冲突和混淆,我们已将所有 tRPC 选项移至名为 trpc
的属性。这个命名空间使特定于 tRPC 的选项变得清晰,并确保我们将来不会与 react-query
发生冲突。
¥To avoid collisions and confusion with any built-in react-query
properties, we have moved all of the tRPC options to a property called trpc
. This namespace brings clarity to options that are specific to tRPC and ensures that we won't collide with react-query
in the future.
tsx
// BeforeuseQuery(['post.byId', '1'], {context: {batching: false,},});// After:useQuery(['post.byId', '1'], {trpc: {context: {batching: false,},},});// or:trpc.post.byId.useQuery('1', {trpc: {batching: false,},});
tsx
// BeforeuseQuery(['post.byId', '1'], {context: {batching: false,},});// After:useQuery(['post.byId', '1'], {trpc: {context: {batching: false,},},});// or:trpc.post.byId.useQuery('1', {trpc: {batching: false,},});
查询键值变化
¥Query key changes
如果你在应用中仅使用 tRPC 提供的 API,则迁移不会有任何问题👍但是,如果你直接使用 tanstack 查询客户端来执行诸如使用 queryClient.setQueriesData
更新多个 tRPC 生成的查询的查询数据之类的操作,你可能需要注意 !
¥If you only use the tRPC provided APIs in your app you will have no problems in
migrating 👍 However if you have been using the tanstack query client directly
to do things like update query data for multiple tRPC generated queries using
queryClient.setQueriesData
you may need to take note!
为了让我们为一些更高级的功能(如 整个路由失效)腾出空间,我们需要改变在幕后使用 tanstack 查询键的方式。
¥To allow us to make room for some more advanced features like invalidation across whole routers, we needed to change how we use tanstack query keys under the hood.
我们已将使用的查询键从使用 .
连接字符串作为过程路径更改为元素子数组。当 query
和 infinite
查询放入缓存时,我们还添加了它们之间的区别。我们还将此查询 type
和输入移动到具有命名属性的对象中。
¥We have changed the query keys we use from using a .
joined string for the
procedure path to a sub array of elements. We have also added a distinction
between query
's and infinite
queries when they are placed in the cache. We
have also moved both this query type
and the input into an object with named
properties.
给出下面的简单路由:
¥Given the simple router below:
tsx
export const appRouter = router({user: router({byId: publicProcedure.input(z.object({ id: z.number() })).query((opts) => ({ user: { id: opts.input.id } })),}),});
tsx
export const appRouter = router({user: router({byId: publicProcedure.input(z.object({ id: z.number() })).query((opts) => ({ user: { id: opts.input.id } })),}),});
用于 trpc.user.byId.useQuery({ id: 10 })
的查询键将更改:
¥The query key used for trpc.user.byId.useQuery({ id: 10 })
would change:
输入 V9:
["user.byId", { id: 10 }]
¥Key in V9:
["user.byId", { id: 10 }]
输入 v10:
[["user", "byId"],{ input: { id:10 }, type: 'query' }]
¥Key in v10:
[["user", "byId"],{ input: { id:10 }, type: 'query' }]
大多数开发者甚至不会注意到此更改,但对于直接使用 tanstack queryClient
操作 tRPC 生成的查询的一小部分人来说,他们将不得不更改他们过滤的密钥!
¥The majority of developers won't even notice this change, but for the small
minority that are using the tanstack queryClient
directly to manipulate tRPC
generated queries, they will have to change the key they are filtering on!
@trpc/client
中止过程
¥Aborting procedures
在 v9 中,.cancel()
方法用于中止过程。
¥In v9, the .cancel()
method was used to abort procedures.
对于 v10,我们已迁移到 AbortController Web API,以便更好地与 Web 标准保持一致。你将为查询提供 AbortSignal
并在其父级 AbortController
上调用 .abort()
,而不是调用 .cancel()
。
¥For v10, we have moved to the AbortController Web API to align better with web standards. Instead of calling .cancel()
, you'll give the query an AbortSignal
and call .abort()
on its parent AbortController
.
tsx
const ac = new AbortController();const helloQuery = client.greeting.query('KATT', { signal: ac.signal });// Abortingac.abort();
tsx
const ac = new AbortController();const helloQuery = client.greeting.query('KATT', { signal: ac.signal });// Abortingac.abort();
HTTP 特定选项从 TRPCClient
移至链接
¥HTTP-specific options moved from TRPCClient
to links
以前,HTTP 选项(如标头)直接放置在 createTRPCClient()
。然而,由于 tRPC 在技术上并不依赖于 HTTP 本身,因此我们已将它们从 TRPCClient
移至 httpLink
和 httpBatchLink
。
¥Previously, HTTP options (like headers) were placed straight onto your createTRPCClient()
. However, since tRPC is technically not tied to HTTP itself, we've moved these from the TRPCClient
to httpLink
and httpBatchLink
.
ts
// Before:import { createTRPCClient } from '@trpc/client';const client = createTRPCClient({url: '...',fetch: myFetchPonyfill,AbortController: myAbortControllerPonyfill,headers() {return {'x-foo': 'bar',};},});// After:import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';const client = createTRPCProxyClient({links: [httpBatchLink({url: '...',fetch: myFetchPonyfill,AbortController: myAbortControllerPonyfill,headers() {return {'x-foo': 'bar',};},})]});
ts
// Before:import { createTRPCClient } from '@trpc/client';const client = createTRPCClient({url: '...',fetch: myFetchPonyfill,AbortController: myAbortControllerPonyfill,headers() {return {'x-foo': 'bar',};},});// After:import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';const client = createTRPCProxyClient({links: [httpBatchLink({url: '...',fetch: myFetchPonyfill,AbortController: myAbortControllerPonyfill,headers() {return {'x-foo': 'bar',};},})]});
此更改也反映在 @trpc/server
包中,其中 http
相关导出以前是从主入口点导出的,但现在已移至其自己的 @trpc/server/http
入口点。
¥This change is also reflected in the @trpc/server
package, where http
related exports were previously exported from the main entrypoint but have now been moved to their own @trpc/server/http
entrypoint.
附加功能
¥Extras
删除拆卸选项
¥Removal of the teardown option
拆卸选项已被删除并且不再可用。
¥The teardown option has been removed and is no longer available.
createContext
返回类型
¥createContext
return type
createContext
函数不能再返回 null
或 undefined
。如果你没有使用自定义上下文,则必须返回一个空对象:
¥The createContext
function can no longer return either null
or undefined
. If you weren't using a custom context, you'll have to return an empty object:
diff
- createContext: () => null,+ createContext: () => ({}),
diff
- createContext: () => null,+ createContext: () => ({}),
queryClient
不再通过 tRPC 上下文公开
¥queryClient
is no longer exposed through tRPC context
tRPC 不再通过 trpc.useContext()
公开 queryClient
实例。如果你需要使用 queryClient
中的某些方法,请检查 trpc.useContext()
是否封装了它们 此处。如果 tRPC 尚未封装相应的方法,你可以从 @tanstack/react-query
导入 queryClient
并以这种方式使用它:
¥tRPC is no longer exposing the queryClient
instance through trpc.useContext()
. If you need to use some methods from queryClient
, check if trpc.useContext()
wraps them here. If tRPC doesn't wrap the respective method yet, you can import the queryClient
from @tanstack/react-query
and use it that way:
tsx
import { useQueryClient } from '@tanstack/react-query';const MyComponent = () => {const queryClient = useQueryClient();// ...};
tsx
import { useQueryClient } from '@tanstack/react-query';const MyComponent = () => {const queryClient = useQueryClient();// ...};
迁移自定义错误格式化程序
¥Migrate custom error formatters
你需要将 formatError()
的内容移至根 t
路由中。有关更多信息,请参阅 文档格式错误。
¥You will need to move the contents of your formatError()
into your root t
router. See the Error Formatting docs for more.