UEA-Prodem
This commit is contained in:
+317
@@ -0,0 +1,317 @@
|
||||
---
|
||||
name: client-setup
|
||||
description: >
|
||||
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.
|
||||
type: core
|
||||
library: trpc
|
||||
library_version: '11.16.0'
|
||||
requires:
|
||||
- server-setup
|
||||
sources:
|
||||
- www/docs/client/overview.md
|
||||
- www/docs/client/vanilla/overview.md
|
||||
- www/docs/client/vanilla/setup.mdx
|
||||
- www/docs/client/vanilla/infer-types.md
|
||||
- www/docs/client/headers.md
|
||||
- packages/client/src/internals/TRPCUntypedClient.ts
|
||||
---
|
||||
|
||||
# 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
|
||||
|
||||
### [CRITICAL] Transformer goes on individual links, not createTRPCClient
|
||||
|
||||
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
|
||||
|
||||
### [CRITICAL] Transformer on server but not on client links
|
||||
|
||||
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
|
||||
+301
@@ -0,0 +1,301 @@
|
||||
---
|
||||
name: links
|
||||
description: >
|
||||
Configure the tRPC client link chain: httpLink, httpBatchLink,
|
||||
httpBatchStreamLink, splitLink, loggerLink, wsLink, createWSClient,
|
||||
httpSubscriptionLink, unstable_localLink, retryLink. Choose the right
|
||||
terminating link. Route subscriptions via splitLink. Build custom links
|
||||
for SOA routing. Link options: url, headers, transformer, maxURLLength,
|
||||
maxItems, connectionParams, EventSource ponyfill.
|
||||
type: core
|
||||
library: trpc
|
||||
library_version: '11.16.0'
|
||||
requires:
|
||||
- client-setup
|
||||
sources:
|
||||
- www/docs/client/links/overview.md
|
||||
- www/docs/client/links/httpLink.md
|
||||
- www/docs/client/links/httpBatchLink.md
|
||||
- www/docs/client/links/httpBatchStreamLink.md
|
||||
- www/docs/client/links/splitLink.mdx
|
||||
- www/docs/client/links/wsLink.md
|
||||
- www/docs/client/links/httpSubscriptionLink.md
|
||||
- www/docs/client/links/localLink.mdx
|
||||
- www/docs/client/links/loggerLink.md
|
||||
- packages/client/src/links/
|
||||
---
|
||||
|
||||
# tRPC -- Links
|
||||
|
||||
## Setup
|
||||
|
||||
```ts
|
||||
import { createTRPCClient, httpBatchLink, loggerLink } from '@trpc/client';
|
||||
import type { AppRouter } from './server';
|
||||
|
||||
const client = createTRPCClient<AppRouter>({
|
||||
links: [
|
||||
loggerLink(),
|
||||
httpBatchLink({
|
||||
url: 'http://localhost:3000/trpc',
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
The `links` array is a chain: non-terminating links (loggerLink, splitLink, retryLink) forward operations; the chain must end with a terminating link (httpBatchLink, httpLink, httpBatchStreamLink, wsLink, httpSubscriptionLink, unstable_localLink).
|
||||
|
||||
## Core Patterns
|
||||
|
||||
### httpBatchLink -- Batch Multiple Calls into One Request
|
||||
|
||||
```ts
|
||||
import { createTRPCClient, httpBatchLink } from '@trpc/client';
|
||||
import type { AppRouter } from './server';
|
||||
|
||||
const client = createTRPCClient<AppRouter>({
|
||||
links: [
|
||||
httpBatchLink({
|
||||
url: 'http://localhost:3000/trpc',
|
||||
maxURLLength: 2083,
|
||||
maxItems: 10,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const [post1, post2, post3] = await Promise.all([
|
||||
client.post.byId.query(1),
|
||||
client.post.byId.query(2),
|
||||
client.post.byId.query(3),
|
||||
]);
|
||||
```
|
||||
|
||||
Concurrent calls are batched into a single HTTP request. Set `maxURLLength` to prevent 414 errors from long URLs.
|
||||
|
||||
### splitLink -- Route Subscriptions to SSE
|
||||
|
||||
```ts
|
||||
import {
|
||||
createTRPCClient,
|
||||
httpBatchLink,
|
||||
httpSubscriptionLink,
|
||||
splitLink,
|
||||
} from '@trpc/client';
|
||||
import type { AppRouter } from './server';
|
||||
|
||||
const client = createTRPCClient<AppRouter>({
|
||||
links: [
|
||||
splitLink({
|
||||
condition: (op) => op.type === 'subscription',
|
||||
true: httpSubscriptionLink({
|
||||
url: 'http://localhost:3000/trpc',
|
||||
}),
|
||||
false: httpBatchLink({
|
||||
url: 'http://localhost:3000/trpc',
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### splitLink -- Disable Batching Per-Request via Context
|
||||
|
||||
```ts
|
||||
import {
|
||||
createTRPCClient,
|
||||
httpBatchLink,
|
||||
httpLink,
|
||||
splitLink,
|
||||
} from '@trpc/client';
|
||||
import type { AppRouter } from './server';
|
||||
|
||||
const client = createTRPCClient<AppRouter>({
|
||||
links: [
|
||||
splitLink({
|
||||
condition: (op) => Boolean(op.context.skipBatch),
|
||||
true: httpLink({ url: 'http://localhost:3000/trpc' }),
|
||||
false: httpBatchLink({ url: 'http://localhost:3000/trpc' }),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const result = await client.post.byId.query(1, {
|
||||
context: { skipBatch: true },
|
||||
});
|
||||
```
|
||||
|
||||
### httpBatchStreamLink -- Stream Responses as They Arrive
|
||||
|
||||
```ts
|
||||
import { createTRPCClient, httpBatchStreamLink } from '@trpc/client';
|
||||
import type { AppRouter } from './server';
|
||||
|
||||
const client = createTRPCClient<AppRouter>({
|
||||
links: [
|
||||
httpBatchStreamLink({
|
||||
url: 'http://localhost:3000/trpc',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const iterable = await client.examples.iterable.query();
|
||||
for await (const value of iterable) {
|
||||
console.log(value);
|
||||
}
|
||||
```
|
||||
|
||||
### wsLink -- WebSocket Terminating Link
|
||||
|
||||
```ts
|
||||
import { createTRPCClient, createWSClient, wsLink } from '@trpc/client';
|
||||
import type { AppRouter } from './server';
|
||||
|
||||
const wsClient = createWSClient({
|
||||
url: 'ws://localhost:3000',
|
||||
});
|
||||
|
||||
const client = createTRPCClient<AppRouter>({
|
||||
links: [wsLink<AppRouter>({ client: wsClient })],
|
||||
});
|
||||
```
|
||||
|
||||
### Custom Link
|
||||
|
||||
```ts
|
||||
import { TRPCLink } from '@trpc/client';
|
||||
import { observable } from '@trpc/server/observable';
|
||||
import type { AppRouter } from './server';
|
||||
|
||||
export const timingLink: TRPCLink<AppRouter> = () => {
|
||||
return ({ next, op }) => {
|
||||
return observable((observer) => {
|
||||
const start = Date.now();
|
||||
const unsubscribe = next(op).subscribe({
|
||||
next(value) {
|
||||
observer.next(value);
|
||||
},
|
||||
error(err) {
|
||||
console.error(`${op.path} failed in ${Date.now() - start}ms`);
|
||||
observer.error(err);
|
||||
},
|
||||
complete() {
|
||||
console.log(`${op.path} completed in ${Date.now() - start}ms`);
|
||||
observer.complete();
|
||||
},
|
||||
});
|
||||
return unsubscribe;
|
||||
});
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
### [CRITICAL] No terminating link in the chain
|
||||
|
||||
Wrong:
|
||||
|
||||
```ts
|
||||
const client = createTRPCClient<AppRouter>({
|
||||
links: [loggerLink()],
|
||||
});
|
||||
```
|
||||
|
||||
Correct:
|
||||
|
||||
```ts
|
||||
const client = createTRPCClient<AppRouter>({
|
||||
links: [loggerLink(), httpBatchLink({ url: 'http://localhost:3000/trpc' })],
|
||||
});
|
||||
```
|
||||
|
||||
The link chain must end with a terminating link. Without one, tRPC throws "No more links to execute - did you forget to add an ending link?"
|
||||
|
||||
Source: packages/client/src/links/internals/createChain.ts
|
||||
|
||||
### [CRITICAL] Sending subscriptions through httpLink or httpBatchLink
|
||||
|
||||
Wrong:
|
||||
|
||||
```ts
|
||||
const client = createTRPCClient<AppRouter>({
|
||||
links: [httpBatchLink({ url: 'http://localhost:3000/trpc' })],
|
||||
});
|
||||
await client.onMessage.subscribe({});
|
||||
```
|
||||
|
||||
Correct:
|
||||
|
||||
```ts
|
||||
const client = createTRPCClient<AppRouter>({
|
||||
links: [
|
||||
splitLink({
|
||||
condition: (op) => op.type === 'subscription',
|
||||
true: httpSubscriptionLink({ url: 'http://localhost:3000/trpc' }),
|
||||
false: httpBatchLink({ url: 'http://localhost:3000/trpc' }),
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
httpLink and httpBatchLink throw on subscription operations. Subscriptions must use httpSubscriptionLink or wsLink, routed via splitLink.
|
||||
|
||||
Source: packages/client/src/links/httpLink.ts
|
||||
|
||||
### [HIGH] httpBatchLink and httpBatchStreamLink headers callback receives { opList }
|
||||
|
||||
`httpBatchLink` and `httpBatchStreamLink` headers callbacks receive `{ opList }` (a `NonEmptyArray<Operation>`), not `{ op }` like `httpLink`. Access per-operation context via `opList[0]?.context`:
|
||||
|
||||
```ts
|
||||
httpBatchLink({
|
||||
url: 'http://localhost:3000/trpc',
|
||||
headers({ opList }) {
|
||||
return { authorization: opList[0]?.context.token };
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
`httpBatchLink` headers callback receives `{ opList }` (an array of operations)
|
||||
|
||||
Source: packages/client/src/links/httpBatchLink.ts
|
||||
|
||||
### [MEDIUM] Default batch limits are Infinity
|
||||
|
||||
Wrong:
|
||||
|
||||
```ts
|
||||
httpBatchLink({ url: 'http://localhost:3000/trpc' });
|
||||
```
|
||||
|
||||
Correct:
|
||||
|
||||
```ts
|
||||
httpBatchLink({
|
||||
url: 'http://localhost:3000/trpc',
|
||||
maxURLLength: 2083,
|
||||
// should be the same or lower than the server's maxBatchSize
|
||||
maxItems: 10,
|
||||
});
|
||||
```
|
||||
|
||||
Both `maxURLLength` and `maxItems` default to `Infinity`, which can cause 413/414 HTTP errors on servers or CDNs with URL length limits. When the server sets `maxBatchSize`, set `maxItems` to the same or lower value so the client auto-splits batches instead of triggering a `400 Bad Request`.
|
||||
|
||||
Source: packages/client/src/links/httpBatchLink.ts
|
||||
|
||||
### [HIGH] httpBatchStreamLink data loss on stream completion
|
||||
|
||||
There is a known race condition where buffered chunks can be lost on normal stream completion. Long streaming responses (e.g., LLM output) may be truncated. If you experience truncated data, switch to `httpBatchLink` for those operations.
|
||||
|
||||
Source: https://github.com/trpc/trpc/issues/7209
|
||||
|
||||
## References
|
||||
|
||||
- [Link options reference](references/link-options.md)
|
||||
|
||||
## See Also
|
||||
|
||||
- `client-setup` -- create the tRPC client and configure links
|
||||
- `superjson` -- add transformer to links for Date/Map/Set support
|
||||
- `subscriptions` -- set up SSE or WebSocket real-time streams
|
||||
- `non-json-content-types` -- route FormData/binary through splitLink + httpLink
|
||||
- `service-oriented-architecture` -- build custom routing links for multi-service backends
|
||||
+248
@@ -0,0 +1,248 @@
|
||||
# Link Options Reference
|
||||
|
||||
## httpLink
|
||||
|
||||
Terminating link that sends one tRPC operation per HTTP request.
|
||||
|
||||
```ts
|
||||
import { httpLink } from '@trpc/client';
|
||||
|
||||
httpLink({
|
||||
url: 'http://localhost:3000/trpc',
|
||||
fetch: customFetch,
|
||||
transformer: superjson,
|
||||
headers: { Authorization: 'Bearer token' },
|
||||
methodOverride: 'POST',
|
||||
});
|
||||
```
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
| ---------------- | --------------------------------------------------------------------------------- | -------------- | --------------------------------------------- |
|
||||
| `url` | `string \| URL` | required | Server endpoint URL |
|
||||
| `fetch` | `typeof fetch` | global `fetch` | Fetch ponyfill |
|
||||
| `transformer` | `DataTransformerOptions` | none | Data transformer (e.g. superjson) |
|
||||
| `headers` | `HTTPHeaders \| (opts: { op: Operation }) => HTTPHeaders \| Promise<HTTPHeaders>` | `{}` | Static headers object or per-request callback |
|
||||
| `methodOverride` | `'POST'` | none | Force all requests as POST |
|
||||
|
||||
## httpBatchLink
|
||||
|
||||
Terminating link that batches multiple operations into a single HTTP request.
|
||||
|
||||
```ts
|
||||
import { httpBatchLink } from '@trpc/client';
|
||||
|
||||
httpBatchLink({
|
||||
url: 'http://localhost:3000/trpc',
|
||||
maxURLLength: 2083,
|
||||
maxItems: 10,
|
||||
headers({ opList }) {
|
||||
return { Authorization: `Bearer ${opList[0]?.context.token}` };
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
```
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
| ---------------- | --------------------------------------------------------------------------------------- | -------------- | ---------------------------------------------------- |
|
||||
| `url` | `string \| URL` | required | Server endpoint URL |
|
||||
| `fetch` | `typeof fetch` | global `fetch` | Fetch ponyfill |
|
||||
| `transformer` | `DataTransformerOptions` | none | Data transformer |
|
||||
| `headers` | `HTTPHeaders \| (opts: { opList: Operation[] }) => HTTPHeaders \| Promise<HTTPHeaders>` | `{}` | Headers callback receives `opList` (array), not `op` |
|
||||
| `maxURLLength` | `number` | `Infinity` | Split batch if URL exceeds this length |
|
||||
| `maxItems` | `number` | `Infinity` | Maximum operations per batch |
|
||||
| `methodOverride` | `'POST'` | none | Force all requests as POST |
|
||||
|
||||
## httpBatchStreamLink
|
||||
|
||||
Terminating link similar to httpBatchLink but streams responses as they arrive instead of waiting for all to complete.
|
||||
|
||||
```ts
|
||||
import { httpBatchStreamLink } from '@trpc/client';
|
||||
|
||||
httpBatchStreamLink({
|
||||
url: 'http://localhost:3000/trpc',
|
||||
maxURLLength: 2083,
|
||||
maxItems: 10,
|
||||
transformer: superjson,
|
||||
streamHeader: 'accept',
|
||||
});
|
||||
```
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
| --------------------------- | --------------------------- | --------------- | ------------------------------------------------------------------------------------------------- |
|
||||
| All `httpBatchLink` options | | | Inherits all httpBatchLink options |
|
||||
| `streamHeader` | `'trpc-accept' \| 'accept'` | `'trpc-accept'` | Header used to signal streaming. Use `'accept'` to avoid CORS preflight on cross-origin requests. |
|
||||
|
||||
Sends `trpc-accept: application/jsonl` (or `Accept: application/jsonl`). Response arrives as `transfer-encoding: chunked` with `content-type: application/jsonl`. Cannot set response headers (including cookies) after stream begins.
|
||||
|
||||
## splitLink
|
||||
|
||||
Non-terminating link that branches the link chain based on a condition.
|
||||
|
||||
```ts
|
||||
import {
|
||||
httpBatchLink,
|
||||
httpLink,
|
||||
httpSubscriptionLink,
|
||||
splitLink,
|
||||
} from '@trpc/client';
|
||||
|
||||
splitLink({
|
||||
condition: (op) => op.type === 'subscription',
|
||||
true: httpSubscriptionLink({ url }),
|
||||
false: httpBatchLink({ url }),
|
||||
});
|
||||
```
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
| ----------- | ---------------------------- | -------- | ------------------------------------------------------------- |
|
||||
| `condition` | `(op: Operation) => boolean` | required | Route predicate |
|
||||
| `true` | `TRPCLink \| TRPCLink[]` | required | Link(s) for condition=true. Must include a terminating link. |
|
||||
| `false` | `TRPCLink \| TRPCLink[]` | required | Link(s) for condition=false. Must include a terminating link. |
|
||||
|
||||
Each branch creates its own sub-chain, so both branches need a terminating link.
|
||||
|
||||
## loggerLink
|
||||
|
||||
Non-terminating link that logs operations to the console.
|
||||
|
||||
```ts
|
||||
import { loggerLink } from '@trpc/client';
|
||||
|
||||
loggerLink({
|
||||
enabled: (opts) =>
|
||||
(process.env.NODE_ENV === 'development' && typeof window !== 'undefined') ||
|
||||
(opts.direction === 'down' && opts.result instanceof Error),
|
||||
colorMode: 'ansi',
|
||||
});
|
||||
```
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
| ------------- | -------------------------------------------------------------------- | ------------------------------------ | -------------------------------- |
|
||||
| `enabled` | `(opts: { direction: 'up' \| 'down'; result?: unknown }) => boolean` | `() => true` | Control when logging is active |
|
||||
| `logger` | `(opts: LoggerOpts) => void` | built-in pretty logger | Custom log function |
|
||||
| `console` | `{ log: Function; error: Function }` | `globalThis.console` | Console implementation |
|
||||
| `colorMode` | `'ansi' \| 'css' \| 'none'` | `'css'` in browser, `'ansi'` in Node | Color output mode |
|
||||
| `withContext` | `boolean` | `false` (true if css) | Include operation context in log |
|
||||
|
||||
## retryLink
|
||||
|
||||
Non-terminating link that retries failed operations.
|
||||
|
||||
```ts
|
||||
import { retryLink } from '@trpc/client';
|
||||
|
||||
retryLink({
|
||||
retry(opts) {
|
||||
if (opts.error.data?.code === 'INTERNAL_SERVER_ERROR') {
|
||||
return opts.attempts <= 3;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
retryDelayMs: (attempt) => Math.min(1000 * 2 ** attempt, 30000),
|
||||
});
|
||||
```
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
| -------------- | -------------------------------------------- | --------- | --------------------------- |
|
||||
| `retry` | `(opts: { op, error, attempts }) => boolean` | required | Return true to retry |
|
||||
| `retryDelayMs` | `(attempt: number) => number` | `() => 0` | Delay between retries in ms |
|
||||
|
||||
When used with subscriptions that use `tracked()`, automatically includes the last known event ID on retry.
|
||||
|
||||
## wsLink
|
||||
|
||||
Terminating link for WebSocket connections. Requires a `TRPCWebSocketClient`.
|
||||
|
||||
```ts
|
||||
import { createWSClient, wsLink } from '@trpc/client';
|
||||
|
||||
const wsClient = createWSClient({
|
||||
url: 'ws://localhost:3000',
|
||||
connectionParams: () => ({ token: 'supersecret' }),
|
||||
lazy: { enabled: true, closeMs: 10_000 },
|
||||
keepAlive: { enabled: true, intervalMs: 5_000, pongTimeoutMs: 1_000 },
|
||||
});
|
||||
|
||||
wsLink<AppRouter>({
|
||||
client: wsClient,
|
||||
transformer: superjson,
|
||||
});
|
||||
```
|
||||
|
||||
### wsLink Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
| ------------- | ------------------------ | -------- | ------------------------------------ |
|
||||
| `client` | `TRPCWebSocketClient` | required | WebSocket client from createWSClient |
|
||||
| `transformer` | `DataTransformerOptions` | none | Data transformer |
|
||||
|
||||
### createWSClient Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
| ------------------------- | ---------------------------------------------------------------------------------------- | ------------------- | ----------------------------------------------------------------- |
|
||||
| `url` | `string \| (() => MaybePromise<string>)` | required | WebSocket server URL |
|
||||
| `connectionParams` | `Record<string, string> \| null \| (() => MaybePromise<Record<string, string> \| null>)` | `null` | Auth params sent as first message, available in `createContext()` |
|
||||
| `WebSocket` | `typeof WebSocket` | global `WebSocket` | WebSocket ponyfill |
|
||||
| `retryDelayMs` | `(attemptIndex: number) => number` | exponential backoff | Reconnection delay |
|
||||
| `onOpen` | `() => void` | none | Connection opened callback |
|
||||
| `onError` | `(evt?: Event) => void` | none | Connection error callback |
|
||||
| `onClose` | `(cause?: { code?: number }) => void` | none | Connection closed callback |
|
||||
| `lazy.enabled` | `boolean` | `false` | Close WS after inactivity |
|
||||
| `lazy.closeMs` | `number` | `0` | Idle timeout before closing |
|
||||
| `keepAlive.enabled` | `boolean` | `false` | Send ping messages |
|
||||
| `keepAlive.intervalMs` | `number` | `5000` | Ping interval |
|
||||
| `keepAlive.pongTimeoutMs` | `number` | `1000` | Close if no pong within this time |
|
||||
|
||||
## httpSubscriptionLink
|
||||
|
||||
Terminating link for Server-Sent Events (SSE) subscriptions.
|
||||
|
||||
```ts
|
||||
import { httpSubscriptionLink } from '@trpc/client';
|
||||
import { EventSourcePolyfill } from 'event-source-polyfill';
|
||||
|
||||
httpSubscriptionLink({
|
||||
url: 'http://localhost:3000/trpc',
|
||||
connectionParams: async () => ({ token: 'supersecret' }),
|
||||
transformer: superjson,
|
||||
EventSource: EventSourcePolyfill,
|
||||
eventSourceOptions: async ({ op }) => ({
|
||||
headers: {
|
||||
authorization: 'Bearer token',
|
||||
},
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
| -------------------- | ------------------------------------------------------------------------------------ | -------------------- | ----------------------------------------- |
|
||||
| `url` | `string \| (() => string \| Promise<string>)` | required | Server endpoint URL |
|
||||
| `connectionParams` | `Record<string, string> \| null \| (() => MaybePromise<...>)` | none | Serialized as URL query param |
|
||||
| `transformer` | `DataTransformerOptions` | none | Data transformer |
|
||||
| `EventSource` | EventSource constructor | global `EventSource` | EventSource ponyfill for custom headers |
|
||||
| `eventSourceOptions` | `EventSourceInit \| ((opts: { op }) => EventSourceInit \| Promise<EventSourceInit>)` | none | Options passed to EventSource constructor |
|
||||
|
||||
For cross-domain cookies, use `eventSourceOptions: () => ({ withCredentials: true })`.
|
||||
|
||||
## unstable_localLink
|
||||
|
||||
Terminating link for direct procedure calls without HTTP. Useful for testing and server-side usage.
|
||||
|
||||
```ts
|
||||
import { unstable_localLink } from '@trpc/client';
|
||||
import { appRouter } from './server';
|
||||
|
||||
unstable_localLink({
|
||||
router: appRouter,
|
||||
createContext: async () => ({ db: prisma }),
|
||||
onError: (opts) => console.error('Error:', opts.error),
|
||||
});
|
||||
```
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
| --------------- | ------------------------------------- | -------- | ------------------------ |
|
||||
| `router` | `AnyRouter` | required | tRPC router instance |
|
||||
| `createContext` | `() => Promise<Context>` | required | Context factory per call |
|
||||
| `onError` | `(opts: ErrorHandlerOptions) => void` | none | Error handler |
|
||||
| `transformer` | `DataTransformerOptions` | none | Data transformer |
|
||||
+273
@@ -0,0 +1,273 @@
|
||||
---
|
||||
name: superjson
|
||||
description: >
|
||||
Configure SuperJSON transformer on both server initTRPC.create({ transformer:
|
||||
superjson }) and every client terminating link (httpBatchLink, httpLink, wsLink,
|
||||
httpSubscriptionLink) to support Date, Map, Set, BigInt over the wire. Transformer
|
||||
must match on both sides. In v11, transformer goes on individual links, not the
|
||||
client constructor.
|
||||
type: composition
|
||||
library: trpc
|
||||
library_version: '11.16.0'
|
||||
requires:
|
||||
- server-setup
|
||||
- client-setup
|
||||
sources:
|
||||
- www/docs/server/data-transformers.md
|
||||
---
|
||||
|
||||
# tRPC -- SuperJSON Transformer
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Install superjson
|
||||
|
||||
```bash
|
||||
npm install superjson
|
||||
```
|
||||
|
||||
### 2. Add to initTRPC on the server
|
||||
|
||||
```ts
|
||||
// server/trpc.ts
|
||||
import { initTRPC } from '@trpc/server';
|
||||
import superjson from 'superjson';
|
||||
|
||||
const t = initTRPC.create({
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
export const router = t.router;
|
||||
export const publicProcedure = t.procedure;
|
||||
```
|
||||
|
||||
### 3. Add to every terminating link on the client
|
||||
|
||||
```ts
|
||||
// client.ts
|
||||
import { createTRPCClient, httpBatchLink } from '@trpc/client';
|
||||
import superjson from 'superjson';
|
||||
import type { AppRouter } from './server/trpc';
|
||||
|
||||
const client = createTRPCClient<AppRouter>({
|
||||
links: [
|
||||
httpBatchLink({
|
||||
url: 'http://localhost:3000/trpc',
|
||||
transformer: superjson,
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
Now Date, Map, Set, BigInt, RegExp, undefined, and other non-JSON types survive the round trip.
|
||||
|
||||
## Core Patterns
|
||||
|
||||
### SuperJSON with splitLink and Subscriptions
|
||||
|
||||
```ts
|
||||
import {
|
||||
createTRPCClient,
|
||||
httpBatchLink,
|
||||
httpSubscriptionLink,
|
||||
splitLink,
|
||||
} from '@trpc/client';
|
||||
import superjson from 'superjson';
|
||||
import type { AppRouter } from './server/trpc';
|
||||
|
||||
const client = createTRPCClient<AppRouter>({
|
||||
links: [
|
||||
splitLink({
|
||||
condition: (op) => op.type === 'subscription',
|
||||
true: httpSubscriptionLink({
|
||||
url: 'http://localhost:3000/trpc',
|
||||
transformer: superjson,
|
||||
}),
|
||||
false: httpBatchLink({
|
||||
url: 'http://localhost:3000/trpc',
|
||||
transformer: superjson,
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
Every terminating link in every branch must have `transformer: superjson`.
|
||||
|
||||
### SuperJSON with wsLink
|
||||
|
||||
```ts
|
||||
import { createTRPCClient, createWSClient, wsLink } from '@trpc/client';
|
||||
import superjson from 'superjson';
|
||||
import type { AppRouter } from './server/trpc';
|
||||
|
||||
const wsClient = createWSClient({
|
||||
url: 'ws://localhost:3000',
|
||||
});
|
||||
|
||||
const client = createTRPCClient<AppRouter>({
|
||||
links: [
|
||||
wsLink<AppRouter>({
|
||||
client: wsClient,
|
||||
transformer: superjson,
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### Returning Dates from Procedures
|
||||
|
||||
```ts
|
||||
// server
|
||||
import { z } from 'zod';
|
||||
import { publicProcedure, router } from './trpc';
|
||||
|
||||
const appRouter = router({
|
||||
getEvent: publicProcedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.query(({ input }) => {
|
||||
return {
|
||||
id: input.id,
|
||||
name: 'Launch Party',
|
||||
date: new Date('2025-01-01T00:00:00Z'),
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
export type AppRouter = typeof appRouter;
|
||||
```
|
||||
|
||||
```ts
|
||||
// client
|
||||
const event = await client.getEvent.query({ id: '1' });
|
||||
console.log(event.date instanceof Date); // true
|
||||
console.log(event.date.getFullYear()); // 2025
|
||||
```
|
||||
|
||||
Without superjson, `event.date` would be a string like `"2025-01-01T00:00:00.000Z"`.
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
### [CRITICAL] Transformer on server but missing from client link
|
||||
|
||||
Wrong:
|
||||
|
||||
```ts
|
||||
// Server
|
||||
const t = initTRPC.create({ transformer: superjson });
|
||||
|
||||
// Client
|
||||
const client = createTRPCClient<AppRouter>({
|
||||
links: [httpBatchLink({ url: 'http://localhost:3000/trpc' })],
|
||||
});
|
||||
```
|
||||
|
||||
Correct:
|
||||
|
||||
```ts
|
||||
// Server
|
||||
const t = initTRPC.create({ transformer: superjson });
|
||||
|
||||
// Client
|
||||
const client = createTRPCClient<AppRouter>({
|
||||
links: [
|
||||
httpBatchLink({
|
||||
url: 'http://localhost:3000/trpc',
|
||||
transformer: superjson,
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
Server encodes with superjson but client tries to parse raw JSON, causing "Unable to transform response" or garbled data.
|
||||
|
||||
Source: www/docs/server/data-transformers.md
|
||||
|
||||
### [CRITICAL] Transformer goes on individual links, not createTRPCClient
|
||||
|
||||
The `transformer` option is on individual terminating links:
|
||||
|
||||
```ts
|
||||
createTRPCClient<AppRouter>({
|
||||
links: [
|
||||
httpBatchLink({
|
||||
url: 'http://localhost:3000/trpc',
|
||||
transformer: superjson,
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
In v11, `transformer` was moved from the client constructor to individual links. Passing it to `createTRPCClient` throws a TypeError.
|
||||
|
||||
Source: packages/client/src/internals/TRPCUntypedClient.ts
|
||||
|
||||
### [CRITICAL] Transformer on only some terminating links in splitLink
|
||||
|
||||
Wrong:
|
||||
|
||||
```ts
|
||||
splitLink({
|
||||
condition: (op) => op.type === 'subscription',
|
||||
true: httpSubscriptionLink({
|
||||
url: 'http://localhost:3000/trpc',
|
||||
// missing transformer!
|
||||
}),
|
||||
false: httpBatchLink({
|
||||
url: 'http://localhost:3000/trpc',
|
||||
transformer: superjson,
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
Correct:
|
||||
|
||||
```ts
|
||||
splitLink({
|
||||
condition: (op) => op.type === 'subscription',
|
||||
true: httpSubscriptionLink({
|
||||
url: 'http://localhost:3000/trpc',
|
||||
transformer: superjson,
|
||||
}),
|
||||
false: httpBatchLink({
|
||||
url: 'http://localhost:3000/trpc',
|
||||
transformer: superjson,
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
Every terminating link must have the same transformer. A missing transformer on one branch causes deserialization failures only for operations routed through that branch.
|
||||
|
||||
Source: www/docs/server/data-transformers.md
|
||||
|
||||
### [HIGH] Using transformer on client but not on server
|
||||
|
||||
Wrong:
|
||||
|
||||
```ts
|
||||
// Server -- no transformer
|
||||
const t = initTRPC.create();
|
||||
|
||||
// Client
|
||||
httpBatchLink({ url, transformer: superjson });
|
||||
```
|
||||
|
||||
Correct:
|
||||
|
||||
```ts
|
||||
// Server
|
||||
const t = initTRPC.create({ transformer: superjson });
|
||||
|
||||
// Client
|
||||
httpBatchLink({ url, transformer: superjson });
|
||||
```
|
||||
|
||||
The transformer must be configured on both `initTRPC.create()` and every client link. Client-only transformer corrupts the request encoding because the server expects plain JSON.
|
||||
|
||||
Source: www/docs/server/data-transformers.md
|
||||
|
||||
## See Also
|
||||
|
||||
- `client-setup` -- create the tRPC client and configure links
|
||||
- `links` -- detailed options for each link type including transformer
|
||||
- `server-setup` -- initTRPC.create() where the server transformer is configured
|
||||
Reference in New Issue
Block a user