trpc-type-safety
Focuses on the tRPC framework, providing end-to-end type-safe development guidance, ensuring type consistency in frontend and backend API calls, and reducing runtime errors.
npx skills add bobmatnyc/claude-mpm-skills --skill trpc-type-safetyBefore / After Comparison
1 组In traditional development, inconsistent frontend and backend interface types often lead to runtime errors, making debugging difficult and severely impacting development efficiency and system stability.
Leveraging tRPC's type safety guidance ensures end-to-end type consistency, significantly reduces runtime errors, and improves development efficiency and system reliability.
trpc-type-safety
tRPC - End-to-End Type Safety
progressive_disclosure: entry_point: summary sections:
- id: summary title: "tRPC Overview" tokens: 70 next: [when_to_use, quick_start]
- id: when_to_use title: "When to Use tRPC" tokens: 150 next: [quick_start, core_concepts]
- id: quick_start title: "Quick Start" tokens: 300 next: [core_concepts, router_definition]
- id: core_concepts title: "Core Concepts" tokens: 400 next: [router_definition, procedures]
- id: router_definition title: "Router Definition" tokens: 350 next: [procedures, context]
- id: procedures title: "Procedures (Query & Mutation)" tokens: 400 next: [input_validation, context]
- id: input_validation title: "Input Validation with Zod" tokens: 350 next: [context, middleware]
- id: context title: "Context Management" tokens: 400 next: [middleware, error_handling]
- id: middleware title: "Middleware" tokens: 400 next: [error_handling, client_setup]
- id: error_handling title: "Error Handling" tokens: 350 next: [client_setup, react_integration]
- id: client_setup title: "Client Setup" tokens: 400 next: [react_integration, nextjs_integration]
- id: react_integration title: "React Query Integration" tokens: 450 next: [nextjs_integration, subscriptions]
- id: nextjs_integration title: "Next.js App Router Integration" tokens: 500 next: [subscriptions, file_uploads]
- id: subscriptions title: "Real-time Subscriptions" tokens: 400 next: [file_uploads, batching]
- id: file_uploads title: "File Uploads" tokens: 300 next: [batching, typescript_inference]
- id: batching title: "Batch Requests & Data Loaders" tokens: 350 next: [typescript_inference, testing]
- id: typescript_inference title: "TypeScript Inference Patterns" tokens: 300 next: [testing, production_patterns]
- id: testing title: "Testing Strategies" tokens: 400 next: [production_patterns, comparison]
- id: production_patterns title: "Production Patterns" tokens: 450 next: [comparison, migration]
- id: comparison title: "Comparison with REST & GraphQL" tokens: 250 next: [migration, best_practices]
- id: migration title: "Migration from REST" tokens: 300 next: [best_practices]
- id: best_practices title: "Best Practices & Performance" tokens: 400
Summary
tRPC enables end-to-end type safety between TypeScript clients and servers without code generation. Define your API once, get automatic type inference everywhere.
Key Benefits: Zero codegen, TypeScript inference, React Query integration, minimal boilerplate.
When to Use tRPC
✅ Perfect For:
-
Full-stack TypeScript applications (Next.js, T3 stack)
-
Projects where client and server share TypeScript codebase
-
Teams wanting REST-like simplicity with GraphQL-like type safety
-
Apps using React Query for data fetching
-
Internal APIs where you control both client and server
❌ Avoid When:
-
Public APIs consumed by non-TypeScript clients
-
Microservices in different languages
-
Mobile apps using Swift/Kotlin (use REST/GraphQL instead)
-
Need API documentation for external developers (OpenAPI better)
When to Choose:
-
tRPC: Full-stack TypeScript, monorepo, internal tools
-
REST: Public APIs, language-agnostic, broad compatibility
-
GraphQL: Complex data graphs, multiple clients, flexible queries
Quick Start
Installation
# Server dependencies
npm install @trpc/server zod
# React/Next.js client dependencies
npm install @trpc/client @trpc/react-query @tanstack/react-query
Define Router (Server)
// server/trpc.ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
const t = initTRPC.create();
export const appRouter = t.router({
hello: t.procedure
.input(z.object({ name: z.string() }))
.query(({ input }) => {
return { greeting: `Hello ${input.name}` };
}),
createPost: t.procedure
.input(z.object({ title: z.string(), content: z.string() }))
.mutation(async ({ input }) => {
// Save to database
return { id: 1, ...input };
}),
});
export type AppRouter = typeof appRouter;
Use in Client (React)
// client/trpc.ts
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../server/trpc';
export const trpc = createTRPCReact<AppRouter>();
// Component
function MyComponent() {
const { data } = trpc.hello.useQuery({ name: 'World' });
const createPost = trpc.createPost.useMutation();
return <div>{data?.greeting}</div>; // Fully typed!
}
Next: Learn core concepts or dive into router definition.
Core Concepts
The tRPC Philosophy
tRPC provides type-safe remote procedure calls by sharing TypeScript types between client and server. No code generation—just TypeScript's inference.
Key Components
-
Router: Collection of procedures (API endpoints)
-
Procedure: Single API operation (query or mutation)
-
Context: Request-scoped data (user, database, etc.)
-
Middleware: Intercept/modify requests (auth, logging)
-
Input/Output: Validated with Zod schemas
Type Flow
// Server defines types
const router = t.router({
getUser: t.procedure
.input(z.string())
.query(({ input }) => ({ id: input, name: 'Alice' })),
});
// Client gets automatic types
const user = await trpc.getUser.query('123');
// user is typed as { id: string, name: string }
Architecture Pattern
┌─────────────┐ Type-safe ┌──────────────┐
│ Client │ ←────────────────→ │ Server │
│ (React) │ No codegen! │ (Node.js) │
└─────────────┘ └──────────────┘
↓ ↓
React Query tRPC Router
(caching) (procedures)
Advantages:
-
Changes propagate instantly (no build step)
-
Rename refactoring works across client/server
-
Impossible to call wrong types
-
Auto-complete for all API methods
Router Definition
Basic Router Structure
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
export const appRouter = t.router({
// Procedures go here
});
export type AppRouter = typeof appRouter;
Nested Routers (Namespacing)
const userRouter = t.router({
getById: t.procedure
.input(z.string())
.query(({ input }) => getUser(input)),
create: t.procedure
.input(z.object({ name: z.string(), email: z.string() }))
.mutation(({ input }) => createUser(input)),
});
const postRouter = t.router({
list: t.procedure.query(() => getPosts()),
create: t.procedure
.input(z.object({ title: z.string() }))
.mutation(({ input }) => createPost(input)),
});
export const appRouter = t.router({
user: userRouter,
post: postRouter,
});
// Client usage:
// trpc.user.getById.useQuery('123')
// trpc.post.list.useQuery()
Router Merging
import { adminRouter } from './admin';
import { publicRouter } from './public';
export const appRouter = t.mergeRouters(publicRouter, adminRouter);
Router Organization Best Practices
server/
├── trpc.ts # tRPC instance, context, middleware
├── routers/
│ ├── user.ts # User-related procedures
│ ├── post.ts # Post-related procedures
│ └── index.ts # Combine all routers
└── index.ts # Export AppRouter type
Procedures (Query & Mutation)
Query Procedures (Read Operations)
const router = t.router({
// Simple query
getUser: t.procedure
.input(z.string())
.query(({ input }) => {
return db.user.findUnique({ where: { id: input } });
}),
// Query with multiple inputs
searchUsers: t.procedure
.input(z.object({
query: z.string(),
limit: z.number().default(10),
}))
.query(({ input }) => {
return db.user.findMany({
where: { name: { contains: input.query } },
take: input.limit,
});
}),
});
Mutation Procedures (Write Operations)
const router = t.router({
createUser: t.procedure
.input(z.object({
name: z.string().min(3),
email: z.string().email(),
}))
.mutation(async ({ input }) => {
return await db.user.create({ data: input });
}),
updateUser: t.procedure
.input(z.object({
id: z.string(),
data: z.object({
name: z.string().optional(),
email: z.string().email().optional(),
}),
}))
.mutation(async ({ input }) => {
return await db.user.update({
where: { id: input.id },
data: input.data,
});
}),
});
Query vs Mutation
Aspect Query Mutation
Purpose Read data Modify data
HTTP Method GET POST
Caching Cached by React Query Not cached
Idempotent Yes No
Side Effects None Database writes, emails, etc.
Output Typing
const router = t.router({
getUser: t.procedure
.input(z.string())
.output(z.object({ id: z.string(), name: z.string() })) // Optional
.query(({ input }) => {
return { id: input, name: 'Alice' };
}),
});
Note: Output validation adds runtime overhead—use for critical data only.
Input Validation with Zod
Why Zod?
tRPC uses Zod for runtime type validation and TypeScript inference. Zod schemas provide:
-
Runtime validation (prevent invalid data)
-
TypeScript types (auto-inferred from schema)
-
Transformation (parse, coerce, default values)
Basic Validation
import { z } from 'zod';
const router = t.router({
createPost: t.procedure
.input(z.object({
title: z.string().min(5).max(100),
content: z.string(),
published: z.boolean().default(false),
tags: z.array(z.string()).optional(),
}))
.mutation(({ input }) => {
// input is fully typed and validated
return createPost(input);
}),
});
Advanced Validation
const createUserInput = z.object({
email: z.string().email(),
password: z.string().min(8),
age: z.number().int().min(18),
role: z.enum(['user', 'admin']),
metadata: z.record(z.string(), z.unknown()).optional(),
});
const router = t.router({
createUser: t.procedure
.input(createUserInput)
.mutation(({ input }) => {
// All validation passed
return saveUser(input);
}),
});
Transformations
const router = t.router({
getUser: t.procedure
.input(
z.object({
id: z.string().transform((id) => parseInt(id, 10)),
})
)
.query(({ input }) => {
// input.id is now a number
return db.user.findUnique({ where: { id: input.id } });
}),
});
Reusable Schemas
// schemas/user.ts
export const CreateUserSchema = z.object({
name: z.string(),
email: z.string().email(),
});
export const UpdateUserSchema = CreateUserSchema.partial().extend({
id: z.string(),
});
// routers/user.ts
const router = t.router({
create: t.procedure.input(CreateUserSchema).mutation(/*...*/),
update: t.procedure.input(UpdateUserSchema).mutation(/*...*/),
});
Context Management
What is Context?
Context provides request-scoped data to all procedures—authentication, database connections, logging, etc.
Creating Context
import { inferAsyncReturnType } from '@trpc/server';
import { CreateNextContextOptions } from '@trpc/server/adapters/next';
export async function createContext(opts: CreateNextContextOptions) {
const session = await getSession(opts.req);
return {
session,
db: prisma,
req: opts.req,
res: opts.res,
};
}
export type Context = inferAsyncReturnType<typeof createContext>;
const t = initTRPC.context<Context>().create();
Using Context in Procedures
const router = t.router({
getMe: t.procedure.query(({ ctx }) => {
if (!ctx.session?.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return ctx.db.user.findUnique({
where: { id: ctx.session.user.id },
});
}),
createPost: t.procedure
.input(z.object({ title: z.string() }))
.mutation(async ({ ctx, input }) => {
return ctx.db.post.create({
data: {
title: input.title,
authorId: ctx.session.user.id,
},
});
}),
});
Context Best Practices
// ✅ Good: Lazy database connection
export async function createContext(opts: CreateNextContextOptions) {
return {
getDB: () => prisma, // Lazy
session: await getSession(opts.req),
};
}
// ❌ Bad: Heavy computation in context
export async function createContext(opts: CreateNextContextOptions) {
const allUsers = await prisma.user.findMany(); // Too expensive!
return { allUsers };
}
Middleware
What is Middleware?
Middleware intercepts procedure calls to add cross-cutting concerns: logging, timing, authentication, rate limiting.
Basic Middleware
const loggerMiddleware = t.middleware(async ({ path, type, next }) => {
const start = Date.now();
console.log(`→ ${type} ${path}`);
const result = await next();
const duration = Date.now() - start;
console.log(`✓ ${type} ${path} - ${duration}ms`);
return result;
});
const loggedProcedure = t.procedure.use(loggerMiddleware);
Authentication Middleware
const isAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.session?.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return next({
ctx: {
...ctx,
user: ctx.session.user, // Narrow type
},
});
});
// Protected procedure builder
const protectedProcedure = t.procedure.use(isAuthed);
const router = t.router({
// Public
getPublicPosts: t.procedure.query(() => getPosts()),
// Protected - requires authentication
getMyPosts: protectedProcedure.query(({ ctx }) => {
// ctx.user is guaranteed to exist
return getPostsByUser(ctx.user.id);
}),
});
Chaining Middleware
const timingMiddleware = t.middleware(async ({ next }) => {
const start = performance.now();
const result = await next();
console.log(`Execution time: ${performance.now() - start}ms`);
return result;
});
const rateLimitMiddleware = t.middleware(async ({ ctx, next }) => {
await checkRateLimit(ctx.session?.user?.id);
return next();
});
const protectedProcedure = t.procedure
.use(timingMiddleware)
.use(rateLimitMiddleware)
.use(isAuthed);
Context Transformation
const enrichContextMiddleware = t.middleware(async ({ ctx, next }) => {
const user = ctx.session?.user
? await ctx.db.user.findUnique({ where: { id: ctx.session.user.id } })
: null;
return next({
ctx: {
...ctx,
user, // Full user object
},
});
});
Error Handling
TRPCError
import { TRPCError } from '@trpc/server';
const router = t.router({
getUser: t.procedure
.input(z.string())
.query(async ({ input }) => {
const user = await db.user.findUnique({ where: { id: input } });
if (!user) {
throw new TRPCError({
code: 'NOT_FOUND',
message: `User ${input} not found`,
});
...
User Reviews (0)
Write a Review
No reviews yet
Statistics
User Rating
Rate this Skill