Jesse A. Green / notebook

tRPC Admin & Architecture Guide

Generated June 2026 from trpc/trpc at c7360d4

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

This guide is for the engineers, architects, and operators who integrate tRPC deeply, reason about its internals, deploy it in production, or contribute to the project. Where the User Guide shows you how to use tRPC, this guide explains how it works: the type-level engine, the wire protocol, the client proxy/link architecture, the adapter contract, security posture, and the monorepo itself.

Everything here is derived from the tRPC source at the commit stamped in the footer (v11, 11.17.0).


Table of Contents

  1. Executive Summary
  2. Architecture Overview
  3. Repository & Package Structure
  4. The Server Core Engine
  5. The Wire Protocol (RPC)
  6. The Client Architecture
  7. The Adapter Layer
  8. Streaming & Subscriptions Internals
  9. Configuration Reference
  10. Security
  11. Requirements & Compatibility
  12. Deployment
  13. Performance & Scaling
  14. Auxiliary Packages: OpenAPI & Upgrade CLI
  15. Contributing & Development
  16. Code Quality & Patterns
  17. Potential Improvements
  18. Quick Reference

1. Executive Summary

tRPC is a TypeScript-first RPC framework whose defining characteristic is that type safety is achieved entirely at the type level — there is no code generation, no schema language, and no build step. The server defines an appRouter; its inferred type (AppRouter) is imported by the client as a type-only import, and TypeScript's inference engine carries every input, output, and error shape across the network boundary.

Architecturally, tRPC is:

  • A core library (@trpc/server) that is runtime-agnostic and has zero runtime dependencies.
  • A set of host adapters that bridge the core to Node HTTP, Express, Fastify, the Fetch/WinterCG standard (edge), AWS Lambda, Next.js, and WebSockets.
  • A client (@trpc/client) built around a recursive JavaScript Proxy and a composable link chain.
  • Framework integrations (@trpc/react-query, @trpc/tanstack-react-query, @trpc/next) layered on top.
  • Auxiliary tooling: an OpenAPI 3.1 generator CLI (@trpc/openapi) and a codemod-based upgrade CLI (@trpc/upgrade).

The wire format is JSON-RPC 2.0-compliant. The library leans on modern web standards (Request/Response, AbortSignal, async generators, Server-sent Events, ReadableStream) so the same core runs across Node, Deno, Bun, Cloudflare Workers, and Vercel Edge.

Note

The product is the published @trpc/* package family — not the www/ documentation site or the examples/ apps that dominate the repository by file count. This guide documents the libraries.


2. Architecture Overview

                        ┌────────────────────────── DEVELOPER WRITES ──────────────────────────┐
                        │  initTRPC.create()  →  t.router({ ...procedures })  →  AppRouter type  │
                        └───────────────────────────────────────────────────────────────────────┘
                                              │ (type-only import)
        SERVER SIDE                           ▼                          CLIENT SIDE
 ┌───────────────────────────┐      ╔══════════════════╗      ┌───────────────────────────────┐
 │  @trpc/server (core)       │      ║   AppRouter type ║      │  @trpc/client                 │
 │                            │      ╚══════════════════╝      │                               │
 │  initTRPC ─ TRPCBuilder    │                                │  createTRPCClient<AppRouter>  │
 │   ├─ procedureBuilder      │                                │   └─ recursive Proxy          │
 │   ├─ router / mergeRouters │                                │       │                       │
 │   ├─ middleware            │                                │   link chain (ordered):       │
 │   ├─ rootConfig            │                                │     loggerLink → splitLink →  │
 │   ├─ transformer           │     ◀── JSON-RPC 2.0 ──▶       │     httpBatchLink (terminal)  │
 │   ├─ error / TRPCError     │     query / mutation /         │       │                       │
 │   └─ stream (jsonl/sse/    │     subscription envelopes     │   observable pipeline         │
 │       tracked)             │                                │                               │
 └─────────────┬─────────────┘                                └───────────────┬───────────────┘
               │                                                               │
        ┌──────▼────────────────────────────────┐                            │
        │  ADAPTER (host glue)                   │ ◀──────── HTTP / WS / SSE ──┘
        │  standalone│express│fastify│fetch│     │
        │  aws-lambda│next│next-app-dir│ws       │
        │   createContext()  onError()           │
        └────────────────────────────────────────┘

Data flow (a query):

  1. Client code calls trpc.user.byId.query(input). The recursive proxy records the path (['user','byId']) and method (query).
  2. The operation passes through the link chain. Non-terminating links (logger, split, retry) can observe/branch; the terminating link (httpBatchLink) serializes the input (via the transformer) and sends an HTTP request.
  3. The adapter receives the request, runs createContext() once per request, parses the input, and dispatches to the procedure.
  4. The procedure runs its middleware chain, validates input, executes the resolver, optionally validates output, and returns a result.
  5. The result is shaped (and errors formatted via errorFormatter), serialized, and sent back as a JSON-RPC response.
  6. The link chain processes the response in reverse; the client deserializes and resolves the promise with the typed result.

3. Repository & Package Structure

tRPC is a pnpm-workspaces monorepo managed with Turbo and Lerna.

trpc/
├── packages/
│   ├── server/                 @trpc/server — core + adapters (zero runtime deps)
│   │   └── src/
│   │       ├── @trpc/server/   public entry assembly
│   │       ├── adapters/       aws-lambda, express, fastify, fetch, next,
│   │       │                   next-app-dir, node-http, standalone, ws
│   │       ├── observable/     observable primitive (used by client links)
│   │       ├── unstable-core-do-not-import/   THE ENGINE (see §4)
│   │       ├── vendor/         standard-schema-v1, unpromise (vendored)
│   │       ├── http.ts  rpc.ts  shared.ts  index.ts
│   ├── client/                 @trpc/client — vanilla client + links
│   │   └── src/links/          httpLink, httpBatchLink, httpBatchStreamLink,
│   │                           httpSubscriptionLink, wsLink/, splitLink,
│   │                           loggerLink, retryLink, localLink
│   ├── react-query/            @trpc/react-query (classic hooks integration)
│   ├── tanstack-react-query/   @trpc/tanstack-react-query (recommended)
│   ├── next/                   @trpc/next (Pages Router + SSR)
│   ├── openapi/                @trpc/openapi (OpenAPI 3.1 generator + CLI)
│   ├── upgrade/                @trpc/upgrade (codemod CLI)
│   └── tests/                  @trpc/tests — integration test suite (private)
├── examples/                   ~30 runnable example apps (Next, Express, Fastify,
│                               Lambda, Cloudflare, Deno, Bun, SSE chat, …)
├── www/                        Docusaurus documentation site (trpc.io)
├── pnpm-workspace.yaml  turbo.json  lerna.json  tsconfig*.json  vitest.config.ts

Note

The test suite is centralized in packages/tests (the @trpc/tests private package). It imports the published packages and exercises them together — making it true integration testing rather than per-package unit isolation. Regression tests are named by issue number (e.g. pnpm test 3085).

Published packages (all 11.17.0)

Package Runtime deps Key peer deps
@trpc/server none typescript >=5.7.2
@trpc/client none @trpc/server@11.17.0, typescript >=5.7.2
@trpc/react-query — @trpc/client, @trpc/server, @tanstack/react-query, react
@trpc/tanstack-react-query — @tanstack/react-query ^5.80.3, @trpc/client@11.17.0, @trpc/server@11.17.0, react >=18.2.0, typescript >=5.7.2
@trpc/next — @trpc/client, @trpc/server, @trpc/react-query, next, react
@trpc/openapi — tRPC server (peer); CLI binary trpc-openapi
@trpc/upgrade @clack/prompts, ts-morph/typescript (codemod) —

@trpc/server subpath exports (the public surface)

.                              → core (initTRPC, TRPCError, tracked, lazy, infer* helpers)
./adapters/aws-lambda          → awsLambdaRequestHandler
./adapters/express             → createExpressMiddleware
./adapters/fastify             → fastifyTRPCPlugin
./adapters/fetch               → fetchRequestHandler
./adapters/next                → createNextApiHandler
./adapters/next-app-dir        → App Router helpers
./adapters/node-http           → low-level node-http handler (used by standalone)
./adapters/standalone          → createHTTPServer, createHTTPHandler, createHTTP2Handler
./adapters/ws                  → applyWSSHandler
./http                         → getHTTPStatusCodeFromError, content-type helpers
./observable                   → observable (for custom links)
./rpc                          → JSON-RPC types/codes
./shared                       → deprecated re-exports (back-compat)
./unstable-core-do-not-import  → internal glue (do not import directly)

4. The Server Core Engine

The entire engine lives in packages/server/src/unstable-core-do-not-import/. The folder name is a deliberate warning: these are internal modules wired together by the official @trpc/* packages. Application code should import from @trpc/server, not this path.

Important

The unstable-core-do-not-import.ts barrel exists primarily to "make TypeScript happy and prevent 'The inferred type of createContext cannot be named without a reference to […]'" errors. If you find yourself needing to import from it directly, that is a signal to open an issue rather than reach in.

4.1 initTRPC and the builder

initTRPC is a singleton instance of TRPCBuilder. The chainable .context<T>() and .meta<T>() methods only carry generics (they return new builders), and .create(opts) materializes the root object:

const t = initTRPC.context<Context>().meta<Meta>().create(opts);

.create() returns a TRPCRootObject containing:

Member Type Purpose
_config RootConfig Internal resolved config (transformer, formatter, flags).
procedure ProcedureBuilder The root builder all procedures extend.
middleware factory Creates reusable middleware.
router RouterBuilder Creates routers.
mergeRouters function Flat-merges routers.
createCallerFactory factory Creates server-side callers.

At create() time, the config is resolved with defaults: transformer → getDataTransformer(opts.transformer ?? defaultTransformer), isDev → globalThis.process?.env['NODE_ENV'] !== 'production', isServer → isServerDefault, errorFormatter → defaultFormatter, allowOutsideOfServer → false. A guard throws if !isServer && allowOutsideOfServer !== true.

isServerDefault is computed as:

typeof window === 'undefined' ||
'Deno' in window ||
process.env.NODE_ENV === 'test' ||
!!process.env.JEST_WORKER_ID ||
!!process.env.VITEST_WORKER_ID;

4.2 The procedure builder

Procedures are an immutable builder chain. Each of .input(), .output(), .use(), .concat(), .meta(), and .unstable_pipe() returns a new builder, accumulating state without mutation. This is what makes "base procedures" (e.g. authedProcedure = t.procedure.use(...)) safe to share — extending one never affects another.

A procedure terminates with .query(resolver), .mutation(resolver), or .subscription(asyncGenerator). The resulting procedure carries an internal _def describing its type, input/output parsers, and middleware list — the metadata the router and client decorators read.

  • Input merging: chained .input() parsers spread object properties together (later keys win); non-object parsers cannot be merged.
  • Middleware: .use(fn) appends to the chain; fn must call opts.next() and return its result. opts.next({ ctx }) performs context extension — adding/overriding ctx keys typesafely for downstream consumers.

4.3 Routers

router(def) builds a Router whose _def.record maps keys to procedures or nested router records. Inline object literals are coerced into sub-routers. mergeRouters(...) flattens multiple routers into one namespace. lazy(() => import(...)) defers loading a sub-router until first use (cold-start optimization).

Each router also exposes router.createCaller(ctx) and integrates with the standalone createCallerFactory.

4.4 Errors

TRPCError extends Error with a code: TRPC_ERROR_CODE_KEY and an optional cause. Two helpers normalize unknown throwables:

  • getCauseFromUnknown(cause) — wraps primitives/objects into Error/UnknownCauseError.
  • getTRPCErrorFromUnknown(cause) — returns the error as-is if it's already a TRPCError (or name-matches 'TRPCError' across realms, per #4848), otherwise wraps it as INTERNAL_SERVER_ERROR, inheriting the original stack.

The name-based check (cause.name === 'TRPCError') is important in environments with multiple module realms where instanceof can fail.

4.5 Transformers

A transformer is normalized into a CombinedDataTransformer ({ input, output }) by getDataTransformer. input.serialize runs on the client before sending; input.deserialize runs on the server before the resolver. output.serialize runs on the server before responding; output.deserialize runs on the client. The default transformer is an identity pass-through.


5. The Wire Protocol (RPC)

tRPC's wire format is JSON-RPC 2.0-compliant. The canonical types live in unstable-core-do-not-import/rpc/ (envelopes.ts, codes.ts).

5.1 Error codes (JSON-RPC numbers)

tRPC maps its string codes to JSON-RPC 2.0 numeric codes (-32000…-32099 are reserved for implementation-defined server errors). The string code, numeric code, and HTTP status all travel in the error response.

String code JSON-RPC number HTTP
PARSE_ERROR -32700 400
BAD_REQUEST -32600 400
INTERNAL_SERVER_ERROR -32603 500
NOT_IMPLEMENTED -32603 501
BAD_GATEWAY -32603 502
SERVICE_UNAVAILABLE -32603 503
GATEWAY_TIMEOUT -32603 504
UNAUTHORIZED -32001 401
PAYMENT_REQUIRED -32002 402
FORBIDDEN -32003 403
NOT_FOUND -32004 404
METHOD_NOT_SUPPORTED -32005 405
TIMEOUT -32008 408
CONFLICT -32009 409
PRECONDITION_FAILED -32012 412
PAYLOAD_TOO_LARGE -32013 413
UNSUPPORTED_MEDIA_TYPE -32015 415
UNPROCESSABLE_CONTENT -32022 422
PRECONDITION_REQUIRED -32028 428
TOO_MANY_REQUESTS -32029 429
CLIENT_CLOSED_REQUEST -32099 499

Note

The non-implementation-specific numbers (PARSE_ERROR, BAD_REQUEST, the 5xx family) follow JSON-RPC convention; the implementation-specific ones encode the trailing digits of the HTTP 4xx status. Note that the 5xx codes share -32603 (the JSON-RPC "Internal error") — the HTTP status disambiguates them.

retryableRpcCodes marks BAD_GATEWAY, SERVICE_UNAVAILABLE, GATEWAY_TIMEOUT, and INTERNAL_SERVER_ERROR as retryable — out-of-the-box SSE reconnects on these.

5.2 HTTP transport

  • Queries → HTTP GET with input as a URI-encoded JSON ?input= parameter (batched as ?batch=1&input={...}).
  • Mutations → HTTP POST with a JSON body.
  • Batching → multiple operations collapse into one request (httpBatchLink), keyed by index.
  • Streaming → httpBatchStreamLink sets trpc-accept: application/jsonl (or Accept: application/jsonl), and the server responds content-type: application/jsonl with transfer-encoding: chunked, emitting each result as it resolves (JSON-lines).

5.3 WebSocket RPC spec

Over WS, messages carry a JSON-RPC envelope:

// query / mutation request
{ id, jsonrpc?: '2.0', method: 'query' | 'mutation', params: { path, input? } }
// response
{ id, jsonrpc?: '2.0', result: { type: 'data', data } }

// subscription lifecycle
{ id, method: 'subscription', params: { path, input? } }          // start
{ id, method: 'subscription.stop' }                                // stop
{ id, result: { type: 'started' | 'data' | 'stopped', data? } }    // server → client

// connection params (first message if ?connectionParams=1)
{ method: 'connectionParams', data: Record<string,string> | null }

A server-to-client notification { id: null, type: 'reconnect' } (sent by wssHandler.broadcastReconnectNotification()) instructs clients to reconnect before a graceful shutdown.


6. The Client Architecture

createTRPCClient<AppRouter>(opts) returns a typed proxy backed by a TRPCUntypedClient.

export function createTRPCClient(opts) {
  const client = new TRPCUntypedClient(opts);   // holds the link chain
  const proxy = createTRPCClientProxy(client);  // recursive Proxy
  return proxy;
}

6.1 The recursive proxy

createTRPCClientProxy wraps the untyped client in a createRecursiveProxy. Accessing trpc.user.byId.query builds the path array ['user','byId','query']; calling it pops the last segment to determine the procedure type (query/mutate/subscribe → query/mutation/subscription) and dispatches to client.query|mutation|subscription(fullPath, ...args). A createFlatProxy wrapper exposes the underlying untyped client via a Symbol.for('trpc_untypedClient') key (retrievable with getUntypedClient).

The client→server method-name map is fixed: query → query, mutate → mutation, subscribe → subscription.

Each operation flows through the link chain. A link is a function returning (({ op, next }) => observable(...)). Links execute in array order outbound and in reverse on the response. The chain must end in a terminating link that actually contacts the server and returns an OperationResultEnvelope. Links communicate via op.context (a mutable per-operation bag).

Observables (from @trpc/server/observable) are the propagation primitive — each link returns one, and next(op).subscribe({ next, error, complete }) wires the chain together. This is what enables splitLink (branching), retryLink (re-subscribing on failure), and loggerLink (side-effects without altering data).

6.3 Client error type

TRPCClientError<TRouterOrProcedure> extends Error and carries shape (the formatted error shape, inferred from the router), data (the error data), and meta (HTTP response/responseJSON when applicable). TRPCClientError.from(cause, opts) normalizes any thrown value into a client error; isTRPCClientError(cause) is the type guard. The error's shape/data types are inferred from the router's errorFormatter, so custom error fields (e.g. zodError) are fully typed on the client.

6.4 Fetch resolution

getFetch(customFetchImpl?) resolves the fetch implementation: explicit override → window.fetch → globalThis.fetch, throwing No fetch implementation found if none exists. Pass a ponyfill via the link's fetch option in runtimes without a global fetch.


7. The Adapter Layer

Adapters are the only runtime-specific code. Each exposes a handler factory accepting { router, createContext?, onError?, responseMeta?, allowBatching?, maxBatchSize?, ...adapterSpecific } and translates the host's request/response into tRPC's internal request and back.

Adapter Factory Notes
node-http (internal) Base node HTTP handling; standalone builds on it.
standalone createHTTPServer, createHTTPHandler, createHTTP2Handler Wraps http.Server/http2. middleware option for connect-style middleware (e.g. cors()). basePath slices a prefix.
express createExpressMiddleware Mounts as Express middleware at a base path.
fastify fastifyTRPCPlugin Fastify plugin.
fetch fetchRequestHandler Web-standard Request → Response. Needs endpoint, req, router, createContext. Runs on any WinterCG runtime.
aws-lambda awsLambdaRequestHandler API Gateway / Lambda URL. Optional response-streaming variant for httpBatchStreamLink.
next createNextApiHandler Next.js Pages Router API route.
next-app-dir (App Router helpers) Next.js App Router.
ws applyWSSHandler Attaches to a ws WebSocketServer; keepAlive (pingMs/pongWaitMs); broadcastReconnectNotification().

The createContext contract differs per adapter via the options type (CreateHTTPContextOptions, CreateExpressContextOptions, FetchCreateContextFnOptions, CreateAWSLambdaContextOptions, CreateWSSContextFnOptions, CreateHTTP2ContextOptions). All provide request access; HTTP adapters give req/res, the Fetch adapter gives req/resHeaders, and WS/SSE expose info.connectionParams.

Tip

A common production topology keeps two entry points: the Standalone adapter for local development and a different adapter (Fetch/Lambda/Next) for deployment. Because the router and context are shared, only the thin handler differs.


8. Streaming & Subscriptions Internals

Subscriptions are async generators. The core's stream/ module implements two transports plus the tracking helper:

  • stream/jsonl.ts — JSON-lines producer for httpBatchStreamLink. Configurable via initTRPC.create({ jsonl: { pingMs } }). Emits each batch result as a line as soon as it resolves; supports yielding async generators from procedures.
  • stream/sse.ts / sse.types.ts — Server-sent-events producer for httpSubscriptionLink. SSEStreamProducerOptions controls ping ({ enabled, intervalMs }), maxDurationMs, emitAndEndImmediately (for serverless that can't stream), and client.reconnectAfterInactivityMs.
  • stream/tracked.ts — tracked(id, data) wraps an event with an id so clients can resume from lastEventId on reconnect. isTrackedEnvelope / TrackedEnvelope support output validation of tracked streams. For SSE this rides the EventSource lastEventId spec; for WS the wsLink tracks and resends it.

Reconnection model: With tracked(), a dropped connection reconnects and replays from the last id. 5xx errors thrown inside a generator trigger automatic SSE reconnection (per retryableRpcCodes); other errors cancel the subscription and surface in onError. The generator's try/finally (and tRPC's call to .return()) guarantees side-effect cleanup. opts.signal (an AbortSignal) aborts upstream listeners when the client disconnects.

Keep-alive: SSE uses server ping + client reconnectAfterInactivityMs; WS uses keepAlive on the server (applyWSSHandler) and keepAlive/lazy on the client (createWSClient).


9. Configuration Reference

9.1 RootConfig (server, set via initTRPC.create())

Field Type Default Notes
transformer CombinedDataTransformer identity Must match the client.
errorFormatter ErrorFormatter default formatter Reshapes error → client; inferred to client.
allowOutsideOfServer boolean false Permit non-server use (testing only).
isServer boolean isServerDefault Server-environment flag.
isDev boolean NODE_ENV !== 'production' Controls inclusion of stack traces.
defaultMeta Meta — Baseline meta for procedures (shallow-merged).
sse object — { enabled?, ping, emitAndEndImmediately, maxDurationMs, client }.
jsonl object — { pingMs } for batch streaming.
experimental object {} Experimental flags.

Caution

isServer and allowOutsideOfServer are escape hatches "to be used with caution," mainly for testing. Misconfiguring isServer can disable the server guard that prevents @trpc/server from running in the browser bundle.

Link Notable options
httpLink url, transformer, headers, fetch, methodOverride: 'POST'
httpBatchLink + maxURLLength, maxItems
httpBatchStreamLink + streamHeader: 'trpc-accept' | 'accept'
httpSubscriptionLink url(fn ok), connectionParams, transformer, EventSource, eventSourceOptions
wsLink / createWSClient client/url, connectionParams, WebSocket, retryDelayMs, onOpen/onClose/onError, lazy, keepAlive, experimental_encoder
loggerLink logger, enabled, console, colorMode, withContext
retryLink retry(opts), retryDelayMs(attempt)
splitLink condition(op), true, false

9.3 Adapter options (common)

router (required), createContext, onError, responseMeta, allowBatching (default on), maxBatchSize, plus adapter-specific (basePath, middleware, endpoint, keepAlive).

Note

tRPC has no environment-variable configuration of its own. The process.env reads found in the codebase (NODE_ENV, CI, VERCEL_URL, DATABASE_URL, WS_URL, etc.) belong to example apps, the docs site, test harnesses, and the upgrade CLI — not to the library runtime. The only runtime env read by the core is NODE_ENV (for the isDev/isServer defaults).


10. Security

tRPC is a library, so its security model is about how you configure it. Lead with these concerns:

10.1 Authentication & authorization

  • Authentication is derived in createContext (per request) from headers/cookies/connection params.
  • Authorization is enforced in resolvers or, preferably, in reusable middleware base procedures (protectedProcedure, adminProcedure). Middleware runs before the resolver.
  • meta enables declarative per-procedure policies (e.g. { authRequired: true }).

10.2 Input/output validation

  • Every externally-reachable procedure should define an .input() validator — input is otherwise unknown. Validation failures return BAD_REQUEST (400).
  • .output() validators prevent over-returning sensitive fields and catch server mistakes; output failures deliberately return INTERNAL_SERVER_ERROR (500) rather than leaking the validation detail.

10.3 Error information disclosure

  • Stack traces are included in error responses only when isDev is true (default NODE_ENV !== 'production'). Ensure NODE_ENV=production in prod, or set isDev: false explicitly for deterministic behavior across runtimes.
  • Use errorFormatter to whitelist exactly which fields reach the client.

10.4 Transport security

  • CORS: adapters don't enable CORS by default. Configure it strictly (not wildcard) for production; for Standalone use the cors middleware. Cross-origin cookies require credentials: 'include' on the client and matching server CORS.
  • Caching: because batching is on by default, a cache header set in responseMeta applies to the whole batch — never cache batches that may contain authenticated/personal data. Use splitLink to segregate public vs. private calls.
  • Batch limits: set maxBatchSize (server) and maxItems (client) to bound request size and mitigate oversized-batch abuse.
  • SSE/WS auth: prefer cookies (same-origin) or connectionParams; note that SSE connectionParams are serialized into the URL, so prefer header-based auth (ponyfilled EventSource) where possible. Because EventSource can't refresh config on reconnect, pair with retryLink for token renewal.

10.5 Supply-chain & vulnerability reporting

  • Use the scoped @trpc/* packages. The bare trpc npm name is not under the @trpc org and represents a baitsquatting risk (#7344).
  • Supported versions: per SECURITY.md, versions > 9.x.x are supported; < 9.0 are not. Report vulnerabilities privately to the maintainer (DM/email per SECURITY.md) rather than opening public issues.

Warning

Do not import server router values into client bundles — import the AppRouter type only. Shipping the router value would bundle server logic (and potentially secrets/db access) to the browser and trip the non-server guard.


11. Requirements & Compatibility

Requirement Value
TypeScript >= 5.7.2 (peer dependency of every package)
tsconfig "strict": true strongly recommended; non-strict not officially supported
React (integrations) >= 18.2.0
@tanstack/react-query ^5.80.3 (for @trpc/tanstack-react-query)
Node (library runtime) 18+ recommended (global fetch); otherwise ponyfill fetch
Node (monorepo development) ^24.0.0 (repo engines; not an end-user runtime requirement)
Edge/other runtimes Any WinterCG-compatible runtime via the Fetch adapter

Note

The repository's engines field (node ^24.0.0, pnpm ^10.33.1) governs contributing to tRPC, not consuming it. Applications using @trpc/server only need a runtime with the Web APIs their chosen adapter requires.

Fetch-adapter required Web APIs: Request, Response, fetch, Headers, URL.

Streaming/subscription client APIs: httpBatchStreamLink needs streaming fetch + TextDecoder/TextDecoderStream; httpSubscriptionLink needs EventSource, Streams API, and AsyncIterator. React Native requires ponyfills (rn-eventsource-reborn, web-streams-polyfill, @azure/core-asynciterator-polyfill).


12. Deployment

tRPC deploys wherever your chosen adapter runs. There is no tRPC server process to operate independently — it lives inside your host.

Target Adapter Notes
Serverful Node standalone / express / fastify Long-lived process; full streaming & WS support.
Next.js (Pages) next adapter + @trpc/next API route at pages/api/trpc/[trpc].ts.
Next.js (App) fetch / next-app-dir Route handlers + RSC prefetch helpers.
Edge (Cloudflare, Vercel Edge, Deno Deploy) fetch fetchRequestHandler; enable streams flags where needed.
AWS Lambda aws-lambda API Gateway or Lambda URL; response-streaming variant for streaming links.
WebSockets ws + (HTTP adapter) Separate WS server/port; or split links HTTP+WS.

Operational guidance:

  • Set NODE_ENV=production to disable stack-trace leakage.
  • Use onError to ship errors to your observability stack; filter on error.code === 'INTERNAL_SERVER_ERROR' for real bugs.
  • Cap batch size (maxBatchSize/maxItems).
  • For graceful WS shutdown, call handler.broadcastReconnectNotification() on SIGTERM, then close the server.
  • For serverless streaming that can't hold a connection open, use sse.emitAndEndImmediately / the Lambda response-streaming setup.

Caution

AWS Lambda streaming: httpBatchStreamLink only streams when your Lambda is configured for response streaming; otherwise it silently behaves like httpBatchLink. Cloudflare Workers: enable the streams_enable_constructors compatibility flag for ReadableStream-based streaming.


13. Performance & Scaling

  • Request batching (httpBatchLink) collapses N client calls made in the same tick into a single HTTP round-trip and is the default recommended terminating link. On the server this often collapses into a single context creation and can enable batched DB access (e.g. DataLoader patterns).
  • Streaming (httpBatchStreamLink) returns results as each resolves rather than waiting for the slowest — better TTFB for mixed-latency batches and a prerequisite for async-generator procedures.
  • Lazy routers (lazy()) cut cold-start cost by deferring sub-router import until first use — valuable on serverless.
  • Response caching via responseMeta HTTP cache headers offloads repeated public GET queries to CDNs (Vercel/Cloudflare/CloudFront), reducing DB load. Guard against caching authenticated batches.
  • Type-checking cost is the main scaling concern at the developer level: very large context types (e.g. a full Prisma client on context) increase compile time across all procedures. The inner/outer context split and importing heavy clients at call sites mitigate this.
  • Keep-alive tuning for long-lived connections: SSE ping/reconnectAfterInactivityMs, WS keepAlive/lazy.

Tip

Set the client's maxItems equal to or below the server's maxBatchSize so oversized batches split into multiple requests on the client instead of being rejected with 400 on the server.


14. Auxiliary Packages: OpenAPI & Upgrade CLI

@trpc/openapi — OpenAPI 3.1 generator (CLI: trpc-openapi)

Generates an OpenAPI 3.1 document from an AppRouter type. The CLI parses args with Node's node:util parseArgs:

trpc-openapi <router-file> [options]

  --export, -e   Exported router symbol     (default: AppRouter)
  --output, -o   Output file path           (default: openapi.json)
  --title        OpenAPI info.title         (default: tRPC API)
  --version      OpenAPI info.version       (default: 0.0.0)
  --help,   -h   Show help

Internally, generate.ts resolves and loads the TypeScript router file (generateOpenAPIDocument) and emits the spec. It throws Could not load TypeScript file: <path> or No module exports found in: <path> on resolution failures. Pair with procedure meta to drive REST-compatible endpoint generation.

Note

The repository also references the community trpc-openapi project in docs. The in-repo @trpc/openapi package provides this CLI and a heyapi-based codegen test corpus (packages/openapi/test/routers/*-heyapi/).

@trpc/upgrade — codemod CLI

A migration tool that applies AST codemods (via ts-morph/TypeScript) to upgrade your tRPC code between versions, with interactive prompts (@clack/prompts).

  • assertCleanGitTree() refuses to run on a dirty working tree unless --force (so changes are reviewable).
  • filterIgnored() respects .gitignore (via git check-ignore) and skips node_modules and the tRPC packages themselves.
  • The package manager is auto-detected from npm_config_user_agent (pnpm/yarn/bun/npm) for install/uninstall steps.
  • Set VERBOSE=1 for detailed file logging.

Run via npx @trpc/upgrade.


15. Contributing & Development

tRPC uses pnpm workspaces.

git clone git@github.com:trpc/trpc.git
cd trpc
pnpm install
pnpm build

Dev loop (two terminals):

# Terminal 1 — watch & rebuild all packages on change
pnpm dev

# Terminal 2 — run an example that consumes the live packages
cd examples/next-prisma-starter && pnpm dev

Testing (Vitest; tests centralized in packages/tests):

pnpm test            # watch mode
pnpm test react      # a specific file
pnpm test 3085       # a regression test by issue number

Linting / formatting:

pnpm lint-fix
pnpm format-fix
pnpm typecheck-packages

Recovering from cryptic errors: pnpm clean && pnpm install.

Documentation lives in www/ (Docusaurus) — see www/README.md.

Tooling Choice
Package manager pnpm 10.33.1
Monorepo orchestration Turbo (turbo.json) + Lerna (lerna.json)
Test runner Vitest (vitest.config.ts)
Lint/format ESLint (eslint.config.js) + Prettier (prettier.config.js)
Coverage Codecov (codecov.yml)

16. Code Quality & Patterns

Recurring, intentional patterns worth understanding when reading or extending the codebase:

  • Type-level programming as the product. The hardest, most valuable code is the conditional/mapped types that infer client shapes from a router (e.g. DecoratedProcedureRecord, DecorateRouterRecord). The runtime is comparatively thin; the types are the engineering.
  • Immutable builder pattern for procedures — every method returns a new builder, enabling safe shared base procedures.
  • Recursive Proxy + flat Proxy on the client to turn trpc.a.b.c.query(...) into a path + dispatch, with zero per-procedure runtime code.
  • Observable-based link pipeline for composable, branchable, retryable client transport.
  • Barrel re-exports assemble the public API; the unstable-core-do-not-import glue keeps internal types nameable across package boundaries.
  • Realm-safe error detection (error.name === 'TRPCError') alongside instanceof.
  • Standard Schema adoption to support any conforming validator without bespoke integrations.
  • Vendored dependencies (standard-schema-v1, unpromise) keep @trpc/server at zero runtime deps.

17. Potential Improvements

Observations from reading the source, framed as forward-looking suggestions (not defects):

  • unstable_* / experimental_* surface. Several capabilities ship behind these prefixes (unstable_pipe, experimental_standaloneMiddleware, experimental_encoder). experimental_standaloneMiddleware is already deprecated in favor of .concat(). A consolidation pass would reduce churn for users.
  • 5xx codes collapse to one JSON-RPC number. NOT_IMPLEMENTED/BAD_GATEWAY/SERVICE_UNAVAILABLE/GATEWAY_TIMEOUT all map to -32603, relying on HTTP status to disambiguate. Distinct numeric codes would aid pure-JSON-RPC consumers.
  • SSE connection params travel in the URL. Documented as why other auth methods are preferred; a header-first default (where the runtime allows) would be safer by default.
  • Two React integrations. The classic @trpc/react-query and the recommended @trpc/tanstack-react-query coexist; a clear, dated deprecation path would reduce decision overhead for new projects.
  • Edge-runtime feature flags. Streaming requires runtime-specific flags (Cloudflare streams_enable_constructors, Lambda response streaming). A capability-probe with a clear runtime warning when streaming silently downgrades would improve operability.

18. Quick Reference

Server

import { initTRPC, TRPCError, tracked, lazy } from '@trpc/server';
const t = initTRPC.context<Ctx>().meta<Meta>().create({ transformer, errorFormatter });
const router = t.router, publicProcedure = t.procedure;
const authed = publicProcedure.use(async (o) => { if (!o.ctx.user) throw new TRPCError({ code: 'UNAUTHORIZED' }); return o.next({ ctx: { user: o.ctx.user } }); });
export const appRouter = router({ /* ... */ });
export type AppRouter = typeof appRouter;

Adapters

import { createHTTPServer } from '@trpc/server/adapters/standalone';
import { createExpressMiddleware } from '@trpc/server/adapters/express';
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { awsLambdaRequestHandler } from '@trpc/server/adapters/aws-lambda';
import { applyWSSHandler } from '@trpc/server/adapters/ws';
import { getHTTPStatusCodeFromError } from '@trpc/server/http';

Client

import {
  createTRPCClient, createWSClient,
  httpLink, httpBatchLink, httpBatchStreamLink, httpSubscriptionLink,
  wsLink, splitLink, loggerLink, retryLink,
  TRPCClientError, isTRPCClientError,
} from '@trpc/client';

Type inference

import type {
  inferRouterInputs, inferRouterOutputs,
  inferProcedureInput, inferProcedureOutput,
  inferProcedureBuilderResolverOptions,
} from '@trpc/server';

Error code → HTTP (most used)

BAD_REQUEST 400 · UNAUTHORIZED 401 · FORBIDDEN 403 · NOT_FOUND 404 · TIMEOUT 408 · CONFLICT 409 · TOO_MANY_REQUESTS 429 · INTERNAL_SERVER_ERROR 500

Monorepo commands

pnpm install && pnpm build      # bootstrap
pnpm dev                        # watch-build all packages
pnpm test [name|issue#]         # vitest
pnpm lint-fix && pnpm format-fix
pnpm clean && pnpm install      # recover from cryptic errors