본문으로 건너뛰기
버전: 11.x

서버 사이드 호출

호스팅 중인 서버에서 프로시저를 직접 호출해야 하는 경우가 있을 수 있으며, createCallerFactory()를 사용하여 이를 달성할 수 있습니다. 이는 서버 사이드 호출 및 tRPC 프로시저의 통합 테스트에 유용합니다.

정보

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

콜러 생성

t.createCallerFactory 함수를 사용하면 어떤 라우터의 서버 사이드 콜러를든 생성할 수 있습니다. 먼저 호출하려는 라우터를 인수로 전달하여 createCallerFactory를 호출하면, 이후 프로시저 호출에 Context를 전달할 수 있는 함수가 반환됩니다.

기본 예시

게시글 목록을 조회하는 쿼리와 게시글을 추가하는 뮤테이션으로 라우터를 생성한 후, 각 메서드를 호출합니다.

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
async function testAddAndGetPost() {
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 });
}
ts
async function testAddAndGetPost() {
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 });
}

router.createCaller()

router.createCaller({}) 함수(첫 번째 인수는 Context)를 사용하여 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 프로시저를 실행하기 전에 컨텍스트를 확인하는 미들웨어를 생성합니다. 아래는 두 가지 예시입니다: 전자는 컨텍스트가 미들웨어 로직에 부합하지 않아 실패하고, 후자는 올바르게 작동합니다.


정보

미들웨어는 프로시저가 호출되기 전에 수행됩니다.


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 엔드포인트에서 콜러를 사용하는 방법을 보여줍니다. tRPC는 이미 API 엔드포인트를 생성해 주므로, 이 파일은 다른 사용자 정의 엔드포인트에서 프로시저를 호출하는 방법을 보여주는 데만 사용됩니다.

ts
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
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}` },
});
}
};

오류 처리

createCallerFactorycreateCaller 함수는 onError 옵션을 통해 오류 핸들러를 받을 수 있습니다. 이를 사용하여 TRPCError로 감싸지지 않은 오류를 발생시키거나, 다른 방식으로 오류에 응답할 수 있습니다. createCallerFactory에 전달된 핸들러는 createCaller에 전달된 핸들러보다 먼저 호출됩니다. 핸들러는 shape 필드를 제외하고 오류 포맷터와 동일한 인수로 호출됩니다:

ts
interface OnErrorShape {
ctx: unknown;
error: TRPCError;
path: string | undefined;
input: unknown;
type: 'query' | 'mutation' | 'subscription' | 'unknown';
}
ts
interface OnErrorShape {
ctx: unknown;
error: TRPCError;
path: string | undefined;
input: unknown;
type: 'query' | 'mutation' | 'subscription' | 'unknown';
}
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 the 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 the error
await caller.greeting({ name: 'invalid' });