相信昨天簡單 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