UNPKG

@trpc/client

Version:

The tRPC client library

351 lines (264 loc) • 12.5 kB
--- name: client-setup description: 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. requires: - server-setup sources: - www/v11/docs/client/vanilla/setup.md - www/v11/docs/client/vanilla/infer-types.md - www/v11/docs/client/vanilla/error-handling.md - www/v11/docs/client/vanilla/aborting-procedure-calls.md - www/v11/docs/client/headers.md metadata: type: core library: trpc library_version: 11.23.1-canary.16.f11951ce3 --- <!-- Generated from www/v11/docs/client/vanilla/setup.md by www/build/skills.ts. Edit the docs instead. --> # 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. ```bash 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. ```ts no-check title="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](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export). ### 3. Initialize the tRPC client Create a tRPC client with the `createTRPCClient` method, and add a `links` array with a [terminating link](https://trpc.io/docs/client/links#the-terminating-link) pointing at your API. To learn more about tRPC links, [click here](https://trpc.io/docs/client/links). ```ts no-check title="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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) which allows you to interact with your tRPC API in a fully type-safe way: ```ts no-check title="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](https://trpc.io/docs/server/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](https://trpc.io/docs/rpc) 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: ```ts no-check title="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: ```ts no-check title="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: ```ts no-check title="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: ```ts no-check title="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` ```ts no-check title="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`](https://trpc.io/docs/client/vanilla/infer-types#infer-trpcclienterror-types). 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: ```ts no-check title="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](https://trpc.io/docs/client/vanilla/infer-types#infer-trpcclienterror-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: ```ts no-check title="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); } ``` > [!NOTE] 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. ```ts no-check title="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`](https://trpc.io/docs/client/links/httpBatchLink), [`httpBatchStreamLink`](https://trpc.io/docs/client/links/httpBatchStreamLink), [`httpLink`](https://trpc.io/docs/client/links/httpLink), or [`httpSubscriptionLink`](https://trpc.io/docs/client/links/httpSubscriptionLink). `headers` can be both an object or a function. If it's a function it will get called dynamically for every HTTP request. ```ts no-check title="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 ```ts no-check title="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. ## Related skills - `server-setup` (https://trpc.io/docs/server/overview): `npx @tanstack/intent@latest load @trpc/server#server-setup`