0
# Advanced Features
1
2
Advanced functionality including local transport for in-process communication, unstable internal APIs, and utility functions for specialized use cases.
3
4
## Capabilities
5
6
### unstable_localLink
7
8
Creates a local transport link that allows direct in-process communication with a tRPC router, bypassing HTTP entirely. This is useful for server-side rendering, testing, or when both client and server code run in the same process.
9
10
```typescript { .api }
11
/**
12
* Creates a local transport link for in-process router communication
13
* @param opts - Local link configuration options
14
* @returns Local transport link that calls router procedures directly
15
*/
16
function unstable_localLink<TRouter extends AnyRouter>(
17
opts: LocalLinkOptions<TRouter>
18
): TRPCLink<TRouter>;
19
20
interface LocalLinkOptions<TRouter extends AnyRouter> {
21
/** tRPC router instance to call directly */
22
router: TRouter;
23
24
/** Function to create request context */
25
createContext: () => Promise<inferRouterContext<TRouter>>;
26
27
/** Error handler for procedure errors */
28
onError?: (opts: ErrorHandlerOptions<inferRouterContext<TRouter>>) => void;
29
30
/** Data transformation configuration */
31
transformer?: inferClientTypes<TRouter>['transformer'];
32
}
33
34
interface ErrorHandlerOptions<TContext> {
35
/** The tRPC error that occurred */
36
error: TRPCError;
37
/** Operation type that failed */
38
type: 'query' | 'mutation' | 'subscription';
39
/** Procedure path that failed */
40
path: string;
41
/** Input data that caused the error */
42
input: unknown;
43
/** Request context at time of error */
44
ctx: TContext | undefined;
45
}
46
47
type inferRouterContext<TRouter extends AnyRouter> =
48
TRouter['_def']['_config']['$types']['ctx'];
49
50
/**
51
* @deprecated Renamed to unstable_localLink, will be removed in future version
52
*/
53
const experimental_localLink = unstable_localLink;
54
```
55
56
**Usage Examples:**
57
58
```typescript
59
import { createTRPCClient, unstable_localLink } from "@trpc/client";
60
import { appRouter } from "./server/router";
61
62
// Basic local link setup
63
const client = createTRPCClient<typeof appRouter>({
64
links: [
65
unstable_localLink({
66
router: appRouter,
67
createContext: async () => ({
68
user: { id: '1', name: 'Test User' },
69
db: mockDatabase,
70
}),
71
}),
72
],
73
});
74
75
// Server-side rendering with Next.js
76
export async function getServerSideProps() {
77
const client = createTRPCClient<AppRouter>({
78
links: [
79
unstable_localLink({
80
router: appRouter,
81
createContext: async () => ({
82
// Create context for SSR
83
user: await getServerSideUser(),
84
db: database,
85
}),
86
}),
87
],
88
});
89
90
const posts = await client.posts.getAll.query();
91
92
return {
93
props: {
94
posts,
95
},
96
};
97
}
98
99
// Testing with local link
100
describe('User operations', () => {
101
const createTestClient = () => createTRPCClient<AppRouter>({
102
links: [
103
unstable_localLink({
104
router: appRouter,
105
createContext: async () => ({
106
user: { id: 'test-user', role: 'admin' },
107
db: testDatabase,
108
}),
109
onError: ({ error, type, path, input }) => {
110
console.error(`Test error in ${type} ${path}:`, error.message);
111
console.error('Input:', input);
112
},
113
}),
114
],
115
});
116
117
it('should create user', async () => {
118
const client = createTestClient();
119
const user = await client.user.create.mutate({
120
name: 'Test User',
121
email: 'test@example.com',
122
});
123
expect(user.name).toBe('Test User');
124
});
125
});
126
127
// Development mode with fallback
128
const isDevelopment = process.env.NODE_ENV === 'development';
129
const useLocalRouter = process.env.USE_LOCAL_ROUTER === 'true';
130
131
const client = createTRPCClient<AppRouter>({
132
links: [
133
splitLink({
134
condition: () => isDevelopment && useLocalRouter,
135
true: unstable_localLink({
136
router: appRouter,
137
createContext: async () => developmentContext,
138
}),
139
false: httpBatchLink({
140
url: 'http://localhost:3000/trpc',
141
}),
142
}),
143
],
144
});
145
```
146
147
### Data Transformers
148
149
Utilities for configuring data transformation between client and server, supporting serialization of complex types like dates, sets, and custom objects.
150
151
```typescript { .api }
152
/**
153
* Resolves transformer configuration to a combined data transformer
154
* @param transformer - Transformer options (undefined, single, or separate input/output)
155
* @returns Combined transformer with input and output serialization
156
*/
157
function getTransformer(
158
transformer: TransformerOptions<any>['transformer']
159
): CombinedDataTransformer;
160
161
interface CombinedDataTransformer {
162
/** Input data transformation (client → server) */
163
input: {
164
serialize: (data: any) => any;
165
deserialize: (data: any) => any;
166
};
167
/** Output data transformation (server → client) */
168
output: {
169
serialize: (data: any) => any;
170
deserialize: (data: any) => any;
171
};
172
}
173
174
interface DataTransformerOptions {
175
/** Serialize function for outgoing data */
176
serialize: (data: any) => any;
177
/** Deserialize function for incoming data */
178
deserialize: (data: any) => any;
179
}
180
181
type TransformerOptions<TRoot extends Pick<AnyClientTypes, 'transformer'>> =
182
TRoot['transformer'] extends true
183
? { transformer: DataTransformerOptions }
184
: { transformer?: never };
185
```
186
187
**Transformer Examples:**
188
189
```typescript
190
import superjson from "superjson";
191
import { createTRPCClient, httpBatchLink } from "@trpc/client";
192
193
// Using superjson for Date, Set, Map, etc.
194
const client = createTRPCClient<AppRouter>({
195
links: [
196
httpBatchLink({
197
url: "http://localhost:3000/trpc",
198
transformer: superjson,
199
}),
200
],
201
});
202
203
// Custom transformer for specific data types
204
const customTransformer = {
205
serialize: (data: any) => {
206
// Convert BigInt to string for JSON serialization
207
return JSON.parse(JSON.stringify(data, (key, value) =>
208
typeof value === 'bigint' ? value.toString() + 'n' : value
209
));
210
},
211
deserialize: (data: any) => {
212
// Convert string back to BigInt
213
return JSON.parse(JSON.stringify(data), (key, value) => {
214
if (typeof value === 'string' && value.endsWith('n')) {
215
return BigInt(value.slice(0, -1));
216
}
217
return value;
218
});
219
},
220
};
221
222
const client = createTRPCClient<AppRouter>({
223
links: [
224
httpBatchLink({
225
url: "http://localhost:3000/trpc",
226
transformer: customTransformer,
227
}),
228
],
229
});
230
231
// Separate input and output transformers
232
const asymmetricTransformer = {
233
input: {
234
serialize: (data: any) => {
235
// Client → Server transformation
236
return JSON.stringify(data);
237
},
238
deserialize: (data: any) => {
239
// Server processing of client data
240
return JSON.parse(data);
241
},
242
},
243
output: {
244
serialize: (data: any) => {
245
// Server → Client preparation
246
return JSON.stringify(data);
247
},
248
deserialize: (data: any) => {
249
// Client processing of server data
250
return JSON.parse(data);
251
},
252
},
253
};
254
```
255
256
### Fetch Utilities
257
258
Utilities for resolving and configuring fetch implementations across different environments.
259
260
```typescript { .api }
261
/**
262
* Resolves fetch implementation from custom, window, or globalThis
263
* @param customFetchImpl - Optional custom fetch implementation
264
* @returns Resolved fetch function
265
* @throws Error if no fetch implementation is available
266
*/
267
function getFetch(customFetchImpl?: FetchEsque | NativeFetchEsque): FetchEsque;
268
269
/** Standard fetch function interface */
270
type FetchEsque = (input: RequestInfo, init?: RequestInit) => Promise<Response>;
271
272
/** Native fetch with additional Node.js compatibility */
273
type NativeFetchEsque = typeof fetch;
274
275
/** Request configuration interface */
276
interface RequestInitEsque {
277
method?: string;
278
headers?: Record<string, string> | Headers;
279
body?: string | FormData | URLSearchParams;
280
signal?: AbortSignal;
281
}
282
283
/** Response interface for fetch operations */
284
interface ResponseEsque {
285
ok: boolean;
286
status: number;
287
statusText: string;
288
headers: Headers;
289
json(): Promise<any>;
290
text(): Promise<string>;
291
}
292
```
293
294
**Fetch Utility Examples:**
295
296
```typescript
297
import { getFetch } from "@trpc/client";
298
import fetch from "node-fetch";
299
300
// Node.js environment
301
const nodeFetch = getFetch(fetch as any);
302
303
// Browser environment (automatic detection)
304
const browserFetch = getFetch(); // Uses window.fetch
305
306
// Custom fetch with middleware
307
const customFetch: FetchEsque = async (input, init) => {
308
console.log("Making request to:", input);
309
const response = await fetch(input, init);
310
console.log("Response status:", response.status);
311
return response;
312
};
313
314
const client = createTRPCClient<AppRouter>({
315
links: [
316
httpBatchLink({
317
url: "http://localhost:3000/trpc",
318
fetch: customFetch,
319
}),
320
],
321
});
322
323
// Fetch with retry logic
324
const retryFetch: FetchEsque = async (input, init) => {
325
const maxRetries = 3;
326
let lastError: Error;
327
328
for (let attempt = 0; attempt < maxRetries; attempt++) {
329
try {
330
const response = await fetch(input, init);
331
if (response.ok) {
332
return response;
333
}
334
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
335
} catch (error) {
336
lastError = error as Error;
337
if (attempt < maxRetries - 1) {
338
await new Promise(resolve => setTimeout(resolve, 1000 * (attempt + 1)));
339
}
340
}
341
}
342
343
throw lastError!;
344
};
345
```
346
347
### Connection State Management
348
349
Types and utilities for managing connection states in WebSocket and subscription scenarios.
350
351
```typescript { .api }
352
/** Connection state for real-time transports */
353
type TRPCConnectionState<TError = unknown> =
354
| { type: 'connecting' }
355
| { type: 'open' }
356
| { type: 'closed' }
357
| { type: 'error'; error: TError };
358
359
/** Observable connection state management */
360
interface ConnectionStateManager<TError> {
361
/** Current connection state */
362
state: TRPCConnectionState<TError>;
363
/** Subscribe to state changes */
364
subscribe: (observer: { next: (state: TRPCConnectionState<TError>) => void }) => Unsubscribable;
365
/** Update connection state */
366
next: (state: TRPCConnectionState<TError>) => void;
367
}
368
```
369
370
**Connection State Examples:**
371
372
```typescript
373
// Monitor connection state in React
374
function useConnectionState() {
375
const [connectionState, setConnectionState] = useState<TRPCConnectionState>({ type: 'closed' });
376
377
useEffect(() => {
378
const wsClient = createWSClient({
379
url: "ws://localhost:3001",
380
onOpen: () => setConnectionState({ type: 'open' }),
381
onClose: () => setConnectionState({ type: 'closed' }),
382
onError: (error) => setConnectionState({ type: 'error', error }),
383
});
384
385
// Monitor connection state changes
386
const subscription = wsClient.connectionState.subscribe({
387
next: setConnectionState,
388
});
389
390
return () => {
391
subscription.unsubscribe();
392
wsClient.close();
393
};
394
}, []);
395
396
return connectionState;
397
}
398
399
// Connection state in local link
400
const client = createTRPCClient<AppRouter>({
401
links: [
402
unstable_localLink({
403
router: appRouter,
404
createContext: async () => ({ db: mockDatabase }),
405
onError: ({ error, type, path }) => {
406
// Handle local procedure errors
407
console.error(`Local ${type} error in ${path}:`, error.message);
408
},
409
}),
410
],
411
});
412
```
413
414
### Type Utilities
415
416
Advanced TypeScript utilities for working with tRPC client types.
417
418
```typescript { .api }
419
/** Infer client types from router definition */
420
type inferClientTypes<TRouter extends AnyRouter> = TRouter['_def']['_config']['$types'];
421
422
/** Infer router client interface */
423
type inferRouterClient<TRouter extends AnyRouter> = TRPCClient<TRouter>;
424
425
/** Infer procedure input type */
426
type inferProcedureInput<TProcedure extends AnyProcedure> = TProcedure['_def']['input'];
427
428
/** Infer procedure output type */
429
type inferProcedureOutput<TProcedure extends AnyProcedure> = TProcedure['_def']['output'];
430
431
/** Any router type constraint */
432
type AnyRouter = {
433
_def: {
434
_config: { $types: any };
435
record: Record<string, any>;
436
};
437
};
438
439
/** Any procedure type constraint */
440
type AnyProcedure = {
441
_def: {
442
type: 'query' | 'mutation' | 'subscription';
443
input: any;
444
output: any;
445
};
446
};
447
```
448
449
**Type Utility Examples:**
450
451
```typescript
452
import type { inferRouterClient, inferProcedureInput } from "@trpc/client";
453
454
// Infer client type from router
455
type MyClient = inferRouterClient<typeof appRouter>;
456
457
// Infer input type for specific procedure
458
type CreateUserInput = inferProcedureInput<typeof appRouter.user.create>;
459
460
// Type-safe client wrapper
461
class TypedTRPCClient<TRouter extends AnyRouter> {
462
constructor(private client: inferRouterClient<TRouter>) {}
463
464
async safeQuery<TPath extends keyof TRouter['_def']['record']>(
465
path: TPath,
466
input?: inferProcedureInput<TRouter['_def']['record'][TPath]>
467
) {
468
try {
469
return await (this.client as any)[path].query(input);
470
} catch (error) {
471
if (isTRPCClientError(error)) {
472
console.error(`Query ${String(path)} failed:`, error.message);
473
return null;
474
}
475
throw error;
476
}
477
}
478
}
479
480
// Usage with inferred types
481
const typedClient = new TypedTRPCClient(client);
482
const user = await typedClient.safeQuery('user.getById', { id: 1 });
483
```
484
485
### Development and Testing Utilities
486
487
Utilities for development, testing, and debugging tRPC applications.
488
489
```typescript { .api }
490
/** Mock client factory for testing */
491
interface MockClientOptions<TRouter extends AnyRouter> {
492
router?: Partial<TRouter>;
493
mockResponses?: Record<string, any>;
494
delay?: number;
495
errorRate?: number;
496
}
497
498
/** Development debugging helpers */
499
interface DebugOptions {
500
logLevel: 'none' | 'error' | 'warn' | 'info' | 'debug';
501
includeContext: boolean;
502
includeInput: boolean;
503
includeOutput: boolean;
504
}
505
```
506
507
**Development Utility Examples:**
508
509
```typescript
510
// Mock client for testing
511
function createMockClient<TRouter extends AnyRouter>(
512
options: MockClientOptions<TRouter>
513
): TRPCClient<TRouter> {
514
return createTRPCClient<TRouter>({
515
links: [
516
unstable_localLink({
517
router: {
518
user: {
519
getById: async ({ input }: { input: { id: number } }) => {
520
await new Promise(resolve => setTimeout(resolve, options.delay || 0));
521
522
if (Math.random() < (options.errorRate || 0)) {
523
throw new Error('Mock error');
524
}
525
526
return options.mockResponses?.[`user.getById.${input.id}`] || {
527
id: input.id,
528
name: `Mock User ${input.id}`,
529
};
530
},
531
},
532
} as any,
533
createContext: async () => ({}),
534
}),
535
],
536
});
537
}
538
539
// Debug client wrapper
540
function createDebugClient<TRouter extends AnyRouter>(
541
baseClient: TRPCClient<TRouter>,
542
options: DebugOptions
543
): TRPCClient<TRouter> {
544
const handler = {
545
get(target: any, prop: string) {
546
const value = target[prop];
547
548
if (typeof value === 'object' && value !== null) {
549
return new Proxy(value, handler);
550
}
551
552
if (typeof value === 'function' && ['query', 'mutate', 'subscribe'].includes(prop)) {
553
return new Proxy(value, {
554
apply(target, thisArg, args) {
555
if (options.logLevel !== 'none') {
556
console.log(`Debug: ${prop} called with:`, args);
557
}
558
559
const result = target.apply(thisArg, args);
560
561
if (result instanceof Promise) {
562
return result.then(
563
(data) => {
564
if (options.includeOutput && options.logLevel !== 'none') {
565
console.log(`Debug: ${prop} result:`, data);
566
}
567
return data;
568
},
569
(error) => {
570
if (options.logLevel !== 'none') {
571
console.error(`Debug: ${prop} error:`, error);
572
}
573
throw error;
574
}
575
);
576
}
577
578
return result;
579
},
580
});
581
}
582
583
return value;
584
},
585
};
586
587
return new Proxy(baseClient, handler);
588
}
589
590
// Usage in tests
591
describe('User API', () => {
592
const mockClient = createMockClient<AppRouter>({
593
mockResponses: {
594
'user.getById.1': { id: 1, name: 'Alice', email: 'alice@example.com' },
595
'user.getById.2': { id: 2, name: 'Bob', email: 'bob@example.com' },
596
},
597
delay: 100,
598
errorRate: 0.1,
599
});
600
601
it('should fetch user by ID', async () => {
602
const user = await mockClient.user.getById.query({ id: 1 });
603
expect(user.name).toBe('Alice');
604
});
605
});
606
```