서버 사이드 호출
호스팅 중인 서버에서 프로시저를 직접 호출해야 하는 경우가 있을 수 있으며, createCallerFactory()를 사용하여 이를 달성할 수 있습니다. 이는 서버 사이드 호출 및 tRPC 프로시저의 통합 테스트에 유용합니다.
createCaller는 다른 프로시저 내에서 프로시저를 호출하는 데 사용해서는 안 됩니다. 이는 컨텍스트를 (잠재적으로) 다시 생성하고, 모든 미들웨어를 실행하며, 입력을 검증하는 등 현재 프로시저에서 이미 수행된 작업들을 반복함으로써 오버헤드를 발생시키기 때문입니다. 대신, 공유 로직을 별도의 함수로 추출하여 프로시저 내에서 해당 함수를 호출해야 합니다. 예시는 다음과 같습니다:


콜러 생성
t.createCallerFactory 함수를 사용하면 어떤 라우터의 서버 사이드 콜러를든 생성할 수 있습니다. 먼저 호출하려는 라우터를 인수로 전달하여 createCallerFactory를 호출하면, 이후 프로시저 호출에 Context를 전달할 수 있는 함수가 반환됩니다.
기본 예시
게시글 목록을 조회하는 쿼리와 게시글을 추가하는 뮤테이션으로 라우터를 생성한 후, 각 메서드를 호출합니다.
tsimport {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 ();
tsimport {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에서 가져옴
tsasync functiontestAddAndGetPost () {constctx = awaitcreateContextInner ({});constcaller =createCaller (ctx );constinput :inferProcedureInput <AppRouter ['post']['add']> = {text : 'hello test',title : 'hello test',};constpost = awaitcaller .post .add (input );constbyId = awaitcaller .post .byId ({id :post .id });}
tsasync functiontestAddAndGetPost () {constctx = awaitcreateContextInner ({});constcaller =createCaller (ctx );constinput :inferProcedureInput <AppRouter ['post']['add']> = {text : 'hello test',title : 'hello test',};constpost = awaitcaller .post .add (input );constbyId = awaitcaller .post .byId ({id :post .id });}
router.createCaller()
router.createCaller({}) 함수(첫 번째 인수는 Context)를 사용하여 RouterCaller의 인스턴스를 가져옵니다.
입력 쿼리 예시
입력을 가진 쿼리로 라우터를 생성한 후, 비동기 greeting 프로시저를 호출하여 결과를 가져옵니다.
tsimport {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' });
tsimport {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 프로시저를 호출하여 결과를 가져옵니다.
tsimport {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');
tsimport {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 프로시저를 실행하기 전에 컨텍스트를 확인하는 미들웨어를 생성합니다. 아래는 두 가지 예시입니다: 전자는 컨텍스트가 미들웨어 로직에 부합하지 않아 실패하고, 후자는 올바르게 작동합니다.
미들웨어는 프로시저가 호출되기 전에 수행됩니다.
tsimport {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 ();}
tsimport {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 엔드포인트를 생성해 주므로, 이 파일은 다른 사용자 정의 엔드포인트에서 프로시저를 호출하는 방법을 보여주는 데만 사용됩니다.
tstypeResponseData = {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 }` },});}};
tstypeResponseData = {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 }` },});}};
오류 처리
createCallerFactory와 createCaller 함수는 onError 옵션을 통해 오류 핸들러를 받을 수 있습니다. 이를 사용하여 TRPCError로 감싸지지 않은 오류를 발생시키거나, 다른 방식으로 오류에 응답할 수 있습니다. createCallerFactory에 전달된 핸들러는 createCaller에 전달된 핸들러보다 먼저 호출됩니다.
핸들러는 shape 필드를 제외하고 오류 포맷터와 동일한 인수로 호출됩니다:
tsinterfaceOnErrorShape {ctx : unknown;error :TRPCError ;path : string | undefined;input : unknown;type : 'query' | 'mutation' | 'subscription' | 'unknown';}
tsinterfaceOnErrorShape {ctx : unknown;error :TRPCError ;path : string | undefined;input : unknown;type : 'query' | 'mutation' | 'subscription' | 'unknown';}
tsimport {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 the errorawaitcaller .greeting ({name : 'invalid' });
tsimport {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 the errorawaitcaller .greeting ({name : 'invalid' });