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
- Executive Summary
- Architecture Overview
- Repository & Package Structure
- The Server Core Engine
- The Wire Protocol (RPC)
- The Client Architecture
- The Adapter Layer
- Streaming & Subscriptions Internals
- Configuration Reference
- Security
- Requirements & Compatibility
- Deployment
- Performance & Scaling
- Auxiliary Packages: OpenAPI & Upgrade CLI
- Contributing & Development
- Code Quality & Patterns
- Potential Improvements
- 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 JavaScriptProxyand 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 thewww/documentation site or theexamples/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):
- Client code calls
trpc.user.byId.query(input). The recursive proxy records the path (['user','byId']) and method (query). - 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. - The adapter receives the request, runs
createContext()once per request, parses the input, and dispatches to the procedure. - The procedure runs its middleware chain, validates input, executes the resolver, optionally validates output, and returns a result.
- The result is shaped (and errors formatted via
errorFormatter), serialized, and sent back as a JSON-RPC response. - 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/testsprivate 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.tsbarrel exists primarily to "make TypeScript happy and prevent 'The inferred type ofcreateContextcannot 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;fnmust callopts.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 intoError/UnknownCauseError.getTRPCErrorFromUnknown(cause)— returns the error as-is if it's already aTRPCError(or name-matches'TRPCError'across realms, per #4848), otherwise wraps it asINTERNAL_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, the5xxfamily) follow JSON-RPC convention; the implementation-specific ones encode the trailing digits of the HTTP 4xx status. Note that the5xxcodes 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
GETwith input as a URI-encoded JSON?input=parameter (batched as?batch=1&input={...}). - Mutations → HTTP
POSTwith a JSON body. - Batching → multiple operations collapse into one request (
httpBatchLink), keyed by index. - Streaming →
httpBatchStreamLinksetstrpc-accept: application/jsonl(orAccept: application/jsonl), and the server respondscontent-type: application/jsonlwithtransfer-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.
6.2 The link chain & observables
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 forhttpBatchStreamLink. Configurable viainitTRPC.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 forhttpSubscriptionLink.SSEStreamProducerOptionscontrolsping({ enabled, intervalMs }),maxDurationMs,emitAndEndImmediately(for serverless that can't stream), andclient.reconnectAfterInactivityMs.stream/tracked.ts—tracked(id, data)wraps an event with an id so clients can resume fromlastEventIdon reconnect.isTrackedEnvelope/TrackedEnvelopesupport output validation of tracked streams. For SSE this rides theEventSourcelastEventIdspec; for WS thewsLinktracks 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
isServerandallowOutsideOfServerare escape hatches "to be used with caution," mainly for testing. MisconfiguringisServercan disable the server guard that prevents@trpc/serverfrom running in the browser bundle.
9.2 Client link options (selected)
| 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.envreads 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 isNODE_ENV(for theisDev/isServerdefaults).
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. metaenables declarative per-procedure policies (e.g.{ authRequired: true }).
10.2 Input/output validation
- Every externally-reachable procedure should define an
.input()validator — input is otherwiseunknown. Validation failures returnBAD_REQUEST(400). .output()validators prevent over-returning sensitive fields and catch server mistakes; output failures deliberately returnINTERNAL_SERVER_ERROR(500) rather than leaking the validation detail.
10.3 Error information disclosure
- Stack traces are included in error responses only when
isDevis true (defaultNODE_ENV !== 'production'). EnsureNODE_ENV=productionin prod, or setisDev: falseexplicitly for deterministic behavior across runtimes. - Use
errorFormatterto 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
corsmiddleware. Cross-origin cookies requirecredentials: 'include'on the client and matching server CORS. - Caching: because batching is on by default, a cache header set in
responseMetaapplies to the whole batch — never cache batches that may contain authenticated/personal data. UsesplitLinkto segregate public vs. private calls. - Batch limits: set
maxBatchSize(server) andmaxItems(client) to bound request size and mitigate oversized-batch abuse. - SSE/WS auth: prefer cookies (same-origin) or
connectionParams; note that SSEconnectionParamsare serialized into the URL, so prefer header-based auth (ponyfilledEventSource) where possible. BecauseEventSourcecan't refresh config on reconnect, pair withretryLinkfor token renewal.
10.5 Supply-chain & vulnerability reporting
- Use the scoped
@trpc/*packages. The baretrpcnpm name is not under the@trpcorg and represents a baitsquatting risk (#7344). - Supported versions: per
SECURITY.md, versions> 9.x.xare supported;< 9.0are not. Report vulnerabilities privately to the maintainer (DM/email perSECURITY.md) rather than opening public issues.
Warning
Do not import server router values into client bundles — import the
AppRoutertype 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
enginesfield (node ^24.0.0,pnpm ^10.33.1) governs contributing to tRPC, not consuming it. Applications using@trpc/serveronly 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=productionto disable stack-trace leakage. - Use
onErrorto ship errors to your observability stack; filter onerror.code === 'INTERNAL_SERVER_ERROR'for real bugs. - Cap batch size (
maxBatchSize/maxItems). - For graceful WS shutdown, call
handler.broadcastReconnectNotification()onSIGTERM, 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:
httpBatchStreamLinkonly streams when your Lambda is configured for response streaming; otherwise it silently behaves likehttpBatchLink. Cloudflare Workers: enable thestreams_enable_constructorscompatibility flag forReadableStream-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
responseMetaHTTP 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, WSkeepAlive/lazy.
Tip
Set the client's
maxItemsequal to or below the server'smaxBatchSizeso oversized batches split into multiple requests on the client instead of being rejected with400on 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-openapiproject in docs. The in-repo@trpc/openapipackage 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(viagit check-ignore) and skipsnode_modulesand 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=1for 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+ flatProxyon the client to turntrpc.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-importglue keeps internal types nameable across package boundaries. - Realm-safe error detection (
error.name === 'TRPCError') alongsideinstanceof. - Standard Schema adoption to support any conforming validator without bespoke integrations.
- Vendored dependencies (
standard-schema-v1,unpromise) keep@trpc/serverat 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_standaloneMiddlewareis 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_TIMEOUTall 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-queryand the recommended@trpc/tanstack-react-querycoexist; 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