相信昨天簡單 demo trpc的使用後,多多少少應該知道他的精髓吧哈哈,今天內容會是 trpc 的 context 運作流程,與其他相關的 helper function 介紹,讀者可以參考看看~
從昨天的例子可以知道 trpc context 裡面包含所有 Procedure 會用到的 function,例如 :
const t = initTRPC.context().create();
const publicProcedure = t.procedure
const createTRPCRouter = t.router;
在 trpc 中可以透過 createContext() 共享 context ,如果讀者不懂 context 可以看Day4 的內容,createContext 通常用於 handle db 的 connections,或是一些 authentication 資訊如以下的 demo
import { initTRPC, type inferAsyncReturnType } from '@trpc/server';
import type { CreateNextContextOptions } from '@trpc/server/adapters/next';
import { getSession } from 'next-auth/react';
export const createContext = async (opts: CreateNextContextOptions) => {
const session = await getSession({ req: opts.req });
return {
session,
};
};
const t1 = initTRPC.context<typeof createContext>().create();
這邊我們拿取 next-auth 的 session 放到我們 context 中,值得注意的是因為每次呼叫 procedure,trpc 同時也會 call createContext function,所以如果讀者有用到以下的功能的話記得要把 createContext 也放進去喔
// 1. HTTP request
import { createHTTPHandler } from '@trpc/server/adapters/standalone';
import { createContext } from './context';
import { appRouter } from './router';
const handler = createHTTPHandler({
router: appRouter,
createContext,
});
// 2. Server-side call
import { createContext } from './context';
import { appRouter } from './router';
const caller = appRouter.createCaller(await createContext());
// 3. servers-side helpers
import { createServerSideHelpers } from '@trpc/react-query/server';
import { createContext } from './context';
import { appRouter } from './router';
const helpers = createServerSideHelpers({
router: appRouter,
ctx: await createContext(),
});
但如果不需要 context 的話 return {} 就 ok~,下面一一介紹這三個功能。
透過 createHTTPHandler 創建 handler 搭配 createServer 成功 create http 連線 。
import { createServer } from 'http';
import { inferAsyncReturnType, initTRPC } from '@trpc/server';
import { createHTTPHandler } from '@trpc/server/adapters/standalone';
const handler = createHTTPHandler({
router: appRouter,
createContext() {
return {};
},
});
createServer((req, res) => {
/**
* Handle the request however you like,
* just call the tRPC handler when you're ready
*/
handler(req, res);
});
server.listen(3333);
但在 next 中你只需要使用 createNextApiHandler 包在你的 api route 就可以~
// /pages/api/trpc/[trpc].ts
import { createNextApiHandler } from '@trpc/server/adapters/next';
import { appRouter } from '@/server/api/root';
// @see https://nextjs.org/docs/api-routes/introduction
export default createNextApiHandler({
router: appRouter,
createContext: () => ({}),
});
一個可以讓你在其他 server 呼叫的功能,確保你定義的 trpc route 執行是沒問題的。
const router = t.router({
// Create procedure at path 'greeting'
greeting: t.procedure
.input(z.object({ name: z.string() }))
.query((opts) => `Hello ${opts.input.name}`),
});
const caller = router.createCaller({});
const result = await caller.greeting({ name: 'trpc' }) // Hello trpc
甚至如果是 mutate 的話
const posts = ['One', 'Two', 'Three'];
const t = initTRPC.create();
const router = t.router({
post: t.router({
add:
t.procedure
.input(z.string())
.mutation(({ input }) => {
posts.push(input)
return posts
})
})
})
const postCaller = router.createCaller({})
const result = await postCaller.post.add('Four') // [ 'One', 'Two', 'Three', 'Four' ]
看到這邊是不是覺得有那種 RPC 的 feeling 了呢!!,相信各個讀者都是很聰明的如果我們可以透過 call 去呼叫 trpc route ,那時不是我們可以透過 next api router 去呼叫呢?沒錯當然是沒問題拉~~來 demo 走起!
這邊寫一個 router 是關於 find post 的 api,透過 input 的 title 去找到對應的 post,如果沒有找到 post 就 throw not found error
// "~/server/api/routers/example"
const postTitles = ['title1', 'title2', 'title3'];
const t = initTRPC.create();
export const helperRouter = t.router({
post: t.router({
getByTitle:
t.procedure
.input(z.object({
title: z.string({
required_error: 'postTitle is required',
invalid_type_error: 'postTitle type is invalidate'
})
}))
.query(({ input, ctx }) => {
const { title } = input
const post = postTitles.find(postTitle => postTitle === title)
if (!post?.length) {
throw new TRPCError({ code: 'NOT_FOUND', message: 'post not found' })
}
return { post, createAt: new Date() }
})
})
})
export const postCaller = helperRouter.createCaller({})
// "~/server/api/root"
export const appRouter = createTRPCRouter({
example: exampleRouter,
helper: helperRouter
});
https://trpc.io/docs/server/error-handling
之後把 caller 放到 next api route 中,這邊我們定義一個 api. endpoint http://localhost:3000/api/trpccaller?postTitle=title1。
end point : /api/trpccallerquery : postTitle
整個邏輯很簡單,透過 caller 去呼叫 getByTitle ,同時把 query 帶進去,如果有找到就 return value,如果有 error 就去判斷是不是 trpc error,再根據 TRPCError error format 去 return result,這也是為什麼推薦在 trpc 用 TRPCError 做 error handler,因為你不管是 server 端還是 caller 端甚至是日後在 client 端呼叫時有一個通用的 error format 可以參照,不用怕其他 api error 不同,最後要記得加 500 error 的情況喔~
getHTTPStatusCodeFromError : TRPC 的 caller 只會回傳 response data ,可以透過 getHTTPStatusCodeFromError 來獲取 status 訊息 。
import { TRPCError } from "@trpc/server";
import { getHTTPStatusCodeFromError } from "@trpc/server/http";
import { NextApiRequest, NextApiResponse } from "next"
import { postCaller } from "~/server/api/routers/example"
import { RouterOutputs } from "~/utils/api";
type ResponseData = {
data?: {
// 這邊遇到一個 bug ,預期 createAt 是 Date type,但結果是 string,所以先用 Omit自行註解
result: Omit<RouterOutputs['helper']['post']['getByTitle'], 'createAt'> & { createAt: Date };
};
error?: {
message: string;
};
};
// title1
export default async (
req: NextApiRequest,
res: NextApiResponse<ResponseData>,
) => {
const postTitle = req.query.postTitle as string
try {
const result = await postCaller.post.getByTitle({ title: postTitle })
res.status(200).json({ data: { result } });
} catch (e) {
if (e instanceof TRPCError) {
const errorMessage = formatTrpcError(e)
const httpStatusCode = getHTTPStatusCodeFromError(e);
res.status(httpStatusCode).json({ error: { message: errorMessage } });
}
res.status(500).json({
error: { message: `Error while accessing post with title: ${postTitle}` },
})
}
}
interface ErrorMessage {
code: string;
expected: string;
received: string;
path: string[];
message: string;
}
const formatTrpcError = (e: TRPCError) => {
const { code, message } = e
if (code === 'BAD_REQUEST') {
const errorResults: ErrorMessage[] = JSON.parse(e.cause?.message as string)
const { code, expected, received, message } = errorResults[0] as ErrorMessage
return `${message} ,expected ${expected} received ${received} `
}
return message
}
RouterOutputs 是一個幫助你查詢 api result 的 interface,可以定義 input 跟 output 結果。
/**
* Inference helper for inputs.
*
* @example type HelloInput = RouterInputs['example']['hello']
*/
export type RouterInputs = inferRouterInputs<AppRouter>;
/**
* Inference helper for outputs.
*
* @example type HelloOutput = RouterOutputs['example']['hello']
*/
export type RouterOutputs = inferRouterOutputs<AppRouter>;
同時 trpc 很貼心的是有先定義好 error format TRPCError , 返回格式如下:
{
"id": null,
"error": {
"message": "\"password\" must be at least 4 characters",
"code": -32600,
"data": {
"code": "BAD_REQUEST",
"httpStatus": 400,
"stack": "...",
"path": "user.changepassword"
}
}
}
所以我們就可以在 formatTrpcError 根據 TRPCError 格式處理 error message。
如果想知道其他 error code 可以參考 官網
最後放上結果~




Server Side helpers 的目的就是 prefetch queries result 在 server 上,通常會是在 next 的 ssr 或是 ssg 上做使用,prefetch 目的是 populating query cache on the server,這樣的好處是可以優化 client 端在在呼叫 useQuery 時 call api的時間,本來是透過 http request 改成拿 query cahce 資料,這是一種前端優化效能的方式,這樣 client 端可以更快載入頁面內容,prefetch 一直是前端性能很大的輔助工具,可以提高 FCP(First Contentful Paint) 用戶體驗。
demo 如下:
import { GetServerSideProps, InferGetServerSidePropsType } from 'next'
import { createServerSideHelpers } from '@trpc/react-query/server';
import React from 'react'
import { helperRouter } from '~/server/api/routers/example';
import superjson from 'superjson';
import { api } from '~/utils/api';
export const getServerSideProps: GetServerSideProps<{ title: string }> = async (context) => {
const title = context.params?.title as string
// 所有 ssg 內容都是透過 createServerSideHelpers 產生
const helpers = createServerSideHelpers({
router: helperRouter,
ctx: {},
transformer: superjson
})
// prefetch 並不會 return result
await helpers.post.getByTitle.prefetch({ title })
return {
props: {
trpcState: helpers.dehydrate(),
title
}
}
}
function Page({ title }: InferGetServerSidePropsType<typeof getServerSideProps>) {
const { data, status, ...rest } = api.helper.post.getByTitle.useQuery({ title })
if (status === 'loading') return 'loading...'
if (data === undefined) return 'no data'
return (
<div>
{data?.post ? data.post : 'no title'}
<em>CreateAt : {new Date(data?.createAt).toLocaleDateString()}</em>
</div>
)
}
export default Page
createServerSideHelpers : 所有 ssr 的 method 都是透過他使用。helpers.dehydrate() : 這一行比較特別的是一定要加,原因有點小複雜,簡單來說在 ssr 中呼叫 trpc 得方式跟在 client 不同的點在於,client 端是透過 fetch 的方式 ,ssr 則是 prefetch,差別在於 fetch 會 return 結果,但 prefetch 並不會甚至不會 throw error ,而如果必須把 prefetch 的內容放到 cache 中,那就必須把 prefetch 結果或序列化後傳到 client 端,也就是透過 helpers.dehydrate 方式 ,使用 prefetch 一定要加這段。
在 context 中你可以指定 request input 或是 output 序列化的方式確保 value 不會因為 js 做型別轉換,例如 new Date() 變成 string。
npm i superjson devalue
// src/transformer.ts
import { uneval } from 'devalue';
import superjson from 'superjson';
export const transformer = {
input: superjson,
output: {
serialize: (object) => uneval(object),
// This `eval` only ever happens on the **client**
deserialize: (object) => eval(`(${object})`),
},
};
import { initTRPC } from '@trpc/server';
import { transformer } from '../../utils/trpc';
export const t = initTRPC.create({
transformer,
});
export const appRouter = t.router({
// [...]
});
export const api = createTRPCNext<AppRouter>({
config() {
return {
/**
* Transformer used for data de-serialization from the server.
*
* @see https://trpc.io/docs/data-transformers
*/
transformer: superjson,
...
}
})
✅ 前端社群 :
https://lihi3.cc/kBe0Y