Skip to content

@trpc/client

The tRPC client library

client-setup

351 linesSource

Create a vanilla tRPC client with createTRPCClient<AppRouter>() and a terminating link such as httpBatchLink, import AppRouter with `import type`, set dynamic auth headers, and put the transformer on links (not the client constructor). Infer types with inferRouterInputs and inferRouterOutputs, handle TRPCClientError with try/catch or safe(), and abort calls with an AbortSignal.

Set up a tRPC Client

From https://trpc.io/docs/client/vanilla/setup

1. Install the tRPC Client library

Use your preferred package manager to install the @trpc/client library, and also install @trpc/server which contains some required types.

shell
npm install @trpc/server @trpc/client

2. Import your App Router

Import your AppRouter type into the client application. This type holds the shape of your entire API.

utils/trpc.ts
import type { AppRouter } from '../server/router';

Tip

By using import type you ensure that the reference will be stripped at compile-time, meaning you don't inadvertently import server-side code into your client. For more information, see the Typescript docs.

3. Initialize the tRPC client

Create a tRPC client with the createTRPCClient method, and add a links array with a terminating link pointing at your API. To learn more about tRPC links, click here.

client.ts
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from './server';

const client = createTRPCClient<AppRouter>({
  links: [
    httpBatchLink({
      url: 'http://localhost:3000/trpc',

      // You can pass any HTTP headers you wish here
      async headers() {
        return {
          authorization: getAuthCookie(),
        };
      },
    }),
  ],
});

4. Use the tRPC Client

Under the hood this creates a typed JavaScript Proxy which allows you to interact with your tRPC API in a fully type-safe way:

client.ts
const bilbo = await client.getUser.query('id_bilbo');
// => { id: 'id_bilbo', name: 'Bilbo' };

const frodo = await client.createUser.mutate({ name: 'Frodo' });
// => { id: 'id_frodo', name: 'Frodo' };

You're all set!

Common mistakes

  • Leaving out the AppRouter type parameter. createTRPCClient({ links }) without <AppRouter> compiles, but none of your procedures are typed. If the AppRouter import doesn't resolve (often a monorepo path or tsconfig issue), fix the import rather than reaching for createTRPCClient<any>.
  • Importing the router value instead of its type. import { appRouter } from '../server/router' pulls your server code into the client bundle. Export type AppRouter = typeof appRouter from the server and import it with import type.
  • Passing transformer to createTRPCClient. The transformer belongs on each terminating link (httpBatchLink, httpLink, wsLink, ...), and TypeScript rejects it on the client itself. See Data Transformers.
  • Using createTRPCProxyClient. It's the v10 name, kept as a deprecated alias of createTRPCClient.
  • Calling procedures like REST endpoints. Procedures are addressed by their dot-separated path, for example GET /trpc/post.byId?input=..., not /trpc/post/1. Call them through the client, or see the HTTP RPC Specification for the wire format.
  • A JSON parse error like Unexpected token '<'. The request reached something that answered with an HTML page, usually a 404, so the link's url doesn't match where your adapter serves tRPC (check the /trpc prefix).

Inferring Types

From https://trpc.io/docs/client/vanilla/infer-types

It is often useful to access the types of your API within your clients. For this purpose, you are able to infer the types contained in your AppRouter.

@trpc/server exports the following helper types to assist with inferring these types from the AppRouter exported by your @trpc/server router:

  • inferRouterInputs<TRouter>
  • inferRouterOutputs<TRouter>
  • inferProcedureInput<TProcedure>
  • inferProcedureOutput<TProcedure>
  • inferSubscriptionInput<TProcedure>
  • inferSubscriptionOutput<TProcedure>

Inferring Input & Output Types

Let's assume we have this example router:

server.ts
import { initTRPC } from '@trpc/server';
import { z } from "zod";

const t = initTRPC.create();

const appRouter = t.router({
  post: t.router({
    list: t.procedure
      .query(() => {
        // imaginary db call
        return [{ id: 1, title: 'tRPC is the best!' }];
    }),
    byId: t.procedure
      .input(z.string())
      .query((opts) => {
        // imaginary db call
        return { id: 1, title: 'tRPC is the best!' };
    }),
    create: t.procedure
      .input(z.object({ title: z.string(), text: z.string(), }))
      .mutation((opts) => {
        // imaginary db call
        return { id: 1, ...opts.input };
    }),
    onPostAdd: t.procedure
      .input(z.object({ authorId: z.string() }))
      .subscription(async function* ({ input }) {
        // imaginary event source
        yield {
          id: 1,
          title: 'tRPC is the best!',
          authorId: input.authorId,
        };
    }),
  }),
});

export type AppRouter = typeof appRouter;

Using the helpers, we can infer the types of our router. The following example shows how to infer the types of the post.create procedure:

client.ts
import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server';
import type { AppRouter } from './server';

type RouterInput = inferRouterInputs<AppRouter>;
type RouterOutput = inferRouterOutputs<AppRouter>;

type PostCreateInput = RouterInput['post']['create'];
type PostCreateOutput = RouterOutput['post']['create'];

Inferring Individual Procedure Types

If you already have access to a specific procedure on your router, you can infer its input or output directly:

client.ts
import type {
  inferProcedureInput,
  inferProcedureOutput,
} from '@trpc/server';
import type { AppRouter } from './server';

type PostByIdInput = inferProcedureInput<AppRouter['post']['byId']>;
type PostByIdOutput = inferProcedureOutput<AppRouter['post']['byId']>;

For subscriptions, you can infer the subscription input and the emitted data type:

client.ts
import type {
  inferSubscriptionInput,
  inferSubscriptionOutput,
} from '@trpc/server';
import type { AppRouter } from './server';

type OnPostAddInput = inferSubscriptionInput<AppRouter['post']['onPostAdd']>;
type OnPostAddOutput = inferSubscriptionOutput<AppRouter['post']['onPostAdd']>;

Infer TRPCClientError types

It's also useful to infer the error type for your AppRouter

client.ts
import { TRPCClientError } from '@trpc/client';
import type { AppRouter } from './server';
import { trpc } from './trpc';

export function isTRPCClientError(
  cause: unknown,
): cause is TRPCClientError<AppRouter> {
  return cause instanceof TRPCClientError;
}

async function main() {
  try {
    await trpc.post.byId.query('1');
  } catch (cause) {
    if (isTRPCClientError(cause)) {
      // `cause` is now typed as your router's `TRPCClientError`
      console.log('data', cause.data);
    } else {
      // [...]
    }
  }
}

main();

Error Handling

From https://trpc.io/docs/client/vanilla/error-handling

A failed procedure call rejects with a TRPCClientError. You can either let it throw and catch it, or use safe() to get it back as a type-safe value.

Catching a thrown error

query() and mutate() behave like any other promise, so a try/catch works as you would expect. TypeScript has no way to describe what a function throws, though, so cause arrives as unknown and has to be narrowed:

client.ts
import { TRPCClientError } from '@trpc/client';
import type { AppRouter } from './server';
import { trpc } from './trpc';

try {
  const post = await trpc.post.byId.query('1');
} catch (cause) {
  if (cause instanceof TRPCClientError) {
    console.log(cause.data);
  } else {
    // something that isn't a tRPC error
  }
}

See Inferring Types for a reusable isTRPCClientError guard.

Errors as values with safe()

safe() awaits a query() or mutate() call without letting it throw, handing back a [data, error] pair instead. Exactly one side is ever set, so checking either one narrows the other:

client.ts
import { safe } from '@trpc/client';
import { trpc } from './trpc';

const [post, error] = await safe(trpc.post.byId.query('1'));

if (error) {
  console.log(error.data);
} else {
  console.log(post.title);
}

Info

Subscriptions don't need safe() - they never throw at the call site, and report failures through onError, which is already typed with the procedure's error shape.

Aborting Procedure Calls

From https://trpc.io/docs/client/vanilla/aborting-procedure-calls

tRPC supports the standard AbortController/AbortSignal API for aborting procedures. All you have to do is pass an AbortSignal to the query or mutation options, and call the AbortController instance's abort method if you need to cancel the request.

utils.ts
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from './server';

const client = createTRPCClient<AppRouter>({
  links: [
    httpBatchLink({
      url: 'http://localhost:3000/trpc',
    }),
  ],
});

// 1. Create an AbortController instance - this is a standard javascript API
const ac = new AbortController();

// 2. Pass the signal to a query or mutation
const query = client.userById.query('id_bilbo', { signal: ac.signal });

// 3. Cancel the request if needed
ac.abort();

Headers

From https://trpc.io/docs/client/headers

The headers option may be used with any of our HTTP links: httpBatchLink, httpBatchStreamLink, httpLink, or httpSubscriptionLink.

headers can be both an object or a function. If it's a function it will get called dynamically for every HTTP request.

utils/trpc.ts
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from './server';

let token: string;

export function setToken(newToken: string) {
  /**
   * You can also save the token to cookies, and initialize from
   * cookies above.
   */
  token = newToken;
}

export const trpc = createTRPCClient<AppRouter>({
  links: [
    httpBatchLink({
      url: 'http://localhost:3000',
      /**
       * Headers will be called on each request.
       */
      headers() {
        return {
          Authorization: token,
        };
      },
    }),
  ],
});

Example with auth login

auth.ts
const result = await trpc.auth.login.mutate({ username: 'user', password: 'pass' });
setToken(result.accessToken);

The token can be whatever you want it to be. It's entirely up to you whether that's just a client-side variable that you update the value of on success or whether you store the token and pull it from local storage.

  • server-setup (https://trpc.io/docs/server/overview): npx @tanstack/intent@latest load @trpc/server#server-setup