client-setup
coreCreate 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
// 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;
// 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
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
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
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
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:
const client = createTRPCClient({ links: [httpBatchLink({ url })] });
Correct:
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
[CRITICAL] Transformer goes on individual links, not createTRPCClient
In v11, the transformer option is on individual terminating links, not the client constructor:
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
[CRITICAL] Transformer on server but not on client links
Wrong:
// Server: initTRPC.create({ transformer: superjson })
// Client:
httpBatchLink({ url: 'http://localhost:3000' });
Correct:
// 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:
import { AppRouter } from '../server/router';
Correct:
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:
import { appRouter } from '../server/router';
type AppRouter = typeof appRouter;
Correct:
// 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:
const client = createTRPCClient<any>({ links: [httpBatchLink({ url })] });
Correct:
// 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:
import { createTRPCProxyClient } from '@trpc/client';
Correct:
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:
fetch('/api/trpc/users/123', { method: 'GET' });
Correct:
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