メインコンテンツへスキップ
バージョン: 11.x

サーバーサイドコール

ホストされているのと同じサーバーからプロシージャを直接呼び出す必要がある場合があります。createCallerFactory()を使用すると、これを実現できます。これは、サーバーサイドコールやtRPCプロシージャの統合テストに役立ちます。

情報

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

Callerの作成

t.createCallerFactory関数を使用すると、任意のルーターのサーバーサイドCallerを作成できます。最初に呼び出したいルーターを引数としてcreateCallerFactoryを呼び出し、その後、次のプロシージャ呼び出しにコンテキストを渡せる関数が返されます。

基本的な例

投稿を一覧表示するクエリと投稿を追加するミューテーションを使用してルーターを作成し、各メソッドを呼び出します。

ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
 
type Context = {
foo: string;
};
 
const t = initTRPC.context<Context>().create();
 
const publicProcedure = t.procedure;
const { createCallerFactory, router } = t;
 
interface Post {
id: string;
title: string;
}
const posts: Post[] = [
{
id: '1',
title: 'Hello world',
},
];
const appRouter = router({
post: router({
add: publicProcedure
.input(
z.object({
title: z.string().min(2),
}),
)
.mutation((opts) => {
const post: Post = {
...opts.input,
id: `${Math.random()}`,
};
posts.push(post);
return post;
}),
list: publicProcedure.query(() => posts),
}),
});
 
// 1. create a caller-function for your router
const createCaller = createCallerFactory(appRouter);
 
// 2. create a caller using your `Context`
const caller = createCaller({
foo: 'bar',
});
 
// 3. use the caller to add and list posts
const addedPost = await caller.post.add({
title: 'How to make server-side call in tRPC',
});
 
const postList = await caller.post.list();
const postList: Post[]
ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
 
type Context = {
foo: string;
};
 
const t = initTRPC.context<Context>().create();
 
const publicProcedure = t.procedure;
const { createCallerFactory, router } = t;
 
interface Post {
id: string;
title: string;
}
const posts: Post[] = [
{
id: '1',
title: 'Hello world',
},
];
const appRouter = router({
post: router({
add: publicProcedure
.input(
z.object({
title: z.string().min(2),
}),
)
.mutation((opts) => {
const post: Post = {
...opts.input,
id: `${Math.random()}`,
};
posts.push(post);
return post;
}),
list: publicProcedure.query(() => posts),
}),
});
 
// 1. create a caller-function for your router
const createCaller = createCallerFactory(appRouter);
 
// 2. create a caller using your `Context`
const caller = createCaller({
foo: 'bar',
});
 
// 3. use the caller to add and list posts
const addedPost = await caller.post.add({
title: 'How to make server-side call in tRPC',
});
 
const postList = await caller.post.list();
const postList: Post[]

統合テストでの使用方法の例

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';
 
const t = initTRPC.create();
 
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' });
const result: string
ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
 
const t = initTRPC.create();
 
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' });
const result: string

ミューテーション例

ミューテーションを使用してルーターを作成し、非同期postプロシージャを呼び出して結果を取得します。

ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
 
const posts = ['One', 'Two', 'Three'];
 
const t = initTRPC.create();
const router = t.router({
post: t.router({
add: t.procedure.input(z.string()).mutation((opts) => {
posts.push(opts.input);
return posts;
}),
}),
});
 
const caller = router.createCaller({});
const result = await caller.post.add('Four');
const result: string[]
ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
 
const posts = ['One', 'Two', 'Three'];
 
const t = initTRPC.create();
const router = t.router({
post: t.router({
add: t.procedure.input(z.string()).mutation((opts) => {
posts.push(opts.input);
return posts;
}),
}),
});
 
const caller = router.createCaller({});
const result = await caller.post.add('Four');
const result: string[]

ミドルウェアを使用したコンテキスト例

secretプロシージャを実行する前にコンテキストをチェックするミドルウェアを作成します。以下に2つの例を示します。前者はコンテキストがミドルウェアのロジックに適合しないため失敗し、後者は正しく機能します。


情報

ミドルウェアは、プロシージャが呼び出される前に実行されます。


ts
import { initTRPC, TRPCError } from '@trpc/server';
 
type Context = {
user?: {
id: string;
};
};
const t = initTRPC.context<Context>().create();
 
const protectedProcedure = t.procedure.use((opts) => {
const { ctx } = opts;
if (!ctx.user) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'You are not authorized',
});
}
 
return opts.next({
ctx: {
// Infers that the `user` is non-nullable
user: ctx.user,
},
});
});
 
const router = t.router({
secret: protectedProcedure.query((opts) => opts.ctx.user),
});
 
{
// ❌ this will return an error because there isn't the right context param
const caller = router.createCaller({});
 
const result = await caller.secret();
}
 
{
// ✅ this will work because user property is present inside context param
const authorizedCaller = router.createCaller({
user: {
id: 'KATT',
},
});
const result = await authorizedCaller.secret();
const result: { id: string; }
}
ts
import { initTRPC, TRPCError } from '@trpc/server';
 
type Context = {
user?: {
id: string;
};
};
const t = initTRPC.context<Context>().create();
 
const protectedProcedure = t.procedure.use((opts) => {
const { ctx } = opts;
if (!ctx.user) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'You are not authorized',
});
}
 
return opts.next({
ctx: {
// Infers that the `user` is non-nullable
user: ctx.user,
},
});
});
 
const router = t.router({
secret: protectedProcedure.query((opts) => opts.ctx.user),
});
 
{
// ❌ this will return an error because there isn't the right context param
const caller = router.createCaller({});
 
const result = await caller.secret();
}
 
{
// ✅ this will work because user property is present inside context param
const authorizedCaller = router.createCaller({
user: {
id: 'KATT',
},
});
const result = await authorizedCaller.secret();
const result: { id: string; }
}

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';
 
type ResponseData = {
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. */
const postId = `this-id-does-not-exist-${Math.random()}`;
 
const caller = appRouter.createCaller({});
 
try {
// the server-side call
const postResult = await caller.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 instanceof TRPCError) {
// We can get the specific HTTP status code coming from tRPC (e.g. 404 for `NOT_FOUND`).
const httpStatusCode = 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';
 
type ResponseData = {
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. */
const postId = `this-id-does-not-exist-${Math.random()}`;
 
const caller = appRouter.createCaller({});
 
try {
// the server-side call
const postResult = await caller.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 instanceof TRPCError) {
// We can get the specific HTTP status code coming from tRPC (e.g. 404 for `NOT_FOUND`).
const httpStatusCode = 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 context
error: TRPCError; // The TRPCError that was thrown
path: string | undefined; // The path of the procedure that threw the error
input: unknown; // The input that was passed to the procedure
type: 'query' | 'mutation' | 'subscription' | 'unknown'; // The type of the procedure that threw the error
}
ts
{
ctx: unknown; // The request context
error: TRPCError; // The TRPCError that was thrown
path: string | undefined; // The path of the procedure that threw the error
input: unknown; // The input that was passed to the procedure
type: 'query' | 'mutation' | 'subscription' | 'unknown'; // The type of the procedure that threw the error
}
ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
 
const t = initTRPC
.context<{
foo?: 'bar';
}>()
.create();
 
const router = t.router({
greeting: t.procedure.input(z.object({ name: z.string() })).query((opts) => {
if (opts.input.name === 'invalid') {
throw new Error('Invalid name');
}
 
return `Hello ${opts.input.name}`;
}),
});
 
const caller = 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"
await caller.greeting({ name: 'invalid' });
ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
 
const t = initTRPC
.context<{
foo?: 'bar';
}>()
.create();
 
const router = t.router({
greeting: t.procedure.input(z.object({ name: z.string() })).query((opts) => {
if (opts.input.name === 'invalid') {
throw new Error('Invalid name');
}
 
return `Hello ${opts.input.name}`;
}),
});
 
const caller = 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"
await caller.greeting({ name: 'invalid' });