Smithery Logo
MCPsSkillsDocsPricing
Login
Smithery Logo

Accelerating the Agent Economy

Resources

DocumentationPrivacy PolicySystem Status

Company

PricingAboutBlog

Connect

© 2026 Smithery. All rights reserved.

    olegakbarov

    trpc

    olegakbarov/trpc
    Coding
    2
    1 installs

    About

    SKILL.md

    Install

    Install via Skills CLI

    or add to your agent
    • Claude Code
      Claude Code
    • Codex
      Codex
    • OpenClaw
      OpenClaw
    • Cursor
      Cursor
    • Amp
      Amp
    • GitHub Copilot
      GitHub Copilot
    • Gemini CLI
      Gemini CLI
    • Kilo Code
      Kilo Code
    • Junie
      Junie
    • Replit
      Replit
    • Windsurf
      Windsurf
    • Cline
      Cline
    • Continue
      Continue
    • OpenCode
      OpenCode
    • OpenHands
      OpenHands
    • Roo Code
      Roo Code
    • Augment
      Augment
    • Goose
      Goose
    • Trae
      Trae
    • Zencoder
      Zencoder
    • Antigravity
      Antigravity
    ├─
    ├─
    └─

    About

    Expert guidance for tRPC v11 type-safe APIs, procedures, middleware, error handling, and TanStack Query integration...

    SKILL.md

    tRPC Skill

    Expert guidance for tRPC v11 with type-safe APIs, procedures, middleware patterns, and TanStack React Query integration.

    Quick Reference

    Project Structure

    src/
    ├── trpc/
    │   ├── trpc.ts          # tRPC initialization (t.router, t.procedure)
    │   ├── context.ts       # Request context type
    │   ├── router.ts        # Root router (merges all sub-routers)
    │   ├── users.ts         # User procedures
    │   └── posts.ts         # Post procedures
    ├── lib/
    │   └── trpc-client.ts   # React client setup
    └── server.ts            # tRPC adapter integration
    

    Server Setup

    Initialize tRPC (Once Per App)

    // src/trpc/trpc.ts
    import { initTRPC, TRPCError } from "@trpc/server"
    import type { Context } from "./context"
    
    const t = initTRPC.context<Context>().create()
    
    export const router = t.router
    export const procedure = t.procedure
    export const publicProcedure = t.procedure
    

    Define Context

    // src/trpc/context.ts
    export interface Context {
      user?: { id: string; role: "user" | "admin" }
      db: DatabaseClient
    }
    
    export function createContext({ req }: { req: Request }): Context {
      const token = req.headers.get("authorization")
      return {
        user: token ? verifyToken(token) : undefined,
        db: getDbClient(),
      }
    }
    

    Root Router

    // src/trpc/router.ts
    import { router } from "./trpc"
    import { usersRouter } from "./users"
    import { postsRouter } from "./posts"
    
    export const appRouter = router({
      users: usersRouter,
      posts: postsRouter,
    })
    
    export type AppRouter = typeof appRouter
    

    Procedures

    Query (Read Data)

    import { z } from "zod"
    import { router, publicProcedure } from "./trpc"
    
    export const usersRouter = router({
      // Simple query
      list: publicProcedure.query(async ({ ctx }) => {
        return ctx.db.users.findMany()
      }),
    
      // Query with input
      getById: publicProcedure
        .input(z.object({ id: z.string() }))
        .query(async ({ ctx, input }) => {
          const user = await ctx.db.users.findUnique({ where: { id: input.id } })
          if (!user) {
            throw new TRPCError({ code: "NOT_FOUND", message: "User not found" })
          }
          return user
        }),
    })
    

    Mutation (Write Data)

    export const postsRouter = router({
      create: publicProcedure
        .input(z.object({
          title: z.string().min(1).max(100),
          content: z.string(),
          published: z.boolean().default(false),
        }))
        .mutation(async ({ ctx, input }) => {
          return ctx.db.posts.create({ data: input })
        }),
    
      update: publicProcedure
        .input(z.object({
          id: z.string(),
          title: z.string().optional(),
          content: z.string().optional(),
        }))
        .mutation(async ({ ctx, input }) => {
          const { id, ...data } = input
          return ctx.db.posts.update({ where: { id }, data })
        }),
    
      delete: publicProcedure
        .input(z.object({ id: z.string() }))
        .mutation(async ({ ctx, input }) => {
          await ctx.db.posts.delete({ where: { id: input.id } })
          return { success: true }
        }),
    })
    

    Subscription (Real-time with SSE)

    import { observable } from "@trpc/server/observable"
    
    export const eventsRouter = router({
      onUpdate: publicProcedure
        .input(z.object({ channelId: z.string() }))
        .subscription(({ input }) => {
          return observable<{ message: string }>((emit) => {
            const handler = (data: { message: string }) => {
              emit.next(data)
            }
    
            eventEmitter.on(input.channelId, handler)
    
            return () => {
              eventEmitter.off(input.channelId, handler)
            }
          })
        }),
    })
    

    Middleware

    Base Procedure Pattern (Recommended)

    // src/trpc/trpc.ts
    import { initTRPC, TRPCError } from "@trpc/server"
    
    const t = initTRPC.context<Context>().create()
    
    // 1. Public procedure with logging
    const loggedProcedure = t.procedure.use(async ({ path, type, next }) => {
      const start = Date.now()
      const result = await next()
      console.log(`${type} ${path} - ${Date.now() - start}ms`)
      return result
    })
    
    // 2. Authenticated procedure (extends logged)
    export const authedProcedure = loggedProcedure.use(async ({ ctx, next }) => {
      if (!ctx.user) {
        throw new TRPCError({ code: "UNAUTHORIZED" })
      }
      return next({
        ctx: { ...ctx, user: ctx.user }, // user is now non-null
      })
    })
    
    // 3. Admin procedure (extends authed)
    export const adminProcedure = authedProcedure.use(async ({ ctx, next }) => {
      if (ctx.user.role !== "admin") {
        throw new TRPCError({ code: "FORBIDDEN", message: "Admin access required" })
      }
      return next({ ctx })
    })
    
    export const router = t.router
    export const publicProcedure = loggedProcedure
    

    Using Protected Procedures

    import { router, authedProcedure, adminProcedure } from "./trpc"
    
    export const adminRouter = router({
      // Requires authentication
      getProfile: authedProcedure.query(({ ctx }) => {
        return ctx.db.users.findUnique({ where: { id: ctx.user.id } })
      }),
    
      // Requires admin role
      deleteUser: adminProcedure
        .input(z.object({ userId: z.string() }))
        .mutation(({ ctx, input }) => {
          return ctx.db.users.delete({ where: { id: input.userId } })
        }),
    })
    

    Context Extension with Middleware

    // Add organization context
    const withOrganization = t.middleware(async ({ ctx, input, next }) => {
      const orgId = (input as { organizationId?: string })?.organizationId
      if (!orgId) {
        throw new TRPCError({ code: "BAD_REQUEST", message: "organizationId required" })
      }
    
      const org = await ctx.db.organizations.findUnique({ where: { id: orgId } })
      if (!org) {
        throw new TRPCError({ code: "NOT_FOUND", message: "Organization not found" })
      }
    
      return next({
        ctx: { ...ctx, organization: org },
      })
    })
    
    export const orgProcedure = authedProcedure.use(withOrganization)
    

    Error Handling

    TRPCError Codes

    Code HTTP Status Use Case
    BAD_REQUEST 400 Invalid input
    UNAUTHORIZED 401 Not logged in
    FORBIDDEN 403 Insufficient permissions
    NOT_FOUND 404 Resource doesn't exist
    CONFLICT 409 Resource conflict
    PRECONDITION_FAILED 412 Condition not met
    TOO_MANY_REQUESTS 429 Rate limited
    INTERNAL_SERVER_ERROR 500 Server error

    Throwing Errors

    import { TRPCError } from "@trpc/server"
    
    // Basic error
    throw new TRPCError({
      code: "NOT_FOUND",
      message: "Post not found",
    })
    
    // With cause (preserves stack trace)
    try {
      await riskyOperation()
    } catch (err) {
      throw new TRPCError({
        code: "INTERNAL_SERVER_ERROR",
        message: "Operation failed",
        cause: err,
      })
    }
    

    Global Error Handler

    // In server setup
    const handler = createHTTPHandler({
      router: appRouter,
      createContext,
      onError: ({ error, type, path, input, ctx }) => {
        console.error(`tRPC Error: ${type} ${path}`, {
          code: error.code,
          message: error.message,
          input,
          userId: ctx?.user?.id,
        })
    
        // Report to error tracking
        if (error.code === "INTERNAL_SERVER_ERROR") {
          reportError(error)
        }
      },
    })
    

    React Client Setup

    v11 New Pattern (Recommended)

    // src/lib/trpc-client.ts
    import { createTRPCContext } from "@trpc/tanstack-react-query"
    import { httpBatchLink } from "@trpc/client"
    import type { AppRouter } from "@/trpc/router"
    
    export const { TRPCProvider, useTRPC, useTRPCClient } = createTRPCContext<AppRouter>()
    
    export function createTRPCClient() {
      return {
        links: [
          httpBatchLink({
            url: "/api/trpc",
            headers: () => ({
              authorization: getAuthToken(),
            }),
          }),
        ],
      }
    }
    

    Provider Setup

    // src/main.tsx
    import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
    import { TRPCProvider, createTRPCClient } from "@/lib/trpc-client"
    
    const queryClient = new QueryClient()
    const trpcClient = createTRPCClient()
    
    function App() {
      return (
        <TRPCProvider client={trpcClient} queryClient={queryClient}>
          <QueryClientProvider client={queryClient}>
            <Router />
          </QueryClientProvider>
        </TRPCProvider>
      )
    }
    

    Classic Pattern (Still Supported)

    // src/lib/trpc-client.ts
    import { createTRPCReact } from "@trpc/react-query"
    import { httpBatchLink } from "@trpc/client"
    import type { AppRouter } from "@/trpc/router"
    
    export const trpc = createTRPCReact<AppRouter>()
    
    export function createTRPCClient() {
      return trpc.createClient({
        links: [
          httpBatchLink({
            url: "/api/trpc",
          }),
        ],
      })
    }
    

    React Usage

    v11 New Pattern

    import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
    import { useTRPC } from "@/lib/trpc-client"
    
    function UserProfile({ userId }: { userId: string }) {
      const trpc = useTRPC()
      const queryClient = useQueryClient()
    
      // Query
      const { data: user, isLoading } = useQuery(
        trpc.users.getById.queryOptions({ id: userId })
      )
    
      // Mutation with cache invalidation
      const updateUser = useMutation({
        ...trpc.users.update.mutationOptions(),
        onSuccess: () => {
          queryClient.invalidateQueries({ queryKey: trpc.users.getById.queryKey({ id: userId }) })
        },
      })
    
      if (isLoading) return <div>Loading...</div>
    
      return (
        <div>
          <h1>{user?.name}</h1>
          <button onClick={() => updateUser.mutate({ id: userId, name: "New Name" })}>
            Update
          </button>
        </div>
      )
    }
    

    Classic Pattern

    import { trpc } from "@/lib/trpc-client"
    
    function UserProfile({ userId }: { userId: string }) {
      const utils = trpc.useUtils()
    
      // Query
      const { data: user, isLoading } = trpc.users.getById.useQuery({ id: userId })
    
      // Mutation
      const updateUser = trpc.users.update.useMutation({
        onSuccess: () => {
          utils.users.getById.invalidate({ id: userId })
        },
      })
    
      if (isLoading) return <div>Loading...</div>
    
      return (
        <div>
          <h1>{user?.name}</h1>
          <button onClick={() => updateUser.mutate({ id: userId, name: "New Name" })}>
            Update
          </button>
        </div>
      )
    }
    

    Optimistic Updates

    const updatePost = useMutation({
      ...trpc.posts.update.mutationOptions(),
      onMutate: async (newData) => {
        await queryClient.cancelQueries({ queryKey: trpc.posts.getById.queryKey({ id: newData.id }) })
    
        const previous = queryClient.getQueryData(trpc.posts.getById.queryKey({ id: newData.id }))
    
        queryClient.setQueryData(
          trpc.posts.getById.queryKey({ id: newData.id }),
          (old) => ({ ...old, ...newData })
        )
    
        return { previous }
      },
      onError: (err, newData, context) => {
        queryClient.setQueryData(
          trpc.posts.getById.queryKey({ id: newData.id }),
          context?.previous
        )
      },
    })
    

    Input Validation with Zod

    Common Patterns

    import { z } from "zod"
    
    // Pagination
    const paginationInput = z.object({
      page: z.number().int().min(1).default(1),
      limit: z.number().int().min(1).max(100).default(20),
    })
    
    // Filters
    const filterInput = z.object({
      search: z.string().optional(),
      status: z.enum(["active", "inactive", "all"]).default("all"),
      sortBy: z.enum(["createdAt", "name", "updatedAt"]).default("createdAt"),
      sortOrder: z.enum(["asc", "desc"]).default("desc"),
    })
    
    // Combined
    export const listUsersInput = paginationInput.merge(filterInput)
    
    // Usage
    list: publicProcedure
      .input(listUsersInput)
      .query(async ({ ctx, input }) => {
        const { page, limit, search, status, sortBy, sortOrder } = input
        // ...
      }),
    

    Complex Validation

    const createUserInput = z.object({
      email: z.string().email(),
      password: z.string().min(8).regex(/[A-Z]/, "Must contain uppercase"),
      profile: z.object({
        name: z.string().min(1).max(100),
        bio: z.string().max(500).optional(),
        avatar: z.string().url().optional(),
      }),
      preferences: z.record(z.string(), z.unknown()).optional(),
    })
    

    Advanced Patterns

    Batch Requests

    // Client automatically batches requests made in same tick
    const [users, posts, settings] = await Promise.all([
      trpc.users.list.query(),
      trpc.posts.list.query(),
      trpc.settings.get.query(),
    ])
    // → Single HTTP request with all 3 procedures
    

    File Uploads (v11)

    // Server
    upload: publicProcedure
      .input(z.object({
        file: z.instanceof(File),
        metadata: z.object({ name: z.string() }),
      }))
      .mutation(async ({ input }) => {
        const buffer = await input.file.arrayBuffer()
        // Process file...
      }),
    
    // Client - use FormData link
    import { unstable_formDataLink } from "@trpc/client"
    

    Type Inference

    import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server"
    import type { AppRouter } from "@/trpc/router"
    
    type RouterInputs = inferRouterInputs<AppRouter>
    type RouterOutputs = inferRouterOutputs<AppRouter>
    
    // Usage
    type CreateUserInput = RouterInputs["users"]["create"]
    type User = RouterOutputs["users"]["getById"]
    

    Gotchas and Tips

    1. Export type only: Use export type { AppRouter } to prevent server code in client bundles
    2. Initialize once: Call initTRPC once per app, not per router file
    3. Base procedures: Create 2-3 reusable base procedures (public, authed, admin)
    4. Context flows through: Middleware can narrow context types for downstream procedures
    5. Zod schemas: Define input schemas at the top of files for reusability
    6. Error codes matter: Use appropriate codes for proper HTTP status mapping
    7. Batch by default: httpBatchLink batches requests automatically
    8. TypeScript 5.7+: Required for v11, enable strict mode

    Migration from v10 to v11

    v10 v11
    trpc.users.getById.useQuery() useQuery(trpc.users.getById.queryOptions())
    trpc.users.create.useMutation() useMutation(trpc.users.create.mutationOptions())
    utils.users.invalidate() queryClient.invalidateQueries({ queryKey: trpc.users.queryKey() })
    WebSocket subscriptions SSE subscriptions (recommended)
    Recommended Servers
    Cloudflare Workers Observability
    Cloudflare Workers Observability
    Prisma
    Prisma
    ThinAir Data
    ThinAir Data
    Repository
    olegakbarov/ispo-code
    Files