trpc
π―Skillfrom oriolrius/pki-manager-web
Enables building fully type-safe, end-to-end APIs with TypeScript, providing seamless client-server communication without code generation.
Installation
npx skills add https://github.com/oriolrius/pki-manager-web --skill trpcSkill Details
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.
Overview
# 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 (
{children}
)
}
```
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
```
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 (
{hello.data?.greeting} onClick={() => createUser.mutate({ name: 'John', email: 'john@example.com' })} > Create User
)
}
```
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
// 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
```
Using Context in tRPC
```typescript
// server/trpc.ts
import { initTRPC } from '@trpc/server'
import { Context } from './context'
const t = initTRPC.context
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
// 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 Usage
Queries
```typescript
'use client'
import { trpc } from '@/lib/trpc'
export default function UserList() {
// Basic query
const users = trpc.user.list.useQuery()
// Query with input
const user = trpc.user.getById.useQuery('user-123')
// Disabled query
const profile = trpc.user.getProfile.useQuery(
{ id: userId },
{ enabled: !!userId }
)
// With options
const posts = trpc.post.list.useQuery(undefined, {
refetchInterval: 5000,
staleTime: 1000,
})
if (users.isLoading) return
if (users.error) return
return (
{users.data?.map(user => (
))}
)
}
```
Mutations
```typescript
'use client'
export default function CreateUser() {
const utils = trpc.useContext()
const createUser = trpc.user.create.useMutation({
onSuccess: () => {
// Invalidate and refetch
utils.user.list.invalidate()
},
onError: (error) => {
console.error('Failed to create user:', error)
},
})
const handleSubmit = (e: React.FormEvent
e.preventDefault()
const formData = new FormData(e.currentTarget)
createUser.mutate({
name: formData.get('name') as string,
email: formData.get('email') as string,
})
}
return (
{createUser.isLoading ? 'Creating...' : 'Create User'}
)
}
```
Optimistic Updates
```typescript
const updatePost = trpc.post.update.useMutation({
onMutate: async (newPost) => {
// Cancel outgoing refetches
await utils.post.list.cancel()
// Snapshot previous value
const previousPosts = utils.post.list.getData()
// Optimistically update
utils.post.list.setData(undefined, (old) =>
old?.map(post =>
post.id === newPost.id ? { ...post, ...newPost } : post
)
)
return { previousPosts }
},
onError: (err, newPost, context) => {
// Rollback on error
utils.post.list.setData(undefined, context?.previousPosts)
},
onSettled: () => {
// Refetch after success or error
utils.post.list.invalidate()
},
})
```
Infinite Queries
```typescript
// Server
export const postRouter = router({
list: publicProcedure
.input(z.object({
cursor: z.string().optional(),
limit: z.number().min(1).max(100).default(10),
}))
.query(async ({ input }) => {
const posts = await db.post.findMany({
take: input.limit + 1,
cursor: input.cursor ? { id: input.cursor } : undefined,
})
let nextCursor: string | undefined = undefined
if (posts.length > input.limit) {
const nextItem = posts.pop()
nextCursor = nextItem!.id
}
return { posts, nextCursor }
}),
})
// Client
export default function InfinitePosts() {
const posts = trpc.post.list.useInfiniteQuery(
{ limit: 10 },
{
getNextPageParam: (lastPage) => lastPage.nextCursor,
}
)
return (
{posts.data?.pages.map((page, i) => ( {page.posts.map(post => ( ))} ))} onClick={() => posts.fetchNextPage()} disabled={!posts.hasNextPage || posts.isFetchingNextPage} > {posts.isFetchingNextPage ? 'Loading...' : 'Load More'}
)
}
```
Error Handling
Server Errors
```typescript
import { TRPCError } from '@trpc/server'
export const postRouter = router({
getById: publicProcedure
.input(z.string())
.query(async ({ input }) => {
const post = await db.post.findUnique({ where: { id: input } })
if (!post) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Post not found',
})
}
return post
}),
create: protectedProcedure
.input(z.object({ title: z.string() }))
.mutation(async ({ ctx, input }) => {
if (!ctx.session.verified) {
throw new TRPCError({
code: 'FORBIDDEN',
message: 'Email must be verified',
})
}
try {
return await db.post.create({ data: input })
} catch (error) {
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Failed to create post',
cause: error,
})
}
}),
})
```
Error Codes
BAD_REQUEST- Invalid inputUNAUTHORIZED- Not authenticatedFORBIDDEN- Not authorizedNOT_FOUND- Resource not foundTIMEOUT- Request timeoutCONFLICT- Resource conflictPRECONDITION_FAILED- Precondition check failedPAYLOAD_TOO_LARGE- Request too largeMETHOD_NOT_SUPPORTED- HTTP method not supportedTOO_MANY_REQUESTS- Rate limitedCLIENT_CLOSED_REQUEST- Client closed requestINTERNAL_SERVER_ERROR- Server error
Client Error Handling
```typescript
const createPost = trpc.post.create.useMutation({
onError: (error) => {
if (error.data?.code === 'UNAUTHORIZED') {
router.push('/login')
} else if (error.data?.code === 'FORBIDDEN') {
alert('You do not have permission')
} else {
alert('Something went wrong')
}
},
})
```
Server-Side Calls
In Server Components
```typescript
// app/users/page.tsx
import { createCaller } from '@/server/routers/_app'
import { createContext } from '@/server/context'
export default async function UsersPage() {
const ctx = await createContext({ req: {} as any })
const caller = createCaller(ctx)
const users = await caller.user.list()
return (
{users.map(user => (
))}
)
}
```
Create Caller
```typescript
// server/routers/_app.ts
export const createCaller = createCallerFactory(appRouter)
// Usage
const caller = createCaller(ctx)
const user = await caller.user.getById('123')
```
Advanced Patterns
Request Batching
```typescript
import { httpBatchLink } from '@trpc/client'
const trpcClient = trpc.createClient({
links: [
httpBatchLink({
url: '/api/trpc',
maxURLLength: 2083, // Reasonable limit
}),
],
})
```
Request Deduplication
Automatic with React Query - multiple components requesting same data will only make one request.
Custom Headers
```typescript
const trpcClient = trpc.createClient({
links: [
httpBatchLink({
url: '/api/trpc',
headers: () => {
return {
Authorization: Bearer ${getToken()},
}
},
}),
],
})
```
Error Formatting
```typescript
// server/trpc.ts
const t = initTRPC.context
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.cause instanceof ZodError
? error.cause.flatten()
: null,
},
}
},
})
```
Testing
Testing Procedures
```typescript
import { appRouter } from '@/server/routers/_app'
import { createCaller } from '@/server/routers/_app'
describe('user router', () => {
it('creates user', async () => {
const ctx = { session: mockSession, db: mockDb }
const caller = createCaller(ctx)
const user = await caller.user.create({
name: 'John',
email: 'john@example.com',
})
expect(user.name).toBe('John')
})
})
```
Best Practices
- Use Zod for validation - Always validate inputs
- Keep procedures small - Single responsibility
- Use middleware for auth - Don't repeat auth checks
- Type your context - Full type safety
- Organize routers - Split into logical domains
- Handle errors properly - Use appropriate error codes
- Leverage React Query - Use its caching and refetching
- Batch requests - Enable batching for better performance
- Use optimistic updates - Better UX
- Document procedures - Add JSDoc comments
Resources
- Docs: https://trpc.io
- Next.js Guide: https://trpc.io/docs/nextjs
- Examples: https://github.com/trpc/trpc/tree/main/examples
More from this repository8
sqlite skill from oriolrius/pki-manager-web
backlog.md skill from oriolrius/pki-manager-web
Guides developers through implementing OAuth 2.0 authorization flows, token management, and secure authentication across various platforms and services.
Renders and manages the web application's frontend routing, server-side rendering, and page components using Next.js framework for the PKI Manager web interface.
linux bash skill from oriolrius/pki-manager-web
Generates and manages X.509 digital certificates with comprehensive configuration options, supporting multiple certificate types like server TLS, client authentication, email S/MIME, and code signi...
Securely stores and manages cryptographic keys for the PKI Manager using Cosmian's Key Management System (KMS), ensuring sensitive key material is protected and accessed safely.
Configures and manages Keycloak identity and access management, enabling single sign-on, user federation, and authentication flows across applications.