@trpc/client

The tRPC client library

client-setup

core
318 linesSource

Create a vanilla tRPC client with createTRPCClient<AppRouter>(), configure link chain with httpBatchLink/httpLink, dynamic headers for auth, transformer on links (not client constructor). Infer types with inferRouterInputs and inferRouterOutputs. AbortController signal support. TRPCClientError typing.

tRPC -- Client Setup

Setup

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

const t = initTRPC.create();

const appRouter = t.router({
  user: t.router({
    byId: t.procedure
      .input(z.object({ id: z.string() }))
      .query(({ input }) => ({ id: input.id, name: 'Bilbo' })),
    create: t.procedure
      .input(z.object({ name: z.string() }))
      .mutation(({ input }) => ({ id: '1', ...input })),
  }),
});

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

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

const user = await client.user.byId.query({ id: '1' });
const created = await client.user.create.mutate({ name: 'Frodo' });

Core Patterns

Dynamic Auth Headers

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

let token = '';

export function setToken(newToken: string) {
  token = newToken;
}

export const client = createTRPCClient<AppRouter>({
  links: [
    httpBatchLink({
      url: 'http://localhost:3000/trpc',
      headers() {
        return {
          Authorization: token ? `Bearer ${token}` : '',
        };
      },
    }),
  ],
});

The headers callback is invoked on every HTTP request, so token changes take effect immediately.

Inferring Procedure Input and Output Types

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

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

type UserCreateInput = RouterInput['user']['create'];
type UserByIdOutput = RouterOutput['user']['byId'];

Aborting Requests with AbortController

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

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

const ac = new AbortController();
const query = client.user.byId.query({ id: '1' }, { signal: ac.signal });
ac.abort();

Typed Error Handling

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

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

try {
  await client.user.byId.query({ id: '1' });
} catch (cause) {
  if (isTRPCClientError(cause)) {
    console.log('tRPC error code:', cause.data?.code);
  }
}

Common Mistakes

[CRITICAL] Missing AppRouter type parameter on createTRPCClient

Wrong:

ts
const client = createTRPCClient({ links: [httpBatchLink({ url })] });

Correct:

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

const client = createTRPCClient<AppRouter>({ links: [httpBatchLink({ url })] });

Without the type parameter, all procedure calls return any and type safety is completely lost.

Source: www/docs/client/vanilla/setup.mdx

In v11, the transformer option is on individual terminating links, not the client constructor:

ts
import superjson from 'superjson';

createTRPCClient<AppRouter>({
  links: [
    httpBatchLink({
      url: 'http://localhost:3000',
      transformer: superjson,
    }),
  ],
});

In v11, the transformer option was moved from the client constructor to individual terminating links. Passing it to createTRPCClient throws a TypeError.

Source: packages/client/src/internals/TRPCUntypedClient.ts

Wrong:

ts
// Server: initTRPC.create({ transformer: superjson })
// Client:
httpBatchLink({ url: 'http://localhost:3000' });

Correct:

ts
// Server: initTRPC.create({ transformer: superjson })
// Client:
httpBatchLink({ url: 'http://localhost:3000', transformer: superjson });

If the server uses a transformer, every terminating link on the client must also specify that transformer. Mismatch causes "Unable to transform response" errors.

Source: https://github.com/trpc/trpc/issues/7083

[CRITICAL] Using import instead of import type for AppRouter

Wrong:

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

Correct:

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

A non-type import pulls the entire server bundle into the client. Use import type so it is erased at build time.

Source: www/docs/client/vanilla/setup.mdx

[CRITICAL] Importing appRouter value to derive type in client

Wrong:

ts
import { appRouter } from '../server/router';

type AppRouter = typeof appRouter;

Correct:

ts
// In server: export type AppRouter = typeof appRouter;
// In client:
import type { AppRouter } from '../server/router';

Importing the appRouter value (not just the type) bundles the entire server into the client, shipping server code to the browser.

Source: www/docs/server/routers.md

[CRITICAL] Using type assertions to bypass AppRouter import errors

Wrong:

ts
const client = createTRPCClient<any>({ links: [httpBatchLink({ url })] });

Correct:

ts
// Fix the import path or monorepo configuration
import type { AppRouter } from '@myorg/api-types';

const client = createTRPCClient<AppRouter>({ links: [httpBatchLink({ url })] });

Casting to any or manually recreating the router type destroys end-to-end type safety. Fix the import path or monorepo config instead.

Source: www/docs/client/vanilla/setup.mdx

[CRITICAL] Using createTRPCProxyClient (renamed in v11)

Wrong:

ts
import { createTRPCProxyClient } from '@trpc/client';

Correct:

ts
import { createTRPCClient } from '@trpc/client';

createTRPCProxyClient was renamed to createTRPCClient in v11.

Source: www/docs/client/vanilla/setup.mdx

[CRITICAL] Treating tRPC as a REST API

Wrong:

ts
fetch('/api/trpc/users/123', { method: 'GET' });

Correct:

ts
const user = await client.user.byId.query({ id: '123' });
// Raw equivalent: GET /api/trpc/user.byId?input={"id":"123"}

tRPC uses JSON-RPC over HTTP. Procedures are called by dot-separated name with JSON input, not by REST resource paths.

Source: www/docs/client/overview.md

[HIGH] HTML error page instead of JSON response

If you see couldn't parse JSON, invalid character '<', the tRPC endpoint returned an HTML page (404/503) instead of JSON. This means the url in your link config is wrong or infrastructure routing is misconfigured -- it is not a tRPC bug. Verify the URL matches your adapter's mount point.

Source: www/docs/client/vanilla/setup.mdx

See Also

  • links -- configure httpBatchLink, httpLink, splitLink, and other link types
  • superjson -- set up SuperJSON transformer on server and client
  • server-setup -- define routers, procedures, context, and export AppRouter type
  • react-query-setup -- use tRPC with TanStack React Query for React applications