or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

advanced-features.mdclient-creation.mderror-handling.mdhttp-links.mdindex.mdutility-links.mdwebsocket-links.md

utility-links.mddocs/

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

```