[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M717CC93623Z1JVMNENKMNPE":3,"skill-same:01M717CC93623Z1JVMNENKMNPE":79},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":26,"publishedAt":47,"linkOnly":48,"markdownContent":49,"markdownBody":50,"frontMatter":51,"refName":59,"commitSha":60,"skillDirectory":61,"forksCount":14,"repositoryHtmlUrl":62,"repositoryHomepageUrl":6,"ownerHtmlUrl":63,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":64,"install":65,"resources":78},"01M717CC93623Z1JVMNENKMNPE","trpc-fullstack",null,"Build end-to-end type-safe APIs with tRPC — routers, procedures, middleware, subscriptions, and Next.js\u002FReact integration patterns.","sapsapshen\u002Fai-memory-router","sapsapshen","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F20347959?v=4","01M6XDQEF1DP50DKF37H81J3J5","shenmian","01M6XDQEJPE41Y3DT2763X8ZJN",0,"MIT","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fcursor\u002Ftrpc-fullstack\u002FSKILL.md",[18,23],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000009","前端开发","SCENARIO","使用场景",{"categoryNo":24,"categoryName":25,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000010","后端与接口",[27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46],"trpc fullstack","trpc","fullstack","patterns","2026","middleware","type-safety","type","end","next","safe","community","Next.js","api","Safety","nextjs","framework","react","gemini","typescript","2026-11-10 10:12:30",false,"---\nname: trpc-fullstack\ndescription: \"Build end-to-end type-safe APIs with tRPC — routers, procedures, middleware, subscriptions, and Next.js\u002FReact integration patterns.\"\ncategory: framework\nrisk: none\nsource: community\ndate_added: \"2026-03-17\"\nauthor: suhaibjanjua\ntags: [typescript, trpc, api, fullstack, nextjs, react, type-safety]\ntools: [claude, cursor, gemini]\n---\n\n# tRPC Full-Stack\n\n## Overview\n\ntRPC lets you build fully type-safe APIs without writing a schema or code-generation step. Your TypeScript types flow from the server router directly to the client — so every API call is autocompleted, validated at compile time, and refactoring-safe. Use this skill when building TypeScript monorepos, Next.js apps, or any project where the server and client share a codebase.\n\n## When to Use This Skill\n\n- Use when building a TypeScript full-stack app (Next.js, Remix, Express + React) where the client and server share a single repo\n- Use when you want end-to-end type safety on API calls without REST\u002FGraphQL schema overhead\n- Use when adding real-time features (subscriptions) to an existing tRPC setup\n- Use when designing multi-step middleware (auth, rate limiting, tenant scoping) on tRPC procedures\n- Use when migrating an existing REST\u002FGraphQL API to tRPC incrementally\n\n## Core Concepts\n\n### Routers and Procedures\n\nA **router** groups related **procedures** (think: endpoints). Procedures are typed functions — `query` for reads, `mutation` for writes, `subscription` for real-time streams.\n\n### Input Validation with Zod\n\nAll procedure inputs are validated with Zod schemas. The validated, typed input is available in the procedure handler — no manual parsing.\n\n### Context\n\n`context` is shared state passed to every procedure — auth session, database client, request headers, etc. It is built once per request in a context factory. **Important:** Next.js App Router and Pages Router require separate context factories because App Router handlers receive a fetch `Request`, not a Node.js `NextApiRequest`.\n\n### Middleware\n\nMiddleware chains run before a procedure. Use them for authentication, logging, and request enrichment. They can extend the context for downstream procedures.\n\n---\n\n## How It Works\n\n### Step 1: Install and Initialize\n\n```bash\nnpm install @trpc\u002Fserver @trpc\u002Fclient @trpc\u002Freact-query @tanstack\u002Freact-query zod\n```\n\nCreate the tRPC instance and reusable builders:\n\n```typescript\n\u002F\u002F src\u002Fserver\u002Ftrpc.ts\nimport { initTRPC, TRPCError } from '@trpc\u002Fserver';\nimport { type Context } from '.\u002Fcontext';\nimport { ZodError } from 'zod';\n\nconst t = initTRPC.context\u003CContext>().create({\n  errorFormatter({ shape, error }) {\n    return {\n      ...shape,\n      data: {\n        ...shape.data,\n        zodError:\n          error.cause instanceof ZodError ? error.cause.flatten() : null,\n      },\n    };\n  },\n});\n\nexport const router = t.router;\nexport const publicProcedure = t.procedure;\nexport const middleware = t.middleware;\n```\n\n### Step 2: Define Two Context Factories\n\nNext.js App Router handlers receive a fetch `Request` (not a Node.js `NextApiRequest`), so the context\nmust be built differently depending on the call site. Define one factory per surface:\n\n```typescript\n\u002F\u002F src\u002Fserver\u002Fcontext.ts\nimport { type FetchCreateContextFnOptions } from '@trpc\u002Fserver\u002Fadapters\u002Ffetch';\nimport { auth } from '@\u002Fserver\u002Fauth'; \u002F\u002F Next-Auth v5 \u002F your auth helper\nimport { db } from '.\u002Fdb';\n\n\u002F**\n * Context for the HTTP handler (App Router Route Handler).\n * `opts.req` is the fetch Request — auth is resolved server-side via `auth()`.\n *\u002F\nexport async function createTRPCContext(opts: FetchCreateContextFnOptions) {\n  const session = await auth(); \u002F\u002F server-side auth — no req\u002Fres needed\n  return { session, db, headers: opts.req.headers };\n}\n\n\u002F**\n * Context for direct server-side callers (Server Components, RSC, cron jobs).\n * No HTTP request is involved, so we call auth() directly from the server.\n *\u002F\nexport async function createServerContext() {\n  const session = await auth();\n  return { session, db };\n}\n\nexport type Context = Awaited\u003CReturnType\u003Ctypeof createTRPCContext>>;\n```\n\n### Step 3: Build an Auth Middleware and Protected Procedure\n\n```typescript\n\u002F\u002F src\u002Fserver\u002Ftrpc.ts (continued)\nconst enforceAuth = middleware(({ ctx, next }) => {\n  if (!ctx.session?.user) {\n    throw new TRPCError({ code: 'UNAUTHORIZED' });\n  }\n  return next({\n    ctx: {\n      \u002F\u002F Narrows type: session is non-null from here\n      session: { ...ctx.session, user: ctx.session.user },\n    },\n  });\n});\n\nexport const protectedProcedure = t.procedure.use(enforceAuth);\n```\n\n### Step 4: Create Routers\n\n```typescript\n\u002F\u002F src\u002Fserver\u002Frouters\u002Fpost.ts\nimport { z } from 'zod';\nimport { router, publicProcedure, protectedProcedure } from '..\u002Ftrpc';\nimport { TRPCError } from '@trpc\u002Fserver';\n\nexport const postRouter = router({\n  list: publicProcedure\n    .input(\n      z.object({\n        limit: z.number().min(1).max(100).default(20),\n        cursor: z.string().optional(),\n      })\n    )\n    .query(async ({ ctx, input }) => {\n      const posts = await ctx.db.post.findMany({\n        take: input.limit + 1,\n        cursor: input.cursor ? { id: input.cursor } : undefined,\n        orderBy: { createdAt: 'desc' },\n      });\n      const nextCursor =\n        posts.length > input.limit ? posts.pop()!.id : undefined;\n      return { posts, nextCursor };\n    }),\n\n  byId: publicProcedure\n    .input(z.object({ id: z.string() }))\n    .query(async ({ ctx, input }) => {\n      const post = await ctx.db.post.findUnique({ where: { id: input.id } });\n      if (!post) throw new TRPCError({ code: 'NOT_FOUND' });\n      return post;\n    }),\n\n  create: protectedProcedure\n    .input(\n      z.object({\n        title: z.string().min(1).max(200),\n        body: z.string().min(1),\n      })\n    )\n    .mutation(async ({ ctx, input }) => {\n      return ctx.db.post.create({\n        data: { ...input, authorId: ctx.session.user.id },\n      });\n    }),\n\n  delete: protectedProcedure\n    .input(z.object({ id: z.string() }))\n    .mutation(async ({ ctx, input }) => {\n      const post = await ctx.db.post.findUnique({ where: { id: input.id } });\n      if (!post) throw new TRPCError({ code: 'NOT_FOUND' });\n      if (post.authorId !== ctx.session.user.id)\n        throw new TRPCError({ code: 'FORBIDDEN' });\n      return ctx.db.post.delete({ where: { id: input.id } });\n    }),\n});\n```\n\n### Step 5: Compose the Root Router and Export Types\n\n```typescript\n\u002F\u002F src\u002Fserver\u002Froot.ts\nimport { router } from '.\u002Ftrpc';\nimport { postRouter } from '.\u002Frouters\u002Fpost';\nimport { userRouter } from '.\u002Frouters\u002Fuser';\n\nexport const appRouter = router({\n  post: postRouter,\n  user: userRouter,\n});\n\n\u002F\u002F Export the type for the client — never import the appRouter itself on the client\nexport type AppRouter = typeof appRouter;\n```\n\n### Step 6: Mount the API Handler (Next.js App Router)\n\nThe App Router handler must use `fetchRequestHandler` and the **fetch-based** context factory.\n`createTRPCContext` receives `FetchCreateContextFnOptions` (with a fetch `Request`), not\na Pages Router `req\u002Fres` pair.\n\n```typescript\n\u002F\u002F src\u002Fapp\u002Fapi\u002Ftrpc\u002F[trpc]\u002Froute.ts\nimport { fetchRequestHandler } from '@trpc\u002Fserver\u002Fadapters\u002Ffetch';\nimport { type FetchCreateContextFnOptions } from '@trpc\u002Fserver\u002Fadapters\u002Ffetch';\nimport { appRouter } from '@\u002Fserver\u002Froot';\nimport { createTRPCContext } from '@\u002Fserver\u002Fcontext';\n\nconst handler = (req: Request) =>\n  fetchRequestHandler({\n    endpoint: '\u002Fapi\u002Ftrpc',\n    req,\n    router: appRouter,\n    \u002F\u002F opts is FetchCreateContextFnOptions — req is the fetch Request\n    createContext: (opts: FetchCreateContextFnOptions) => createTRPCContext(opts),\n  });\n\nexport { handler as GET, handler as POST };\n```\n\n### Step 7: Set Up the Client (React Query)\n\n```typescript\n\u002F\u002F src\u002Futils\u002Ftrpc.ts\nimport { createTRPCReact } from '@trpc\u002Freact-query';\nimport type { AppRouter } from '@\u002Fserver\u002Froot';\n\nexport const trpc = createTRPCReact\u003CAppRouter>();\n```\n\n```typescript\n\u002F\u002F src\u002Fapp\u002Fproviders.tsx\n'use client';\nimport { QueryClient, QueryClientProvider } from '@tanstack\u002Freact-query';\nimport { httpBatchLink } from '@trpc\u002Fclient';\nimport { useState } from 'react';\nimport { trpc } from '@\u002Futils\u002Ftrpc';\n\nexport function TRPCProvider({ children }: { children: React.ReactNode }) {\n  const [queryClient] = useState(() => new QueryClient());\n  const [trpcClient] = useState(() =>\n    trpc.createClient({\n      links: [\n        httpBatchLink({\n          url: '\u002Fapi\u002Ftrpc',\n          headers: () => ({ 'x-trpc-source': 'react' }),\n        }),\n      ],\n    })\n  );\n\n  return (\n    \u003Ctrpc.Provider client={trpcClient} queryClient={queryClient}>\n      \u003CQueryClientProvider client={queryClient}>{children}\u003C\u002FQueryClientProvider>\n    \u003C\u002Ftrpc.Provider>\n  );\n}\n```\n\n---\n\n## Examples\n\n### Example 1: Fetching Data in a Component\n\n```typescript\n\u002F\u002F components\u002FPostList.tsx\n'use client';\nimport { trpc } from '@\u002Futils\u002Ftrpc';\n\nexport function PostList() {\n  const { data, isLoading, error } = trpc.post.list.useQuery({ limit: 10 });\n\n  if (isLoading) return \u003Cp>Loading…\u003C\u002Fp>;\n  if (error) return \u003Cp>Error: {error.message}\u003C\u002Fp>;\n\n  return (\n    \u003Cul>\n      {data?.posts.map((post) => (\n        \u003Cli key={post.id}>{post.title}\u003C\u002Fli>\n      ))}\n    \u003C\u002Ful>\n  );\n}\n```\n\n### Example 2: Mutation with Cache Invalidation\n\n```typescript\n'use client';\nimport { trpc } from '@\u002Futils\u002Ftrpc';\n\nexport function CreatePost() {\n  const utils = trpc.useUtils();\n\n  const createPost = trpc.post.create.useMutation({\n    onSuccess: () => {\n      \u002F\u002F Invalidate and refetch the post list\n      utils.post.list.invalidate();\n    },\n  });\n\n  const handleSubmit = (e: React.FormEvent\u003CHTMLFormElement>) => {\n    e.preventDefault();\n    const form = e.currentTarget;\n    const data = new FormData(form);\n    createPost.mutate({\n      title: data.get('title') as string,\n      body: data.get('body') as string,\n    });\n    form.reset();\n  };\n\n  return (\n    \u003Cform onSubmit={handleSubmit}>\n      \u003Cinput name=\"title\" placeholder=\"Title\" required \u002F>\n      \u003Ctextarea name=\"body\" placeholder=\"Body\" required \u002F>\n      \u003Cbutton type=\"submit\" disabled={createPost.isPending}>\n        {createPost.isPending ? 'Creating…' : 'Create Post'}\n      \u003C\u002Fbutton>\n      {createPost.error && \u003Cp>{createPost.error.message}\u003C\u002Fp>}\n    \u003C\u002Fform>\n  );\n}\n```\n\n### Example 3: Server-Side Caller (Server Components \u002F SSR)\n\nUse `createServerContext` — the dedicated server-side factory — so that `auth()` is called\ncorrectly without needing a synthetic or empty request object:\n\n```typescript\n\u002F\u002F app\u002Fposts\u002Fpage.tsx (Next.js Server Component)\nimport { appRouter } from '@\u002Fserver\u002Froot';\nimport { createCallerFactory } from '@trpc\u002Fserver';\nimport { createServerContext } from '@\u002Fserver\u002Fcontext';\n\nconst createCaller = createCallerFactory(appRouter);\n\nexport default async function PostsPage() {\n  \u002F\u002F Uses createServerContext — calls auth() server-side, no req\u002Fres cast needed\n  const caller = createCaller(await createServerContext());\n  const { posts } = await caller.post.list({ limit: 20 });\n\n  return (\n    \u003Cul>\n      {posts.map((post) => (\n        \u003Cli key={post.id}>{post.title}\u003C\u002Fli>\n      ))}\n    \u003C\u002Ful>\n  );\n}\n```\n\n### Example 4: Real-Time Subscriptions (WebSocket)\n\n```typescript\n\u002F\u002F server\u002Frouters\u002Fnotifications.ts\nimport { observable } from '@trpc\u002Fserver\u002Fobservable';\nimport { EventEmitter } from 'events';\n\nconst ee = new EventEmitter();\n\nexport const notificationRouter = router({\n  onNew: protectedProcedure.subscription(({ ctx }) => {\n    return observable\u003C{ message: string; at: Date }>((emit) => {\n      const onNotification = (data: { message: string }) => {\n        emit.next({ message: data.message, at: new Date() });\n      };\n\n      const channel = `user:${ctx.session.user.id}`;\n      ee.on(channel, onNotification);\n      return () => ee.off(channel, onNotification);\n    });\n  }),\n});\n```\n\n```typescript\n\u002F\u002F Client usage — requires wsLink in the client config\ntrpc.notification.onNew.useSubscription(undefined, {\n  onData(data) {\n    toast(data.message);\n  },\n});\n```\n\n---\n\n## Best Practices\n\n- ✅ **Export only `AppRouter` type** from server code — never import `appRouter` on the client\n- ✅ **Use separate context factories** — `createTRPCContext` for the HTTP handler, `createServerContext` for Server Components and callers\n- ✅ **Validate all inputs with Zod** — never trust raw `input` without a schema\n- ✅ **Split routers by domain** (posts, users, billing) and merge in `root.ts`\n- ✅ **Extend context in middleware** rather than querying the DB multiple times per request\n- ✅ **Use `utils.invalidate()`** after mutations to keep the cache fresh\n- ❌ **Don't cast context with `as any`** to silence type errors — the mismatch will surface as a runtime failure when auth or session lookups return undefined\n- ❌ **Don't use `createContext({} as any)`** in Server Components — use `createServerContext()` which calls `auth()` directly\n- ❌ **Don't put business logic in the route handler** — keep it in the procedure or a service layer\n- ❌ **Don't share the tRPC client instance globally** — create it per-provider to avoid stale closures\n\n---\n\n## Security & Safety Notes\n\n- Always enforce authorization in `protectedProcedure` — never rely on client-side checks alone\n- Validate all input shapes with Zod, including pagination cursors and IDs, to prevent injection via malformed inputs\n- Avoid exposing internal error details to clients — use `TRPCError` with a public-safe `message` and keep stack traces server-side only\n- Rate-limit public procedures using middleware to prevent abuse\n\n---\n\n## Common Pitfalls\n\n- **Problem:** Auth session is `null` in protected procedures even when the user is logged in\n  **Solution:** Ensure `createTRPCContext` uses the correct server-side auth call (e.g. `auth()` from Next-Auth v5) and is not receiving a Pages Router `req\u002Fres` cast via `as any` in an App Router handler\n\n- **Problem:** Server Component caller fails for auth-dependent queries\n  **Solution:** Use `createServerContext()` (the dedicated server-side factory) instead of passing an empty or synthetic object to `createContext`\n\n- **Problem:** \"Type error: AppRouter is not assignable to AnyRouter\"\n  **Solution:** Import `AppRouter` as a `type` import (`import type { AppRouter }`) on the client, not the full module\n\n- **Problem:** Mutations not reflecting in the UI after success\n  **Solution:** Call `utils.\u003Crouter>.\u003Cprocedure>.invalidate()` in `onSuccess` to trigger a refetch via React Query\n\n- **Problem:** \"Cannot find module '@trpc\u002Fserver\u002Fadapters\u002Fnext'\" with App Router\n  **Solution:** Use `@trpc\u002Fserver\u002Fadapters\u002Ffetch` and `fetchRequestHandler` for the App Router; the `nextjs` adapter is for Pages Router only\n\n- **Problem:** Subscriptions not connecting\n  **Solution:** Subscriptions require `splitLink` — route subscriptions to `wsLink` and queries\u002Fmutations to `httpBatchLink`\n\n---\n\n## Related Skills\n\n- `@typescript-expert` — Deep TypeScript patterns used inside tRPC routers and generic utilities\n- `@react-patterns` — React hooks patterns that pair with `trpc.*.useQuery` and `useMutation`\n- `@test-driven-development` — Write procedure unit tests using `createCallerFactory` without an HTTP server\n- `@security-auditor` — Review tRPC middleware chains for auth bypass and input validation gaps\n\n## Additional Resources\n\n- [tRPC Official Docs](https:\u002F\u002Ftrpc.io\u002Fdocs)\n- [create-t3-app](https:\u002F\u002Fcreate.t3.gg) — Production Next.js starter with tRPC wired in\n- [tRPC GitHub](https:\u002F\u002Fgithub.com\u002Ftrpc\u002Ftrpc)\n- [TanStack Query Docs](https:\u002F\u002Ftanstack.com\u002Fquery\u002Flatest)\n","# tRPC Full-Stack\n\n## Overview\n\ntRPC lets you build fully type-safe APIs without writing a schema or code-generation step. Your TypeScript types flow from the server router directly to the client — so every API call is autocompleted, validated at compile time, and refactoring-safe. Use this skill when building TypeScript monorepos, Next.js apps, or any project where the server and client share a codebase.\n\n## When to Use This Skill\n\n- Use when building a TypeScript full-stack app (Next.js, Remix, Express + React) where the client and server share a single repo\n- Use when you want end-to-end type safety on API calls without REST\u002FGraphQL schema overhead\n- Use when adding real-time features (subscriptions) to an existing tRPC setup\n- Use when designing multi-step middleware (auth, rate limiting, tenant scoping) on tRPC procedures\n- Use when migrating an existing REST\u002FGraphQL API to tRPC incrementally\n\n## Core Concepts\n\n### Routers and Procedures\n\nA **router** groups related **procedures** (think: endpoints). Procedures are typed functions — `query` for reads, `mutation` for writes, `subscription` for real-time streams.\n\n### Input Validation with Zod\n\nAll procedure inputs are validated with Zod schemas. The validated, typed input is available in the procedure handler — no manual parsing.\n\n### Context\n\n`context` is shared state passed to every procedure — auth session, database client, request headers, etc. It is built once per request in a context factory. **Important:** Next.js App Router and Pages Router require separate context factories because App Router handlers receive a fetch `Request`, not a Node.js `NextApiRequest`.\n\n### Middleware\n\nMiddleware chains run before a procedure. Use them for authentication, logging, and request enrichment. They can extend the context for downstream procedures.\n\n---\n\n## How It Works\n\n### Step 1: Install and Initialize\n\n```bash\nnpm install @trpc\u002Fserver @trpc\u002Fclient @trpc\u002Freact-query @tanstack\u002Freact-query zod\n```\n\nCreate the tRPC instance and reusable builders:\n\n```typescript\n\u002F\u002F src\u002Fserver\u002Ftrpc.ts\nimport { initTRPC, TRPCError } from '@trpc\u002Fserver';\nimport { type Context } from '.\u002Fcontext';\nimport { ZodError } from 'zod';\n\nconst t = initTRPC.context\u003CContext>().create({\n  errorFormatter({ shape, error }) {\n    return {\n      ...shape,\n      data: {\n        ...shape.data,\n        zodError:\n          error.cause instanceof ZodError ? error.cause.flatten() : null,\n      },\n    };\n  },\n});\n\nexport const router = t.router;\nexport const publicProcedure = t.procedure;\nexport const middleware = t.middleware;\n```\n\n### Step 2: Define Two Context Factories\n\nNext.js App Router handlers receive a fetch `Request` (not a Node.js `NextApiRequest`), so the context\nmust be built differently depending on the call site. Define one factory per surface:\n\n```typescript\n\u002F\u002F src\u002Fserver\u002Fcontext.ts\nimport { type FetchCreateContextFnOptions } from '@trpc\u002Fserver\u002Fadapters\u002Ffetch';\nimport { auth } from '@\u002Fserver\u002Fauth'; \u002F\u002F Next-Auth v5 \u002F your auth helper\nimport { db } from '.\u002Fdb';\n\n\u002F**\n * Context for the HTTP handler (App Router Route Handler).\n * `opts.req` is the fetch Request — auth is resolved server-side via `auth()`.\n *\u002F\nexport async function createTRPCContext(opts: FetchCreateContextFnOptions) {\n  const session = await auth(); \u002F\u002F server-side auth — no req\u002Fres needed\n  return { session, db, headers: opts.req.headers };\n}\n\n\u002F**\n * Context for direct server-side callers (Server Components, RSC, cron jobs).\n * No HTTP request is involved, so we call auth() directly from the server.\n *\u002F\nexport async function createServerContext() {\n  const session = await auth();\n  return { session, db };\n}\n\nexport type Context = Awaited\u003CReturnType\u003Ctypeof createTRPCContext>>;\n```\n\n### Step 3: Build an Auth Middleware and Protected Procedure\n\n```typescript\n\u002F\u002F src\u002Fserver\u002Ftrpc.ts (continued)\nconst enforceAuth = middleware(({ ctx, next }) => {\n  if (!ctx.session?.user) {\n    throw new TRPCError({ code: 'UNAUTHORIZED' });\n  }\n  return next({\n    ctx: {\n      \u002F\u002F Narrows type: session is non-null from here\n      session: { ...ctx.session, user: ctx.session.user },\n    },\n  });\n});\n\nexport const protectedProcedure = t.procedure.use(enforceAuth);\n```\n\n### Step 4: Create Routers\n\n```typescript\n\u002F\u002F src\u002Fserver\u002Frouters\u002Fpost.ts\nimport { z } from 'zod';\nimport { router, publicProcedure, protectedProcedure } from '..\u002Ftrpc';\nimport { TRPCError } from '@trpc\u002Fserver';\n\nexport const postRouter = router({\n  list: publicProcedure\n    .input(\n      z.object({\n        limit: z.number().min(1).max(100).default(20),\n        cursor: z.string().optional(),\n      })\n    )\n    .query(async ({ ctx, input }) => {\n      const posts = await ctx.db.post.findMany({\n        take: input.limit + 1,\n        cursor: input.cursor ? { id: input.cursor } : undefined,\n        orderBy: { createdAt: 'desc' },\n      });\n      const nextCursor =\n        posts.length > input.limit ? posts.pop()!.id : undefined;\n      return { posts, nextCursor };\n    }),\n\n  byId: publicProcedure\n    .input(z.object({ id: z.string() }))\n    .query(async ({ ctx, input }) => {\n      const post = await ctx.db.post.findUnique({ where: { id: input.id } });\n      if (!post) throw new TRPCError({ code: 'NOT_FOUND' });\n      return post;\n    }),\n\n  create: protectedProcedure\n    .input(\n      z.object({\n        title: z.string().min(1).max(200),\n        body: z.string().min(1),\n      })\n    )\n    .mutation(async ({ ctx, input }) => {\n      return ctx.db.post.create({\n        data: { ...input, authorId: ctx.session.user.id },\n      });\n    }),\n\n  delete: protectedProcedure\n    .input(z.object({ id: z.string() }))\n    .mutation(async ({ ctx, input }) => {\n      const post = await ctx.db.post.findUnique({ where: { id: input.id } });\n      if (!post) throw new TRPCError({ code: 'NOT_FOUND' });\n      if (post.authorId !== ctx.session.user.id)\n        throw new TRPCError({ code: 'FORBIDDEN' });\n      return ctx.db.post.delete({ where: { id: input.id } });\n    }),\n});\n```\n\n### Step 5: Compose the Root Router and Export Types\n\n```typescript\n\u002F\u002F src\u002Fserver\u002Froot.ts\nimport { router } from '.\u002Ftrpc';\nimport { postRouter } from '.\u002Frouters\u002Fpost';\nimport { userRouter } from '.\u002Frouters\u002Fuser';\n\nexport const appRouter = router({\n  post: postRouter,\n  user: userRouter,\n});\n\n\u002F\u002F Export the type for the client — never import the appRouter itself on the client\nexport type AppRouter = typeof appRouter;\n```\n\n### Step 6: Mount the API Handler (Next.js App Router)\n\nThe App Router handler must use `fetchRequestHandler` and the **fetch-based** context factory.\n`createTRPCContext` receives `FetchCreateContextFnOptions` (with a fetch `Request`), not\na Pages Router `req\u002Fres` pair.\n\n```typescript\n\u002F\u002F src\u002Fapp\u002Fapi\u002Ftrpc\u002F[trpc]\u002Froute.ts\nimport { fetchRequestHandler } from '@trpc\u002Fserver\u002Fadapters\u002Ffetch';\nimport { type FetchCreateContextFnOptions } from '@trpc\u002Fserver\u002Fadapters\u002Ffetch';\nimport { appRouter } from '@\u002Fserver\u002Froot';\nimport { createTRPCContext } from '@\u002Fserver\u002Fcontext';\n\nconst handler = (req: Request) =>\n  fetchRequestHandler({\n    endpoint: '\u002Fapi\u002Ftrpc',\n    req,\n    router: appRouter,\n    \u002F\u002F opts is FetchCreateContextFnOptions — req is the fetch Request\n    createContext: (opts: FetchCreateContextFnOptions) => createTRPCContext(opts),\n  });\n\nexport { handler as GET, handler as POST };\n```\n\n### Step 7: Set Up the Client (React Query)\n\n```typescript\n\u002F\u002F src\u002Futils\u002Ftrpc.ts\nimport { createTRPCReact } from '@trpc\u002Freact-query';\nimport type { AppRouter } from '@\u002Fserver\u002Froot';\n\nexport const trpc = createTRPCReact\u003CAppRouter>();\n```\n\n```typescript\n\u002F\u002F src\u002Fapp\u002Fproviders.tsx\n'use client';\nimport { QueryClient, QueryClientProvider } from '@tanstack\u002Freact-query';\nimport { httpBatchLink } from '@trpc\u002Fclient';\nimport { useState } from 'react';\nimport { trpc } from '@\u002Futils\u002Ftrpc';\n\nexport function TRPCProvider({ children }: { children: React.ReactNode }) {\n  const [queryClient] = useState(() => new QueryClient());\n  const [trpcClient] = useState(() =>\n    trpc.createClient({\n      links: [\n        httpBatchLink({\n          url: '\u002Fapi\u002Ftrpc',\n          headers: () => ({ 'x-trpc-source': 'react' }),\n        }),\n      ],\n    })\n  );\n\n  return (\n    \u003Ctrpc.Provider client={trpcClient} queryClient={queryClient}>\n      \u003CQueryClientProvider client={queryClient}>{children}\u003C\u002FQueryClientProvider>\n    \u003C\u002Ftrpc.Provider>\n  );\n}\n```\n\n---\n\n## Examples\n\n### Example 1: Fetching Data in a Component\n\n```typescript\n\u002F\u002F components\u002FPostList.tsx\n'use client';\nimport { trpc } from '@\u002Futils\u002Ftrpc';\n\nexport function PostList() {\n  const { data, isLoading, error } = trpc.post.list.useQuery({ limit: 10 });\n\n  if (isLoading) return \u003Cp>Loading…\u003C\u002Fp>;\n  if (error) return \u003Cp>Error: {error.message}\u003C\u002Fp>;\n\n  return (\n    \u003Cul>\n      {data?.posts.map((post) => (\n        \u003Cli key={post.id}>{post.title}\u003C\u002Fli>\n      ))}\n    \u003C\u002Ful>\n  );\n}\n```\n\n### Example 2: Mutation with Cache Invalidation\n\n```typescript\n'use client';\nimport { trpc } from '@\u002Futils\u002Ftrpc';\n\nexport function CreatePost() {\n  const utils = trpc.useUtils();\n\n  const createPost = trpc.post.create.useMutation({\n    onSuccess: () => {\n      \u002F\u002F Invalidate and refetch the post list\n      utils.post.list.invalidate();\n    },\n  });\n\n  const handleSubmit = (e: React.FormEvent\u003CHTMLFormElement>) => {\n    e.preventDefault();\n    const form = e.currentTarget;\n    const data = new FormData(form);\n    createPost.mutate({\n      title: data.get('title') as string,\n      body: data.get('body') as string,\n    });\n    form.reset();\n  };\n\n  return (\n    \u003Cform onSubmit={handleSubmit}>\n      \u003Cinput name=\"title\" placeholder=\"Title\" required \u002F>\n      \u003Ctextarea name=\"body\" placeholder=\"Body\" required \u002F>\n      \u003Cbutton type=\"submit\" disabled={createPost.isPending}>\n        {createPost.isPending ? 'Creating…' : 'Create Post'}\n      \u003C\u002Fbutton>\n      {createPost.error && \u003Cp>{createPost.error.message}\u003C\u002Fp>}\n    \u003C\u002Fform>\n  );\n}\n```\n\n### Example 3: Server-Side Caller (Server Components \u002F SSR)\n\nUse `createServerContext` — the dedicated server-side factory — so that `auth()` is called\ncorrectly without needing a synthetic or empty request object:\n\n```typescript\n\u002F\u002F app\u002Fposts\u002Fpage.tsx (Next.js Server Component)\nimport { appRouter } from '@\u002Fserver\u002Froot';\nimport { createCallerFactory } from '@trpc\u002Fserver';\nimport { createServerContext } from '@\u002Fserver\u002Fcontext';\n\nconst createCaller = createCallerFactory(appRouter);\n\nexport default async function PostsPage() {\n  \u002F\u002F Uses createServerContext — calls auth() server-side, no req\u002Fres cast needed\n  const caller = createCaller(await createServerContext());\n  const { posts } = await caller.post.list({ limit: 20 });\n\n  return (\n    \u003Cul>\n      {posts.map((post) => (\n        \u003Cli key={post.id}>{post.title}\u003C\u002Fli>\n      ))}\n    \u003C\u002Ful>\n  );\n}\n```\n\n### Example 4: Real-Time Subscriptions (WebSocket)\n\n```typescript\n\u002F\u002F server\u002Frouters\u002Fnotifications.ts\nimport { observable } from '@trpc\u002Fserver\u002Fobservable';\nimport { EventEmitter } from 'events';\n\nconst ee = new EventEmitter();\n\nexport const notificationRouter = router({\n  onNew: protectedProcedure.subscription(({ ctx }) => {\n    return observable\u003C{ message: string; at: Date }>((emit) => {\n      const onNotification = (data: { message: string }) => {\n        emit.next({ message: data.message, at: new Date() });\n      };\n\n      const channel = `user:${ctx.session.user.id}`;\n      ee.on(channel, onNotification);\n      return () => ee.off(channel, onNotification);\n    });\n  }),\n});\n```\n\n```typescript\n\u002F\u002F Client usage — requires wsLink in the client config\ntrpc.notification.onNew.useSubscription(undefined, {\n  onData(data) {\n    toast(data.message);\n  },\n});\n```\n\n---\n\n## Best Practices\n\n- ✅ **Export only `AppRouter` type** from server code — never import `appRouter` on the client\n- ✅ **Use separate context factories** — `createTRPCContext` for the HTTP handler, `createServerContext` for Server Components and callers\n- ✅ **Validate all inputs with Zod** — never trust raw `input` without a schema\n- ✅ **Split routers by domain** (posts, users, billing) and merge in `root.ts`\n- ✅ **Extend context in middleware** rather than querying the DB multiple times per request\n- ✅ **Use `utils.invalidate()`** after mutations to keep the cache fresh\n- ❌ **Don't cast context with `as any`** to silence type errors — the mismatch will surface as a runtime failure when auth or session lookups return undefined\n- ❌ **Don't use `createContext({} as any)`** in Server Components — use `createServerContext()` which calls `auth()` directly\n- ❌ **Don't put business logic in the route handler** — keep it in the procedure or a service layer\n- ❌ **Don't share the tRPC client instance globally** — create it per-provider to avoid stale closures\n\n---\n\n## Security & Safety Notes\n\n- Always enforce authorization in `protectedProcedure` — never rely on client-side checks alone\n- Validate all input shapes with Zod, including pagination cursors and IDs, to prevent injection via malformed inputs\n- Avoid exposing internal error details to clients — use `TRPCError` with a public-safe `message` and keep stack traces server-side only\n- Rate-limit public procedures using middleware to prevent abuse\n\n---\n\n## Common Pitfalls\n\n- **Problem:** Auth session is `null` in protected procedures even when the user is logged in\n  **Solution:** Ensure `createTRPCContext` uses the correct server-side auth call (e.g. `auth()` from Next-Auth v5) and is not receiving a Pages Router `req\u002Fres` cast via `as any` in an App Router handler\n\n- **Problem:** Server Component caller fails for auth-dependent queries\n  **Solution:** Use `createServerContext()` (the dedicated server-side factory) instead of passing an empty or synthetic object to `createContext`\n\n- **Problem:** \"Type error: AppRouter is not assignable to AnyRouter\"\n  **Solution:** Import `AppRouter` as a `type` import (`import type { AppRouter }`) on the client, not the full module\n\n- **Problem:** Mutations not reflecting in the UI after success\n  **Solution:** Call `utils.\u003Crouter>.\u003Cprocedure>.invalidate()` in `onSuccess` to trigger a refetch via React Query\n\n- **Problem:** \"Cannot find module '@trpc\u002Fserver\u002Fadapters\u002Fnext'\" with App Router\n  **Solution:** Use `@trpc\u002Fserver\u002Fadapters\u002Ffetch` and `fetchRequestHandler` for the App Router; the `nextjs` adapter is for Pages Router only\n\n- **Problem:** Subscriptions not connecting\n  **Solution:** Subscriptions require `splitLink` — route subscriptions to `wsLink` and queries\u002Fmutations to `httpBatchLink`\n\n---\n\n## Related Skills\n\n- `@typescript-expert` — Deep TypeScript patterns used inside tRPC routers and generic utilities\n- `@react-patterns` — React hooks patterns that pair with `trpc.*.useQuery` and `useMutation`\n- `@test-driven-development` — Write procedure unit tests using `createCallerFactory` without an HTTP server\n- `@security-auditor` — Review tRPC middleware chains for auth bypass and input validation gaps\n\n## Additional Resources\n\n- [tRPC Official Docs](https:\u002F\u002Ftrpc.io\u002Fdocs)\n- [create-t3-app](https:\u002F\u002Fcreate.t3.gg) — Production Next.js starter with tRPC wired in\n- [tRPC GitHub](https:\u002F\u002Fgithub.com\u002Ftrpc\u002Ftrpc)\n- [TanStack Query Docs](https:\u002F\u002Ftanstack.com\u002Fquery\u002Flatest)\n",{"name":5,"risk":52,"tags":53,"tools":54,"author":57,"source":38,"category":43,"date_added":58,"description":7},"none",[46,28,40,29,42,44,33],[55,56,45],"claude","cursor","suhaibjanjua","2026-03-17","main","7a38d8e7073e61c16eda85681cb32fb68a8657a5","skills-store\u002Fcursor\u002Ftrpc-fullstack","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router","https:\u002F\u002Fgithub.com\u002Fsapsapshen","2026-04-14 21:12:12",{"source":8,"skillName":5,"commands":66,"prompt":76,"zipAvailable":77},[67,70,73],{"tool":68,"command":69},"npx","npx skills add sapsapshen\u002Fai-memory-router --skill trpc-fullstack",{"tool":71,"command":72},"bunx","bunx skills add sapsapshen\u002Fai-memory-router --skill trpc-fullstack",{"tool":74,"command":75},"pnpm","pnpm dlx skills add sapsapshen\u002Fai-memory-router --skill trpc-fullstack","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fcursor\u002Ftrpc-fullstack\u002FSKILL.md",true,[],[80,104,129,139,152,169],{"skillNo":81,"name":82,"nameZh":6,"description":83,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":84,"categories":85,"keywords":91,"publishedAt":47},"01M71ATQRKG48SH06FQ697T5G9","wechat-search","Search WeChat Official Account articles using OpenClaw's web search and fetch capabilities with compliance-focused design.","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fopenclaw\u002Fwechat-search-release\u002FSKILL.md",[86],{"categoryNo":87,"categoryName":88,"taxonomyType":89,"taxonomyTypeDesc":90},"01JBSEEDCAT000000000000037","律师与法务","PROFESSION","适用职业",[92,93,94,95,96,97,98,99,100,101,102,103],"wechat search","wechat","search","capabilities","openclaw","design","web search","compliance","web","official","fetch","with",{"skillNo":105,"name":106,"nameZh":6,"description":107,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":108,"categories":109,"keywords":110,"publishedAt":47},"01M71ATNAVGZB35PFYM2GC5N00","trend-tap","Real-time trending topics aggregator across 7 platforms (X\u002FTwitter, Reddit, Google Trends, Hacker News, Zhihu, Bilibili, Weibo). Trigger: when user says 'trend', 'trends', 'trend tap', '热点', '热搜', or asks what is trending\u002Fhot. Do NOT use web_search — this skill already fetches live data from all platforms with zero API keys.","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fopenclaw\u002Ftrend-tap\u002FSKILL.md",[],[111,112,113,114,115,116,94,117,118,119,120,121,122,123,124,125,126,127,128,40],"trend","trend tap","tap","api keys","Bash","time","trending","hacker","aggregator","do","when","brew","live","data","google","install","read","across",{"skillNo":130,"name":131,"nameZh":6,"description":132,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":133,"categories":134,"keywords":135,"publishedAt":47},"01M71ATJ9AM5KM15AA8PS302EB","trend-radar","Real-time trending topics aggregator across 7 platforms (X\u002FTwitter, Reddit, Google Trends, Hacker News, Zhihu, Bilibili, Weibo). Trigger: when user says 'trend', 'trends', 'trend pulse', '热点', '热搜', or asks what is trending\u002Fhot. Do NOT use web_search — this skill already fetches live data from all platforms with zero API keys.","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fopenclaw\u002Ftrend-radar\u002FSKILL.md",[],[136,137,111,122,123,124,125,115,94,127,138,128,40,121,119,118,117,126,116,114],"trend radar","radar","pulse",{"skillNo":140,"name":141,"nameZh":6,"description":142,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":143,"categories":144,"keywords":145,"publishedAt":47},"01M71ATGF095N3EJM9G8SD0BB5","tech-news-daily","每日科技新闻汇——从全球多个 RSS 源抓取最新科技新闻，翻译（英→中）、去重、排版成每日简报。当用户询问科技新闻、科技新鲜事、每日科技、技术动态、Daily Tech News 或类似表达时触发此技能。","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fopenclaw\u002Ftech-news-daily\u002FSKILL.md",[],[146,147,148,149,150,151],"news","tech","tech news daily","daily","rss","翻译",{"skillNo":153,"name":154,"nameZh":6,"description":155,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":156,"categories":157,"keywords":161,"publishedAt":47},"01M71ATFAEAMQ17M70NQNY7SR8","summarize","Summarize URLs or files with the summarize CLI (web, PDFs, images, audio, YouTube).","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fopenclaw\u002Fsummarize\u002FSKILL.md",[158],{"categoryNo":159,"categoryName":160,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000021","资料检索与研究",[154,100,162,163,164,103,122,165,166,167,113,168,126],"Audio","cli","youtube","images","https","files","pdfs",{"skillNo":170,"name":171,"nameZh":6,"description":172,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":173,"categories":174,"keywords":175,"publishedAt":47},"01M71ATDNS3R0M4H936PACQAWZ","sonoscli","Control Sonos speakers (discover\u002Fstatus\u002Fplay\u002Fvolume\u002Fgroup).","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fopenclaw\u002Fsonoscli\u002FSKILL.md",[],[171,176,177,178,126,179,166,180,181,182,183,184],"status","play","go","latest","github","discover","control","cmd","volume"]