サーバーサイドコール
ホストされているのと同じサーバーからプロシージャを直接呼び出す必要がある場合があります。createCallerFactory()
を使用すると、これを実現できます。これは、サーバーサイドコールやtRPCプロシージャの統合テストに役立ちます。
createCaller
は、他のプロシージャ内からプロシージャを呼び出すために使用しないでください。これにより、(可能性として)コンテキストの再作成、すべてのミドルウェアの実行、入力の検証というオーバーヘッドが発生します。これらはすべて、現在のプロシージャですでに実行されています。代わりに、共有ロジックを別の関数に抽出し、次のようにプロシージャ内からその関数呼び出す必要があります。


Callerの作成
t.createCallerFactory
関数を使用すると、任意のルーターのサーバーサイドCallerを作成できます。最初に呼び出したいルーターを引数としてcreateCallerFactory
を呼び出し、その後、次のプロシージャ呼び出しにコンテキスト
を渡せる関数が返されます。
基本的な例
投稿を一覧表示するクエリと投稿を追加するミューテーションを使用してルーターを作成し、各メソッドを呼び出します。
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({})
関数(最初の引数はコンテキスト
)を使用して、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
プロシージャを実行する前にコンテキストをチェックするミドルウェアを作成します。以下に2つの例を示します。前者はコンテキストがミドルウェアのロジックに適合しないため失敗し、後者は正しく機能します。
ミドルウェアは、プロシージャが呼び出される前に実行されます。
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エンドポイントでCallerを使用する方法を示しています。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`に渡されたハンドラーの前に呼び出されます。ハンドラーは、shapeフィールドを除いて、エラーフォーマッターと同じ引数で呼び出されます。
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' });