← Back to Skills
Browser

trpc-best-practices

ifoster01 By ifoster01 👁 4 views ▲ 0 votes

Expert guidance for tRPC

GitHub
---
name: tRPC
description: Expert guidance for tRPC (TypeScript Remote Procedure Call) including router setup, procedures, middleware, context, client configuration, and Next.js integration. Use this when building type-safe APIs, integrating tRPC with Next.js, or implementing client-server communication with full TypeScript inference.
---

# 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
# Core packages
npm install @trpc/server@next @trpc/client@next @trpc/react-query@next

# Peer dependencies
npm install @tanstack/react-query@latest zod
```

### Basic Setup (Next.js App Router)

**1. Create tRPC Router**
```typescript
// server/trpc.ts
import { initTRPC } from '@trpc/server'
import { z } from 'zod'

const t = initTRPC.create()

export const router = t.router
export const publicProcedure = t.procedure
```

**2. Define API Router**
```typescript
// server/routers/_app.ts
import { router, publicProcedure } from '../trpc'
import { z } from 'zod'

export const appRouter = router({
  hello: publicProcedure
    .input(z.object({ name: z.string() }))
    .query(({ input }) => {
      return { greeting: `Hello ${input.name}!` }
    }),

  createUser: publicProcedure
    .input(z.object({
      name: z.string(),
      email: z.string().email(),
    }))
    .mutation(async ({ input }) => {
      const user = await db.user.create({ data: input })
      return user
    }),
})

export type AppRouter = typeof appRouter
```

**3. Create API Route**
```typescript
// app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from '@trpc/server/adapters/fetch'
import { appRouter } from '@/server/routers/_app'

const handler = (req: Request) =>
  fetchRequestHandler({
    endpoint: '/api/trpc',
    req,
    router: appRouter,
    createContext: () => ({}),
  })

export { handler as GET, handler as POST }
```

**4. Setup Client Provider**
```typescript
// app/providers.tsx
'use client'

import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { httpBatchLink } from '@trpc/client'
import { useState } from 'react'
import { trpc } from '@/lib/trpc'

export function Providers({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient())
  const [trpcClient] = useState(() =>
    trpc.createClient({
      links: [
        httpBatchLink({
          url: 'http://localhost:3000/api/trpc',
        }),
      ],
    })
  )

  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>
        {children}
      </QueryClientProvider>
    </trpc.Provider>
  )
}
```

**5. Create tRPC Client**
```typescript
// lib/trpc.ts
import { createTRPCReact } from '@trpc/react-query'
import type { AppRouter } from '@/server/routers/_app'

export const trpc = createTRPCReact<AppRouter>()
```

**6. Use in Components**
```typescript
'use client'

import { trpc } from '@/lib/trpc'

export default function Home() {
  const hello = trpc.hello.useQuery({ name: 'World' })
  const createUser = trpc.createUser.useMutation()

  return (
    <div>
      <p>{hello.data?.greeting}</p>
      <button
        onClick={() => createUser.mutate({
          name: 'John',
          email: '[email protected]'
        })}
      >
        Create User
      </button>
    </div>
  )
}
```

## Router Definition

### Basic Router
```typescript
import { router, publicProcedure } from './trpc'
import { z } from 'zod'

export const userRouter = router({
  // Query - for fetching data
  getById: publicProcedure
    .input(z.string())
    .query(async ({ input }) => {
      return await db.user.findUnique({ where: { id: input } })
    }),

  // Mutation - for creating/updating/deleting
  create: publicProcedure
    .input(z.object({
      name: z.string(),
      email: z.string().email(),
    }))
    .mutation(async ({ input }) => {
      return await db.user.create({ data: input })
    }),

  // Subscription - for real-time updates
  onUpdate: publicProcedure
    .subscription(() => {
      return observable<User>((emit) => {
        // Implementation
      })
    }),
})
```

### Nested Routers
```typescript
import { router } from './trpc'
import { userRouter } from './routers/user'
import { postRouter } from './routers/post'
import { commentRouter } from './routers/comment'

export const appRouter = router({
  user: userRouter,
  post: postRouter,
  comment: commentRouter,
})

// Usage on client:
// trpc.user.getById.useQuery('123')
// trpc.post.list.useQuery()
// trpc.comment.create.useMutation()
```

### Merging Routers
```typescript
import { router, publicProcedure } from './trpc'

const userRouter = router({
  list: publicProcedure.query(() => {/* ... */}),
  getById: publicProcedure.input(z.string()).query(() => {/* ... */}),
})

const postRouter = router({
  list: publicProcedure.query(() => {/* ... */}),
  create: publicProcedure.input(z.object({})).mutation(() => {/* ... */}),
})

// Merge into app router
export const appRouter = router({
  user: userRouter,
  post: postRouter,
})
```

## Input Validation with Zod

### Basic Validation
```typescript
import { z } from 'zod'

export const userRouter = router({
  create: publicProcedure
    .input(z.object({
      name: z.string().min(2).max(50),
      email: z.string().email(),
      age: z.number().int().positive().optional(),
      role: z.enum(['user', 'admin']),
    }))
    .mutation(async ({ input }) => {
      // input is fully typed!
      return await db.user.create({ data: input })
    }),
})
```

### Complex Validation
```typescript
const createPostInput = z.object({
  title: z.string().min(5).max(100),
  content: z.string().min(10),
  published: z.boolean().default(false),
  tags: z.array(z.string()).min(1).max(5),
  metadata: z.object({
    views: z.number().default(0),
    likes: z.number().default(0),
  }).optional(),
})

export const postRouter = router({
  create: publicProcedure
    .input(createPostInput)
    .mutation(async ({ input }) => {
      return await db.post.create({ data: input })
    }),
})
```

### Reusable Schemas
```typescript
// schemas/user.ts
export const userSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string().email(),
})

export const createUserSchema = userSchema.omit({ id: true })
export const updateUserSchema = userSchema.partial()

// Use in router
export const userRouter = router({
  create: publicProcedure
    .input(createUserSchema)
    .mutation(({ input }) => {/* ... */}),

  update: publicProcedure
    .input(z.object({
      id: z.string(),
      data: updateUserSchema,
    }))
    .mutation(({ input }) => {/* ... */}),
})
```

## Context

### Creating Context
```typescript
// server/context.ts
import { inferAsyncReturnType } from '@trpc/server'
import { FetchCreateContextFnOptions } from '@trpc/server/adapters/fetch'

export async function createContext(opts: FetchCreateContextFnOptions) {
  // Get session from cookies/headers
  const session = await getSession(opts.req)

  return {
    session,
    db,
  }
}

export type Context = inferAsyncReturnType<typeof createContext>
```

### Using Context in tRPC
```typescript
// server/trpc.ts
import { initTRPC } from '@trpc/server'
import { Context } from './context'

const t = initTRPC.context<Context>().create()

export const router = t.router
export const publicProcedure = t.procedure
```

### Accessing Context in Procedures
```typescript
export const userRouter = router({
  me: publicProcedure.query(({ ctx }) => {
    // ctx.session, ctx.db are available
    if (!ctx.session) {
      throw new TRPCError({ code: 'UNAUTHORIZED' })
    }

    return ctx.db.user.findUnique({
      where: { id: ctx.session.userId }
    })
  }),
})
```

## Middleware

### Creating Middleware
```typescript
// server/trpc.ts
import { initTRPC, TRPCError } from '@trpc/server'

const t = initTRPC.context<Context>().create()

// Logging middleware
const loggerMiddleware = t.middleware(async ({ path, type, next }) => {
  const start = Date.now()
  const result = await next()
  const duration = Date.now() - start

  console.log(`${type} ${path} took ${duration}ms`)

  return result
})

// Auth middleware
const isAuthed = t.middleware(({ ctx, next }) => {
  if (!ctx.session) {
    throw new TRPCError({ code: 'UNAUTHORIZED' })
  }

  return next({
    ctx: {
      // Infers session is non-nullable
      session: ctx.session,
    },
  })
})

// Create procedures with middleware
export const publicProcedure = t.procedure.use(loggerMiddleware)
export const protectedProcedure = t.procedure.use(loggerMiddleware).use(isAuthed)
```

### Using Protected Procedures
```typescript
export const postRouter = router({
  // Public - anyone can access
  list: publicProcedure.query(() => {
    return db.post.findMany({ where: { published: true } })
  }),

  // Protected - requires authentication
  create: protectedProcedure
    .input(z.object({ title: z.string() }))
    .mutation(({ ctx, input }) => {
      // ctx.session is guaranteed to exist
      return db.post.create({
        data: {
          ...input,
          authorId: ctx.session.userId,
        },
      })
    }),
})
```

### Role-Based Middleware
```typescript
const requireRole = (role: string) =>
  t.middleware(({ ctx, next }) => {
    if (!ctx.session || ctx.session.role !== role) {
      throw new TRPCError({ code: 'FORBIDDEN' })
    }
    return next()
  })

export const adminProcedure = protectedProcedure.use(requireRole('admin'))

export const userRouter = router({
  delete: adminProcedure
    .input(z.string())
    .mutation(({ input }) => {
      return db.user.delete({ where: { id: input } })
    }),
})
```

## Client Usa

... (truncated)
browser

Comments

Sign in to leave a comment

Loading comments...