KS
Killer-Skills

tRPC — how to use tRPC how to use tRPC, what is tRPC, tRPC alternative to GraphQL, tRPC vs REST, tRPC install guide, tRPC setup with Next.js, tRPC and React Query integration, TypeScript API development with tRPC

v1.0.0
GitHub

About this Skill

Perfect for TypeScript-based AI Agents needing end-to-end typesafe APIs without schemas or code generation. tRPC is an end-to-end typesafe API framework that enables building fully typesafe APIs without schemas or code generation using TypeScript.

Features

Full TypeScript inference from server to client
No code generation needed for API development
Excellent developer experience with autocomplete and type safety
Works seamlessly with Next.js, React Query, and other popular frameworks
Supports installation via npm with @trpc/server, @trpc/client, and @trpc/react-query packages

# Core Topics

dblodorn dblodorn
[0]
[0]
Updated: 3/6/2026

Quality Score

Top 5%
54
Excellent
Based on code quality & docs
Installation
SYS Universal Install (Auto-Detect)
Cursor IDE Windsurf IDE VS Code IDE
> npx killer-skills add dblodorn/dmbk-world/tRPC

Agent Capability Analysis

The tRPC MCP Server by dblodorn is an open-source Categories.community integration for Claude and other AI agents, enabling seamless task automation and capability expansion. Optimized for how to use tRPC, what is tRPC, tRPC alternative to GraphQL.

Ideal Agent Persona

Perfect for TypeScript-based AI Agents needing end-to-end typesafe APIs without schemas or code generation.

Core Value

Empowers agents to build fully typesafe APIs with excellent developer experience, utilizing autocomplete and type safety, seamlessly integrating with Next.js, React Query, and more, via @trpc/server, @trpc/client, and @trpc/react-query libraries.

Capabilities Granted for tRPC MCP Server

Building end-to-end typesafe APIs without code generation
Integrating with Next.js for robust frontend development
Utilizing React Query for efficient data fetching and caching

! Prerequisites & Limits

  • Requires TypeScript inference and compatibility
  • Dependent on @trpc/server, @trpc/client, and @trpc/react-query libraries
Project
SKILL.md
17.6 KB
.cursorrules
1.2 KB
package.json
240 B
Ready
UTF-8

# Tags

[No tags]
SKILL.md
Readonly

tRPC

Expert assistance with tRPC - End-to-end typesafe APIs with TypeScript.

Overview

tRPC enables building fully typesafe APIs without schemas or code generation:

  • Full TypeScript inference from server to client
  • No code generation needed
  • Excellent DX with autocomplete and type safety
  • Works great with Next.js, React Query, and more

Quick Start

Installation

bash
1# Core packages 2npm install @trpc/server@next @trpc/client@next @trpc/react-query@next 3 4# Peer dependencies 5npm install @tanstack/react-query@latest zod

Basic Setup (Next.js App Router)

1. Create tRPC Router

typescript
1// server/trpc.ts 2import { initTRPC } from '@trpc/server' 3import { z } from 'zod' 4 5const t = initTRPC.create() 6 7export const router = t.router 8export const publicProcedure = t.procedure

2. Define API Router

typescript
1// server/routers/_app.ts 2import { router, publicProcedure } from '../trpc' 3import { z } from 'zod' 4 5export const appRouter = router({ 6 hello: publicProcedure 7 .input(z.object({ name: z.string() })) 8 .query(({ input }) => { 9 return { greeting: `Hello ${input.name}!` } 10 }), 11 12 createUser: publicProcedure 13 .input(z.object({ 14 name: z.string(), 15 email: z.string().email(), 16 })) 17 .mutation(async ({ input }) => { 18 const user = await db.user.create({ data: input }) 19 return user 20 }), 21}) 22 23export type AppRouter = typeof appRouter

3. Create API Route

typescript
1// app/api/trpc/[trpc]/route.ts 2import { fetchRequestHandler } from '@trpc/server/adapters/fetch' 3import { appRouter } from '@/server/routers/_app' 4 5const handler = (req: Request) => 6 fetchRequestHandler({ 7 endpoint: '/api/trpc', 8 req, 9 router: appRouter, 10 createContext: () => ({}), 11 }) 12 13export { handler as GET, handler as POST }

4. Setup Client Provider

typescript
1// app/providers.tsx 2'use client' 3 4import { QueryClient, QueryClientProvider } from '@tanstack/react-query' 5import { httpBatchLink } from '@trpc/client' 6import { useState } from 'react' 7import { trpc } from '@/lib/trpc' 8 9export function Providers({ children }: { children: React.ReactNode }) { 10 const [queryClient] = useState(() => new QueryClient()) 11 const [trpcClient] = useState(() => 12 trpc.createClient({ 13 links: [ 14 httpBatchLink({ 15 url: 'http://localhost:3000/api/trpc', 16 }), 17 ], 18 }) 19 ) 20 21 return ( 22 <trpc.Provider client={trpcClient} queryClient={queryClient}> 23 <QueryClientProvider client={queryClient}> 24 {children} 25 </QueryClientProvider> 26 </trpc.Provider> 27 ) 28}

5. Create tRPC Client

typescript
1// lib/trpc.ts 2import { createTRPCReact } from '@trpc/react-query' 3import type { AppRouter } from '@/server/routers/_app' 4 5export const trpc = createTRPCReact<AppRouter>()

6. Use in Components

typescript
1'use client' 2 3import { trpc } from '@/lib/trpc' 4 5export default function Home() { 6 const hello = trpc.hello.useQuery({ name: 'World' }) 7 const createUser = trpc.createUser.useMutation() 8 9 return ( 10 <div> 11 <p>{hello.data?.greeting}</p> 12 <button 13 onClick={() => createUser.mutate({ 14 name: 'John', 15 email: 'john@example.com' 16 })} 17 > 18 Create User 19 </button> 20 </div> 21 ) 22}

Router Definition

Basic Router

typescript
1import { router, publicProcedure } from './trpc' 2import { z } from 'zod' 3 4export const userRouter = router({ 5 // Query - for fetching data 6 getById: publicProcedure 7 .input(z.string()) 8 .query(async ({ input }) => { 9 return await db.user.findUnique({ where: { id: input } }) 10 }), 11 12 // Mutation - for creating/updating/deleting 13 create: publicProcedure 14 .input(z.object({ 15 name: z.string(), 16 email: z.string().email(), 17 })) 18 .mutation(async ({ input }) => { 19 return await db.user.create({ data: input }) 20 }), 21 22 // Subscription - for real-time updates 23 onUpdate: publicProcedure 24 .subscription(() => { 25 return observable<User>((emit) => { 26 // Implementation 27 }) 28 }), 29})

Nested Routers

typescript
1import { router } from './trpc' 2import { userRouter } from './routers/user' 3import { postRouter } from './routers/post' 4import { commentRouter } from './routers/comment' 5 6export const appRouter = router({ 7 user: userRouter, 8 post: postRouter, 9 comment: commentRouter, 10}) 11 12// Usage on client: 13// trpc.user.getById.useQuery('123') 14// trpc.post.list.useQuery() 15// trpc.comment.create.useMutation()

Merging Routers

typescript
1import { router, publicProcedure } from './trpc' 2 3const userRouter = router({ 4 list: publicProcedure.query(() => {/* ... */}), 5 getById: publicProcedure.input(z.string()).query(() => {/* ... */}), 6}) 7 8const postRouter = router({ 9 list: publicProcedure.query(() => {/* ... */}), 10 create: publicProcedure.input(z.object({})).mutation(() => {/* ... */}), 11}) 12 13// Merge into app router 14export const appRouter = router({ 15 user: userRouter, 16 post: postRouter, 17})

Input Validation with Zod

Basic Validation

typescript
1import { z } from 'zod' 2 3export const userRouter = router({ 4 create: publicProcedure 5 .input(z.object({ 6 name: z.string().min(2).max(50), 7 email: z.string().email(), 8 age: z.number().int().positive().optional(), 9 role: z.enum(['user', 'admin']), 10 })) 11 .mutation(async ({ input }) => { 12 // input is fully typed! 13 return await db.user.create({ data: input }) 14 }), 15})

Complex Validation

typescript
1const createPostInput = z.object({ 2 title: z.string().min(5).max(100), 3 content: z.string().min(10), 4 published: z.boolean().default(false), 5 tags: z.array(z.string()).min(1).max(5), 6 metadata: z.object({ 7 views: z.number().default(0), 8 likes: z.number().default(0), 9 }).optional(), 10}) 11 12export const postRouter = router({ 13 create: publicProcedure 14 .input(createPostInput) 15 .mutation(async ({ input }) => { 16 return await db.post.create({ data: input }) 17 }), 18})

Reusable Schemas

typescript
1// schemas/user.ts 2export const userSchema = z.object({ 3 id: z.string(), 4 name: z.string(), 5 email: z.string().email(), 6}) 7 8export const createUserSchema = userSchema.omit({ id: true }) 9export const updateUserSchema = userSchema.partial() 10 11// Use in router 12export const userRouter = router({ 13 create: publicProcedure 14 .input(createUserSchema) 15 .mutation(({ input }) => {/* ... */}), 16 17 update: publicProcedure 18 .input(z.object({ 19 id: z.string(), 20 data: updateUserSchema, 21 })) 22 .mutation(({ input }) => {/* ... */}), 23})

Context

Creating Context

typescript
1// server/context.ts 2import { inferAsyncReturnType } from '@trpc/server' 3import { FetchCreateContextFnOptions } from '@trpc/server/adapters/fetch' 4 5export async function createContext(opts: FetchCreateContextFnOptions) { 6 // Get session from cookies/headers 7 const session = await getSession(opts.req) 8 9 return { 10 session, 11 db, 12 } 13} 14 15export type Context = inferAsyncReturnType<typeof createContext>

Using Context in tRPC

typescript
1// server/trpc.ts 2import { initTRPC } from '@trpc/server' 3import { Context } from './context' 4 5const t = initTRPC.context<Context>().create() 6 7export const router = t.router 8export const publicProcedure = t.procedure

Accessing Context in Procedures

typescript
1export const userRouter = router({ 2 me: publicProcedure.query(({ ctx }) => { 3 // ctx.session, ctx.db are available 4 if (!ctx.session) { 5 throw new TRPCError({ code: 'UNAUTHORIZED' }) 6 } 7 8 return ctx.db.user.findUnique({ 9 where: { id: ctx.session.userId } 10 }) 11 }), 12})

Middleware

Creating Middleware

typescript
1// server/trpc.ts 2import { initTRPC, TRPCError } from '@trpc/server' 3 4const t = initTRPC.context<Context>().create() 5 6// Logging middleware 7const loggerMiddleware = t.middleware(async ({ path, type, next }) => { 8 const start = Date.now() 9 const result = await next() 10 const duration = Date.now() - start 11 12 console.log(`${type} ${path} took ${duration}ms`) 13 14 return result 15}) 16 17// Auth middleware 18const isAuthed = t.middleware(({ ctx, next }) => { 19 if (!ctx.session) { 20 throw new TRPCError({ code: 'UNAUTHORIZED' }) 21 } 22 23 return next({ 24 ctx: { 25 // Infers session is non-nullable 26 session: ctx.session, 27 }, 28 }) 29}) 30 31// Create procedures with middleware 32export const publicProcedure = t.procedure.use(loggerMiddleware) 33export const protectedProcedure = t.procedure.use(loggerMiddleware).use(isAuthed)

Using Protected Procedures

typescript
1export const postRouter = router({ 2 // Public - anyone can access 3 list: publicProcedure.query(() => { 4 return db.post.findMany({ where: { published: true } }) 5 }), 6 7 // Protected - requires authentication 8 create: protectedProcedure 9 .input(z.object({ title: z.string() })) 10 .mutation(({ ctx, input }) => { 11 // ctx.session is guaranteed to exist 12 return db.post.create({ 13 data: { 14 ...input, 15 authorId: ctx.session.userId, 16 }, 17 }) 18 }), 19})

Role-Based Middleware

typescript
1const requireRole = (role: string) => 2 t.middleware(({ ctx, next }) => { 3 if (!ctx.session || ctx.session.role !== role) { 4 throw new TRPCError({ code: 'FORBIDDEN' }) 5 } 6 return next() 7 }) 8 9export const adminProcedure = protectedProcedure.use(requireRole('admin')) 10 11export const userRouter = router({ 12 delete: adminProcedure 13 .input(z.string()) 14 .mutation(({ input }) => { 15 return db.user.delete({ where: { id: input } }) 16 }), 17})

Client Usage

Queries

typescript
1'use client' 2 3import { trpc } from '@/lib/trpc' 4 5export default function UserList() { 6 // Basic query 7 const users = trpc.user.list.useQuery() 8 9 // Query with input 10 const user = trpc.user.getById.useQuery('user-123') 11 12 // Disabled query 13 const profile = trpc.user.getProfile.useQuery( 14 { id: userId }, 15 { enabled: !!userId } 16 ) 17 18 // With options 19 const posts = trpc.post.list.useQuery(undefined, { 20 refetchInterval: 5000, 21 staleTime: 1000, 22 }) 23 24 if (users.isLoading) return <div>Loading...</div> 25 if (users.error) return <div>Error: {users.error.message}</div> 26 27 return ( 28 <ul> 29 {users.data?.map(user => ( 30 <li key={user.id}>{user.name}</li> 31 ))} 32 </ul> 33 ) 34}

Mutations

typescript
1'use client' 2 3export default function CreateUser() { 4 const utils = trpc.useContext() 5 6 const createUser = trpc.user.create.useMutation({ 7 onSuccess: () => { 8 // Invalidate and refetch 9 utils.user.list.invalidate() 10 }, 11 onError: (error) => { 12 console.error('Failed to create user:', error) 13 }, 14 }) 15 16 const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { 17 e.preventDefault() 18 const formData = new FormData(e.currentTarget) 19 20 createUser.mutate({ 21 name: formData.get('name') as string, 22 email: formData.get('email') as string, 23 }) 24 } 25 26 return ( 27 <form onSubmit={handleSubmit}> 28 <input name="name" required /> 29 <input name="email" type="email" required /> 30 <button type="submit" disabled={createUser.isLoading}> 31 {createUser.isLoading ? 'Creating...' : 'Create User'} 32 </button> 33 </form> 34 ) 35}

Optimistic Updates

typescript
1const updatePost = trpc.post.update.useMutation({ 2 onMutate: async (newPost) => { 3 // Cancel outgoing refetches 4 await utils.post.list.cancel() 5 6 // Snapshot previous value 7 const previousPosts = utils.post.list.getData() 8 9 // Optimistically update 10 utils.post.list.setData(undefined, (old) => 11 old?.map(post => 12 post.id === newPost.id ? { ...post, ...newPost } : post 13 ) 14 ) 15 16 return { previousPosts } 17 }, 18 onError: (err, newPost, context) => { 19 // Rollback on error 20 utils.post.list.setData(undefined, context?.previousPosts) 21 }, 22 onSettled: () => { 23 // Refetch after success or error 24 utils.post.list.invalidate() 25 }, 26})

Infinite Queries

typescript
1// Server 2export const postRouter = router({ 3 list: publicProcedure 4 .input(z.object({ 5 cursor: z.string().optional(), 6 limit: z.number().min(1).max(100).default(10), 7 })) 8 .query(async ({ input }) => { 9 const posts = await db.post.findMany({ 10 take: input.limit + 1, 11 cursor: input.cursor ? { id: input.cursor } : undefined, 12 }) 13 14 let nextCursor: string | undefined = undefined 15 if (posts.length > input.limit) { 16 const nextItem = posts.pop() 17 nextCursor = nextItem!.id 18 } 19 20 return { posts, nextCursor } 21 }), 22}) 23 24// Client 25export default function InfinitePosts() { 26 const posts = trpc.post.list.useInfiniteQuery( 27 { limit: 10 }, 28 { 29 getNextPageParam: (lastPage) => lastPage.nextCursor, 30 } 31 ) 32 33 return ( 34 <div> 35 {posts.data?.pages.map((page, i) => ( 36 <div key={i}> 37 {page.posts.map(post => ( 38 <div key={post.id}>{post.title}</div> 39 ))} 40 </div> 41 ))} 42 43 <button 44 onClick={() => posts.fetchNextPage()} 45 disabled={!posts.hasNextPage || posts.isFetchingNextPage} 46 > 47 {posts.isFetchingNextPage ? 'Loading...' : 'Load More'} 48 </button> 49 </div> 50 ) 51}

Error Handling

Server Errors

typescript
1import { TRPCError } from '@trpc/server' 2 3export const postRouter = router({ 4 getById: publicProcedure 5 .input(z.string()) 6 .query(async ({ input }) => { 7 const post = await db.post.findUnique({ where: { id: input } }) 8 9 if (!post) { 10 throw new TRPCError({ 11 code: 'NOT_FOUND', 12 message: 'Post not found', 13 }) 14 } 15 16 return post 17 }), 18 19 create: protectedProcedure 20 .input(z.object({ title: z.string() })) 21 .mutation(async ({ ctx, input }) => { 22 if (!ctx.session.verified) { 23 throw new TRPCError({ 24 code: 'FORBIDDEN', 25 message: 'Email must be verified', 26 }) 27 } 28 29 try { 30 return await db.post.create({ data: input }) 31 } catch (error) { 32 throw new TRPCError({ 33 code: 'INTERNAL_SERVER_ERROR', 34 message: 'Failed to create post', 35 cause: error, 36 }) 37 } 38 }), 39})

Error Codes

  • BAD_REQUEST - Invalid input
  • UNAUTHORIZED - Not authenticated
  • FORBIDDEN - Not authorized
  • NOT_FOUND - Resource not found
  • TIMEOUT - Request timeout
  • CONFLICT - Resource conflict
  • PRECONDITION_FAILED - Precondition check failed
  • PAYLOAD_TOO_LARGE - Request too large
  • METHOD_NOT_SUPPORTED - HTTP method not supported
  • TOO_MANY_REQUESTS - Rate limited
  • CLIENT_CLOSED_REQUEST - Client closed request
  • INTERNAL_SERVER_ERROR - Server error

Client Error Handling

typescript
1const createPost = trpc.post.create.useMutation({ 2 onError: (error) => { 3 if (error.data?.code === 'UNAUTHORIZED') { 4 router.push('/login') 5 } else if (error.data?.code === 'FORBIDDEN') { 6 alert('You do not have permission') 7 } else { 8 alert('Something went wrong') 9 } 10 }, 11})

Server-Side Calls

In Server Components

typescript
1// app/users/page.tsx 2import { createCaller } from '@/server/routers/_app' 3import { createContext } from '@/server/context' 4 5export default async function UsersPage() { 6 const ctx = await createContext({ req: {} as any }) 7 const caller = createCaller(ctx) 8 9 const users = await caller.user.list() 10 11 return ( 12 <ul> 13 {users.map(user => ( 14 <li key={user.id}>{user.name}</li> 15 ))} 16 </ul> 17 ) 18}

Create Caller

typescript
1// server/routers/_app.ts 2export const createCaller = createCallerFactory(appRouter) 3 4// Usage 5const caller = createCaller(ctx) 6const user = await caller.user.getById('123')

Advanced Patterns

Request Batching

typescript
1import { httpBatchLink } from '@trpc/client' 2 3const trpcClient = trpc.createClient({ 4 links: [ 5 httpBatchLink({ 6 url: '/api/trpc', 7 maxURLLength: 2083, // Reasonable limit 8 }), 9 ], 10})

Request Deduplication

Automatic with React Query - multiple components requesting same data will only make one request.

Custom Headers

typescript
1const trpcClient = trpc.createClient({ 2 links: [ 3 httpBatchLink({ 4 url: '/api/trpc', 5 headers: () => { 6 return { 7 Authorization: `Bearer ${getToken()}`, 8 } 9 }, 10 }), 11 ], 12})

Error Formatting

typescript
1// server/trpc.ts 2const t = initTRPC.context<Context>().create({ 3 errorFormatter({ shape, error }) { 4 return { 5 ...shape, 6 data: { 7 ...shape.data, 8 zodError: 9 error.cause instanceof ZodError 10 ? error.cause.flatten() 11 : null, 12 }, 13 } 14 }, 15})

Testing

Testing Procedures

typescript
1import { appRouter } from '@/server/routers/_app' 2import { createCaller } from '@/server/routers/_app' 3 4describe('user router', () => { 5 it('creates user', async () => { 6 const ctx = { session: mockSession, db: mockDb } 7 const caller = createCaller(ctx) 8 9 const user = await caller.user.create({ 10 name: 'John', 11 email: 'john@example.com', 12 }) 13 14 expect(user.name).toBe('John') 15 }) 16})

Best Practices

  1. Use Zod for validation - Always validate inputs
  2. Keep procedures small - Single responsibility
  3. Use middleware for auth - Don't repeat auth checks
  4. Type your context - Full type safety
  5. Organize routers - Split into logical domains
  6. Handle errors properly - Use appropriate error codes
  7. Leverage React Query - Use its caching and refetching
  8. Batch requests - Enable batching for better performance
  9. Use optimistic updates - Better UX
  10. Document procedures - Add JSDoc comments

Resources

Related Skills

Looking for an alternative to tRPC or building a Categories.community AI Agent? Explore these related open-source MCP Servers.

View All

widget-generator

Logo of f
f

widget-generator is an open-source AI agent skill for creating widget plugins that are injected into prompt feeds on prompts.chat. It supports two rendering modes: standard prompt widgets using default PromptCard styling and custom render widgets built as full React components.

149.6k
0
Design

chat-sdk

Logo of lobehub
lobehub

chat-sdk is a unified TypeScript SDK for building chat bots across multiple platforms, providing a single interface for deploying bot logic.

73.0k
0
Communication

zustand

Logo of lobehub
lobehub

The ultimate space for work and life — to find, build, and collaborate with agent teammates that grow with you. We are taking agent harness to the next level — enabling multi-agent collaboration, effortless agent team design, and introducing agents as the unit of work interaction.

72.8k
0
Communication

data-fetching

Logo of lobehub
lobehub

The ultimate space for work and life — to find, build, and collaborate with agent teammates that grow with you. We are taking agent harness to the next level — enabling multi-agent collaboration, effortless agent team design, and introducing agents as the unit of work interaction.

72.8k
0
Communication