[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill-6ac130f1-3edc-48a2-a55e-f289422456cd":3,"$f4EYwK-SJbjv8NgHMHgg6YQ_viseKQbSo1Qwvc02_XWI":43},{"id":4,"title":5,"description":6,"categoryId":7,"moduleId":8,"tags":9,"prompt":10,"icon":11,"source":12,"sourceUrl":13,"authorId":14,"authorName":15,"isPublic":16,"stars":17,"runs":18,"createdAt":19,"updatedAt":19,"module":20,"category":27,"packages":34},"6ac130f1-3edc-48a2-a55e-f289422456cd","trpc-fullstack","使用 tRPC 构建端到端类型安全的 API —— 路由、过程、中间件、订阅以及 Next.js\u002FReact 集成模式。","cat_coding_frontend","mod_coding","sickn33,coding","---\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\n## Limitations\n- Use this skill only when the task clearly matches the scope described above.\n- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.\n- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.\n","","imported","https:\u002F\u002Fgithub.com\u002Fsickn33\u002Fantigravity-awesome-skills","user_system_seed","SkillOPIC",true,93,1280,"2026-05-16 13:44:50",{"id":8,"name":21,"slug":22,"icon":23,"description":24,"sort":25,"createdAt":26},"编程开发","coding","mdi-code-braces","代码生成、调试、审查，提升开发效率",2,"2026-05-16 12:53:40",{"id":7,"name":28,"slug":29,"icon":30,"description":31,"moduleId":8,"sort":32,"skillCount":33,"createdAt":26},"前端开发","frontend","mdi-language-html5","HTML\u002FCSS\u002FJavaScript\u002F框架相关",1,96,[35],{"id":36,"skillId":4,"version":37,"fileName":38,"fileSize":39,"filePath":40,"fileHash":41,"manifest":42,"createdAt":19},"08aec12a-0d16-4add-a6b9-0ac18eb4ede8","1.0.0","trpc-fullstack.zip",5769,"uploads\u002Fskills\u002F6ac130f1-3edc-48a2-a55e-f289422456cd\u002Ftrpc-fullstack.zip","e94d39c9314091a0dadd3d7134462b3fa16d3cd943d6ce5d0c81ee553b6e1ce9","[{\"path\":\"SKILL.md\",\"isDirectory\":false,\"size\":15863}]",{"code":44,"message":45,"data":46},200,"success",{"items":47,"stats":48,"page":51},[],{"averageRating":49,"totalRatings":49,"ratingCounts":50},0,[49,49,49,49,49],{"limit":52,"offset":49,"hasMore":53,"nextOffset":52,"ratedOnly":16},15,false]