@trpc/server
Version:
345 lines (266 loc) • 10.4 kB
Markdown
---
name: server-side-calls
description: Call tRPC procedures directly from server code using t.createCallerFactory(router) and router.createCaller(ctx) for integration testing, internal server logic, and custom API endpoints. Pass the context as an object or a function, e.g. an inner context without a request. Catch TRPCError and extract HTTP status with getHTTPStatusCodeFromError(), report errors with createCaller's onError option. Don't use a caller inside another procedure; share a plain function instead.
requires:
- server-setup
sources:
- www/v11/docs/server/server-side-calls.md
metadata:
type: core
library: trpc
library_version: 11.23.0
---
<!-- Generated from www/v11/docs/server/server-side-calls.md by www/build/skills.ts. Edit the docs instead. -->
# Server Side Calls
From https://trpc.io/docs/server/server-side-calls
You may need to call your procedure(s) directly from the same server they're hosted in, `createCallerFactory()` can be used to achieve this. This is useful for server-side calls and for integration testing of your tRPC procedures.
> [!NOTE] Info
>
> `createCaller` should not be used to call procedures from within other procedures. This creates overhead by (potentially) creating context again, executing all middlewares, and validating the input - all of which were already done by the current procedure. Instead, you should extract the shared logic into a separate function and call that from within the procedures, like so:
>
> <div class="flex gap-2 w-full justify-between pt-2">
> <img src="https://assets.trpc.io/www/docs/server/server-side-calls/bad.png" alt="Instead of this: procedures in paymentRouter call customerRouter.createCaller(ctx).byUserId(ctx.user.id) to reuse its logic" class="w-[49.5%]" />
> <img src="https://assets.trpc.io/www/docs/server/server-side-calls/good.png" alt="Do this: a standalone getCustomerId(userId, prisma) function holds the shared logic, and every procedure calls it with values from ctx and input" class="w-[49.5%]" />
> </div>
## Create caller
With the `t.createCallerFactory`-function you can create a server-side caller of any router. You first call `createCallerFactory` with an argument of the router you want to call, then this returns a function where you can pass in a `Context` for the following procedure calls.
### Basic example
We create the router with a query to list posts and a mutation to add posts, and then we call each method.
```ts no-check
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();
```
The caller doesn't run your adapter's `createContext`, so pass everything your procedures and middlewares expect. An [inner context](https://trpc.io/docs/server/context#inner-and-outer-context) works well here, as it doesn't need a request.
Instead of a context object, you can also pass a function that returns the context, or a promise of it. The caller calls it for every procedure call, so cache it (e.g. with React's `cache()`) if it's expensive:
```ts no-check
const createCaller = t.createCallerFactory(appRouter);
const caller = createCaller(async () => createContext());
const greeting = await caller.hello();
```
### Example usage in an integration test
> Taken from [https://github.com/trpc/examples-next-prisma-starter/blob/main/src/server/routers/post.test.ts](https://github.com/trpc/examples-next-prisma-starter/blob/main/src/server/routers/post.test.ts)
```ts no-check
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()`
With the `router.createCaller({})` function (first argument is `Context`) we retrieve an instance of `RouterCaller`.
### Input query example
We create the router with an input query, and then we call the asynchronous `greeting` procedure to get the result.
```ts no-check
import { initTRPC } from '/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' });
```
### Mutation example
We create the router with a mutation, and then we call the asynchronous `post` procedure to get the result.
```ts no-check
import { initTRPC } from '/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');
```
### Context with middleware example
We create a middleware to check the context before executing the `secret` procedure. Below are two examples: the former fails because the context doesn't fit the middleware logic, and the latter works correctly.
<br />
> [!NOTE] Info
>
> Middlewares are performed before any procedure(s) are called.
<br />
```ts no-check
import { initTRPC, TRPCError } from '/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();
}
```
### Example for a Next.js API endpoint
> [!TIP]
>
> This example shows how to use the caller in a Next.js API endpoint. tRPC creates API endpoints for you already, so this file is only meant to show
> how to call a procedure from another, custom endpoint.
```ts no-check
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}` },
});
}
};
```
### Error handling
A procedure called through a caller throws its error as a `TRPCError`; errors that weren't thrown as one become an `INTERNAL_SERVER_ERROR` with the original error as their `cause`.
The `createCaller` function can also take an error handler through the `onError` option, for example to log or report errors. The caller still throws the error after the handler runs. Its options also take a `signal`, which your procedures receive as `opts.signal`.
The handler is called with the same arguments as an error formatter would be, except for the shape field:
```ts no-check
interface OnErrorShape {
ctx: unknown;
error: TRPCError;
path: string | undefined;
input: unknown;
type: 'query' | 'mutation' | 'subscription' | 'unknown';
}
```
```ts no-check
import { initTRPC } from '/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' });
```
## Related skills
- `server-setup` (https://trpc.io/docs/server/overview): `npx @tanstack/intent@latest load @trpc/server#server-setup`