服务器端调用
您可能需要直接从托管它们的同一服务器调用您的过程,createCallerFactory()
可用于实现此目的。这对于服务器端调用和 tRPC 过程的集成测试很有用。
createCaller
不应用于从其他过程内部调用过程。这会通过(可能)再次创建上下文、执行所有中间件和验证输入来产生开销 - 所有这些操作在当前过程中已经完成。相反,您应该将共享逻辑提取到一个单独的函数中,并从过程内部调用该函数,如下所示
创建调用者
使用 t.createCallerFactory
函数,您可以创建任何路由器的服务器端调用者。您首先使用要调用的路由器作为参数调用 createCallerFactory
,然后它返回一个函数,您可以在其中传入 Context
以进行后续过程调用。
基本示例
我们使用查询创建路由器以列出帖子,并使用变异来添加帖子,然后我们调用每个方法。
ts
import {initTRPC } from '@trpc/server';import {z } from 'zod';typeContext = {foo : string;};constt =initTRPC .context <Context >().create ();constpublicProcedure =t .procedure ;const {createCallerFactory ,router } =t ;interfacePost {id : string;title : string;}constposts :Post [] = [{id : '1',title : 'Hello world',},];constappRouter =router ({post :router ({add :publicProcedure .input (z .object ({title :z .string ().min (2),}),).mutation ((opts ) => {constpost :Post = {...opts .input ,id : `${Math .random ()}`,};posts .push (post );returnpost ;}),list :publicProcedure .query (() =>posts ),}),});// 1. create a caller-function for your routerconstcreateCaller =createCallerFactory (appRouter );// 2. create a caller using your `Context`constcaller =createCaller ({foo : 'bar',});// 3. use the caller to add and list postsconstaddedPost = awaitcaller .post .add ({title : 'How to make server-side call in tRPC',});constpostList = awaitcaller .post .list ();
ts
import {initTRPC } from '@trpc/server';import {z } from 'zod';typeContext = {foo : string;};constt =initTRPC .context <Context >().create ();constpublicProcedure =t .procedure ;const {createCallerFactory ,router } =t ;interfacePost {id : string;title : string;}constposts :Post [] = [{id : '1',title : 'Hello world',},];constappRouter =router ({post :router ({add :publicProcedure .input (z .object ({title :z .string ().min (2),}),).mutation ((opts ) => {constpost :Post = {...opts .input ,id : `${Math .random ()}`,};posts .push (post );returnpost ;}),list :publicProcedure .query (() =>posts ),}),});// 1. create a caller-function for your routerconstcreateCaller =createCallerFactory (appRouter );// 2. create a caller using your `Context`constcaller =createCaller ({foo : 'bar',});// 3. use the caller to add and list postsconstaddedPost = awaitcaller .post .add ({title : 'How to make server-side call in tRPC',});constpostList = awaitcaller .post .list ();
集成测试中的示例用法
摘自 https://github.com/trpc/examples-next-prisma-starter/blob/main/src/server/routers/post.test.ts
ts
import { inferProcedureInput } from '@trpc/server';import { createContextInner } from '../context';import { AppRouter, createCaller } from './_app';test('add and get post', async () => {const ctx = await createContextInner({});const caller = createCaller(ctx);const input: inferProcedureInput<AppRouter['post']['add']> = {text: 'hello test',title: 'hello test',};const post = await caller.post.add(input);const byId = await caller.post.byId({ id: post.id });expect(byId).toMatchObject(input);});
ts
import { inferProcedureInput } from '@trpc/server';import { createContextInner } from '../context';import { AppRouter, createCaller } from './_app';test('add and get post', async () => {const ctx = await createContextInner({});const caller = createCaller(ctx);const input: inferProcedureInput<AppRouter['post']['add']> = {text: 'hello test',title: 'hello test',};const post = await caller.post.add(input);const byId = await caller.post.byId({ id: post.id });expect(byId).toMatchObject(input);});
router.createCaller()
router.createCaller()
已被弃用,将在 tRPC 的 v11 或 v12 中删除。
使用 router.createCaller({})
函数(第一个参数是 Context
),我们检索 RouterCaller
的实例。
输入查询示例
我们使用输入查询创建路由器,然后调用异步 greeting
过程以获取结果。
ts
import {initTRPC } from '@trpc/server';import {z } from 'zod';constt =initTRPC .create ();constrouter =t .router ({// Create procedure at path 'greeting'greeting :t .procedure .input (z .object ({name :z .string () })).query ((opts ) => `Hello ${opts .input .name }`),});constcaller =router .createCaller ({});constresult = awaitcaller .greeting ({name : 'tRPC' });
ts
import {initTRPC } from '@trpc/server';import {z } from 'zod';constt =initTRPC .create ();constrouter =t .router ({// Create procedure at path 'greeting'greeting :t .procedure .input (z .object ({name :z .string () })).query ((opts ) => `Hello ${opts .input .name }`),});constcaller =router .createCaller ({});constresult = awaitcaller .greeting ({name : 'tRPC' });
变异示例
我们使用变异创建路由器,然后调用异步 post
过程以获取结果。
ts
import {initTRPC } from '@trpc/server';import {z } from 'zod';constposts = ['One', 'Two', 'Three'];constt =initTRPC .create ();constrouter =t .router ({post :t .router ({add :t .procedure .input (z .string ()).mutation ((opts ) => {posts .push (opts .input );returnposts ;}),}),});constcaller =router .createCaller ({});constresult = awaitcaller .post .add ('Four');
ts
import {initTRPC } from '@trpc/server';import {z } from 'zod';constposts = ['One', 'Two', 'Three'];constt =initTRPC .create ();constrouter =t .router ({post :t .router ({add :t .procedure .input (z .string ()).mutation ((opts ) => {posts .push (opts .input );returnposts ;}),}),});constcaller =router .createCaller ({});constresult = awaitcaller .post .add ('Four');
带有中间件的上下文示例
我们创建一个中间件来在执行 secret
过程之前检查上下文。以下是两个示例:前者失败,因为上下文不符合中间件逻辑,而后者则正常工作。
中间件在调用任何过程之前执行。
ts
import {initTRPC ,TRPCError } from '@trpc/server';typeContext = {user ?: {id : string;};};constt =initTRPC .context <Context >().create ();constprotectedProcedure =t .procedure .use ((opts ) => {const {ctx } =opts ;if (!ctx .user ) {throw newTRPCError ({code : 'UNAUTHORIZED',message : 'You are not authorized',});}returnopts .next ({ctx : {// Infers that the `user` is non-nullableuser :ctx .user ,},});});constrouter =t .router ({secret :protectedProcedure .query ((opts ) =>opts .ctx .user ),});{// ❌ this will return an error because there isn't the right context paramconstcaller =router .createCaller ({});constresult = awaitcaller .secret ();}{// ✅ this will work because user property is present inside context paramconstauthorizedCaller =router .createCaller ({user : {id : 'KATT',},});constresult = awaitauthorizedCaller .secret ();}
ts
import {initTRPC ,TRPCError } from '@trpc/server';typeContext = {user ?: {id : string;};};constt =initTRPC .context <Context >().create ();constprotectedProcedure =t .procedure .use ((opts ) => {const {ctx } =opts ;if (!ctx .user ) {throw newTRPCError ({code : 'UNAUTHORIZED',message : 'You are not authorized',});}returnopts .next ({ctx : {// Infers that the `user` is non-nullableuser :ctx .user ,},});});constrouter =t .router ({secret :protectedProcedure .query ((opts ) =>opts .ctx .user ),});{// ❌ this will return an error because there isn't the right context paramconstcaller =router .createCaller ({});constresult = awaitcaller .secret ();}{// ✅ this will work because user property is present inside context paramconstauthorizedCaller =router .createCaller ({user : {id : 'KATT',},});constresult = awaitauthorizedCaller .secret ();}
Next.js API 端点的示例
此示例展示了如何在 Next.js API 端点中使用调用者。tRPC 已经为您创建了 API 端点,因此此文件仅用于展示如何从另一个自定义端点调用过程。
ts
import {TRPCError } from '@trpc/server';import {getHTTPStatusCodeFromError } from '@trpc/server/http';import {appRouter } from '~/server/routers/_app';import type {NextApiRequest ,NextApiResponse } from 'next';typeResponseData = {data ?: {postTitle : string;};error ?: {message : string;};};export default async (req :NextApiRequest ,res :NextApiResponse <ResponseData >,) => {/** We want to simulate an error, so we pick a post ID that does not exist in the database. */constpostId = `this-id-does-not-exist-${Math .random ()}`;constcaller =appRouter .createCaller ({});try {// the server-side callconstpostResult = awaitcaller .post .byId ({id :postId });res .status (200).json ({data : {postTitle :postResult .title } });} catch (cause ) {// If this a tRPC error, we can extract additional information.if (cause instanceofTRPCError ) {// We can get the specific HTTP status code coming from tRPC (e.g. 404 for `NOT_FOUND`).consthttpStatusCode =getHTTPStatusCodeFromError (cause );res .status (httpStatusCode ).json ({error : {message :cause .message } });return;}// This is not a tRPC error, so we don't have specific information.res .status (500).json ({error : {message : `Error while accessing post with ID ${postId }` },});}};
ts
import {TRPCError } from '@trpc/server';import {getHTTPStatusCodeFromError } from '@trpc/server/http';import {appRouter } from '~/server/routers/_app';import type {NextApiRequest ,NextApiResponse } from 'next';typeResponseData = {data ?: {postTitle : string;};error ?: {message : string;};};export default async (req :NextApiRequest ,res :NextApiResponse <ResponseData >,) => {/** We want to simulate an error, so we pick a post ID that does not exist in the database. */constpostId = `this-id-does-not-exist-${Math .random ()}`;constcaller =appRouter .createCaller ({});try {// the server-side callconstpostResult = awaitcaller .post .byId ({id :postId });res .status (200).json ({data : {postTitle :postResult .title } });} catch (cause ) {// If this a tRPC error, we can extract additional information.if (cause instanceofTRPCError ) {// We can get the specific HTTP status code coming from tRPC (e.g. 404 for `NOT_FOUND`).consthttpStatusCode =getHTTPStatusCodeFromError (cause );res .status (httpStatusCode ).json ({error : {message :cause .message } });return;}// This is not a tRPC error, so we don't have specific information.res .status (500).json ({error : {message : `Error while accessing post with ID ${postId }` },});}};
错误处理
createFactoryCaller
和 createCaller
函数可以通过 onError
选项接受错误处理程序。这可用于抛出未包装在 TRPCError 中的错误,或以其他方式响应错误。传递给 createCallerFactory 的任何处理程序都将在传递给 createCaller 的处理程序之前被调用。处理程序的调用参数与错误格式化程序相同,但形状字段除外
ts
{ctx: unknown; // The request contexterror: TRPCError; // The TRPCError that was thrownpath: string | undefined; // The path of the procedure that threw the errorinput: unknown; // The input that was passed to the proceduretype: 'query' | 'mutation' | 'subscription' | 'unknown'; // The type of the procedure that threw the error}
ts
{ctx: unknown; // The request contexterror: TRPCError; // The TRPCError that was thrownpath: string | undefined; // The path of the procedure that threw the errorinput: unknown; // The input that was passed to the proceduretype: 'query' | 'mutation' | 'subscription' | 'unknown'; // The type of the procedure that threw the error}
ts
import {initTRPC } from '@trpc/server';import {z } from 'zod';constt =initTRPC .context <{foo ?: 'bar';}>().create ();constrouter =t .router ({greeting :t .procedure .input (z .object ({name :z .string () })).query ((opts ) => {if (opts .input .name === 'invalid') {throw newError ('Invalid name');}return `Hello ${opts .input .name }`;}),});constcaller =router .createCaller ({/* context */},{onError : (opts ) => {console .error ('An error occurred:',opts .error );},},);// The following will log "An error occurred: Error: Invalid name", and then throw a plain error// with the message "This is a custom error"awaitcaller .greeting ({name : 'invalid' });
ts
import {initTRPC } from '@trpc/server';import {z } from 'zod';constt =initTRPC .context <{foo ?: 'bar';}>().create ();constrouter =t .router ({greeting :t .procedure .input (z .object ({name :z .string () })).query ((opts ) => {if (opts .input .name === 'invalid') {throw newError ('Invalid name');}return `Hello ${opts .input .name }`;}),});constcaller =router .createCaller ({/* context */},{onError : (opts ) => {console .error ('An error occurred:',opts .error );},},);// The following will log "An error occurred: Error: Invalid name", and then throw a plain error// with the message "This is a custom error"awaitcaller .greeting ({name : 'invalid' });