0
# Utility Links
1
2
Middleware links for logging, conditional routing, retry logic, and other cross-cutting concerns in the link chain. These links provide essential functionality for debugging, reliability, and routing operations.
3
4
## Capabilities
5
6
### loggerLink
7
8
Logs tRPC operations for debugging purposes, providing detailed information about requests, responses, and errors.
9
10
```typescript { .api }
11
/**
12
* Creates a logging link for debugging tRPC operations
13
* @param opts - Logger configuration options
14
* @returns Logger middleware link for the client chain
15
*/
16
function loggerLink<TRouter extends AnyRouter>(
17
opts?: LoggerLinkOptions<TRouter>
18
): TRPCLink<TRouter>;
19
20
interface LoggerLinkOptions<TRouter extends AnyRouter> {
21
/** Custom logging function */
22
logger?: LoggerLinkFn<TRouter>;
23
24
/** Conditional logging function */
25
enabled?: EnabledFn<TRouter>;
26
27
/** Console implementation to use */
28
console?: ConsoleEsque;
29
30
/** Color mode for log output */
31
colorMode?: ColorMode;
32
33
/** Include operation context in logs */
34
withContext?: boolean;
35
}
36
37
type LoggerLinkFn<TRouter extends AnyRouter> = (
38
opts: LoggerLinkFnOptions<TRouter>
39
) => void;
40
41
interface LoggerLinkFnOptions<TRouter extends AnyRouter> extends Operation {
42
direction: 'up' | 'down';
43
result?: OperationResultEnvelope<unknown, TRPCClientError<TRouter>> | TRPCClientError<TRouter>;
44
elapsedMs?: number;
45
}
46
47
type EnabledFn<TRouter extends AnyRouter> = (
48
opts: EnableFnOptions<TRouter>
49
) => boolean;
50
51
type ColorMode = 'ansi' | 'css' | 'none';
52
53
interface ConsoleEsque {
54
log: (...args: any[]) => void;
55
error: (...args: any[]) => void;
56
}
57
```
58
59
**Usage Examples:**
60
61
```typescript
62
import { createTRPCClient, httpBatchLink, loggerLink } from "@trpc/client";
63
64
// Basic logging (logs all operations)
65
const client = createTRPCClient<AppRouter>({
66
links: [
67
loggerLink(),
68
httpBatchLink({
69
url: "http://localhost:3000/trpc",
70
}),
71
],
72
});
73
74
// Conditional logging (only errors)
75
const client = createTRPCClient<AppRouter>({
76
links: [
77
loggerLink({
78
enabled: (opts) =>
79
opts.direction === 'down' && opts.result instanceof Error,
80
}),
81
httpBatchLink({
82
url: "http://localhost:3000/trpc",
83
}),
84
],
85
});
86
87
// Custom logger with detailed information
88
const client = createTRPCClient<AppRouter>({
89
links: [
90
loggerLink({
91
logger: ({ op, direction, result, elapsedMs }) => {
92
if (direction === 'up') {
93
console.log(`π ${op.type.toUpperCase()} ${op.path}`, {
94
input: op.input,
95
context: op.context,
96
});
97
} else {
98
const success = !(result instanceof Error);
99
const icon = success ? 'β ' : 'β';
100
console.log(`${icon} ${op.type.toUpperCase()} ${op.path} (${elapsedMs}ms)`, {
101
result: success ? result : result.message,
102
});
103
}
104
},
105
}),
106
httpBatchLink({
107
url: "http://localhost:3000/trpc",
108
}),
109
],
110
});
111
112
// Development vs production logging
113
const client = createTRPCClient<AppRouter>({
114
links: [
115
loggerLink({
116
enabled: () => process.env.NODE_ENV === 'development',
117
colorMode: process.env.NODE_ENV === 'development' ? 'ansi' : 'none',
118
withContext: true,
119
}),
120
httpBatchLink({
121
url: "http://localhost:3000/trpc",
122
}),
123
],
124
});
125
```
126
127
### splitLink
128
129
Conditionally routes operations to different link chains based on operation characteristics, enabling different transport or middleware for different types of operations.
130
131
```typescript { .api }
132
/**
133
* Creates a conditional routing link that splits operations between different link chains
134
* @param opts - Split configuration options
135
* @returns Split routing link for conditional operation handling
136
*/
137
function splitLink<TRouter extends AnyRouter>(opts: {
138
/** Routing predicate function */
139
condition: (op: Operation) => boolean;
140
/** Link(s) for operations where condition returns true */
141
true: TRPCLink<TRouter> | TRPCLink<TRouter>[];
142
/** Link(s) for operations where condition returns false */
143
false: TRPCLink<TRouter> | TRPCLink<TRouter>[];
144
}): TRPCLink<TRouter>;
145
146
interface Operation {
147
/** Operation type */
148
type: 'query' | 'mutation' | 'subscription';
149
/** Procedure path */
150
path: string;
151
/** Operation input data */
152
input: unknown;
153
/** Operation context */
154
context: OperationContext;
155
/** Request ID */
156
id: number;
157
/** Abort signal for cancellation */
158
signal: AbortSignal | null;
159
}
160
```
161
162
**Usage Examples:**
163
164
```typescript
165
import {
166
createTRPCClient,
167
httpBatchLink,
168
wsLink,
169
splitLink,
170
createWSClient
171
} from "@trpc/client";
172
173
// Split by operation type (HTTP for queries/mutations, WebSocket for subscriptions)
174
const wsClient = createWSClient({
175
url: "ws://localhost:3001",
176
});
177
178
const client = createTRPCClient<AppRouter>({
179
links: [
180
splitLink({
181
condition: (op) => op.type === 'subscription',
182
true: wsLink({
183
client: wsClient,
184
}),
185
false: httpBatchLink({
186
url: "http://localhost:3000/trpc",
187
}),
188
}),
189
],
190
});
191
192
// Split by procedure path (different endpoints for different services)
193
const client = createTRPCClient<AppRouter>({
194
links: [
195
splitLink({
196
condition: (op) => op.path.startsWith('analytics.'),
197
true: httpBatchLink({
198
url: "http://analytics-service:3000/trpc",
199
}),
200
false: httpBatchLink({
201
url: "http://main-service:3000/trpc",
202
}),
203
}),
204
],
205
});
206
207
// Split by input characteristics (large file uploads use different endpoint)
208
const client = createTRPCClient<AppRouter>({
209
links: [
210
splitLink({
211
condition: (op) => {
212
return op.type === 'mutation' &&
213
op.input instanceof FormData ||
214
(op.input as any)?.fileSize > 1024 * 1024; // 1MB
215
},
216
true: httpLink({
217
url: "http://upload-service:3000/trpc",
218
}),
219
false: httpBatchLink({
220
url: "http://localhost:3000/trpc",
221
}),
222
}),
223
],
224
});
225
226
// Multi-level split with nested conditions
227
const client = createTRPCClient<AppRouter>({
228
links: [
229
splitLink({
230
condition: (op) => op.type === 'subscription',
231
true: wsLink({ client: wsClient }),
232
false: splitLink({
233
condition: (op) => op.path.startsWith('admin.'),
234
true: httpLink({
235
url: "http://admin-service:3000/trpc",
236
headers: {
237
'X-Admin-Access': 'true',
238
},
239
}),
240
false: httpBatchLink({
241
url: "http://localhost:3000/trpc",
242
}),
243
}),
244
}),
245
],
246
});
247
```
248
249
### retryLink
250
251
Adds retry logic to failed operations with configurable retry conditions and delay strategies.
252
253
```typescript { .api }
254
/**
255
* Creates a retry link that automatically retries failed operations
256
* @param opts - Retry configuration options
257
* @returns Retry middleware link for automatic operation retry
258
*/
259
function retryLink<TInferrable extends InferrableClientTypes>(
260
opts: RetryLinkOptions<TInferrable>
261
): TRPCLink<TInferrable>;
262
263
interface RetryLinkOptions<TInferrable extends InferrableClientTypes> {
264
/** Function to determine if operation should be retried */
265
retry: (opts: RetryFnOptions<TInferrable>) => boolean;
266
/** Delay calculation function between retries */
267
retryDelayMs?: (attempt: number) => number;
268
}
269
270
interface RetryFnOptions<TInferrable extends InferrableClientTypes> {
271
/** The operation that failed */
272
op: Operation;
273
/** The error that occurred */
274
error: TRPCClientError<TInferrable>;
275
/** Number of attempts made (including initial call) */
276
attempts: number;
277
}
278
```
279
280
**Usage Examples:**
281
282
```typescript
283
import { createTRPCClient, httpBatchLink, retryLink, isTRPCClientError } from "@trpc/client";
284
285
// Basic retry for network errors
286
const client = createTRPCClient<AppRouter>({
287
links: [
288
retryLink({
289
retry: ({ error, attempts }) => {
290
// Retry up to 3 times for network errors
291
return attempts <= 3 &&
292
(error.cause?.name === 'TypeError' ||
293
error.meta?.response?.status >= 500);
294
},
295
retryDelayMs: (attempt) => Math.min(1000 * Math.pow(2, attempt), 30000),
296
}),
297
httpBatchLink({
298
url: "http://localhost:3000/trpc",
299
}),
300
],
301
});
302
303
// Sophisticated retry strategy
304
const client = createTRPCClient<AppRouter>({
305
links: [
306
retryLink({
307
retry: ({ op, error, attempts }) => {
308
// Never retry mutations (they might not be idempotent)
309
if (op.type === 'mutation') {
310
return false;
311
}
312
313
// Don't retry client-side validation errors
314
if (error.data?.code === 'BAD_REQUEST') {
315
return false;
316
}
317
318
// Retry up to 5 times for queries
319
if (attempts > 5) {
320
return false;
321
}
322
323
// Retry network errors and 5xx server errors
324
const isNetworkError = error.cause?.name === 'TypeError';
325
const isServerError = error.meta?.response?.status >= 500;
326
const isRateLimit = error.meta?.response?.status === 429;
327
328
return isNetworkError || isServerError || isRateLimit;
329
},
330
retryDelayMs: (attempt) => {
331
// Exponential backoff with jitter
332
const baseDelay = Math.min(1000 * Math.pow(2, attempt), 30000);
333
const jitter = Math.random() * 0.3 * baseDelay;
334
return baseDelay + jitter;
335
},
336
}),
337
httpBatchLink({
338
url: "http://localhost:3000/trpc",
339
}),
340
],
341
});
342
343
// Conditional retry based on operation
344
const client = createTRPCClient<AppRouter>({
345
links: [
346
retryLink({
347
retry: ({ op, error, attempts }) => {
348
// Critical operations get more retries
349
const isCritical = op.path.includes('payment') || op.path.includes('auth');
350
const maxAttempts = isCritical ? 5 : 3;
351
352
if (attempts > maxAttempts) {
353
return false;
354
}
355
356
// Handle rate limiting with exponential backoff
357
if (error.meta?.response?.status === 429) {
358
return attempts <= 10; // Allow more retries for rate limits
359
}
360
361
// Standard retry conditions
362
return error.cause?.name === 'TypeError' ||
363
error.meta?.response?.status >= 500;
364
},
365
retryDelayMs: (attempt) => {
366
// Different delays for different error types
367
return attempt === 0 ? 0 : Math.min(500 * attempt, 10000);
368
},
369
}),
370
httpBatchLink({
371
url: "http://localhost:3000/trpc",
372
}),
373
],
374
});
375
```
376
377
### Custom Utility Links
378
379
Examples of creating custom utility links for specific use cases.
380
381
```typescript { .api }
382
/** Generic link function signature */
383
type TRPCLink<TInferrable extends InferrableClientTypes> = (
384
runtime: TRPCClientRuntime
385
) => OperationLink<TInferrable>;
386
387
/** Operation link after runtime initialization */
388
type OperationLink<TInferrable extends InferrableClientTypes> = (
389
opts: { op: Operation }
390
) => Observable<OperationResultEnvelope>;
391
392
/** Client runtime configuration */
393
interface TRPCClientRuntime {
394
[key: string]: unknown;
395
}
396
```
397
398
**Custom Link Examples:**
399
400
```typescript
401
import { observable } from "@trpc/server/observable";
402
403
// Authentication link that adds auth headers
404
function authLink(): TRPCLink<any> {
405
return () => {
406
return ({ op, next }) => {
407
return observable((observer) => {
408
// Add authentication context
409
const authedOp = {
410
...op,
411
context: {
412
...op.context,
413
authorization: getAuthToken(),
414
},
415
};
416
417
return next(authedOp).subscribe(observer);
418
});
419
};
420
};
421
}
422
423
// Caching link for read operations
424
function cacheLink(cache: Map<string, any>): TRPCLink<any> {
425
return () => {
426
return ({ op, next }) => {
427
return observable((observer) => {
428
if (op.type === 'query') {
429
const cacheKey = `${op.path}:${JSON.stringify(op.input)}`;
430
const cached = cache.get(cacheKey);
431
432
if (cached) {
433
observer.next({ result: { type: 'data', data: cached } });
434
observer.complete();
435
return;
436
}
437
438
return next(op).subscribe({
439
next: (envelope) => {
440
// Cache successful query results
441
if (envelope.result.type === 'data') {
442
cache.set(cacheKey, envelope.result.data);
443
}
444
observer.next(envelope);
445
},
446
error: (err) => observer.error(err),
447
complete: () => observer.complete(),
448
});
449
}
450
451
return next(op).subscribe(observer);
452
});
453
};
454
};
455
}
456
457
// Rate limiting link
458
function rateLimitLink(requestsPerSecond: number): TRPCLink<any> {
459
const requests: number[] = [];
460
461
return () => {
462
return ({ op, next }) => {
463
return observable((observer) => {
464
const now = Date.now();
465
466
// Clean old requests
467
while (requests.length > 0 && now - requests[0] > 1000) {
468
requests.shift();
469
}
470
471
// Check rate limit
472
if (requests.length >= requestsPerSecond) {
473
const delay = 1000 - (now - requests[0]);
474
setTimeout(() => {
475
requests.push(now + delay);
476
next(op).subscribe(observer);
477
}, delay);
478
return;
479
}
480
481
requests.push(now);
482
return next(op).subscribe(observer);
483
});
484
};
485
};
486
}
487
488
// Usage of custom links
489
const client = createTRPCClient<AppRouter>({
490
links: [
491
loggerLink({ enabled: () => process.env.NODE_ENV === 'development' }),
492
authLink(),
493
cacheLink(new Map()),
494
rateLimitLink(10), // 10 requests per second
495
retryLink({
496
retry: ({ attempts }) => attempts <= 3,
497
retryDelayMs: (attempt) => 1000 * attempt,
498
}),
499
httpBatchLink({
500
url: "http://localhost:3000/trpc",
501
}),
502
],
503
});
504
```
505
506
### Link Chain Composition
507
508
Understanding how utility links work together in the client chain.
509
510
```typescript { .api }
511
/** Example of a complete link chain flow */
512
interface LinkChainFlow {
513
/** 1. Request starts from client */
514
clientRequest: Operation;
515
516
/** 2. Goes through each link in order */
517
linkProcessing: Array<{
518
linkName: string;
519
transforms: string[];
520
canModifyOp: boolean;
521
canModifyResult: boolean;
522
}>;
523
524
/** 3. Reaches terminating link (HTTP, WebSocket, etc.) */
525
terminalTransport: string;
526
527
/** 4. Response flows back through links in reverse order */
528
responseProcessing: Array<{
529
linkName: string;
530
transforms: string[];
531
}>;
532
533
/** 5. Final result delivered to client */
534
clientResponse: any;
535
}
536
```
537
538
**Link Chain Examples:**
539
540
```typescript
541
// Understanding link execution order
542
const client = createTRPCClient<AppRouter>({
543
links: [
544
// 1. Logger (request up) - logs outgoing request
545
loggerLink(),
546
547
// 2. Auth - adds authentication headers
548
authLink(),
549
550
// 3. Retry - handles retry logic
551
retryLink({
552
retry: ({ attempts }) => attempts <= 3,
553
}),
554
555
// 4. Split - routes based on operation type
556
splitLink({
557
condition: (op) => op.type === 'subscription',
558
true: wsLink({ client: wsClient }),
559
false: [
560
// 5a. Cache (for HTTP operations)
561
cacheLink(new Map()),
562
563
// 6a. HTTP transport (terminating link)
564
httpBatchLink({
565
url: "http://localhost:3000/trpc",
566
}),
567
],
568
}),
569
],
570
});
571
572
// Flow for a query operation:
573
// Request: Client β Logger β Auth β Retry β Split β Cache β HTTP β Server
574
// Response: Server β HTTP β Cache β Split β Retry β Auth β Logger β Client
575
576
// Flow for a subscription:
577
// Request: Client β Logger β Auth β Retry β Split β WebSocket β Server
578
// Response: Server β WebSocket β Split β Retry β Auth β Logger β Client
579
```