Links Overview
On this page
Links enable you to customize the flow of data between the tRPC Client and Server. A link should do only one thing, which can be either a self-contained modification to a tRPC operation (query, mutation, or subscription) or a side-effect based on the operation (such as logging).
You can compose links together into an array that you can provide to the tRPC client configuration via the links property, which represents a link chain. This means that the tRPC client will execute the links in the order they are added to the links array when doing a request and will execute them again in reverse when it's handling a response. Here's a visual representation of the link chain:
import { function createTRPCClient<TRouter extends AnyRouter>(opts: CreateTRPCClientOptions<TRouter>): TRPCClient<TRouter>createTRPCClient, function httpBatchLink<TRouter extends AnyRouter>(opts: HTTPBatchLinkOptions<TRouter["_def"]["_config"]["$types"]>): TRPCLink<TRouter>httpBatchLink, function loggerLink<TRouter extends AnyRouter = AnyRouter>(opts?: LoggerLinkOptions<TRouter>): TRPCLink<TRouter>loggerLink } from '@trpc/client';
import type { type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter } from './server';
export const const trpc: TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>
trpc = createTRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: CreateTRPCClientOptions<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>): TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<...>>>
createTRPCClient<type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter>({
links: TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>[]
links: [
loggerLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts?: LoggerLinkOptions<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>> | undefined): TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<...>>>
loggerLink(),
httpBatchLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: HTTPBatchLinkOptions<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>): TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>
httpBatchLink({
url: string | URLurl: 'http://localhost:3000',
}),
],
});Creating a custom link
A link is a function that follows the TRPCLink type. Each link is composed of three parts:
- The link returns a function that has no parameters. This is the setup phase where the link is initialized — it happens once per app and is useful for storing caches or other state.
- The function in step 1 returns another function that receives an object with two properties:
opwhich is theOperationthat is being executed by the client, andnextwhich is the function we use to call the next link down the chain. - The function in step 2 returns a final function that returns the
observablefunction provided by@trpc/server. Theobservableaccepts a function that receives anobserverwhich helps our link notify the next link up the chain how they should handle the operation result. In this function, we can just returnnext(op)and leave it as is, or we can subscribe tonext, which enables our link to handle the operation result.
Example
import { type TRPCLink<TInferrable extends InferrableClientTypes> = (opts: TRPCClientRuntime) => OperationLink<TInferrable>TRPCLink } from '@trpc/client';
import { function observable<TValue, TError = unknown>(subscribe: (observer: Observer<TValue, TError>) => TeardownLogic): Observable<TValue, TError>observable } from '@trpc/server/observable';
import type { type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter } from './server';
export const const customLink: TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>
customLink: type TRPCLink<TInferrable extends InferrableClientTypes> = (opts: TRPCClientRuntime) => OperationLink<TInferrable>TRPCLink<type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter> = () => {
// here we just got initialized in the app - this happens once per app
// useful for storing cache for instance
return ({ next: (op: Operation<unknown>) => OperationResultObservable<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>, unknown>
next, op: Operation<unknown>op }) => {
// this is when passing the result to the next link
// each link needs to return an observable which propagates results
return observable<OperationResultEnvelope<unknown, TRPCClientError<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>>, TRPCClientError<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>>(subscribe: (observer: Observer<OperationResultEnvelope<unknown, TRPCClientError<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<...>>>>, TRPCClientError<...>>) => TeardownLogic): Observable<...>
observable((observer: Observer<OperationResultEnvelope<unknown, TRPCClientError<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>>, TRPCClientError<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>>
observer) => {
var console: Consoleconsole.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)log('performing operation:', op: Operation<unknown>op);
const const unsubscribe: Unsubscribableunsubscribe = next: (op: Operation<unknown>) => OperationResultObservable<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>, unknown>
next(op: Operation<unknown>op).Subscribable<OperationResultEnvelope<unknown, TRPCClientError<BuiltRouter<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{}>>>>, TRPCClientError<...>>.subscribe(observer: Partial<Observer<OperationResultEnvelope<unknown, TRPCClientError<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>>, TRPCClientError<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>>>): Unsubscribable
subscribe({
next?: ((value: OperationResultEnvelope<unknown, TRPCClientError<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>>) => void) | undefined
next(value: OperationResultEnvelope<unknown, TRPCClientError<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>>
value) {
var console: Consoleconsole.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)log('we received value', value: OperationResultEnvelope<unknown, TRPCClientError<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>>
value);
observer: Observer<OperationResultEnvelope<unknown, TRPCClientError<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>>, TRPCClientError<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>>
observer.Observer<OperationResultEnvelope<unknown, TRPCClientError<BuiltRouter<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{}>>>>, TRPCClientError<...>>.next: (value: OperationResultEnvelope<unknown, TRPCClientError<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>>) => void
next(value: OperationResultEnvelope<unknown, TRPCClientError<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>>
value);
},
error?: ((err: TRPCClientError<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>) => void) | undefined
error(err: TRPCClientError<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>
err) {
var console: Consoleconsole.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)log('we received error', err: TRPCClientError<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>
err);
observer: Observer<OperationResultEnvelope<unknown, TRPCClientError<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>>, TRPCClientError<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>>
observer.Observer<OperationResultEnvelope<unknown, TRPCClientError<BuiltRouter<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{}>>>>, TRPCClientError<...>>.error: (err: TRPCClientError<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>) => void
error(err: TRPCClientError<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>
err);
},
complete?: (() => void) | undefinedcomplete() {
observer: Observer<OperationResultEnvelope<unknown, TRPCClientError<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>>, TRPCClientError<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>>
observer.Observer<OperationResultEnvelope<unknown, TRPCClientError<BuiltRouter<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{}>>>>, TRPCClientError<...>>.complete: () => voidcomplete();
},
});
return const unsubscribe: Unsubscribableunsubscribe;
});
};
};References
If you need a more real reference for creating your custom link, you can check out some of the built-in links tRPC provides on GitHub.
The terminating link
The terminating link is the last link in a link chain. Instead of calling the next function, the terminating link is responsible for sending your composed tRPC operation to the tRPC server and returning an OperationResultEnvelope.
The links array that you add to the tRPC client config should have at least one link, and that link should be a terminating link. If links don't have a terminating link at the end of them, the tRPC operation will not be sent to the tRPC server.
httpBatchLink is the recommended terminating link by tRPC. Pick another one depending on your needs:
| Link | Use it for |
|---|---|
httpBatchLink | Queries and mutations. Calls made together, for example in a Promise.all, are sent as one HTTP request. |
httpLink | One HTTP request per call, for example to skip batching or to send FormData and binary inputs. |
httpBatchStreamLink | Batching, but each response is streamed as soon as it's ready. Required for procedures that return async generators or promises nested in the output. |
httpSubscriptionLink | Subscriptions over Server-sent Events. |
wsLink | Any operation over a WebSocket. |
unstable_localLink | Calling a router in the same process, without HTTP. |
The HTTP links don't handle subscriptions, so to use subscriptions alongside them, route by operation type with a splitLink:
import {
function createTRPCClient<TRouter extends AnyRouter>(opts: CreateTRPCClientOptions<TRouter>): TRPCClient<TRouter>createTRPCClient,
function httpBatchLink<TRouter extends AnyRouter>(opts: HTTPBatchLinkOptions<TRouter["_def"]["_config"]["$types"]>): TRPCLink<TRouter>httpBatchLink,
function httpSubscriptionLink<TInferrable extends InferrableClientTypes, TEventSource extends EventSourceLike.AnyConstructor>(opts: HTTPSubscriptionLinkOptions<inferClientTypes<TInferrable>, TEventSource>): TRPCLink<TInferrable>httpSubscriptionLink,
function loggerLink<TRouter extends AnyRouter = AnyRouter>(opts?: LoggerLinkOptions<TRouter>): TRPCLink<TRouter>loggerLink,
function splitLink<TRouter extends AnyRouter = AnyRouter>(opts: {
condition: (op: Operation) => boolean;
true: TRPCLink<TRouter> | TRPCLink<TRouter>[];
false: TRPCLink<TRouter> | TRPCLink<TRouter>[];
}): TRPCLink<TRouter>
splitLink,
} from '@trpc/client';
import type { type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter } from './server';
export const const trpc: TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>
trpc = createTRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: CreateTRPCClientOptions<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>): TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<...>>>
createTRPCClient<type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter>({
links: TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>[]
links: [
loggerLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts?: LoggerLinkOptions<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>> | undefined): TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<...>>>
loggerLink(),
splitLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: {
condition: (op: Operation) => boolean;
true: TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>> | TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<...>>>[];
false: TRPCLink<...> | TRPCLink<...>[];
}): TRPCLink<...>
splitLink({
condition: (op: Operation) => booleancondition: (op: Operationop) => op: Operationop.type: "query" | "mutation" | "subscription"type === 'subscription',
true: TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>> | TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>[]
true: httpSubscriptionLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>, EventSourceLike.AnyConstructor>(opts: HTTPSubscriptionLinkOptions<PickTypes<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>, EventSourceLike.AnyConstructor>): TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<...>>>
httpSubscriptionLink({ UrlOptionsWithConnectionParams.url: CallbackOrValue<string>url: 'http://localhost:3000' }),
false: TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>> | TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>[]
false: httpBatchLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: HTTPBatchLinkOptions<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>): TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>
httpBatchLink({ url: string | URLurl: 'http://localhost:3000' }),
}),
],
});Managing context
As an operation moves along your link chain, it maintains a context that each link can read and modify. This allows links to pass metadata along the chain that other links use in their execution logic.
Obtain the current context object and modify it by accessing op.context.
You can set the context object's initial value for a particular operation by providing the context parameter to the query or useQuery hook (or mutation, subscription, etc.).
For an example use case, see Disable batching for certain requests.
Common mistakes
- No terminating link at the end of the chain. Calls fail with
No more links to execute - did you forget to add an ending link?. Links listed after the terminating link never run, so putloggerLink,retryLinkand your own links before it. - A
splitLinkbranch without a terminating link. Each branch is its own chain, so each one needs to end in a terminating link. - Sending subscriptions through
httpLink,httpBatchLinkorhttpBatchStreamLink. They throw on subscriptions. Route them tohttpSubscriptionLinkorwsLinkwith asplitLink, as shown in The terminating link. - Returning an async generator from a query through
httpLinkorhttpBatchLink. The server responds withCannot use stream-like response in non-streaming request - use httpBatchStreamLink. - Reading
opin a batch link'sheaderscallback.httpBatchLinkandhttpBatchStreamLinkcallheaders({ opList })once per batch, with all its operations. OnlyhttpLinkgets a single{ op }. - Unbounded batches.
maxURLLengthandmaxItemsdefault toInfinity. If long URLs hit limits in your server or CDN, setmaxURLLength. If the server setsmaxBatchSize, setmaxItemsto the same or a lower value, or larger batches are rejected with400 Bad Request. SeehttpBatchLink. - Forgetting the transformer on one link. If your server uses a data transformer, every terminating link needs the same
transformer, in everysplitLinkbranch.
Pages under Links
- HTTP LinkA terminating link that sends each tRPC call as its own HTTP request
- HTTP Batch LinkA terminating link that batches concurrent tRPC calls into a single HTTP request
- HTTP Batch Stream LinkA terminating link that batches tRPC calls and streams each response as soon as it is ready
- HTTP Subscription LinkA terminating link for subscriptions over Server-sent Events
- Local LinkA link for direct procedure calls without HTTP overhead
- WebSocket LinkA terminating link that sends tRPC operations over a WebSocket
- Split LinkA link that routes each operation down one of two link chains based on a condition
- Logger LinkA link that logs tRPC operations and their results
- Retry LinkA link that retries failed operations
- Headers
- CORS & Cookies