{
  "schemaVersion": "1.0",
  "item": {
    "slug": "trpc-best-practices",
    "name": "tRPC Best Practices",
    "source": "tencent",
    "type": "skill",
    "category": "开发工具",
    "sourceUrl": "https://clawhub.ai/ifoster01/trpc-best-practices",
    "canonicalUrl": "https://clawhub.ai/ifoster01/trpc-best-practices",
    "targetPlatform": "OpenClaw"
  },
  "install": {
    "downloadMode": "redirect",
    "downloadUrl": "/downloads/trpc-best-practices",
    "sourceDownloadUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=trpc-best-practices",
    "sourcePlatform": "tencent",
    "targetPlatform": "OpenClaw",
    "installMethod": "Manual import",
    "extraction": "Extract archive",
    "prerequisites": [
      "OpenClaw"
    ],
    "packageFormat": "ZIP package",
    "includedAssets": [
      "SKILL.md"
    ],
    "primaryDoc": "SKILL.md",
    "quickSetup": [
      "Download the package from Yavira.",
      "Extract the archive and review SKILL.md first.",
      "Import or place the package into your OpenClaw setup."
    ],
    "agentAssist": {
      "summary": "Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.",
      "steps": [
        "Download the package from Yavira.",
        "Extract it into a folder your agent can access.",
        "Paste one of the prompts below and point your agent at the extracted folder."
      ],
      "prompts": [
        {
          "label": "New install",
          "body": "I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Tell me what you changed and call out any manual steps you could not complete."
        },
        {
          "label": "Upgrade existing",
          "body": "I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Summarize what changed and any follow-up checks I should run."
        }
      ]
    },
    "sourceHealth": {
      "source": "tencent",
      "status": "healthy",
      "reason": "direct_download_ok",
      "recommendedAction": "download",
      "checkedAt": "2026-05-07T17:22:31.273Z",
      "expiresAt": "2026-05-14T17:22:31.273Z",
      "httpStatus": 200,
      "finalUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=afrexai-annual-report",
      "contentType": "application/zip",
      "probeMethod": "head",
      "details": {
        "probeUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=afrexai-annual-report",
        "contentDisposition": "attachment; filename=\"afrexai-annual-report-1.0.0.zip\"",
        "redirectLocation": null,
        "bodySnippet": null
      },
      "scope": "source",
      "summary": "Source download looks usable.",
      "detail": "Yavira can redirect you to the upstream package for this source.",
      "primaryActionLabel": "Download for OpenClaw",
      "primaryActionHref": "/downloads/trpc-best-practices"
    },
    "validation": {
      "installChecklist": [
        "Use the Yavira download entry.",
        "Review SKILL.md after the package is downloaded.",
        "Confirm the extracted package contains the expected setup assets."
      ],
      "postInstallChecks": [
        "Confirm the extracted package includes the expected docs or setup files.",
        "Validate the skill or prompts are available in your target agent workspace.",
        "Capture any manual follow-up steps the agent could not complete."
      ]
    },
    "downloadPageUrl": "https://openagent3.xyz/downloads/trpc-best-practices",
    "agentPageUrl": "https://openagent3.xyz/skills/trpc-best-practices/agent",
    "manifestUrl": "https://openagent3.xyz/skills/trpc-best-practices/agent.json",
    "briefUrl": "https://openagent3.xyz/skills/trpc-best-practices/agent.md"
  },
  "agentAssist": {
    "summary": "Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.",
    "steps": [
      "Download the package from Yavira.",
      "Extract it into a folder your agent can access.",
      "Paste one of the prompts below and point your agent at the extracted folder."
    ],
    "prompts": [
      {
        "label": "New install",
        "body": "I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Tell me what you changed and call out any manual steps you could not complete."
      },
      {
        "label": "Upgrade existing",
        "body": "I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Summarize what changed and any follow-up checks I should run."
      }
    ]
  },
  "documentation": {
    "source": "clawhub",
    "primaryDoc": "SKILL.md",
    "sections": [
      {
        "title": "tRPC",
        "body": "Expert assistance with tRPC - End-to-end typesafe APIs with TypeScript."
      },
      {
        "title": "Overview",
        "body": "tRPC enables building fully typesafe APIs without schemas or code generation:\n\nFull TypeScript inference from server to client\nNo code generation needed\nExcellent DX with autocomplete and type safety\nWorks great with Next.js, React Query, and more"
      },
      {
        "title": "Installation",
        "body": "# Core packages\nnpm install @trpc/server@next @trpc/client@next @trpc/react-query@next\n\n# Peer dependencies\nnpm install @tanstack/react-query@latest zod"
      },
      {
        "title": "Basic Setup (Next.js App Router)",
        "body": "1. Create tRPC Router\n\n// server/trpc.ts\nimport { initTRPC } from '@trpc/server'\nimport { z } from 'zod'\n\nconst t = initTRPC.create()\n\nexport const router = t.router\nexport const publicProcedure = t.procedure\n\n2. Define API Router\n\n// server/routers/_app.ts\nimport { router, publicProcedure } from '../trpc'\nimport { z } from 'zod'\n\nexport const appRouter = router({\n  hello: publicProcedure\n    .input(z.object({ name: z.string() }))\n    .query(({ input }) => {\n      return { greeting: `Hello ${input.name}!` }\n    }),\n\n  createUser: publicProcedure\n    .input(z.object({\n      name: z.string(),\n      email: z.string().email(),\n    }))\n    .mutation(async ({ input }) => {\n      const user = await db.user.create({ data: input })\n      return user\n    }),\n})\n\nexport type AppRouter = typeof appRouter\n\n3. Create API Route\n\n// app/api/trpc/[trpc]/route.ts\nimport { fetchRequestHandler } from '@trpc/server/adapters/fetch'\nimport { appRouter } from '@/server/routers/_app'\n\nconst handler = (req: Request) =>\n  fetchRequestHandler({\n    endpoint: '/api/trpc',\n    req,\n    router: appRouter,\n    createContext: () => ({}),\n  })\n\nexport { handler as GET, handler as POST }\n\n4. Setup Client Provider\n\n// app/providers.tsx\n'use client'\n\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query'\nimport { httpBatchLink } from '@trpc/client'\nimport { useState } from 'react'\nimport { trpc } from '@/lib/trpc'\n\nexport function Providers({ children }: { children: React.ReactNode }) {\n  const [queryClient] = useState(() => new QueryClient())\n  const [trpcClient] = useState(() =>\n    trpc.createClient({\n      links: [\n        httpBatchLink({\n          url: 'http://localhost:3000/api/trpc',\n        }),\n      ],\n    })\n  )\n\n  return (\n    <trpc.Provider client={trpcClient} queryClient={queryClient}>\n      <QueryClientProvider client={queryClient}>\n        {children}\n      </QueryClientProvider>\n    </trpc.Provider>\n  )\n}\n\n5. Create tRPC Client\n\n// lib/trpc.ts\nimport { createTRPCReact } from '@trpc/react-query'\nimport type { AppRouter } from '@/server/routers/_app'\n\nexport const trpc = createTRPCReact<AppRouter>()\n\n6. Use in Components\n\n'use client'\n\nimport { trpc } from '@/lib/trpc'\n\nexport default function Home() {\n  const hello = trpc.hello.useQuery({ name: 'World' })\n  const createUser = trpc.createUser.useMutation()\n\n  return (\n    <div>\n      <p>{hello.data?.greeting}</p>\n      <button\n        onClick={() => createUser.mutate({\n          name: 'John',\n          email: 'john@example.com'\n        })}\n      >\n        Create User\n      </button>\n    </div>\n  )\n}"
      },
      {
        "title": "Basic Router",
        "body": "import { router, publicProcedure } from './trpc'\nimport { z } from 'zod'\n\nexport const userRouter = router({\n  // Query - for fetching data\n  getById: publicProcedure\n    .input(z.string())\n    .query(async ({ input }) => {\n      return await db.user.findUnique({ where: { id: input } })\n    }),\n\n  // Mutation - for creating/updating/deleting\n  create: publicProcedure\n    .input(z.object({\n      name: z.string(),\n      email: z.string().email(),\n    }))\n    .mutation(async ({ input }) => {\n      return await db.user.create({ data: input })\n    }),\n\n  // Subscription - for real-time updates\n  onUpdate: publicProcedure\n    .subscription(() => {\n      return observable<User>((emit) => {\n        // Implementation\n      })\n    }),\n})"
      },
      {
        "title": "Nested Routers",
        "body": "import { router } from './trpc'\nimport { userRouter } from './routers/user'\nimport { postRouter } from './routers/post'\nimport { commentRouter } from './routers/comment'\n\nexport const appRouter = router({\n  user: userRouter,\n  post: postRouter,\n  comment: commentRouter,\n})\n\n// Usage on client:\n// trpc.user.getById.useQuery('123')\n// trpc.post.list.useQuery()\n// trpc.comment.create.useMutation()"
      },
      {
        "title": "Merging Routers",
        "body": "import { router, publicProcedure } from './trpc'\n\nconst userRouter = router({\n  list: publicProcedure.query(() => {/* ... */}),\n  getById: publicProcedure.input(z.string()).query(() => {/* ... */}),\n})\n\nconst postRouter = router({\n  list: publicProcedure.query(() => {/* ... */}),\n  create: publicProcedure.input(z.object({})).mutation(() => {/* ... */}),\n})\n\n// Merge into app router\nexport const appRouter = router({\n  user: userRouter,\n  post: postRouter,\n})"
      },
      {
        "title": "Basic Validation",
        "body": "import { z } from 'zod'\n\nexport const userRouter = router({\n  create: publicProcedure\n    .input(z.object({\n      name: z.string().min(2).max(50),\n      email: z.string().email(),\n      age: z.number().int().positive().optional(),\n      role: z.enum(['user', 'admin']),\n    }))\n    .mutation(async ({ input }) => {\n      // input is fully typed!\n      return await db.user.create({ data: input })\n    }),\n})"
      },
      {
        "title": "Complex Validation",
        "body": "const createPostInput = z.object({\n  title: z.string().min(5).max(100),\n  content: z.string().min(10),\n  published: z.boolean().default(false),\n  tags: z.array(z.string()).min(1).max(5),\n  metadata: z.object({\n    views: z.number().default(0),\n    likes: z.number().default(0),\n  }).optional(),\n})\n\nexport const postRouter = router({\n  create: publicProcedure\n    .input(createPostInput)\n    .mutation(async ({ input }) => {\n      return await db.post.create({ data: input })\n    }),\n})"
      },
      {
        "title": "Reusable Schemas",
        "body": "// schemas/user.ts\nexport const userSchema = z.object({\n  id: z.string(),\n  name: z.string(),\n  email: z.string().email(),\n})\n\nexport const createUserSchema = userSchema.omit({ id: true })\nexport const updateUserSchema = userSchema.partial()\n\n// Use in router\nexport const userRouter = router({\n  create: publicProcedure\n    .input(createUserSchema)\n    .mutation(({ input }) => {/* ... */}),\n\n  update: publicProcedure\n    .input(z.object({\n      id: z.string(),\n      data: updateUserSchema,\n    }))\n    .mutation(({ input }) => {/* ... */}),\n})"
      },
      {
        "title": "Creating Context",
        "body": "// server/context.ts\nimport { inferAsyncReturnType } from '@trpc/server'\nimport { FetchCreateContextFnOptions } from '@trpc/server/adapters/fetch'\n\nexport async function createContext(opts: FetchCreateContextFnOptions) {\n  // Get session from cookies/headers\n  const session = await getSession(opts.req)\n\n  return {\n    session,\n    db,\n  }\n}\n\nexport type Context = inferAsyncReturnType<typeof createContext>"
      },
      {
        "title": "Using Context in tRPC",
        "body": "// server/trpc.ts\nimport { initTRPC } from '@trpc/server'\nimport { Context } from './context'\n\nconst t = initTRPC.context<Context>().create()\n\nexport const router = t.router\nexport const publicProcedure = t.procedure"
      },
      {
        "title": "Accessing Context in Procedures",
        "body": "export const userRouter = router({\n  me: publicProcedure.query(({ ctx }) => {\n    // ctx.session, ctx.db are available\n    if (!ctx.session) {\n      throw new TRPCError({ code: 'UNAUTHORIZED' })\n    }\n\n    return ctx.db.user.findUnique({\n      where: { id: ctx.session.userId }\n    })\n  }),\n})"
      },
      {
        "title": "Creating Middleware",
        "body": "// server/trpc.ts\nimport { initTRPC, TRPCError } from '@trpc/server'\n\nconst t = initTRPC.context<Context>().create()\n\n// Logging middleware\nconst loggerMiddleware = t.middleware(async ({ path, type, next }) => {\n  const start = Date.now()\n  const result = await next()\n  const duration = Date.now() - start\n\n  console.log(`${type} ${path} took ${duration}ms`)\n\n  return result\n})\n\n// Auth middleware\nconst isAuthed = t.middleware(({ ctx, next }) => {\n  if (!ctx.session) {\n    throw new TRPCError({ code: 'UNAUTHORIZED' })\n  }\n\n  return next({\n    ctx: {\n      // Infers session is non-nullable\n      session: ctx.session,\n    },\n  })\n})\n\n// Create procedures with middleware\nexport const publicProcedure = t.procedure.use(loggerMiddleware)\nexport const protectedProcedure = t.procedure.use(loggerMiddleware).use(isAuthed)"
      },
      {
        "title": "Using Protected Procedures",
        "body": "export const postRouter = router({\n  // Public - anyone can access\n  list: publicProcedure.query(() => {\n    return db.post.findMany({ where: { published: true } })\n  }),\n\n  // Protected - requires authentication\n  create: protectedProcedure\n    .input(z.object({ title: z.string() }))\n    .mutation(({ ctx, input }) => {\n      // ctx.session is guaranteed to exist\n      return db.post.create({\n        data: {\n          ...input,\n          authorId: ctx.session.userId,\n        },\n      })\n    }),\n})"
      },
      {
        "title": "Role-Based Middleware",
        "body": "const requireRole = (role: string) =>\n  t.middleware(({ ctx, next }) => {\n    if (!ctx.session || ctx.session.role !== role) {\n      throw new TRPCError({ code: 'FORBIDDEN' })\n    }\n    return next()\n  })\n\nexport const adminProcedure = protectedProcedure.use(requireRole('admin'))\n\nexport const userRouter = router({\n  delete: adminProcedure\n    .input(z.string())\n    .mutation(({ input }) => {\n      return db.user.delete({ where: { id: input } })\n    }),\n})"
      },
      {
        "title": "Queries",
        "body": "'use client'\n\nimport { trpc } from '@/lib/trpc'\n\nexport default function UserList() {\n  // Basic query\n  const users = trpc.user.list.useQuery()\n\n  // Query with input\n  const user = trpc.user.getById.useQuery('user-123')\n\n  // Disabled query\n  const profile = trpc.user.getProfile.useQuery(\n    { id: userId },\n    { enabled: !!userId }\n  )\n\n  // With options\n  const posts = trpc.post.list.useQuery(undefined, {\n    refetchInterval: 5000,\n    staleTime: 1000,\n  })\n\n  if (users.isLoading) return <div>Loading...</div>\n  if (users.error) return <div>Error: {users.error.message}</div>\n\n  return (\n    <ul>\n      {users.data?.map(user => (\n        <li key={user.id}>{user.name}</li>\n      ))}\n    </ul>\n  )\n}"
      },
      {
        "title": "Mutations",
        "body": "'use client'\n\nexport default function CreateUser() {\n  const utils = trpc.useContext()\n\n  const createUser = trpc.user.create.useMutation({\n    onSuccess: () => {\n      // Invalidate and refetch\n      utils.user.list.invalidate()\n    },\n    onError: (error) => {\n      console.error('Failed to create user:', error)\n    },\n  })\n\n  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {\n    e.preventDefault()\n    const formData = new FormData(e.currentTarget)\n\n    createUser.mutate({\n      name: formData.get('name') as string,\n      email: formData.get('email') as string,\n    })\n  }\n\n  return (\n    <form onSubmit={handleSubmit}>\n      <input name=\"name\" required />\n      <input name=\"email\" type=\"email\" required />\n      <button type=\"submit\" disabled={createUser.isLoading}>\n        {createUser.isLoading ? 'Creating...' : 'Create User'}\n      </button>\n    </form>\n  )\n}"
      },
      {
        "title": "Optimistic Updates",
        "body": "const updatePost = trpc.post.update.useMutation({\n  onMutate: async (newPost) => {\n    // Cancel outgoing refetches\n    await utils.post.list.cancel()\n\n    // Snapshot previous value\n    const previousPosts = utils.post.list.getData()\n\n    // Optimistically update\n    utils.post.list.setData(undefined, (old) =>\n      old?.map(post =>\n        post.id === newPost.id ? { ...post, ...newPost } : post\n      )\n    )\n\n    return { previousPosts }\n  },\n  onError: (err, newPost, context) => {\n    // Rollback on error\n    utils.post.list.setData(undefined, context?.previousPosts)\n  },\n  onSettled: () => {\n    // Refetch after success or error\n    utils.post.list.invalidate()\n  },\n})"
      },
      {
        "title": "Infinite Queries",
        "body": "// Server\nexport const postRouter = router({\n  list: publicProcedure\n    .input(z.object({\n      cursor: z.string().optional(),\n      limit: z.number().min(1).max(100).default(10),\n    }))\n    .query(async ({ input }) => {\n      const posts = await db.post.findMany({\n        take: input.limit + 1,\n        cursor: input.cursor ? { id: input.cursor } : undefined,\n      })\n\n      let nextCursor: string | undefined = undefined\n      if (posts.length > input.limit) {\n        const nextItem = posts.pop()\n        nextCursor = nextItem!.id\n      }\n\n      return { posts, nextCursor }\n    }),\n})\n\n// Client\nexport default function InfinitePosts() {\n  const posts = trpc.post.list.useInfiniteQuery(\n    { limit: 10 },\n    {\n      getNextPageParam: (lastPage) => lastPage.nextCursor,\n    }\n  )\n\n  return (\n    <div>\n      {posts.data?.pages.map((page, i) => (\n        <div key={i}>\n          {page.posts.map(post => (\n            <div key={post.id}>{post.title}</div>\n          ))}\n        </div>\n      ))}\n\n      <button\n        onClick={() => posts.fetchNextPage()}\n        disabled={!posts.hasNextPage || posts.isFetchingNextPage}\n      >\n        {posts.isFetchingNextPage ? 'Loading...' : 'Load More'}\n      </button>\n    </div>\n  )\n}"
      },
      {
        "title": "Server Errors",
        "body": "import { TRPCError } from '@trpc/server'\n\nexport const postRouter = router({\n  getById: publicProcedure\n    .input(z.string())\n    .query(async ({ input }) => {\n      const post = await db.post.findUnique({ where: { id: input } })\n\n      if (!post) {\n        throw new TRPCError({\n          code: 'NOT_FOUND',\n          message: 'Post not found',\n        })\n      }\n\n      return post\n    }),\n\n  create: protectedProcedure\n    .input(z.object({ title: z.string() }))\n    .mutation(async ({ ctx, input }) => {\n      if (!ctx.session.verified) {\n        throw new TRPCError({\n          code: 'FORBIDDEN',\n          message: 'Email must be verified',\n        })\n      }\n\n      try {\n        return await db.post.create({ data: input })\n      } catch (error) {\n        throw new TRPCError({\n          code: 'INTERNAL_SERVER_ERROR',\n          message: 'Failed to create post',\n          cause: error,\n        })\n      }\n    }),\n})"
      },
      {
        "title": "Error Codes",
        "body": "BAD_REQUEST - Invalid input\nUNAUTHORIZED - Not authenticated\nFORBIDDEN - Not authorized\nNOT_FOUND - Resource not found\nTIMEOUT - Request timeout\nCONFLICT - Resource conflict\nPRECONDITION_FAILED - Precondition check failed\nPAYLOAD_TOO_LARGE - Request too large\nMETHOD_NOT_SUPPORTED - HTTP method not supported\nTOO_MANY_REQUESTS - Rate limited\nCLIENT_CLOSED_REQUEST - Client closed request\nINTERNAL_SERVER_ERROR - Server error"
      },
      {
        "title": "Client Error Handling",
        "body": "const createPost = trpc.post.create.useMutation({\n  onError: (error) => {\n    if (error.data?.code === 'UNAUTHORIZED') {\n      router.push('/login')\n    } else if (error.data?.code === 'FORBIDDEN') {\n      alert('You do not have permission')\n    } else {\n      alert('Something went wrong')\n    }\n  },\n})"
      },
      {
        "title": "In Server Components",
        "body": "// app/users/page.tsx\nimport { createCaller } from '@/server/routers/_app'\nimport { createContext } from '@/server/context'\n\nexport default async function UsersPage() {\n  const ctx = await createContext({ req: {} as any })\n  const caller = createCaller(ctx)\n\n  const users = await caller.user.list()\n\n  return (\n    <ul>\n      {users.map(user => (\n        <li key={user.id}>{user.name}</li>\n      ))}\n    </ul>\n  )\n}"
      },
      {
        "title": "Create Caller",
        "body": "// server/routers/_app.ts\nexport const createCaller = createCallerFactory(appRouter)\n\n// Usage\nconst caller = createCaller(ctx)\nconst user = await caller.user.getById('123')"
      },
      {
        "title": "Request Batching",
        "body": "import { httpBatchLink } from '@trpc/client'\n\nconst trpcClient = trpc.createClient({\n  links: [\n    httpBatchLink({\n      url: '/api/trpc',\n      maxURLLength: 2083, // Reasonable limit\n    }),\n  ],\n})"
      },
      {
        "title": "Request Deduplication",
        "body": "Automatic with React Query - multiple components requesting same data will only make one request."
      },
      {
        "title": "Custom Headers",
        "body": "const trpcClient = trpc.createClient({\n  links: [\n    httpBatchLink({\n      url: '/api/trpc',\n      headers: () => {\n        return {\n          Authorization: `Bearer ${getToken()}`,\n        }\n      },\n    }),\n  ],\n})"
      },
      {
        "title": "Error Formatting",
        "body": "// server/trpc.ts\nconst t = initTRPC.context<Context>().create({\n  errorFormatter({ shape, error }) {\n    return {\n      ...shape,\n      data: {\n        ...shape.data,\n        zodError:\n          error.cause instanceof ZodError\n            ? error.cause.flatten()\n            : null,\n      },\n    }\n  },\n})"
      },
      {
        "title": "Testing Procedures",
        "body": "import { appRouter } from '@/server/routers/_app'\nimport { createCaller } from '@/server/routers/_app'\n\ndescribe('user router', () => {\n  it('creates user', async () => {\n    const ctx = { session: mockSession, db: mockDb }\n    const caller = createCaller(ctx)\n\n    const user = await caller.user.create({\n      name: 'John',\n      email: 'john@example.com',\n    })\n\n    expect(user.name).toBe('John')\n  })\n})"
      },
      {
        "title": "Best Practices",
        "body": "Use Zod for validation - Always validate inputs\nKeep procedures small - Single responsibility\nUse middleware for auth - Don't repeat auth checks\nType your context - Full type safety\nOrganize routers - Split into logical domains\nHandle errors properly - Use appropriate error codes\nLeverage React Query - Use its caching and refetching\nBatch requests - Enable batching for better performance\nUse optimistic updates - Better UX\nDocument procedures - Add JSDoc comments"
      },
      {
        "title": "Resources",
        "body": "Docs: https://trpc.io\nNext.js Guide: https://trpc.io/docs/nextjs\nExamples: https://github.com/trpc/trpc/tree/main/examples"
      }
    ],
    "body": "tRPC\n\nExpert assistance with tRPC - End-to-end typesafe APIs with TypeScript.\n\nOverview\n\ntRPC enables building fully typesafe APIs without schemas or code generation:\n\nFull TypeScript inference from server to client\nNo code generation needed\nExcellent DX with autocomplete and type safety\nWorks great with Next.js, React Query, and more\nQuick Start\nInstallation\n# Core packages\nnpm install @trpc/server@next @trpc/client@next @trpc/react-query@next\n\n# Peer dependencies\nnpm install @tanstack/react-query@latest zod\n\nBasic Setup (Next.js App Router)\n\n1. Create tRPC Router\n\n// server/trpc.ts\nimport { initTRPC } from '@trpc/server'\nimport { z } from 'zod'\n\nconst t = initTRPC.create()\n\nexport const router = t.router\nexport const publicProcedure = t.procedure\n\n\n2. Define API Router\n\n// server/routers/_app.ts\nimport { router, publicProcedure } from '../trpc'\nimport { z } from 'zod'\n\nexport const appRouter = router({\n  hello: publicProcedure\n    .input(z.object({ name: z.string() }))\n    .query(({ input }) => {\n      return { greeting: `Hello ${input.name}!` }\n    }),\n\n  createUser: publicProcedure\n    .input(z.object({\n      name: z.string(),\n      email: z.string().email(),\n    }))\n    .mutation(async ({ input }) => {\n      const user = await db.user.create({ data: input })\n      return user\n    }),\n})\n\nexport type AppRouter = typeof appRouter\n\n\n3. Create API Route\n\n// app/api/trpc/[trpc]/route.ts\nimport { fetchRequestHandler } from '@trpc/server/adapters/fetch'\nimport { appRouter } from '@/server/routers/_app'\n\nconst handler = (req: Request) =>\n  fetchRequestHandler({\n    endpoint: '/api/trpc',\n    req,\n    router: appRouter,\n    createContext: () => ({}),\n  })\n\nexport { handler as GET, handler as POST }\n\n\n4. Setup Client Provider\n\n// app/providers.tsx\n'use client'\n\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query'\nimport { httpBatchLink } from '@trpc/client'\nimport { useState } from 'react'\nimport { trpc } from '@/lib/trpc'\n\nexport function Providers({ children }: { children: React.ReactNode }) {\n  const [queryClient] = useState(() => new QueryClient())\n  const [trpcClient] = useState(() =>\n    trpc.createClient({\n      links: [\n        httpBatchLink({\n          url: 'http://localhost:3000/api/trpc',\n        }),\n      ],\n    })\n  )\n\n  return (\n    <trpc.Provider client={trpcClient} queryClient={queryClient}>\n      <QueryClientProvider client={queryClient}>\n        {children}\n      </QueryClientProvider>\n    </trpc.Provider>\n  )\n}\n\n\n5. Create tRPC Client\n\n// lib/trpc.ts\nimport { createTRPCReact } from '@trpc/react-query'\nimport type { AppRouter } from '@/server/routers/_app'\n\nexport const trpc = createTRPCReact<AppRouter>()\n\n\n6. Use in Components\n\n'use client'\n\nimport { trpc } from '@/lib/trpc'\n\nexport default function Home() {\n  const hello = trpc.hello.useQuery({ name: 'World' })\n  const createUser = trpc.createUser.useMutation()\n\n  return (\n    <div>\n      <p>{hello.data?.greeting}</p>\n      <button\n        onClick={() => createUser.mutate({\n          name: 'John',\n          email: 'john@example.com'\n        })}\n      >\n        Create User\n      </button>\n    </div>\n  )\n}\n\nRouter Definition\nBasic Router\nimport { router, publicProcedure } from './trpc'\nimport { z } from 'zod'\n\nexport const userRouter = router({\n  // Query - for fetching data\n  getById: publicProcedure\n    .input(z.string())\n    .query(async ({ input }) => {\n      return await db.user.findUnique({ where: { id: input } })\n    }),\n\n  // Mutation - for creating/updating/deleting\n  create: publicProcedure\n    .input(z.object({\n      name: z.string(),\n      email: z.string().email(),\n    }))\n    .mutation(async ({ input }) => {\n      return await db.user.create({ data: input })\n    }),\n\n  // Subscription - for real-time updates\n  onUpdate: publicProcedure\n    .subscription(() => {\n      return observable<User>((emit) => {\n        // Implementation\n      })\n    }),\n})\n\nNested Routers\nimport { router } from './trpc'\nimport { userRouter } from './routers/user'\nimport { postRouter } from './routers/post'\nimport { commentRouter } from './routers/comment'\n\nexport const appRouter = router({\n  user: userRouter,\n  post: postRouter,\n  comment: commentRouter,\n})\n\n// Usage on client:\n// trpc.user.getById.useQuery('123')\n// trpc.post.list.useQuery()\n// trpc.comment.create.useMutation()\n\nMerging Routers\nimport { router, publicProcedure } from './trpc'\n\nconst userRouter = router({\n  list: publicProcedure.query(() => {/* ... */}),\n  getById: publicProcedure.input(z.string()).query(() => {/* ... */}),\n})\n\nconst postRouter = router({\n  list: publicProcedure.query(() => {/* ... */}),\n  create: publicProcedure.input(z.object({})).mutation(() => {/* ... */}),\n})\n\n// Merge into app router\nexport const appRouter = router({\n  user: userRouter,\n  post: postRouter,\n})\n\nInput Validation with Zod\nBasic Validation\nimport { z } from 'zod'\n\nexport const userRouter = router({\n  create: publicProcedure\n    .input(z.object({\n      name: z.string().min(2).max(50),\n      email: z.string().email(),\n      age: z.number().int().positive().optional(),\n      role: z.enum(['user', 'admin']),\n    }))\n    .mutation(async ({ input }) => {\n      // input is fully typed!\n      return await db.user.create({ data: input })\n    }),\n})\n\nComplex Validation\nconst createPostInput = z.object({\n  title: z.string().min(5).max(100),\n  content: z.string().min(10),\n  published: z.boolean().default(false),\n  tags: z.array(z.string()).min(1).max(5),\n  metadata: z.object({\n    views: z.number().default(0),\n    likes: z.number().default(0),\n  }).optional(),\n})\n\nexport const postRouter = router({\n  create: publicProcedure\n    .input(createPostInput)\n    .mutation(async ({ input }) => {\n      return await db.post.create({ data: input })\n    }),\n})\n\nReusable Schemas\n// schemas/user.ts\nexport const userSchema = z.object({\n  id: z.string(),\n  name: z.string(),\n  email: z.string().email(),\n})\n\nexport const createUserSchema = userSchema.omit({ id: true })\nexport const updateUserSchema = userSchema.partial()\n\n// Use in router\nexport const userRouter = router({\n  create: publicProcedure\n    .input(createUserSchema)\n    .mutation(({ input }) => {/* ... */}),\n\n  update: publicProcedure\n    .input(z.object({\n      id: z.string(),\n      data: updateUserSchema,\n    }))\n    .mutation(({ input }) => {/* ... */}),\n})\n\nContext\nCreating Context\n// server/context.ts\nimport { inferAsyncReturnType } from '@trpc/server'\nimport { FetchCreateContextFnOptions } from '@trpc/server/adapters/fetch'\n\nexport async function createContext(opts: FetchCreateContextFnOptions) {\n  // Get session from cookies/headers\n  const session = await getSession(opts.req)\n\n  return {\n    session,\n    db,\n  }\n}\n\nexport type Context = inferAsyncReturnType<typeof createContext>\n\nUsing Context in tRPC\n// server/trpc.ts\nimport { initTRPC } from '@trpc/server'\nimport { Context } from './context'\n\nconst t = initTRPC.context<Context>().create()\n\nexport const router = t.router\nexport const publicProcedure = t.procedure\n\nAccessing Context in Procedures\nexport const userRouter = router({\n  me: publicProcedure.query(({ ctx }) => {\n    // ctx.session, ctx.db are available\n    if (!ctx.session) {\n      throw new TRPCError({ code: 'UNAUTHORIZED' })\n    }\n\n    return ctx.db.user.findUnique({\n      where: { id: ctx.session.userId }\n    })\n  }),\n})\n\nMiddleware\nCreating Middleware\n// server/trpc.ts\nimport { initTRPC, TRPCError } from '@trpc/server'\n\nconst t = initTRPC.context<Context>().create()\n\n// Logging middleware\nconst loggerMiddleware = t.middleware(async ({ path, type, next }) => {\n  const start = Date.now()\n  const result = await next()\n  const duration = Date.now() - start\n\n  console.log(`${type} ${path} took ${duration}ms`)\n\n  return result\n})\n\n// Auth middleware\nconst isAuthed = t.middleware(({ ctx, next }) => {\n  if (!ctx.session) {\n    throw new TRPCError({ code: 'UNAUTHORIZED' })\n  }\n\n  return next({\n    ctx: {\n      // Infers session is non-nullable\n      session: ctx.session,\n    },\n  })\n})\n\n// Create procedures with middleware\nexport const publicProcedure = t.procedure.use(loggerMiddleware)\nexport const protectedProcedure = t.procedure.use(loggerMiddleware).use(isAuthed)\n\nUsing Protected Procedures\nexport const postRouter = router({\n  // Public - anyone can access\n  list: publicProcedure.query(() => {\n    return db.post.findMany({ where: { published: true } })\n  }),\n\n  // Protected - requires authentication\n  create: protectedProcedure\n    .input(z.object({ title: z.string() }))\n    .mutation(({ ctx, input }) => {\n      // ctx.session is guaranteed to exist\n      return db.post.create({\n        data: {\n          ...input,\n          authorId: ctx.session.userId,\n        },\n      })\n    }),\n})\n\nRole-Based Middleware\nconst requireRole = (role: string) =>\n  t.middleware(({ ctx, next }) => {\n    if (!ctx.session || ctx.session.role !== role) {\n      throw new TRPCError({ code: 'FORBIDDEN' })\n    }\n    return next()\n  })\n\nexport const adminProcedure = protectedProcedure.use(requireRole('admin'))\n\nexport const userRouter = router({\n  delete: adminProcedure\n    .input(z.string())\n    .mutation(({ input }) => {\n      return db.user.delete({ where: { id: input } })\n    }),\n})\n\nClient Usage\nQueries\n'use client'\n\nimport { trpc } from '@/lib/trpc'\n\nexport default function UserList() {\n  // Basic query\n  const users = trpc.user.list.useQuery()\n\n  // Query with input\n  const user = trpc.user.getById.useQuery('user-123')\n\n  // Disabled query\n  const profile = trpc.user.getProfile.useQuery(\n    { id: userId },\n    { enabled: !!userId }\n  )\n\n  // With options\n  const posts = trpc.post.list.useQuery(undefined, {\n    refetchInterval: 5000,\n    staleTime: 1000,\n  })\n\n  if (users.isLoading) return <div>Loading...</div>\n  if (users.error) return <div>Error: {users.error.message}</div>\n\n  return (\n    <ul>\n      {users.data?.map(user => (\n        <li key={user.id}>{user.name}</li>\n      ))}\n    </ul>\n  )\n}\n\nMutations\n'use client'\n\nexport default function CreateUser() {\n  const utils = trpc.useContext()\n\n  const createUser = trpc.user.create.useMutation({\n    onSuccess: () => {\n      // Invalidate and refetch\n      utils.user.list.invalidate()\n    },\n    onError: (error) => {\n      console.error('Failed to create user:', error)\n    },\n  })\n\n  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {\n    e.preventDefault()\n    const formData = new FormData(e.currentTarget)\n\n    createUser.mutate({\n      name: formData.get('name') as string,\n      email: formData.get('email') as string,\n    })\n  }\n\n  return (\n    <form onSubmit={handleSubmit}>\n      <input name=\"name\" required />\n      <input name=\"email\" type=\"email\" required />\n      <button type=\"submit\" disabled={createUser.isLoading}>\n        {createUser.isLoading ? 'Creating...' : 'Create User'}\n      </button>\n    </form>\n  )\n}\n\nOptimistic Updates\nconst updatePost = trpc.post.update.useMutation({\n  onMutate: async (newPost) => {\n    // Cancel outgoing refetches\n    await utils.post.list.cancel()\n\n    // Snapshot previous value\n    const previousPosts = utils.post.list.getData()\n\n    // Optimistically update\n    utils.post.list.setData(undefined, (old) =>\n      old?.map(post =>\n        post.id === newPost.id ? { ...post, ...newPost } : post\n      )\n    )\n\n    return { previousPosts }\n  },\n  onError: (err, newPost, context) => {\n    // Rollback on error\n    utils.post.list.setData(undefined, context?.previousPosts)\n  },\n  onSettled: () => {\n    // Refetch after success or error\n    utils.post.list.invalidate()\n  },\n})\n\nInfinite Queries\n// Server\nexport const postRouter = router({\n  list: publicProcedure\n    .input(z.object({\n      cursor: z.string().optional(),\n      limit: z.number().min(1).max(100).default(10),\n    }))\n    .query(async ({ input }) => {\n      const posts = await db.post.findMany({\n        take: input.limit + 1,\n        cursor: input.cursor ? { id: input.cursor } : undefined,\n      })\n\n      let nextCursor: string | undefined = undefined\n      if (posts.length > input.limit) {\n        const nextItem = posts.pop()\n        nextCursor = nextItem!.id\n      }\n\n      return { posts, nextCursor }\n    }),\n})\n\n// Client\nexport default function InfinitePosts() {\n  const posts = trpc.post.list.useInfiniteQuery(\n    { limit: 10 },\n    {\n      getNextPageParam: (lastPage) => lastPage.nextCursor,\n    }\n  )\n\n  return (\n    <div>\n      {posts.data?.pages.map((page, i) => (\n        <div key={i}>\n          {page.posts.map(post => (\n            <div key={post.id}>{post.title}</div>\n          ))}\n        </div>\n      ))}\n\n      <button\n        onClick={() => posts.fetchNextPage()}\n        disabled={!posts.hasNextPage || posts.isFetchingNextPage}\n      >\n        {posts.isFetchingNextPage ? 'Loading...' : 'Load More'}\n      </button>\n    </div>\n  )\n}\n\nError Handling\nServer Errors\nimport { TRPCError } from '@trpc/server'\n\nexport const postRouter = router({\n  getById: publicProcedure\n    .input(z.string())\n    .query(async ({ input }) => {\n      const post = await db.post.findUnique({ where: { id: input } })\n\n      if (!post) {\n        throw new TRPCError({\n          code: 'NOT_FOUND',\n          message: 'Post not found',\n        })\n      }\n\n      return post\n    }),\n\n  create: protectedProcedure\n    .input(z.object({ title: z.string() }))\n    .mutation(async ({ ctx, input }) => {\n      if (!ctx.session.verified) {\n        throw new TRPCError({\n          code: 'FORBIDDEN',\n          message: 'Email must be verified',\n        })\n      }\n\n      try {\n        return await db.post.create({ data: input })\n      } catch (error) {\n        throw new TRPCError({\n          code: 'INTERNAL_SERVER_ERROR',\n          message: 'Failed to create post',\n          cause: error,\n        })\n      }\n    }),\n})\n\nError Codes\nBAD_REQUEST - Invalid input\nUNAUTHORIZED - Not authenticated\nFORBIDDEN - Not authorized\nNOT_FOUND - Resource not found\nTIMEOUT - Request timeout\nCONFLICT - Resource conflict\nPRECONDITION_FAILED - Precondition check failed\nPAYLOAD_TOO_LARGE - Request too large\nMETHOD_NOT_SUPPORTED - HTTP method not supported\nTOO_MANY_REQUESTS - Rate limited\nCLIENT_CLOSED_REQUEST - Client closed request\nINTERNAL_SERVER_ERROR - Server error\nClient Error Handling\nconst createPost = trpc.post.create.useMutation({\n  onError: (error) => {\n    if (error.data?.code === 'UNAUTHORIZED') {\n      router.push('/login')\n    } else if (error.data?.code === 'FORBIDDEN') {\n      alert('You do not have permission')\n    } else {\n      alert('Something went wrong')\n    }\n  },\n})\n\nServer-Side Calls\nIn Server Components\n// app/users/page.tsx\nimport { createCaller } from '@/server/routers/_app'\nimport { createContext } from '@/server/context'\n\nexport default async function UsersPage() {\n  const ctx = await createContext({ req: {} as any })\n  const caller = createCaller(ctx)\n\n  const users = await caller.user.list()\n\n  return (\n    <ul>\n      {users.map(user => (\n        <li key={user.id}>{user.name}</li>\n      ))}\n    </ul>\n  )\n}\n\nCreate Caller\n// server/routers/_app.ts\nexport const createCaller = createCallerFactory(appRouter)\n\n// Usage\nconst caller = createCaller(ctx)\nconst user = await caller.user.getById('123')\n\nAdvanced Patterns\nRequest Batching\nimport { httpBatchLink } from '@trpc/client'\n\nconst trpcClient = trpc.createClient({\n  links: [\n    httpBatchLink({\n      url: '/api/trpc',\n      maxURLLength: 2083, // Reasonable limit\n    }),\n  ],\n})\n\nRequest Deduplication\n\nAutomatic with React Query - multiple components requesting same data will only make one request.\n\nCustom Headers\nconst trpcClient = trpc.createClient({\n  links: [\n    httpBatchLink({\n      url: '/api/trpc',\n      headers: () => {\n        return {\n          Authorization: `Bearer ${getToken()}`,\n        }\n      },\n    }),\n  ],\n})\n\nError Formatting\n// server/trpc.ts\nconst t = initTRPC.context<Context>().create({\n  errorFormatter({ shape, error }) {\n    return {\n      ...shape,\n      data: {\n        ...shape.data,\n        zodError:\n          error.cause instanceof ZodError\n            ? error.cause.flatten()\n            : null,\n      },\n    }\n  },\n})\n\nTesting\nTesting Procedures\nimport { appRouter } from '@/server/routers/_app'\nimport { createCaller } from '@/server/routers/_app'\n\ndescribe('user router', () => {\n  it('creates user', async () => {\n    const ctx = { session: mockSession, db: mockDb }\n    const caller = createCaller(ctx)\n\n    const user = await caller.user.create({\n      name: 'John',\n      email: 'john@example.com',\n    })\n\n    expect(user.name).toBe('John')\n  })\n})\n\nBest Practices\nUse Zod for validation - Always validate inputs\nKeep procedures small - Single responsibility\nUse middleware for auth - Don't repeat auth checks\nType your context - Full type safety\nOrganize routers - Split into logical domains\nHandle errors properly - Use appropriate error codes\nLeverage React Query - Use its caching and refetching\nBatch requests - Enable batching for better performance\nUse optimistic updates - Better UX\nDocument procedures - Add JSDoc comments\nResources\nDocs: https://trpc.io\nNext.js Guide: https://trpc.io/docs/nextjs\nExamples: https://github.com/trpc/trpc/tree/main/examples"
  },
  "trust": {
    "sourceLabel": "tencent",
    "provenanceUrl": "https://clawhub.ai/ifoster01/trpc-best-practices",
    "publisherUrl": "https://clawhub.ai/ifoster01/trpc-best-practices",
    "owner": "ifoster01",
    "version": "1.0.0",
    "license": null,
    "verificationStatus": "Indexed source record"
  },
  "links": {
    "detailUrl": "https://openagent3.xyz/skills/trpc-best-practices",
    "downloadUrl": "https://openagent3.xyz/downloads/trpc-best-practices",
    "agentUrl": "https://openagent3.xyz/skills/trpc-best-practices/agent",
    "manifestUrl": "https://openagent3.xyz/skills/trpc-best-practices/agent.json",
    "briefUrl": "https://openagent3.xyz/skills/trpc-best-practices/agent.md"
  }
}