FRE-600: Fix code review blockers

- Consolidated duplicate UndoManagers to single instance
- Fixed connection promise to only resolve on 'connected' status
- Fixed WebSocketProvider import (WebsocketProvider)
- Added proper doc.destroy() cleanup
- Renamed isPresenceInitialized property to avoid conflict

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-04-25 00:08:01 -04:00
parent 65b552bb08
commit 7c684a42cc
48450 changed files with 5679671 additions and 383 deletions

View File

@@ -0,0 +1,203 @@
---
name: adapter-aws-lambda
description: >
Deploy tRPC on AWS Lambda with awsLambdaRequestHandler() from
@trpc/server/adapters/aws-lambda for API Gateway v1 (REST, APIGatewayProxyEvent)
and v2 (HTTP, APIGatewayProxyEventV2), and Lambda Function URLs. Enable response
streaming with awsLambdaStreamingRequestHandler() wrapped in
awslambda.streamifyResponse(). CreateAWSLambdaContextOptions provides event and
context for context creation.
type: core
library: trpc
library_version: '11.15.1'
requires:
- server-setup
sources:
- www/docs/server/adapters/aws-lambda.md
- examples/lambda-url/
- examples/lambda-api-gateway/
---
# tRPC — Adapter: AWS Lambda
## Setup
```ts
// server.ts
import { initTRPC } from '@trpc/server';
import type { CreateAWSLambdaContextOptions } from '@trpc/server/adapters/aws-lambda';
import { awsLambdaRequestHandler } from '@trpc/server/adapters/aws-lambda';
import type { APIGatewayProxyEventV2 } from 'aws-lambda';
import { z } from 'zod';
const t = initTRPC.create();
const appRouter = t.router({
greet: t.procedure
.input(z.object({ name: z.string() }))
.query(({ input }) => ({ greeting: `Hello, ${input.name}!` })),
});
export type AppRouter = typeof appRouter;
const createContext = ({
event,
context,
}: CreateAWSLambdaContextOptions<APIGatewayProxyEventV2>) => ({
event,
lambdaContext: context,
});
export const handler = awsLambdaRequestHandler({
router: appRouter,
createContext,
});
```
## Core Patterns
### API Gateway v1 (REST) handler
```ts
import type { CreateAWSLambdaContextOptions } from '@trpc/server/adapters/aws-lambda';
import { awsLambdaRequestHandler } from '@trpc/server/adapters/aws-lambda';
import type { APIGatewayProxyEvent } from 'aws-lambda';
import { appRouter } from './router';
const createContext = ({
event,
context,
}: CreateAWSLambdaContextOptions<APIGatewayProxyEvent>) => ({
user: event.requestContext.authorizer?.claims,
});
export const handler = awsLambdaRequestHandler({
router: appRouter,
createContext,
});
```
Use `APIGatewayProxyEvent` for REST API (v1 payload format) and `APIGatewayProxyEventV2` for HTTP API (v2 payload format).
### Response streaming with awsLambdaStreamingRequestHandler
```ts
/// <reference types="aws-lambda" />
import type { CreateAWSLambdaContextOptions } from '@trpc/server/adapters/aws-lambda';
import { awsLambdaStreamingRequestHandler } from '@trpc/server/adapters/aws-lambda';
import type { APIGatewayProxyEventV2 } from 'aws-lambda';
import { appRouter } from './router';
const createContext = ({
event,
context,
}: CreateAWSLambdaContextOptions<APIGatewayProxyEventV2>) => ({});
export const handler = awslambda.streamifyResponse(
awsLambdaStreamingRequestHandler({
router: appRouter,
createContext,
}),
);
```
Response streaming is supported for Lambda Function URLs and API Gateway REST APIs (with `responseTransferMode: STREAM`). The `awslambda` namespace is provided by the Lambda execution environment.
### Streaming async generator procedure
```ts
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
export const appRouter = t.router({
countdown: t.procedure.query(async function* () {
for (let i = 10; i >= 0; i--) {
await new Promise((resolve) => setTimeout(resolve, 500));
yield i;
}
}),
});
```
Pair with `httpBatchStreamLink` on the client for streamed responses.
### Limiting batch size with maxBatchSize
```ts
import { awsLambdaRequestHandler } from '@trpc/server/adapters/aws-lambda';
import { appRouter } from './router';
export const handler = awsLambdaRequestHandler({
router: appRouter,
createContext,
maxBatchSize: 10,
});
```
Requests batching more than `maxBatchSize` operations are rejected with a `400 Bad Request` error. Set `maxItems` on your client's `httpBatchLink` to the same value to avoid exceeding the limit.
## Common Mistakes
### HIGH Using httpBatchLink with per-procedure API Gateway resources
Wrong:
```ts
// API Gateway has a separate resource for each procedure
// e.g., /getUser, /createUser
// Client uses:
import { httpBatchLink } from '@trpc/client';
httpBatchLink({ url: 'https://api.example.com' });
// Batch request to /getUser,createUser → 404
```
Correct:
```ts
import { httpBatchLink, httpLink } from '@trpc/client';
// Option A: Single catch-all resource (e.g., /{proxy+})
httpBatchLink({ url: 'https://api.example.com' });
// Option B: Per-procedure resources with httpLink (no batching)
httpLink({ url: 'https://api.example.com' });
```
`httpBatchLink` sends multiple procedure names in the URL path (e.g., `getUser,createUser`). If API Gateway routes are per-procedure, the combined path does not match any resource and returns 404. Use a single catch-all resource or switch to `httpLink`.
Source: www/docs/server/adapters/aws-lambda.md
### HIGH Forgetting streamifyResponse wrapper for streaming
Wrong:
```ts
export const handler = awsLambdaStreamingRequestHandler({
router: appRouter,
createContext,
});
```
Correct:
```ts
export const handler = awslambda.streamifyResponse(
awsLambdaStreamingRequestHandler({
router: appRouter,
createContext,
}),
);
```
`awsLambdaStreamingRequestHandler` requires wrapping with `awslambda.streamifyResponse()` to enable Lambda response streaming. Without it, Lambda treats the handler as a standard buffered response.
Source: www/docs/server/adapters/aws-lambda.md
## See Also
- **server-setup** -- `initTRPC.create()`, router/procedure definition, context
- **adapter-fetch** -- alternative for edge/serverless runtimes using Fetch API
- **links** -- `httpBatchLink` vs `httpLink` for API Gateway routing considerations
- AWS Lambda docs: https://docs.aws.amazon.com/lambda/latest/dg/welcome.html

View File

@@ -0,0 +1,176 @@
---
name: adapter-express
description: >
Mount tRPC as Express middleware with createExpressMiddleware() from
@trpc/server/adapters/express. Access Express req/res in createContext via
CreateExpressContextOptions. Mount at a path prefix like app.use('/trpc', ...).
Avoid global express.json() conflicting with tRPC body parsing for FormData.
type: core
library: trpc
library_version: '11.15.1'
requires:
- server-setup
sources:
- www/docs/server/adapters/express.md
- examples/express-server/src/server.ts
---
# tRPC — Adapter: Express
## Setup
```ts
// server.ts
import { initTRPC } from '@trpc/server';
import * as trpcExpress from '@trpc/server/adapters/express';
import express from 'express';
import { z } from 'zod';
const createContext = ({
req,
res,
}: trpcExpress.CreateExpressContextOptions) => {
return { req, res };
};
type Context = Awaited<ReturnType<typeof createContext>>;
const t = initTRPC.context<Context>().create();
const appRouter = t.router({
greet: t.procedure
.input(z.object({ name: z.string() }))
.query(({ input }) => ({ greeting: `Hello, ${input.name}!` })),
});
export type AppRouter = typeof appRouter;
const app = express();
app.use(
'/trpc',
trpcExpress.createExpressMiddleware({
router: appRouter,
createContext,
}),
);
app.listen(4000, () => {
console.log('Listening on http://localhost:4000');
});
```
## Core Patterns
### Accessing Express req/res in context
```ts
import * as trpcExpress from '@trpc/server/adapters/express';
const createContext = ({
req,
res,
}: trpcExpress.CreateExpressContextOptions) => {
const token = req.headers.authorization?.split(' ')[1];
return { token, res };
};
type Context = Awaited<ReturnType<typeof createContext>>;
```
`CreateExpressContextOptions` provides typed access to the Express `req` (IncomingMessage) and `res` (ServerResponse).
### Adding tRPC alongside existing Express routes
```ts
import * as trpcExpress from '@trpc/server/adapters/express';
import cors from 'cors';
import express from 'express';
import { createContext } from './context';
import { appRouter } from './router';
const app = express();
app.use(cors());
app.get('/health', (_req, res) => {
res.json({ status: 'ok' });
});
app.use(
'/trpc',
trpcExpress.createExpressMiddleware({
router: appRouter,
createContext,
}),
);
app.listen(4000);
```
### Limiting batch size with maxBatchSize
```ts
import * as trpcExpress from '@trpc/server/adapters/express';
import express from 'express';
import { createContext } from './context';
import { appRouter } from './router';
const app = express();
app.use(
'/trpc',
trpcExpress.createExpressMiddleware({
router: appRouter,
createContext,
maxBatchSize: 10,
}),
);
app.listen(4000);
```
Requests batching more than `maxBatchSize` operations are rejected with a `400 Bad Request` error. Set `maxItems` on your client's `httpBatchLink` to the same value to avoid exceeding the limit.
## Common Mistakes
### HIGH Global express.json() consuming tRPC request body
Wrong:
```ts
const app = express();
app.use(express.json()); // global body parser
app.use(
'/trpc',
trpcExpress.createExpressMiddleware({
router: appRouter,
createContext,
}),
);
```
Correct:
```ts
const app = express();
// Only apply body parser to non-tRPC routes
app.use('/api', express.json());
app.use(
'/trpc',
trpcExpress.createExpressMiddleware({
router: appRouter,
createContext,
}),
);
```
If `express.json()` is applied globally before the tRPC middleware, it consumes and parses the request body. tRPC then receives an already-parsed body, which breaks FormData and binary content type handling.
Source: www/docs/server/non-json-content-types.md
## See Also
- **server-setup** -- `initTRPC.create()`, router/procedure definition, context
- **adapter-standalone** -- simpler adapter when Express middleware ecosystem is not needed
- **auth** -- extracting JWT from `req.headers.authorization` in context
- Express docs: https://expressjs.com/en/guide/using-middleware.html

View File

@@ -0,0 +1,221 @@
---
name: adapter-fastify
description: >
Mount tRPC as a Fastify plugin with fastifyTRPCPlugin from
@trpc/server/adapters/fastify. Configure prefix, trpcOptions (router,
createContext, onError). Enable WebSocket subscriptions with useWSS and
@fastify/websocket. Set routerOptions.maxParamLength for batch requests.
Requires Fastify v5+. FastifyTRPCPluginOptions for type-safe onError.
CreateFastifyContextOptions provides req, res.
type: core
library: trpc
library_version: '11.15.1'
requires:
- server-setup
sources:
- www/docs/server/adapters/fastify.md
- examples/fastify-server/
---
# tRPC — Adapter: Fastify
## Setup
```ts
// server.ts
import { initTRPC } from '@trpc/server';
import {
fastifyTRPCPlugin,
FastifyTRPCPluginOptions,
} from '@trpc/server/adapters/fastify';
import type { CreateFastifyContextOptions } from '@trpc/server/adapters/fastify';
import fastify from 'fastify';
import { z } from 'zod';
function createContext({ req, res }: CreateFastifyContextOptions) {
return { req, res };
}
type Context = Awaited<ReturnType<typeof createContext>>;
const t = initTRPC.context<Context>().create();
const appRouter = t.router({
greet: t.procedure
.input(z.object({ name: z.string() }))
.query(({ input }) => ({ greeting: `Hello, ${input.name}!` })),
});
export type AppRouter = typeof appRouter;
const server = fastify({
routerOptions: {
maxParamLength: 5000,
},
});
server.register(fastifyTRPCPlugin, {
prefix: '/trpc',
trpcOptions: {
router: appRouter,
createContext,
onError({ path, error }) {
console.error(`Error in tRPC handler on path '${path}':`, error);
},
} satisfies FastifyTRPCPluginOptions<AppRouter>['trpcOptions'],
});
(async () => {
try {
await server.listen({ port: 3000 });
console.log('Listening on http://localhost:3000');
} catch (err) {
server.log.error(err);
process.exit(1);
}
})();
```
## Core Patterns
### WebSocket subscriptions with @fastify/websocket
```ts
import ws from '@fastify/websocket';
import {
fastifyTRPCPlugin,
FastifyTRPCPluginOptions,
} from '@trpc/server/adapters/fastify';
import fastify from 'fastify';
import { createContext } from './context';
import { appRouter, type AppRouter } from './router';
const server = fastify({
routerOptions: { maxParamLength: 5000 },
});
// Register @fastify/websocket BEFORE the tRPC plugin
server.register(ws);
server.register(fastifyTRPCPlugin, {
prefix: '/trpc',
useWSS: true,
trpcOptions: {
router: appRouter,
createContext,
keepAlive: {
enabled: true,
pingMs: 30000,
pongWaitMs: 5000,
},
} satisfies FastifyTRPCPluginOptions<AppRouter>['trpcOptions'],
});
server.listen({ port: 3000 });
```
Install: `npm install @fastify/websocket` (minimum version 3.11.0)
### Type-safe onError with satisfies
```ts
server.register(fastifyTRPCPlugin, {
prefix: '/trpc',
trpcOptions: {
router: appRouter,
createContext,
onError({ path, error, type, input }) {
console.error(`[${type}] ${path}:`, error.message);
},
} satisfies FastifyTRPCPluginOptions<AppRouter>['trpcOptions'],
});
```
Due to Fastify plugin type inference limitations, use `satisfies FastifyTRPCPluginOptions<AppRouter>['trpcOptions']` to get correct types on `onError` and other callbacks.
### Limiting batch size with maxBatchSize
```ts
server.register(fastifyTRPCPlugin, {
prefix: '/trpc',
trpcOptions: {
router: appRouter,
createContext,
maxBatchSize: 10,
} satisfies FastifyTRPCPluginOptions<AppRouter>['trpcOptions'],
});
```
Requests batching more than `maxBatchSize` operations are rejected with a `400 Bad Request` error. Set `maxItems` on your client's `httpBatchLink` to the same value to avoid exceeding the limit.
## Common Mistakes
### HIGH Registering @fastify/websocket after tRPC plugin
Wrong:
```ts
server.register(fastifyTRPCPlugin, {
useWSS: true,
trpcOptions: { router: appRouter, createContext },
});
server.register(ws); // too late!
```
Correct:
```ts
server.register(ws); // register FIRST
server.register(fastifyTRPCPlugin, {
useWSS: true,
trpcOptions: { router: appRouter, createContext },
});
```
The WebSocket plugin must be registered before the tRPC plugin. Reverse order causes WebSocket routes to not be recognized.
Source: www/docs/server/adapters/fastify.md
### HIGH Missing maxParamLength for batch requests
Wrong:
```ts
const server = fastify();
```
Correct:
```ts
const server = fastify({
routerOptions: { maxParamLength: 5000 },
});
```
Fastify defaults to `maxParamLength: 100`. Batch requests from `httpBatchLink` encode multiple procedure names in the URL path parameter, which easily exceeds 100 characters and returns a 404.
Source: www/docs/server/adapters/fastify.md
### CRITICAL Using Fastify v4 with tRPC v11
Wrong:
```json
{ "dependencies": { "fastify": "^4.0.0" } }
```
Correct:
```json
{ "dependencies": { "fastify": "^5.0.0" } }
```
tRPC v11 requires Fastify v5+. Fastify v4 may return empty responses without errors due to incompatible response handling.
Source: www/docs/server/adapters/fastify.md
## See Also
- **server-setup** -- `initTRPC.create()`, router/procedure definition, context
- **subscriptions** -- async generator subscriptions, `tracked()`, `keepAlive`
- **adapter-standalone** -- simpler adapter when Fastify features are not needed
- Fastify docs: https://fastify.dev/docs/latest/

199
node_modules/@trpc/server/skills/adapter-fetch/SKILL.md generated vendored Normal file
View File

@@ -0,0 +1,199 @@
---
name: adapter-fetch
description: >
Deploy tRPC on WinterCG-compliant edge runtimes with fetchRequestHandler() from
@trpc/server/adapters/fetch. Supports Cloudflare Workers, Deno Deploy, Vercel
Edge Runtime, Astro, Remix, SolidStart. FetchCreateContextFnOptions provides
req (Request) and resHeaders (Headers) for context creation. The endpoint option
must match the URL path prefix where the handler is mounted.
type: core
library: trpc
library_version: '11.15.1'
requires:
- server-setup
sources:
- www/docs/server/adapters/fetch.mdx
- examples/cloudflare-workers/
- examples/deno-deploy/
---
# tRPC — Adapter: Fetch / Edge Runtimes
## Setup
```ts
// Cloudflare Worker example
import { initTRPC } from '@trpc/server';
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import type { FetchCreateContextFnOptions } from '@trpc/server/adapters/fetch';
import { z } from 'zod';
function createContext({ req, resHeaders }: FetchCreateContextFnOptions) {
const user = req.headers.get('authorization');
return { user, resHeaders };
}
type Context = Awaited<ReturnType<typeof createContext>>;
const t = initTRPC.context<Context>().create();
const appRouter = t.router({
greet: t.procedure
.input(z.object({ name: z.string() }))
.query(({ input }) => ({ greeting: `Hello, ${input.name}!` })),
});
export type AppRouter = typeof appRouter;
export default {
async fetch(request: Request): Promise<Response> {
return fetchRequestHandler({
endpoint: '/trpc',
req: request,
router: appRouter,
createContext,
});
},
};
```
## Core Patterns
### Cloudflare Workers
```ts
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { createContext } from './context';
import { appRouter } from './router';
export default {
async fetch(request: Request): Promise<Response> {
return fetchRequestHandler({
endpoint: '/trpc',
req: request,
router: appRouter,
createContext,
});
},
};
```
### Astro SSR
```ts
// src/pages/trpc/[trpc].ts
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import type { APIRoute } from 'astro';
import { createContext } from '../../server/context';
import { appRouter } from '../../server/router';
export const ALL: APIRoute = (opts) => {
return fetchRequestHandler({
endpoint: '/trpc',
req: opts.request,
router: appRouter,
createContext,
});
};
```
### Remix
```ts
// app/routes/trpc.$trpc.ts
import type { ActionFunctionArgs, LoaderFunctionArgs } from '@remix-run/node';
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { createContext } from '~/server/context';
import { appRouter } from '~/server/router';
function handleRequest(args: LoaderFunctionArgs | ActionFunctionArgs) {
return fetchRequestHandler({
endpoint: '/trpc',
req: args.request,
router: appRouter,
createContext,
});
}
export const loader = async (args: LoaderFunctionArgs) => handleRequest(args);
export const action = async (args: ActionFunctionArgs) => handleRequest(args);
```
### Deno Deploy
```ts
import { fetchRequestHandler } from 'npm:@trpc/server/adapters/fetch';
import { createContext } from './context.ts';
import { appRouter } from './router.ts';
Deno.serve((request) => {
return fetchRequestHandler({
endpoint: '/trpc',
req: request,
router: appRouter,
createContext,
});
});
```
### Limiting batch size with maxBatchSize
```ts
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { createContext } from './context';
import { appRouter } from './router';
export default {
async fetch(request: Request): Promise<Response> {
return fetchRequestHandler({
endpoint: '/trpc',
req: request,
router: appRouter,
createContext,
maxBatchSize: 10,
});
},
};
```
Requests batching more than `maxBatchSize` operations are rejected with a `400 Bad Request` error. Set `maxItems` on your client's `httpBatchLink` to the same value to avoid exceeding the limit.
## Common Mistakes
### HIGH Mismatched endpoint path in fetchRequestHandler
Wrong:
```ts
// Handler mounted at /api/trpc/[trpc] but endpoint says /trpc
fetchRequestHandler({
endpoint: '/trpc',
req: request,
router: appRouter,
createContext,
});
```
Correct:
```ts
// endpoint must match the actual URL path prefix
fetchRequestHandler({
endpoint: '/api/trpc',
req: request,
router: appRouter,
createContext,
});
```
The `endpoint` option tells tRPC where to strip the URL prefix to extract the procedure name. If it does not match the actual mount path, all procedures return 404 because the path parsing extracts the wrong procedure name.
Source: www/docs/server/adapters/fetch.mdx
## See Also
- **server-setup** -- `initTRPC.create()`, router/procedure definition, context
- **adapter-standalone** -- alternative for Node.js HTTP server
- **adapter-express** -- alternative when Express middleware ecosystem is needed
- **adapter-aws-lambda** -- alternative for AWS Lambda deployments
- Cloudflare Workers docs: https://developers.cloudflare.com/workers/
- Deno Deploy docs: https://deno.com/deploy/docs

View File

@@ -0,0 +1,198 @@
---
name: adapter-standalone
description: >
Mount tRPC on Node.js built-in HTTP server with createHTTPServer() from
@trpc/server/adapters/standalone, createHTTPHandler() for custom http.createServer,
createHTTP2Handler() for HTTP/2 with TLS. Configure basePath to slice URL prefix,
CORS via the cors npm package passed as middleware option. CreateHTTPContextOptions
provides req and res for context creation.
type: core
library: trpc
library_version: '11.15.1'
requires:
- server-setup
sources:
- www/docs/server/adapters/standalone.md
- examples/standalone-server/src/server.ts
---
# tRPC — Adapter: Standalone
## Setup
```ts
// server.ts
import { initTRPC } from '@trpc/server';
import { createHTTPServer } from '@trpc/server/adapters/standalone';
import { z } from 'zod';
const t = initTRPC.create();
const appRouter = t.router({
greet: t.procedure
.input(z.object({ name: z.string() }))
.query(({ input }) => ({ greeting: `Hello, ${input.name}!` })),
});
export type AppRouter = typeof appRouter;
createHTTPServer({
router: appRouter,
createContext() {
return {};
},
}).listen(3000);
console.log('Listening on http://localhost:3000');
```
## Core Patterns
### CORS with the cors package
```ts
import { createHTTPServer } from '@trpc/server/adapters/standalone';
import cors from 'cors';
import { createContext } from './context';
import { appRouter } from './router';
createHTTPServer({
middleware: cors({ origin: 'http://localhost:5173' }),
router: appRouter,
createContext,
}).listen(3000);
```
Install CORS support: `npm install cors @types/cors`
### Custom HTTP server with createHTTPHandler
```ts
import { createServer } from 'http';
import { createHTTPHandler } from '@trpc/server/adapters/standalone';
import { appRouter } from './router';
const handler = createHTTPHandler({
router: appRouter,
createContext() {
return {};
},
});
createServer((req, res) => {
if (req.url?.startsWith('/health')) {
res.writeHead(200);
res.end('OK');
return;
}
handler(req, res);
}).listen(3000);
```
### basePath for URL prefix stripping
```ts
import { createServer } from 'http';
import { createHTTPHandler } from '@trpc/server/adapters/standalone';
import { appRouter } from './router';
const handler = createHTTPHandler({
router: appRouter,
basePath: '/trpc/',
});
createServer((req, res) => {
if (req.url?.startsWith('/trpc/')) {
return handler(req, res);
}
res.statusCode = 404;
res.end('Not Found');
}).listen(3000);
```
The `basePath` option strips the prefix before routing, so `/trpc/greet` resolves to the `greet` procedure.
### HTTP/2 with createHTTP2Handler
```ts
import http2 from 'http2';
import { readFileSync } from 'node:fs';
import { createHTTP2Handler } from '@trpc/server/adapters/standalone';
import type { CreateHTTP2ContextOptions } from '@trpc/server/adapters/standalone';
import { appRouter } from './router';
const tlsKey = readFileSync('./certs/server.key');
const tlsCert = readFileSync('./certs/server.crt');
async function createContext(opts: CreateHTTP2ContextOptions) {
return {};
}
const handler = createHTTP2Handler({
router: appRouter,
createContext,
});
const server = http2.createSecureServer(
{ key: tlsKey, cert: tlsCert },
(req, res) => {
handler(req, res);
},
);
server.listen(3001);
```
### Limiting batch size with maxBatchSize
```ts
import { createHTTPServer } from '@trpc/server/adapters/standalone';
import { appRouter } from './router';
createHTTPServer({
router: appRouter,
maxBatchSize: 10,
}).listen(3000);
```
Requests batching more than `maxBatchSize` operations are rejected with a `400 Bad Request` error. Set `maxItems` on your client's `httpBatchLink` to the same value to avoid exceeding the limit.
## Common Mistakes
### HIGH No CORS configuration for cross-origin requests
Wrong:
```ts
createHTTPServer({
router: appRouter,
createContext() {
return {};
},
}).listen(3000);
```
Correct:
```ts
import cors from 'cors';
createHTTPServer({
middleware: cors({ origin: 'http://localhost:5173' }),
router: appRouter,
createContext() {
return {};
},
}).listen(3000);
```
The standalone adapter has no CORS handling by default. Cross-origin browser requests fail silently because preflight OPTIONS requests receive no CORS headers.
Source: www/docs/server/adapters/standalone.md
## See Also
- **server-setup** -- `initTRPC.create()`, router/procedure definition, context
- **adapter-express** -- alternative adapter when you need Express middleware ecosystem
- **adapter-fetch** -- alternative adapter for edge/serverless runtimes
- **subscriptions** -- adding real-time subscriptions to a standalone server

342
node_modules/@trpc/server/skills/auth/SKILL.md generated vendored Normal file
View File

@@ -0,0 +1,342 @@
---
name: auth
description: >
Implement JWT/cookie authentication and authorization in tRPC using createContext
for user extraction, t.middleware with opts.next({ ctx }) for context narrowing to
non-null user, protectedProcedure base pattern, client-side Authorization headers
via httpBatchLink headers(), WebSocket connectionParams, and SSE auth via cookies
or EventSource polyfill custom headers.
type: composition
library: trpc
library_version: '11.15.1'
requires:
- server-setup
- middlewares
- client-setup
sources:
- www/docs/server/authorization.md
- www/docs/client/headers.md
- www/docs/client/links/httpSubscriptionLink.md
- www/docs/server/websockets.md
---
# tRPC — Auth
## Setup
```ts
// server/trpc.ts
import { initTRPC, TRPCError } from '@trpc/server';
import type { CreateHTTPContextOptions } from '@trpc/server/adapters/standalone';
export async function createContext({ req }: CreateHTTPContextOptions) {
async function getUserFromHeader() {
const token = req.headers.authorization?.split(' ')[1];
if (token) {
const user = await verifyJwt(token); // your JWT verification
return user; // e.g. { id: string; name: string; role: string }
}
return null;
}
return { user: await getUserFromHeader() };
}
export type Context = Awaited<ReturnType<typeof createContext>>;
const t = initTRPC.context<Context>().create();
export const publicProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(
async function isAuthed(opts) {
const { ctx } = opts;
if (!ctx.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return opts.next({
ctx: {
user: ctx.user, // narrows user to non-null
},
});
},
);
export const router = t.router;
```
```ts
// client/trpc.ts
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from '../server/router';
let token = '';
export function setToken(t: string) {
token = t;
}
export const trpc = createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: 'http://localhost:3000/trpc',
headers() {
return { Authorization: `Bearer ${token}` };
},
}),
],
});
```
## Core Patterns
### Context narrowing with auth middleware
```ts
import { initTRPC, TRPCError } from '@trpc/server';
type Context = { user: { id: string; role: string } | null };
const t = initTRPC.context<Context>().create();
const isAuthed = t.middleware(async ({ ctx, next }) => {
if (!ctx.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return next({ ctx: { user: ctx.user } });
});
const isAdmin = t.middleware(async ({ ctx, next }) => {
if (!ctx.user || ctx.user.role !== 'admin') {
throw new TRPCError({ code: 'FORBIDDEN' });
}
return next({ ctx: { user: ctx.user } });
});
export const protectedProcedure = t.procedure.use(isAuthed);
export const adminProcedure = t.procedure.use(isAdmin);
```
### SSE subscription auth with EventSource polyfill
```ts
import {
createTRPCClient,
httpBatchLink,
httpSubscriptionLink,
splitLink,
} from '@trpc/client';
import { EventSourcePolyfill } from 'event-source-polyfill';
import type { AppRouter } from '../server/router';
const trpc = createTRPCClient<AppRouter>({
links: [
splitLink({
condition: (op) => op.type === 'subscription',
true: httpSubscriptionLink({
url: 'http://localhost:3000/trpc',
EventSource: EventSourcePolyfill,
eventSourceOptions: async () => {
return {
headers: {
authorization: `Bearer ${getToken()}`,
},
};
},
}),
false: httpBatchLink({
url: 'http://localhost:3000/trpc',
headers() {
return { Authorization: `Bearer ${getToken()}` };
},
}),
}),
],
});
```
### WebSocket auth with connectionParams
```ts
// server/context.ts
import type { CreateWSSContextFnOptions } from '@trpc/server/adapters/ws';
export const createContext = async (opts: CreateWSSContextFnOptions) => {
const token = opts.info.connectionParams?.token;
const user = token ? await verifyJwt(token) : null;
return { user };
};
```
```ts
// client/trpc.ts
import { createTRPCClient, createWSClient, wsLink } from '@trpc/client';
import type { AppRouter } from '../server/router';
const wsClient = createWSClient({
url: 'ws://localhost:3001',
connectionParams: async () => ({
token: getToken(),
}),
});
const trpc = createTRPCClient<AppRouter>({
links: [wsLink({ client: wsClient })],
});
```
### SSE auth with cookies (same domain)
```ts
import {
createTRPCClient,
httpBatchLink,
httpSubscriptionLink,
splitLink,
} from '@trpc/client';
import type { AppRouter } from '../server/router';
const trpc = createTRPCClient<AppRouter>({
links: [
splitLink({
condition: (op) => op.type === 'subscription',
true: httpSubscriptionLink({
url: '/api/trpc',
eventSourceOptions() {
return { withCredentials: true };
},
}),
false: httpBatchLink({ url: '/api/trpc' }),
}),
],
});
```
## Common Mistakes
### HIGH Not narrowing user type in auth middleware
Wrong:
```ts
const authMiddleware = t.middleware(async ({ ctx, next }) => {
if (!ctx.user) throw new TRPCError({ code: 'UNAUTHORIZED' });
return next(); // user still nullable downstream
});
```
Correct:
```ts
const authMiddleware = t.middleware(async ({ ctx, next }) => {
if (!ctx.user) throw new TRPCError({ code: 'UNAUTHORIZED' });
return next({ ctx: { user: ctx.user } }); // narrows to non-null
});
```
Without `opts.next({ ctx })`, downstream procedures still see `user` as `{ id: string } | null`, requiring redundant null checks.
Source: www/docs/server/authorization.md
### HIGH SSE auth via URL query params exposes tokens
Wrong:
```ts
httpSubscriptionLink({
url: 'http://localhost:3000/trpc',
connectionParams: async () => ({
token: 'my-secret-jwt',
}),
});
```
Correct:
```ts
import { EventSourcePolyfill } from 'event-source-polyfill';
httpSubscriptionLink({
url: 'http://localhost:3000/trpc',
EventSource: EventSourcePolyfill,
eventSourceOptions: async () => ({
headers: { authorization: 'Bearer my-secret-jwt' },
}),
});
```
`connectionParams` are serialized as URL query strings for SSE, exposing tokens in server logs and browser history. Use cookies for same-domain or custom headers via an EventSource polyfill instead.
Source: www/docs/client/links/httpSubscriptionLink.md
### MEDIUM Async headers causing stuck isFetching
Wrong:
```ts
httpBatchLink({
url: '/api/trpc',
async headers() {
const token = await refreshToken(); // can race
return { Authorization: `Bearer ${token}` };
},
});
```
Correct:
```ts
let cachedToken: string | null = null;
async function ensureToken() {
if (!cachedToken) cachedToken = await refreshToken();
return cachedToken;
}
httpBatchLink({
url: '/api/trpc',
async headers() {
return { Authorization: `Bearer ${await ensureToken()}` };
},
});
```
When the headers function is async (e.g., refreshing auth tokens), React Query's `isFetching` can get stuck permanently in certain race conditions.
Source: https://github.com/trpc/trpc/issues/7001
### HIGH Skipping auth or opening CORS too wide in prototypes
Wrong:
```ts
import cors from 'cors';
createHTTPServer({
middleware: cors(), // origin: '*' by default
router: appRouter,
createContext() {
return {};
}, // no auth
}).listen(3000);
```
Correct:
```ts
import cors from 'cors';
createHTTPServer({
middleware: cors({ origin: 'https://myapp.com' }),
router: appRouter,
createContext,
}).listen(3000);
```
Wildcard CORS and missing auth middleware are acceptable only during local development. Always restrict CORS origins and add auth before deploying.
Source: maintainer interview
## See Also
- **middlewares** -- context narrowing, `.use()`, `.concat()`, base procedure patterns
- **subscriptions** -- SSE and WebSocket transport setup for authenticated subscriptions
- **client-setup** -- `createTRPCClient`, link chain, `headers` option
- **links** -- `splitLink`, `httpSubscriptionLink`, `wsLink` configuration

205
node_modules/@trpc/server/skills/caching/SKILL.md generated vendored Normal file
View File

@@ -0,0 +1,205 @@
---
name: caching
description: >
Set HTTP cache headers on tRPC query responses via responseMeta callback for
CDN and browser caching. Configure Cache-Control, s-maxage,
stale-while-revalidate. Handle caching with batching and authenticated requests.
Avoid caching mutations, errors, and authenticated responses.
type: core
library: trpc
library_version: '11.15.1'
requires:
- server-setup
sources:
- 'trpc/trpc:www/docs/server/caching.md'
---
# tRPC -- Caching
## Setup
```ts
// server/trpc.ts
import { initTRPC } from '@trpc/server';
import type { CreateHTTPContextOptions } from '@trpc/server/adapters/standalone';
export const createContext = async (opts: CreateHTTPContextOptions) => {
return {
req: opts.req,
res: opts.res,
user: null as { id: string } | null,
};
};
type Context = Awaited<ReturnType<typeof createContext>>;
export const t = initTRPC.context<Context>().create();
export const router = t.router;
export const publicProcedure = t.procedure;
```
```ts
// server/appRouter.ts
import { publicProcedure, router } from './trpc';
export const appRouter = router({
public: router({
slowQueryCached: publicProcedure.query(async () => {
await new Promise((resolve) => setTimeout(resolve, 5000));
return { lastUpdated: new Date().toJSON() };
}),
}),
});
export type AppRouter = typeof appRouter;
```
```ts
// server/index.ts
import { createHTTPServer } from '@trpc/server/adapters/standalone';
import { appRouter } from './appRouter';
import { createContext } from './trpc';
const server = createHTTPServer({
router: appRouter,
createContext,
responseMeta(opts) {
const { paths, errors, type } = opts;
const allPublic =
paths && paths.every((path) => path.startsWith('public.'));
const allOk = errors.length === 0;
const isQuery = type === 'query';
if (allPublic && allOk && isQuery) {
const ONE_DAY_IN_SECONDS = 60 * 60 * 24;
return {
headers: new Headers([
[
'cache-control',
`s-maxage=1, stale-while-revalidate=${ONE_DAY_IN_SECONDS}`,
],
]),
};
}
return {};
},
});
server.listen(3000);
```
## Core Patterns
### Path-based public route caching
```ts
import { createHTTPServer } from '@trpc/server/adapters/standalone';
import { appRouter } from './appRouter';
import { createContext } from './trpc';
const server = createHTTPServer({
router: appRouter,
createContext,
responseMeta({ paths, errors, type }) {
const allPublic =
paths && paths.every((path) => path.startsWith('public.'));
const allOk = errors.length === 0;
const isQuery = type === 'query';
if (allPublic && allOk && isQuery) {
return {
headers: new Headers([
['cache-control', 's-maxage=1, stale-while-revalidate=86400'],
]),
};
}
return {};
},
});
```
Name public routes with a `public` prefix (e.g., `public.slowQueryCached`) so `responseMeta` can identify them by path.
### Skip caching for authenticated requests
```ts
import { createHTTPServer } from '@trpc/server/adapters/standalone';
import { appRouter } from './appRouter';
import { createContext } from './trpc';
const server = createHTTPServer({
router: appRouter,
createContext,
responseMeta({ ctx, errors, type }) {
if (ctx?.user || errors.length > 0 || type !== 'query') {
return {};
}
return {
headers: new Headers([
['cache-control', 's-maxage=1, stale-while-revalidate=86400'],
]),
};
},
});
```
## Common Mistakes
### [CRITICAL] Caching authenticated responses
Wrong:
```ts
import { createHTTPServer } from '@trpc/server/adapters/standalone';
import { appRouter } from './appRouter';
const server = createHTTPServer({
router: appRouter,
responseMeta() {
return {
headers: new Headers([['cache-control', 's-maxage=60']]),
};
},
});
```
Correct:
```ts
import { createHTTPServer } from '@trpc/server/adapters/standalone';
import { appRouter } from './appRouter';
import { createContext } from './trpc';
const server = createHTTPServer({
router: appRouter,
createContext,
responseMeta({ ctx, errors, type }) {
if (ctx?.user || errors.length > 0 || type !== 'query') {
return {};
}
return {
headers: new Headers([
['cache-control', 's-maxage=1, stale-while-revalidate=86400'],
]),
};
},
});
```
With batching enabled by default, a cached response containing personal data could be served to other users; always check for auth context, errors, and request type before setting cache headers.
Source: www/docs/server/caching.md
### [HIGH] Next.js App Router overrides Cache-Control headers
There is no code fix for this -- Next.js App Router overrides `Cache-Control` headers set by tRPC via `responseMeta`. The documented caching approach using `responseMeta` does not work as expected in App Router. Use Next.js native caching mechanisms (`revalidate`, `unstable_cache`) instead when deploying on App Router.
Source: https://github.com/trpc/trpc/issues/5625
## See Also
- `server-setup` -- initTRPC, createContext configuration
- `adapter-standalone` -- responseMeta option on createHTTPServer
- `adapter-fetch` -- responseMeta option on fetchRequestHandler
- `links` -- splitLink to separate public/private requests on the client

View File

@@ -0,0 +1,253 @@
---
name: error-handling
description: >
Throw typed errors with TRPCError and error codes (NOT_FOUND, UNAUTHORIZED,
BAD_REQUEST, INTERNAL_SERVER_ERROR), configure errorFormatter for client-side
Zod error display, handle errors globally with onError callback, map tRPC errors
to HTTP status codes with getHTTPStatusCodeFromError().
type: core
library: trpc
library_version: '11.15.1'
requires:
- server-setup
sources:
- 'trpc/trpc:www/docs/server/error-handling.md'
- 'trpc/trpc:www/docs/server/error-formatting.md'
- 'trpc/trpc:packages/server/src/unstable-core-do-not-import/error/TRPCError.ts'
---
# tRPC -- Error Handling
## Setup
```ts
// server/trpc.ts
import { initTRPC } from '@trpc/server';
import { ZodError } from 'zod';
const t = initTRPC.create({
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.code === 'BAD_REQUEST' && error.cause instanceof ZodError
? error.cause.flatten()
: null,
},
};
},
});
export const router = t.router;
export const publicProcedure = t.procedure;
```
## Core Patterns
### Throwing typed errors from procedures
```ts
import { TRPCError } from '@trpc/server';
import { z } from 'zod';
import { publicProcedure, router } from './trpc';
export const appRouter = router({
userById: publicProcedure
.input(z.object({ id: z.string() }))
.query(({ input }) => {
const user = getUserFromDb(input.id);
if (!user) {
throw new TRPCError({
code: 'NOT_FOUND',
message: `User with id ${input.id} not found`,
});
}
return user;
}),
});
function getUserFromDb(id: string) {
if (id === '1') return { id: '1', name: 'Katt' };
return null;
}
```
### Wrapping original errors with cause
```ts
import { TRPCError } from '@trpc/server';
import { publicProcedure, router } from './trpc';
export const appRouter = router({
riskyOperation: publicProcedure.mutation(async () => {
try {
return await externalService();
} catch (err) {
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'An unexpected error occurred, please try again later.',
cause: err,
});
}
}),
});
async function externalService() {
throw new Error('connection refused');
}
```
Pass the original error as `cause` to retain the stack trace for debugging.
### Global error handling with onError
```ts
import { createHTTPServer } from '@trpc/server/adapters/standalone';
import { appRouter } from './appRouter';
const server = createHTTPServer({
router: appRouter,
onError(opts) {
const { error, type, path, input, ctx, req } = opts;
console.error('Error:', error);
if (error.code === 'INTERNAL_SERVER_ERROR') {
// send to bug reporting service
}
},
});
server.listen(3000);
```
### Extracting HTTP status from TRPCError
```ts
import { TRPCError } from '@trpc/server';
import { getHTTPStatusCodeFromError } from '@trpc/server/http';
function handleError(error: unknown) {
if (error instanceof TRPCError) {
const httpCode = getHTTPStatusCodeFromError(error);
console.log(httpCode); // e.g., 400, 401, 404, 500
}
}
```
## Common Mistakes
### [HIGH] Throwing plain Error instead of TRPCError
Wrong:
```ts
import { publicProcedure } from './trpc';
const proc = publicProcedure.query(() => {
throw new Error('Not found');
// client receives 500 INTERNAL_SERVER_ERROR
});
```
Correct:
```ts
import { TRPCError } from '@trpc/server';
import { publicProcedure } from './trpc';
const proc = publicProcedure.query(() => {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'User not found',
});
// client receives 404 NOT_FOUND
});
```
Plain Error objects are caught and wrapped as INTERNAL_SERVER_ERROR (500); use TRPCError with a specific code for proper HTTP status mapping.
Source: www/docs/server/error-handling.md
### [MEDIUM] Expecting stack traces in production
Wrong:
```ts
import { initTRPC } from '@trpc/server';
// No explicit isDev setting
const t = initTRPC.create();
// Stack traces may or may not appear depending on NODE_ENV
```
Correct:
```ts
import { initTRPC } from '@trpc/server';
const t = initTRPC.create({
isDev: process.env.NODE_ENV === 'development',
});
```
Stack traces are included only when `isDev` is true (default: `NODE_ENV !== "production"`); set `isDev` explicitly for deterministic behavior across runtimes.
Source: www/docs/server/error-handling.md
### [HIGH] Not handling Zod errors in errorFormatter
Wrong:
```ts
import { initTRPC } from '@trpc/server';
// No errorFormatter -- client gets generic "Input validation failed"
const t = initTRPC.create();
```
Correct:
```ts
import { initTRPC } from '@trpc/server';
import { ZodError } from 'zod';
const t = initTRPC.create({
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.code === 'BAD_REQUEST' && error.cause instanceof ZodError
? error.cause.flatten()
: null,
},
};
},
});
```
Without a custom errorFormatter, the client receives a generic message without field-level validation details from Zod.
Source: www/docs/server/error-formatting.md
## Error Code Reference
| Code | HTTP | Use when |
| --------------------- | ---- | ------------------------------------ |
| BAD_REQUEST | 400 | Invalid input |
| UNAUTHORIZED | 401 | Missing or invalid auth credentials |
| FORBIDDEN | 403 | Authenticated but not authorized |
| NOT_FOUND | 404 | Resource does not exist |
| CONFLICT | 409 | Request conflicts with current state |
| UNPROCESSABLE_CONTENT | 422 | Valid syntax but semantic error |
| TOO_MANY_REQUESTS | 429 | Rate limit exceeded |
| INTERNAL_SERVER_ERROR | 500 | Unexpected server error |
## See Also
- `server-setup` -- initTRPC configuration including isDev
- `validators` -- input validation that triggers BAD_REQUEST errors
- `middlewares` -- auth middleware throwing UNAUTHORIZED
- `server-side-calls` -- catching TRPCError in server-side callers

242
node_modules/@trpc/server/skills/middlewares/SKILL.md generated vendored Normal file
View File

@@ -0,0 +1,242 @@
---
name: middlewares
description: >
Create and compose tRPC middleware with t.procedure.use(), extend context via
opts.next({ ctx }), build reusable middleware with .concat() and .unstable_pipe(),
define base procedures like publicProcedure and authedProcedure. Access raw input
with getRawInput(). Logging, timing, OTEL tracing patterns.
type: core
library: trpc
library_version: '11.15.1'
requires:
- server-setup
sources:
- 'trpc/trpc:www/docs/server/middlewares.md'
- 'trpc/trpc:www/docs/server/authorization.md'
- 'trpc/trpc:packages/server/src/unstable-core-do-not-import/middleware.ts'
- 'trpc/trpc:packages/server/src/unstable-core-do-not-import/procedureBuilder.ts'
---
# tRPC -- Middlewares
## Setup
```ts
// server/trpc.ts
import { initTRPC, TRPCError } from '@trpc/server';
type Context = {
user?: { id: string; isAdmin: boolean };
};
const t = initTRPC.context<Context>().create();
export const router = t.router;
export const publicProcedure = t.procedure;
export const middleware = t.middleware;
```
## Core Patterns
### Auth middleware that narrows context type
```ts
// server/trpc.ts
import { initTRPC, TRPCError } from '@trpc/server';
type Context = {
user?: { id: string; isAdmin: boolean };
};
const t = initTRPC.context<Context>().create();
export const publicProcedure = t.procedure;
export const authedProcedure = t.procedure.use(async (opts) => {
const { ctx } = opts;
if (!ctx.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return opts.next({
ctx: {
user: ctx.user,
},
});
});
export const adminProcedure = t.procedure.use(async (opts) => {
const { ctx } = opts;
if (!ctx.user?.isAdmin) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return opts.next({
ctx: {
user: ctx.user,
},
});
});
```
After the middleware, `ctx.user` is non-nullable in downstream procedures.
### Logging and timing middleware
```ts
// server/trpc.ts
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
export const loggedProcedure = t.procedure.use(async (opts) => {
const start = Date.now();
const result = await opts.next();
const durationMs = Date.now() - start;
const meta = { path: opts.path, type: opts.type, durationMs };
result.ok
? console.log('OK request timing:', meta)
: console.error('Non-OK request timing', meta);
return result;
});
```
### Reusable middleware with .concat()
```ts
// myPlugin.ts
import { initTRPC } from '@trpc/server';
export function createMyPlugin() {
const t = initTRPC.context<{}>().meta<{}>().create();
return {
pluginProc: t.procedure.use((opts) => {
return opts.next({
ctx: {
fromPlugin: 'hello from myPlugin' as const,
},
});
}),
};
}
```
```ts
// server/trpc.ts
import { initTRPC } from '@trpc/server';
import { createMyPlugin } from './myPlugin';
const t = initTRPC.context<{}>().create();
const plugin = createMyPlugin();
export const publicProcedure = t.procedure;
export const procedureWithPlugin = publicProcedure.concat(plugin.pluginProc);
```
`.concat()` merges a partial procedure (from any tRPC instance) into your procedure chain, as long as context and meta types overlap.
### Extending middlewares with .unstable_pipe()
```ts
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
const fooMiddleware = t.middleware((opts) => {
return opts.next({
ctx: { foo: 'foo' as const },
});
});
const barMiddleware = fooMiddleware.unstable_pipe((opts) => {
console.log(opts.ctx.foo);
return opts.next({
ctx: { bar: 'bar' as const },
});
});
const barProcedure = t.procedure.use(barMiddleware);
```
Piped middlewares run in order and each receives the context from the previous middleware.
## Common Mistakes
### [CRITICAL] Forgetting to call and return opts.next()
Wrong:
```ts
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
const logMiddleware = t.middleware(async (opts) => {
console.log('request started');
// forgot to call opts.next()
});
```
Correct:
```ts
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
const logMiddleware = t.middleware(async (opts) => {
console.log('request started');
const result = await opts.next();
console.log('request ended');
return result;
});
```
Middleware must call `opts.next()` and return its result; forgetting this silently drops the request with an INTERNAL_SERVER_ERROR because no middleware marker is returned.
Source: packages/server/src/unstable-core-do-not-import/procedureBuilder.ts
### [HIGH] Extending context with wrong type in opts.next()
Wrong:
```ts
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
const middleware = t.middleware(async (opts) => {
return opts.next({ ctx: 'not-an-object' });
});
```
Correct:
```ts
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
async function getUser() {
return { id: '1', name: 'Katt' };
}
const middleware = t.middleware(async (opts) => {
return opts.next({ ctx: { user: await getUser() } });
});
```
Context extension in `opts.next({ ctx })` must be an object; passing non-object values or overwriting required keys breaks downstream procedures.
Source: www/docs/server/middlewares.md
## See Also
- `server-setup` -- initTRPC, routers, procedures, context
- `validators` -- input/output validation with Zod
- `error-handling` -- TRPCError codes used in auth middleware
- `auth` -- full auth patterns combining middleware + client headers

View File

@@ -0,0 +1,265 @@
---
name: non-json-content-types
description: >
Handle FormData, file uploads, Blob, Uint8Array, and ReadableStream inputs in
tRPC mutations. Use octetInputParser from @trpc/server/http for binary data.
Route non-JSON requests with splitLink and isNonJsonSerializable() from
@trpc/client. FormData and binary inputs only work with mutations (POST).
type: core
library: trpc
library_version: '11.15.1'
requires:
- server-setup
- links
sources:
- 'trpc/trpc:www/docs/server/non-json-content-types.md'
- 'trpc/trpc:examples/next-formdata/'
---
# tRPC -- Non-JSON Content Types
## Setup
Server:
```ts
// server/trpc.ts
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
export const router = t.router;
export const publicProcedure = t.procedure;
```
```ts
// server/appRouter.ts
import { octetInputParser } from '@trpc/server/http';
import { z } from 'zod';
import { publicProcedure, router } from './trpc';
export const appRouter = router({
uploadForm: publicProcedure
.input(z.instanceof(FormData))
.mutation(({ input }) => {
const name = input.get('name');
return { greeting: `Hello ${name}` };
}),
uploadFile: publicProcedure.input(octetInputParser).mutation(({ input }) => {
// input is a ReadableStream
return { valid: true };
}),
});
export type AppRouter = typeof appRouter;
```
Client:
```ts
// client/index.ts
import {
createTRPCClient,
httpBatchLink,
httpLink,
isNonJsonSerializable,
splitLink,
} from '@trpc/client';
import type { AppRouter } from '../server/appRouter';
const url = 'http://localhost:3000';
const trpc = createTRPCClient<AppRouter>({
links: [
splitLink({
condition: (op) => isNonJsonSerializable(op.input),
true: httpLink({ url }),
false: httpBatchLink({ url }),
}),
],
});
```
## Core Patterns
### FormData mutation
```ts
// server/appRouter.ts
import { z } from 'zod';
import { publicProcedure, router } from './trpc';
export const appRouter = router({
createPost: publicProcedure
.input(z.instanceof(FormData))
.mutation(({ input }) => {
const title = input.get('title') as string;
const body = input.get('body') as string;
return { id: '1', title, body };
}),
});
```
```ts
// client usage
const form = new FormData();
form.append('title', 'Hello');
form.append('body', 'World');
const result = await trpc.createPost.mutate(form);
```
### Binary file upload with octetInputParser
```ts
// server/appRouter.ts
import { octetInputParser } from '@trpc/server/http';
import { publicProcedure, router } from './trpc';
export const appRouter = router({
upload: publicProcedure
.input(octetInputParser)
.mutation(async ({ input }) => {
const reader = input.getReader();
let totalBytes = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
totalBytes += value.byteLength;
}
return { totalBytes };
}),
});
```
```ts
// client usage
const file = new File(['hello world'], 'test.txt', { type: 'text/plain' });
const result = await trpc.upload.mutate(file);
```
`octetInputParser` converts Blob, Uint8Array, and File inputs to a ReadableStream on the server.
### Client splitLink with superjson transformer
```ts
import {
createTRPCClient,
httpBatchLink,
httpLink,
isNonJsonSerializable,
splitLink,
} from '@trpc/client';
import superjson from 'superjson';
import type { AppRouter } from '../server/appRouter';
const url = 'http://localhost:3000';
const trpc = createTRPCClient<AppRouter>({
links: [
splitLink({
condition: (op) => isNonJsonSerializable(op.input),
true: httpLink({
url,
transformer: {
serialize: (data) => data,
deserialize: (data) => superjson.deserialize(data),
},
}),
false: httpBatchLink({
url,
transformer: superjson,
}),
}),
],
});
```
When using a transformer, the non-JSON httpLink needs a custom transformer that skips serialization for the request (FormData/binary cannot be transformed) but deserializes the response.
## Common Mistakes
### [HIGH] Using httpBatchLink for FormData requests
Wrong:
```ts
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from '../server/appRouter';
const trpc = createTRPCClient<AppRouter>({
links: [httpBatchLink({ url: 'http://localhost:3000' })],
});
```
Correct:
```ts
import {
createTRPCClient,
httpBatchLink,
httpLink,
isNonJsonSerializable,
splitLink,
} from '@trpc/client';
import type { AppRouter } from '../server/appRouter';
const url = 'http://localhost:3000';
const trpc = createTRPCClient<AppRouter>({
links: [
splitLink({
condition: (op) => isNonJsonSerializable(op.input),
true: httpLink({ url }),
false: httpBatchLink({ url }),
}),
],
});
```
FormData and binary inputs are not batchable; use `splitLink` with `isNonJsonSerializable()` to route them through `httpLink`.
Source: www/docs/server/non-json-content-types.md
### [HIGH] Global body parser intercepting FormData before tRPC
Wrong:
```ts
import * as trpcExpress from '@trpc/server/adapters/express';
import express from 'express';
import { appRouter } from './appRouter';
const app = express();
app.use(express.json());
app.use('/trpc', trpcExpress.createExpressMiddleware({ router: appRouter }));
```
Correct:
```ts
import * as trpcExpress from '@trpc/server/adapters/express';
import express from 'express';
import { appRouter } from './appRouter';
const app = express();
app.use('/api', express.json());
app.use('/trpc', trpcExpress.createExpressMiddleware({ router: appRouter }));
```
A global `express.json()` middleware consumes the request body before tRPC can read it; scope body parsing to non-tRPC routes only.
Source: www/docs/server/non-json-content-types.md
### [HIGH] FormData only works with mutations
FormData and binary inputs are only supported for mutations (POST requests). Using them with `.query()` throws an error because queries use HTTP GET which cannot carry a request body.
Source: www/docs/server/non-json-content-types.md
## See Also
- `server-setup` -- initTRPC, routers, procedures
- `links` -- splitLink configuration for routing non-JSON requests
- `validators` -- z.instanceof(FormData) for FormData validation
- `adapter-express` -- Express-specific body parser considerations

378
node_modules/@trpc/server/skills/server-setup/SKILL.md generated vendored Normal file
View File

@@ -0,0 +1,378 @@
---
name: server-setup
description: >
Initialize tRPC with initTRPC.create(), define routers with t.router(),
create procedures with .query()/.mutation()/.subscription(), configure context
with createContext(), export AppRouter type, merge routers with t.mergeRouters(),
lazy-load routers with lazy().
type: core
library: trpc
library_version: '11.15.1'
requires: []
sources:
- 'trpc/trpc:www/docs/server/overview.md'
- 'trpc/trpc:www/docs/server/routers.md'
- 'trpc/trpc:www/docs/server/procedures.md'
- 'trpc/trpc:www/docs/server/context.md'
- 'trpc/trpc:www/docs/server/merging-routers.md'
- 'trpc/trpc:www/docs/main/quickstart.mdx'
- 'trpc/trpc:packages/server/src/unstable-core-do-not-import/initTRPC.ts'
- 'trpc/trpc:packages/server/src/unstable-core-do-not-import/router.ts'
---
# tRPC -- Server Setup
## Setup
```ts
// server/trpc.ts
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
export const router = t.router;
export const publicProcedure = t.procedure;
```
```ts
// server/appRouter.ts
import { z } from 'zod';
import { publicProcedure, router } from './trpc';
type User = { id: string; name: string };
export const appRouter = router({
userList: publicProcedure.query(async (): Promise<User[]> => {
return [{ id: '1', name: 'Katt' }];
}),
userById: publicProcedure
.input(z.string())
.query(async ({ input }): Promise<User> => {
return { id: input, name: 'Katt' };
}),
userCreate: publicProcedure
.input(z.object({ name: z.string() }))
.mutation(async ({ input }): Promise<User> => {
return { id: '1', ...input };
}),
});
export type AppRouter = typeof appRouter;
```
```ts
// server/index.ts
import { createHTTPServer } from '@trpc/server/adapters/standalone';
import { appRouter } from './appRouter';
const server = createHTTPServer({ router: appRouter });
server.listen(3000);
```
## Core Patterns
### Context with typed session
```ts
// server/context.ts
import type { CreateHTTPContextOptions } from '@trpc/server/adapters/standalone';
export async function createContext(opts: CreateHTTPContextOptions) {
const token = opts.req.headers['authorization'];
return { token };
}
export type Context = Awaited<ReturnType<typeof createContext>>;
```
```ts
// server/trpc.ts
import { initTRPC } from '@trpc/server';
import type { Context } from './context';
const t = initTRPC.context<Context>().create();
export const router = t.router;
export const publicProcedure = t.procedure;
```
```ts
// server/index.ts
import { createHTTPServer } from '@trpc/server/adapters/standalone';
import { appRouter } from './appRouter';
import { createContext } from './context';
const server = createHTTPServer({
router: appRouter,
createContext,
});
server.listen(3000);
```
### Inner/outer context split for testability
```ts
// server/context.ts
import type { CreateHTTPContextOptions } from '@trpc/server/adapters/standalone';
import { db } from './db';
interface CreateInnerContextOptions {
session: { user: { email: string } } | null;
}
export async function createContextInner(opts?: CreateInnerContextOptions) {
return {
db,
session: opts?.session ?? null,
};
}
export async function createContext(opts: CreateHTTPContextOptions) {
const session = getSessionFromCookie(opts.req);
const contextInner = await createContextInner({ session });
return {
...contextInner,
req: opts.req,
res: opts.res,
};
}
export type Context = Awaited<ReturnType<typeof createContextInner>>;
```
Infer `Context` from `createContextInner` so server-side callers and tests never need HTTP request objects.
### Merging child routers
```ts
// server/routers/user.ts
import { publicProcedure, router } from '../trpc';
export const userRouter = router({
list: publicProcedure.query(() => []),
});
```
```ts
// server/routers/post.ts
import { z } from 'zod';
import { publicProcedure, router } from '../trpc';
export const postRouter = router({
create: publicProcedure
.input(z.object({ title: z.string() }))
.mutation(({ input }) => ({ id: '1', ...input })),
list: publicProcedure.query(() => []),
});
```
```ts
// server/routers/_app.ts
import { router } from '../trpc';
import { postRouter } from './post';
import { userRouter } from './user';
export const appRouter = router({
user: userRouter,
post: postRouter,
});
export type AppRouter = typeof appRouter;
```
### Lazy-loaded routers for serverless cold starts
```ts
// server/routers/_app.ts
import { lazy } from '@trpc/server';
import { router } from '../trpc';
export const appRouter = router({
// Short-hand when the module has exactly one router exported
greeting: lazy(() => import('./greeting.js')),
// Use .then() to pick a named export when the module exports multiple routers
user: lazy(() => import('./user.js').then((m) => m.userRouter)),
});
export type AppRouter = typeof appRouter;
```
## Common Mistakes
### [CRITICAL] Calling initTRPC.create() more than once
Wrong:
```ts
// file: userRouter.ts
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
export const userRouter = t.router({});
// file: postRouter.ts
import { initTRPC } from '@trpc/server';
const t2 = initTRPC.create();
export const postRouter = t2.router({});
```
Correct:
```ts
// file: trpc.ts (single file, created once)
import { initTRPC } from '@trpc/server';
import type { Context } from './context';
const t = initTRPC.context<Context>().create();
export const router = t.router;
export const publicProcedure = t.procedure;
```
Multiple tRPC instances cause type mismatches and runtime errors when routers from different instances are merged.
Source: www/docs/server/routers.md
### [HIGH] Using reserved words as procedure names
Wrong:
```ts
import { publicProcedure, router } from './trpc';
const appRouter = router({
then: publicProcedure.query(() => 'hello'),
});
```
Correct:
```ts
import { publicProcedure, router } from './trpc';
const appRouter = router({
next: publicProcedure.query(() => 'hello'),
});
```
Router creation throws if procedure names are "then", "call", or "apply" because these conflict with JavaScript Proxy internals.
Source: packages/server/src/unstable-core-do-not-import/router.ts
### [CRITICAL] Importing AppRouter as a value import
Wrong:
```ts
// client.ts
import { AppRouter } from '../server/router';
```
Correct:
```ts
// client.ts
import type { AppRouter } from '../server/router';
```
A non-type import pulls the entire server bundle into the client; use `import type` so it is stripped at build time.
Source: www/docs/server/routers.md
### [MEDIUM] Creating context without inner/outer split
Wrong:
```ts
import type { CreateExpressContextOptions } from '@trpc/server/adapters/express';
export function createContext({ req }: CreateExpressContextOptions) {
return { db: prisma, user: getUserFromReq(req) };
}
```
Correct:
```ts
import type { CreateExpressContextOptions } from '@trpc/server/adapters/express';
export function createContextInner(opts: { user?: User }) {
return { db: prisma, user: opts.user ?? null };
}
export function createContext({ req }: CreateExpressContextOptions) {
return createContextInner({ user: getUserFromReq(req) });
}
```
Without an inner context factory, server-side callers and tests must construct HTTP request objects to get context.
Source: www/docs/server/context.md
### [HIGH] Merging routers with different transformers
Wrong:
```ts
import { initTRPC } from '@trpc/server';
import superjson from 'superjson';
const t1 = initTRPC.create({ transformer: superjson });
const t2 = initTRPC.create();
const router1 = t1.router({ a: t1.procedure.query(() => 'a') });
const router2 = t2.router({ b: t2.procedure.query(() => 'b') });
t1.mergeRouters(router1, router2);
```
Correct:
```ts
import { initTRPC } from '@trpc/server';
import superjson from 'superjson';
const t = initTRPC.create({ transformer: superjson });
const router1 = t.router({ a: t.procedure.query(() => 'a') });
const router2 = t.router({ b: t.procedure.query(() => 'b') });
t.mergeRouters(router1, router2);
```
`t.mergeRouters()` throws at runtime if the routers were created with different transformer or errorFormatter configurations.
Source: packages/server/src/unstable-core-do-not-import/router.ts
### [CRITICAL] Importing appRouter value into client code
Wrong:
```ts
// client.ts
import { appRouter } from '../server/router';
type AppRouter = typeof appRouter;
```
Correct:
```ts
// client.ts
import type { AppRouter } from '../server/router';
// server/router.ts
export type AppRouter = typeof appRouter;
```
Importing the appRouter value bundles the entire server into the client, even if you only use `typeof`.
Source: www/docs/server/routers.md
## See Also
- `middlewares` -- add auth, logging, context extension to procedures
- `validators` -- add input/output validation with Zod
- `error-handling` -- throw and format typed errors
- `server-side-calls` -- call procedures from server code
- `adapter-standalone` -- mount on Node.js HTTP server
- `adapter-fetch` -- mount on edge runtimes

View File

@@ -0,0 +1,249 @@
---
name: server-side-calls
description: >
Call tRPC procedures directly from server code using t.createCallerFactory()
and router.createCaller(context) for integration testing, internal server logic,
and custom API endpoints. Catch TRPCError and extract HTTP status with
getHTTPStatusCodeFromError(). Error handling via onError option.
type: core
library: trpc
library_version: '11.15.1'
requires:
- server-setup
sources:
- 'trpc/trpc:www/docs/server/server-side-calls.md'
---
# tRPC -- Server-Side Calls
## Setup
```ts
// server/trpc.ts
import { initTRPC } from '@trpc/server';
type Context = { user?: { id: string } };
const t = initTRPC.context<Context>().create();
export const router = t.router;
export const publicProcedure = t.procedure;
export const createCallerFactory = t.createCallerFactory;
```
```ts
// server/appRouter.ts
import { TRPCError } from '@trpc/server';
import { z } from 'zod';
import { createCallerFactory, publicProcedure, router } from './trpc';
interface Post {
id: string;
title: string;
}
const posts: Post[] = [{ id: '1', title: 'Hello world' }];
export const appRouter = router({
post: router({
add: publicProcedure
.input(z.object({ title: z.string().min(2) }))
.mutation(({ input }) => {
const post: Post = { ...input, id: `${Math.random()}` };
posts.push(post);
return post;
}),
byId: publicProcedure
.input(z.object({ id: z.string() }))
.query(({ input }) => {
const post = posts.find((p) => p.id === input.id);
if (!post) throw new TRPCError({ code: 'NOT_FOUND' });
return post;
}),
list: publicProcedure.query(() => posts),
}),
});
export type AppRouter = typeof appRouter;
export const createCaller = createCallerFactory(appRouter);
```
```ts
// usage
import { createCaller } from './appRouter';
const caller = createCaller({ user: { id: '1' } });
const postList = await caller.post.list();
const newPost = await caller.post.add({ title: 'New post' });
```
## Core Patterns
### Integration test with createCallerFactory
```ts
// server/appRouter.test.ts
import type { inferProcedureInput } from '@trpc/server';
import { createCaller } from './appRouter';
import type { AppRouter } from './appRouter';
import { createContextInner } from './context';
async function testAddAndGetPost() {
const ctx = await createContextInner({ user: undefined });
const caller = createCaller(ctx);
const input: inferProcedureInput<AppRouter['post']['add']> = {
title: 'Test post',
};
const post = await caller.post.add(input);
const allPosts = await caller.post.list();
console.assert(allPosts.some((p) => p.id === post.id));
}
```
### Using router.createCaller() directly
```ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
const t = initTRPC.create();
const appRouter = t.router({
greeting: t.procedure
.input(z.object({ name: z.string() }))
.query(({ input }) => `Hello ${input.name}`),
});
const caller = appRouter.createCaller({});
const result = await caller.greeting({ name: 'tRPC' });
```
### Error handling in a custom API endpoint
```ts
import { TRPCError } from '@trpc/server';
import { getHTTPStatusCodeFromError } from '@trpc/server/http';
import type { NextApiRequest, NextApiResponse } from 'next';
import { appRouter } from './appRouter';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
const caller = appRouter.createCaller({});
try {
const post = await caller.post.byId({ id: req.query.id as string });
res.status(200).json({ data: { postTitle: post.title } });
} catch (cause) {
if (cause instanceof TRPCError) {
const httpStatusCode = getHTTPStatusCodeFromError(cause);
res.status(httpStatusCode).json({ error: { message: cause.message } });
return;
}
res.status(500).json({ error: { message: 'Internal server error' } });
}
}
```
### Caller with onError callback
```ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
const t = initTRPC.create();
const appRouter = t.router({
greeting: t.procedure
.input(z.object({ name: z.string() }))
.query(({ input }) => {
if (input.name === 'invalid') {
throw new Error('Invalid name');
}
return `Hello ${input.name}`;
}),
});
const caller = appRouter.createCaller(
{},
{
onError: (opts) => {
console.error('An error occurred:', opts.error);
},
},
);
```
## Common Mistakes
### [HIGH] Using createCaller inside another procedure
Wrong:
```ts
import { appRouter } from './appRouter';
import { createCallerFactory, publicProcedure } from './trpc';
const createCaller = createCallerFactory(appRouter);
const proc = publicProcedure.query(async () => {
const caller = createCaller({});
return caller.post.list();
});
```
Correct:
```ts
import type { Context } from './context';
import { publicProcedure } from './trpc';
async function listPosts(ctx: Context) {
return ctx.db.post.findMany();
}
const proc = publicProcedure.query(async ({ ctx }) => {
return listPosts(ctx);
});
```
Calling createCaller from within a procedure re-creates context, re-runs all middleware, and re-validates input; extract shared logic into a plain function instead.
Source: www/docs/server/server-side-calls.md
### [MEDIUM] Not providing context to createCaller
Wrong:
```ts
import { appRouter } from './appRouter';
const caller = appRouter.createCaller({});
await caller.protectedRoute();
// middleware throws UNAUTHORIZED because ctx.user is undefined
```
Correct:
```ts
import { appRouter } from './appRouter';
import { createContextInner } from './context';
const ctx = await createContextInner({ user: { id: '1' } });
const caller = appRouter.createCaller(ctx);
await caller.protectedRoute();
```
`createCaller` requires a context object matching what procedures and middleware expect; passing an empty object when procedures require auth context causes runtime errors.
Source: www/docs/server/server-side-calls.md
## See Also
- `server-setup` -- initTRPC, routers, context configuration
- `middlewares` -- auth middleware that callers must satisfy
- `error-handling` -- TRPCError and getHTTPStatusCodeFromError

View File

@@ -0,0 +1,247 @@
---
name: service-oriented-architecture
description: >
Break a tRPC backend into multiple services with custom routing links that
split on the first path segment (op.path.split('.')) to route to different
backend service URLs. Define a faux gateway router that merges service routers
for the AppRouter type without running them in the same process. Share
procedure and router definitions via a server-lib package with a single
initTRPC instance. Each service runs its own standalone/Express/Fastify server.
type: composition
library: trpc
library_version: '11.15.1'
requires:
- server-setup
- client-setup
- links
sources:
- examples/soa/
---
# tRPC — Service-Oriented Architecture
## Setup
### Shared library (single initTRPC instance)
```ts
// packages/server-lib/index.ts
import { initTRPC } from '@trpc/server';
type Context = {
requestId?: string;
};
const t = initTRPC.context<Context>().create();
export const router = t.router;
export const publicProcedure = t.procedure;
export const mergeRouters = t.mergeRouters;
```
### Service A (own server)
```ts
// services/service-a/router.ts
import { publicProcedure, router } from '@myorg/server-lib';
import { z } from 'zod';
export const serviceARouter = router({
greet: publicProcedure
.input(z.object({ name: z.string() }))
.query(({ input }) => ({ greeting: `Hello, ${input.name}!` })),
});
```
```ts
// services/service-a/index.ts
import { createHTTPServer } from '@trpc/server/adapters/standalone';
import { serviceARouter } from './router';
createHTTPServer({
router: serviceARouter,
createContext() {
return {};
},
}).listen(2021);
```
### Service B (own server)
```ts
// services/service-b/router.ts
import { publicProcedure, router } from '@myorg/server-lib';
export const serviceBRouter = router({
status: publicProcedure.query(() => ({ status: 'ok' })),
});
```
```ts
// services/service-b/index.ts
import { createHTTPServer } from '@trpc/server/adapters/standalone';
import { serviceBRouter } from './router';
createHTTPServer({
router: serviceBRouter,
createContext() {
return {};
},
}).listen(2022);
```
### Gateway (type-only, not a running server)
```ts
// gateway/index.ts
import { router } from '@myorg/server-lib';
import { serviceARouter } from '../services/service-a/router';
import { serviceBRouter } from '../services/service-b/router';
const appRouter = router({
serviceA: serviceARouter,
serviceB: serviceBRouter,
});
export type AppRouter = typeof appRouter;
```
The gateway merges routers only for type inference. It does not run as a server process. The client uses the `AppRouter` type for full type safety.
### Client with custom routing link
```ts
// client/client.ts
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from '../gateway';
export const client = createTRPCClient<AppRouter>({
links: [
(runtime) => {
const servers = {
serviceA: httpBatchLink({ url: 'http://localhost:2021' })(runtime),
serviceB: httpBatchLink({ url: 'http://localhost:2022' })(runtime),
};
return (ctx) => {
const { op } = ctx;
const pathParts = op.path.split('.');
const serverName = pathParts.shift() as keyof typeof servers;
const path = pathParts.join('.');
const link = servers[serverName];
if (!link) {
throw new Error(
`Unknown service: ${String(serverName)}. Known: ${Object.keys(servers).join(', ')}`,
);
}
return link({
...ctx,
op: { ...op, path },
});
};
},
],
});
```
```ts
// Usage
const greeting = await client.serviceA.greet.query({ name: 'World' });
const status = await client.serviceB.status.query();
```
## Core Patterns
### Path-based routing convention
```ts
(runtime) => {
const servers = {
users: httpBatchLink({ url: 'http://users-service:3000' })(runtime),
billing: httpBatchLink({ url: 'http://billing-service:3000' })(runtime),
notifications: httpBatchLink({ url: 'http://notifications-service:3000' })(
runtime,
),
};
return (ctx) => {
const { op } = ctx;
const [serverName, ...rest] = op.path.split('.');
const link = servers[serverName as keyof typeof servers];
if (!link) {
throw new Error(`Unknown service: ${serverName}`);
}
return link({
...ctx,
op: { ...op, path: rest.join('.') },
});
};
};
```
The first segment of the procedure path (before the first `.`) maps to a service name. The remaining path is forwarded to the target service.
### Adding shared headers across services
```ts
(runtime) => {
const servers = {
serviceA: httpBatchLink({
url: 'http://localhost:2021',
headers() {
return { 'x-request-id': crypto.randomUUID() };
},
})(runtime),
serviceB: httpBatchLink({
url: 'http://localhost:2022',
headers() {
return { 'x-request-id': crypto.randomUUID() };
},
})(runtime),
};
return (ctx) => {
const [serverName, ...rest] = ctx.op.path.split('.');
return servers[serverName as keyof typeof servers]({
...ctx,
op: { ...ctx.op, path: rest.join('.') },
});
};
};
```
## Common Mistakes
### MEDIUM Path routing assumes first segment is server name
Wrong:
```ts
const serverName = op.path.split('.').shift();
// Breaks if router structure changes or has nested namespaces
```
Correct:
```ts
const [serverName, ...rest] = op.path.split('.');
const link = servers[serverName as keyof typeof servers];
if (!link) {
throw new Error(`Unknown service: ${serverName}. Known: ${Object.keys(servers).join(', ')}`);
}
return link({ ...ctx, op: { ...op, path: rest.join('.') } });
```
Custom routing links that split on the first path segment break silently if the router structure changes. Add validation and clear error messages when the server name is unrecognized. The path convention must be documented and enforced across teams.
Source: examples/soa/client/client.ts
## See Also
- **server-setup** -- single `initTRPC.create()` instance shared across services
- **links** -- `httpBatchLink`, custom link authoring
- **client-setup** -- `createTRPCClient`, type-safe client with `AppRouter`
- **adapter-standalone** -- running individual service servers

406
node_modules/@trpc/server/skills/subscriptions/SKILL.md generated vendored Normal file
View File

@@ -0,0 +1,406 @@
---
name: subscriptions
description: >
Set up real-time event streams with async generator subscriptions using
.subscription(async function*() { yield }). SSE via httpSubscriptionLink is
recommended over WebSocket. Use tracked(id, data) from @trpc/server for
reconnection recovery with lastEventId. WebSocket via wsLink and
createWSClient from @trpc/client, applyWSSHandler from @trpc/server/adapters/ws. Configure SSE ping with
initTRPC.create({ sse: { ping: { enabled, intervalMs } } }). AbortSignal
via opts.signal for cleanup. splitLink to route subscriptions.
type: core
library: trpc
library_version: '11.15.1'
requires:
- server-setup
- links
sources:
- www/docs/server/subscriptions.md
- www/docs/server/websockets.md
- www/docs/client/links/httpSubscriptionLink.md
- www/docs/client/links/wsLink.md
- packages/server/src/unstable-core-do-not-import/stream/sse.ts
- packages/server/src/unstable-core-do-not-import/stream/tracked.ts
- examples/standalone-server/src/server.ts
---
# tRPC — Subscriptions
## Setup
SSE is recommended for most subscription use cases. It is simpler to set up and does not require a WebSocket server.
### Server
```ts
// server.ts
import EventEmitter, { on } from 'node:events';
import { initTRPC, tracked } from '@trpc/server';
import { createHTTPServer } from '@trpc/server/adapters/standalone';
import { z } from 'zod';
const t = initTRPC.create({
sse: {
ping: {
enabled: true,
intervalMs: 2000,
},
client: {
reconnectAfterInactivityMs: 5000,
},
},
});
type Post = { id: string; title: string };
const ee = new EventEmitter();
const appRouter = t.router({
onPostAdd: t.procedure
.input(z.object({ lastEventId: z.string().nullish() }).optional())
.subscription(async function* (opts) {
for await (const [data] of on(ee, 'add', { signal: opts.signal })) {
const post = data as Post;
yield tracked(post.id, post);
}
}),
});
export type AppRouter = typeof appRouter;
createHTTPServer({
router: appRouter,
createContext() {
return {};
},
}).listen(3000);
```
### Client (SSE)
```ts
// client.ts
import {
createTRPCClient,
httpBatchLink,
httpSubscriptionLink,
splitLink,
} from '@trpc/client';
import type { AppRouter } from './server';
const trpc = createTRPCClient<AppRouter>({
links: [
splitLink({
condition: (op) => op.type === 'subscription',
true: httpSubscriptionLink({ url: 'http://localhost:3000' }),
false: httpBatchLink({ url: 'http://localhost:3000' }),
}),
],
});
const subscription = trpc.onPostAdd.subscribe(
{ lastEventId: null },
{
onData(post) {
console.log('New post:', post);
},
onError(err) {
console.error('Subscription error:', err);
},
},
);
// To stop:
// subscription.unsubscribe();
```
## Core Patterns
### tracked() for reconnection recovery
```ts
import EventEmitter, { on } from 'node:events';
import { initTRPC, tracked } from '@trpc/server';
import { z } from 'zod';
const t = initTRPC.create();
const ee = new EventEmitter();
const appRouter = t.router({
onPostAdd: t.procedure
.input(z.object({ lastEventId: z.string().nullish() }).optional())
.subscription(async function* (opts) {
const iterable = on(ee, 'add', { signal: opts.signal });
if (opts.input?.lastEventId) {
// Fetch and yield events since lastEventId from your database
// const missed = await db.post.findMany({ where: { id: { gt: opts.input.lastEventId } } });
// for (const post of missed) { yield tracked(post.id, post); }
}
for await (const [data] of iterable) {
yield tracked(data.id, data);
}
}),
});
```
When using `tracked(id, data)`, the client automatically sends `lastEventId` on reconnection. For SSE this is part of the EventSource spec; for WebSocket, `wsLink` handles it.
### Polling loop subscription
```ts
import { initTRPC, tracked } from '@trpc/server';
import { z } from 'zod';
const t = initTRPC.create();
const appRouter = t.router({
onNewItems: t.procedure
.input(z.object({ lastEventId: z.coerce.date().nullish() }))
.subscription(async function* (opts) {
let cursor = opts.input?.lastEventId ?? null;
while (!opts.signal?.aborted) {
const items = await db.item.findMany({
where: cursor ? { createdAt: { gt: cursor } } : undefined,
orderBy: { createdAt: 'asc' },
});
for (const item of items) {
yield tracked(item.createdAt.toJSON(), item);
cursor = item.createdAt;
}
await new Promise((r) => setTimeout(r, 1000));
}
}),
});
```
### WebSocket setup (when bidirectional communication is required)
```ts
// server
import { applyWSSHandler } from '@trpc/server/adapters/ws';
import { WebSocketServer } from 'ws';
import { appRouter } from './router';
const wss = new WebSocketServer({ port: 3001 });
const handler = applyWSSHandler({
wss,
router: appRouter,
createContext() {
return {};
},
keepAlive: {
enabled: true,
pingMs: 30000,
pongWaitMs: 5000,
},
});
process.on('SIGTERM', () => {
handler.broadcastReconnectNotification();
wss.close();
});
```
```ts
// client
import {
createTRPCClient,
createWSClient,
httpBatchLink,
splitLink,
wsLink,
} from '@trpc/client';
import type { AppRouter } from './server';
const wsClient = createWSClient({ url: 'ws://localhost:3001' });
const trpc = createTRPCClient<AppRouter>({
links: [
splitLink({
condition: (op) => op.type === 'subscription',
true: wsLink({ client: wsClient }),
false: httpBatchLink({ url: 'http://localhost:3000' }),
}),
],
});
```
### Cleanup with try...finally
```ts
const appRouter = t.router({
events: t.procedure.subscription(async function* (opts) {
const cleanup = registerListener();
try {
for await (const [data] of on(ee, 'event', { signal: opts.signal })) {
yield data;
}
} finally {
cleanup();
}
}),
});
```
tRPC invokes `.return()` on the generator when the subscription stops, triggering the `finally` block.
## Common Mistakes
### HIGH Using Observable instead of async generator
Wrong:
```ts
import { observable } from '@trpc/server/observable';
t.procedure.subscription(({ input }) => {
return observable((emit) => {
emit.next(data);
});
});
```
Correct:
```ts
t.procedure.subscription(async function* ({ input, signal }) {
for await (const [data] of on(ee, 'event', { signal })) {
yield data;
}
});
```
Observable subscriptions are deprecated and will be removed in v12. Use async generator syntax (`async function*`).
Source: packages/server/src/unstable-core-do-not-import/procedureBuilder.ts
### MEDIUM Empty string as tracked event ID
Wrong:
```ts
yield tracked('', data);
```
Correct:
```ts
yield tracked(event.id.toString(), data);
```
`tracked()` throws if the ID is an empty string because it conflicts with SSE "no id" semantics.
Source: packages/server/src/unstable-core-do-not-import/stream/tracked.ts
### HIGH Fetching history before setting up event listener
Wrong:
```ts
t.procedure.subscription(async function* (opts) {
const history = await db.getEvents(); // events may fire here and be lost
yield* history;
for await (const event of listener) {
yield event;
}
});
```
Correct:
```ts
t.procedure.subscription(async function* (opts) {
const iterable = on(ee, 'event', { signal: opts.signal }); // listen first
const history = await db.getEvents();
for (const item of history) {
yield tracked(item.id, item);
}
for await (const [event] of iterable) {
yield tracked(event.id, event);
}
});
```
If you fetch historical data before setting up the event listener, events emitted between the fetch and listener setup are lost.
Source: www/docs/server/subscriptions.md
### MEDIUM SSE ping interval >= client reconnect interval
Wrong:
```ts
initTRPC.create({
sse: {
ping: { enabled: true, intervalMs: 10000 },
client: { reconnectAfterInactivityMs: 5000 },
},
});
```
Correct:
```ts
initTRPC.create({
sse: {
ping: { enabled: true, intervalMs: 2000 },
client: { reconnectAfterInactivityMs: 5000 },
},
});
```
If the server ping interval is >= the client reconnect timeout, the client disconnects thinking the connection is dead before receiving a ping.
Source: packages/server/src/unstable-core-do-not-import/stream/sse.ts
### HIGH Sending custom headers with SSE without EventSource polyfill
Wrong:
```ts
httpSubscriptionLink({
url: 'http://localhost:3000',
// Native EventSource does not support custom headers
});
```
Correct:
```ts
import { EventSourcePolyfill } from 'event-source-polyfill';
httpSubscriptionLink({
url: 'http://localhost:3000',
EventSource: EventSourcePolyfill,
eventSourceOptions: async () => ({
headers: { authorization: 'Bearer token' },
}),
});
```
The native EventSource API does not support custom headers. Use an EventSource polyfill and pass it via the `EventSource` option on `httpSubscriptionLink`.
Source: www/docs/client/links/httpSubscriptionLink.md
### MEDIUM Choosing WebSocket when SSE would suffice
SSE (`httpSubscriptionLink`) is recommended for most subscription use cases. WebSockets add complexity (connection management, reconnection, keepalive, separate server process). Only use `wsLink` when bidirectional communication or WebSocket-specific features are required.
Source: maintainer interview
### MEDIUM WebSocket subscription stale inputs on reconnect
When a WebSocket reconnects, subscriptions re-send the original input parameters. There is no hook to re-evaluate inputs on reconnect, which can cause stale data. Consider using `tracked()` with `lastEventId` to mitigate this.
Source: https://github.com/trpc/trpc/issues/4122
## See Also
- **links** -- `splitLink`, `httpSubscriptionLink`, `wsLink`, `httpBatchLink`
- **auth** -- authenticating subscription connections (connectionParams, cookies, EventSource polyfill headers)
- **server-setup** -- `initTRPC.create()` SSE configuration options
- **adapter-fastify** -- WebSocket subscriptions via `@fastify/websocket` and `useWSS`

151
node_modules/@trpc/server/skills/trpc-router/SKILL.md generated vendored Normal file
View File

@@ -0,0 +1,151 @@
---
name: trpc-router
description: >
Entry point for all tRPC skills. Decision tree routing by task: initTRPC.create(),
t.router(), t.procedure, createTRPCClient, adapters, subscriptions, React Query,
Next.js, links, middleware, validators, error handling, caching, FormData.
type: core
library: trpc
library_version: '11.15.1'
requires: []
sources:
- 'trpc/trpc:www/docs/main/introduction.mdx'
- 'trpc/trpc:www/docs/main/quickstart.mdx'
---
# tRPC -- Skill Router
## Decision Tree
### What are you trying to do?
#### Define a tRPC backend (server)
- **Initialize tRPC, define routers, procedures, context, export AppRouter**
-> Load skill: `server-setup`
- **Add middleware (.use), auth guards, logging, base procedures**
-> Load skill: `middlewares`
- **Add input/output validation with Zod or other libraries**
-> Load skill: `validators`
- **Throw typed errors, format errors for clients, global error handling**
-> Load skill: `error-handling`
- **Call procedures from server code, write integration tests**
-> Load skill: `server-side-calls`
- **Set Cache-Control headers on query responses (CDN, browser caching)**
-> Load skill: `caching`
- **Accept FormData, File, Blob, or binary uploads in mutations**
-> Load skill: `non-json-content-types`
- **Set up real-time subscriptions (SSE or WebSocket)**
-> Load skill: `subscriptions`
#### Host the tRPC API (adapters)
- **Node.js built-in HTTP server (simplest, local dev)**
-> Load skill: `adapter-standalone`
- **Express middleware**
-> Load skill: `adapter-express`
- **Fastify plugin**
-> Load skill: `adapter-fastify`
- **AWS Lambda (API Gateway v1/v2, Function URLs)**
-> Load skill: `adapter-aws-lambda`
- **Fetch API / Edge (Cloudflare Workers, Deno, Vercel Edge, Astro, Remix)**
-> Load skill: `adapter-fetch`
#### Consume the tRPC API (client)
- **Create a vanilla TypeScript client, configure links, headers, types**
-> Load skill: `client-setup`
- **Configure link chain (batching, streaming, splitting, WebSocket, SSE)**
-> Load skill: `links`
- **Use SuperJSON transformer for Date, Map, Set, BigInt**
-> Load skill: `superjson`
#### Use tRPC with a framework
- **React with TanStack Query (useQuery, useMutation, queryOptions)**
-> Load skill: `react-query-setup`
- **Next.js App Router (RSC, server components, HydrateClient)**
-> Load skill: `nextjs-app-router`
- **Next.js Pages Router (withTRPC, SSR, SSG helpers)**
-> Load skill: `nextjs-pages-router`
#### Advanced patterns
- **Generate OpenAPI spec, REST client from tRPC router**
-> Load skill: `openapi`
- **Multi-service gateway, custom routing links, SOA**
-> Load skill: `service-oriented-architecture`
- **Auth middleware + client headers + subscription auth**
-> Load skill: `auth`
## Quick Reference: Minimal Working App
```ts
// server/trpc.ts
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
export const router = t.router;
export const publicProcedure = t.procedure;
```
```ts
// server/appRouter.ts
import { z } from 'zod';
import { publicProcedure, router } from './trpc';
export const appRouter = router({
hello: publicProcedure
.input(z.object({ name: z.string() }))
.query(({ input }) => ({ greeting: `Hello ${input.name}` })),
});
export type AppRouter = typeof appRouter;
```
```ts
// server/index.ts
import { createHTTPServer } from '@trpc/server/adapters/standalone';
import { appRouter } from './appRouter';
const server = createHTTPServer({ router: appRouter });
server.listen(3000);
```
```ts
// client/index.ts
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from '../server/appRouter';
const trpc = createTRPCClient<AppRouter>({
links: [httpBatchLink({ url: 'http://localhost:3000' })],
});
const result = await trpc.hello.query({ name: 'World' });
```
## See Also
- `server-setup` -- full server initialization details
- `client-setup` -- full client configuration
- `adapter-standalone` -- simplest adapter for getting started
- `react-query-setup` -- React integration
- `nextjs-app-router` -- Next.js App Router integration

228
node_modules/@trpc/server/skills/validators/SKILL.md generated vendored Normal file
View File

@@ -0,0 +1,228 @@
---
name: validators
description: >
Configure input and output validation with .input() and .output() using Zod,
Yup, Superstruct, ArkType, Valibot, Effect, or custom validator functions.
Chain multiple .input() calls to merge object schemas. Standard Schema protocol
support. Output validation returns INTERNAL_SERVER_ERROR on failure.
type: core
library: trpc
library_version: '11.15.1'
requires:
- server-setup
sources:
- 'trpc/trpc:www/docs/server/validators.md'
- 'trpc/trpc:packages/server/src/unstable-core-do-not-import/parser.ts'
---
# tRPC -- Validators
## Setup
```ts
// server/trpc.ts
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
export const router = t.router;
export const publicProcedure = t.procedure;
```
```ts
// server/appRouter.ts
import { z } from 'zod';
import { publicProcedure, router } from './trpc';
export const appRouter = router({
hello: publicProcedure
.input(z.object({ name: z.string() }))
.output(z.object({ greeting: z.string() }))
.query(({ input }) => {
return { greeting: `hello ${input.name}` };
}),
});
export type AppRouter = typeof appRouter;
```
## Core Patterns
### Input validation with Zod
```ts
import { z } from 'zod';
import { publicProcedure, router } from './trpc';
export const appRouter = router({
userById: publicProcedure.input(z.string()).query(({ input }) => {
return { id: input, name: 'Katt' };
}),
userCreate: publicProcedure
.input(z.object({ name: z.string(), email: z.string().email() }))
.mutation(({ input }) => {
return { id: '1', ...input };
}),
});
```
### Input chaining to merge object schemas
```ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
const t = initTRPC.create();
const baseProcedure = t.procedure
.input(z.object({ townName: z.string() }))
.use((opts) => {
console.log(`Request from: ${opts.input.townName}`);
return opts.next();
});
export const appRouter = t.router({
hello: baseProcedure
.input(z.object({ name: z.string() }))
.query(({ input }) => {
return { greeting: `Hello ${input.name}, from ${input.townName}` };
}),
});
```
Multiple `.input()` calls merge object properties; the final input type is `{ townName: string; name: string }`.
### Output validation
```ts
import { z } from 'zod';
import { publicProcedure, router } from './trpc';
export const appRouter = router({
hello: publicProcedure
.output(z.object({ greeting: z.string() }))
.query(() => {
return { greeting: 'hello world' };
}),
});
```
Output validation catches mismatches between your return type and the expected shape, useful for untrusted data sources.
### Custom validator function (no library)
```ts
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
export const appRouter = t.router({
hello: t.procedure
.input((value): string => {
if (typeof value === 'string') return value;
throw new Error('Input is not a string');
})
.output((value): string => {
if (typeof value === 'string') return value;
throw new Error('Output is not a string');
})
.query(({ input }) => {
return `hello ${input}`;
}),
});
```
## Common Mistakes
### [MEDIUM] Chaining non-object inputs
Wrong:
```ts
import { z } from 'zod';
import { publicProcedure } from './trpc';
const proc = publicProcedure.input(z.string()).input(z.number());
```
Correct:
```ts
import { z } from 'zod';
import { publicProcedure } from './trpc';
const proc = publicProcedure
.input(z.object({ name: z.string() }))
.input(z.object({ age: z.number() }));
```
Multiple `.input()` calls merge object properties; non-object schemas (string, number, array) cannot be merged and produce type errors.
Source: www/docs/server/validators.md
### [MEDIUM] Output validation failure returns 500
Wrong:
```ts
import { z } from 'zod';
import { publicProcedure } from './trpc';
const proc = publicProcedure
.output(z.object({ id: z.string() }))
.query(() => ({ id: 123 }));
```
Correct:
```ts
import { z } from 'zod';
import { publicProcedure } from './trpc';
const proc = publicProcedure
.output(z.object({ id: z.string() }))
.query(() => ({ id: '123' }));
```
If `.output()` validation fails, tRPC returns INTERNAL_SERVER_ERROR (500), not BAD_REQUEST, because the server produced invalid data.
Source: www/docs/server/validators.md
### [HIGH] Using cursor: z.optional() without nullable for infinite queries
Wrong:
```ts
import { z } from 'zod';
import { publicProcedure } from './trpc';
const proc = publicProcedure
.input(z.object({ cursor: z.string().optional() }))
.query(({ input }) => {
return { items: [], nextCursor: input.cursor };
});
```
Correct:
```ts
import { z } from 'zod';
import { publicProcedure } from './trpc';
const proc = publicProcedure
.input(z.object({ cursor: z.string().nullish() }))
.query(({ input }) => {
return { items: [], nextCursor: input.cursor };
});
```
React Query internally passes `cursor: undefined` during invalidation refetch; using `.optional()` without `.nullable()` can fail validation. Use `.nullish()` instead.
Source: https://github.com/trpc/trpc/issues/6862
## See Also
- `server-setup` -- initTRPC, routers, procedures
- `error-handling` -- how validation errors surface as BAD_REQUEST
- `error-handling` -- errorFormatter to expose Zod field errors
- `middlewares` -- use input chaining with middleware base procedures