Trpc
Build end-to-end type-safe APIs with tRPC for TypeScript full-stack applications.
You are a tRPC specialist who builds fully type-safe APIs that share types between server and client without code generation. tRPC uses TypeScript inference to give clients compile-time knowledge of every procedure's input, output, and error types. You define routers and procedures on the server, and the client gets autocompletion, type checking, and refactoring support automatically. ## Key Points - **Importing server code on the client** -- only import `type AppRouter` from the server; importing actual server modules bundles server code (secrets, database clients) into the client build. - **Skipping input validation** -- always use `.input(z.object(...))` even for simple inputs; unvalidated inputs bypass tRPC's type guarantees at runtime and create security holes. - **Creating one giant router file** -- split routers by domain (user, post, order) into separate files and merge them in the app router; monolithic routers become unnavigable quickly. - You are building a full-stack TypeScript application (Next.js, Remix, or similar) where server and client share a monorepo. - You want compile-time type safety for API calls without maintaining separate API schemas or running code generation. - You need React Query integration with automatic cache invalidation, optimistic updates, and infinite scroll patterns. - You want to refactor API contracts and get immediate TypeScript errors everywhere a breaking change impacts the client. - You are building an internal tool or product API where all consumers are TypeScript applications you control. ## Quick Example ```bash # Server npm install @trpc/server zod # Client (React) npm install @trpc/client @trpc/react-query @tanstack/react-query ``` ```bash TRPC_API_URL=http://localhost:3000/api/trpc DATABASE_URL=postgresql://localhost:5432/myapp JWT_SECRET=your-secret-key ```
skilldb get api-gateway-services-skills/TrpcFull skill: 237 linestRPC End-to-End Type-Safe APIs
You are a tRPC specialist who builds fully type-safe APIs that share types between server and client without code generation. tRPC uses TypeScript inference to give clients compile-time knowledge of every procedure's input, output, and error types. You define routers and procedures on the server, and the client gets autocompletion, type checking, and refactoring support automatically.
Core Philosophy
Types Flow, Never Duplicate
tRPC's fundamental insight is that when server and client share a TypeScript codebase (monorepo), types should flow automatically via inference rather than be generated or manually synchronized. Define your input schema with Zod on the server, and the client knows the exact shape. Change a field name, and TypeScript errors appear in every client call site immediately. No codegen step, no schema files, no drift.
Procedures as the Unit of API Design
Think in procedures (query, mutation, subscription), not endpoints or resources. A procedure is a single function with typed input, typed output, and middleware. Group related procedures into routers, and merge routers into an app router. This model maps naturally to function calls, which is what tRPC calls feel like on the client -- trpc.user.getById.useQuery({ id: "123" }).
Middleware for Shared Concerns
Use tRPC middleware to handle authentication, logging, rate limiting, and error handling. Middleware runs before the procedure and can modify the context, short-circuit with an error, or pass through. Because middleware modifies the context type, downstream procedures get typed access to the authenticated user, database connection, or other injected values.
Setup
Install / Configuration
# Server
npm install @trpc/server zod
# Client (React)
npm install @trpc/client @trpc/react-query @tanstack/react-query
// server/trpc.ts
import { initTRPC, TRPCError } from "@trpc/server";
import { z } from "zod";
interface Context {
user: { id: string; role: string } | null;
db: Database;
}
const t = initTRPC.context<Context>().create();
export const router = t.router;
export const publicProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(async ({ ctx, next }) => {
if (!ctx.user) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next({ ctx: { user: ctx.user } });
});
Environment Variables
TRPC_API_URL=http://localhost:3000/api/trpc
DATABASE_URL=postgresql://localhost:5432/myapp
JWT_SECRET=your-secret-key
Key Patterns
1. Router Definition with Input Validation
// server/routers/user.ts
import { z } from "zod";
import { router, publicProcedure, protectedProcedure } from "../trpc";
export const userRouter = router({
getById: publicProcedure
.input(z.object({ id: z.string().uuid() }))
.query(async ({ input, ctx }) => {
const user = await ctx.db.user.findUnique({ where: { id: input.id } });
if (!user) throw new TRPCError({ code: "NOT_FOUND" });
return user; // Return type is inferred automatically
}),
update: protectedProcedure
.input(
z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
})
)
.mutation(async ({ input, ctx }) => {
return ctx.db.user.update({
where: { id: ctx.user.id },
data: input,
});
}),
list: publicProcedure
.input(
z.object({
cursor: z.string().optional(),
limit: z.number().min(1).max(50).default(20),
})
)
.query(async ({ input, ctx }) => {
const items = await ctx.db.user.findMany({
take: input.limit + 1,
cursor: input.cursor ? { id: input.cursor } : undefined,
});
const nextCursor = items.length > input.limit ? items.pop()?.id : undefined;
return { items, nextCursor };
}),
});
2. App Router and Type Export
// server/routers/_app.ts
import { router } from "../trpc";
import { userRouter } from "./user";
import { postRouter } from "./post";
export const appRouter = router({
user: userRouter,
post: postRouter,
});
// Export the type -- this is the ONLY thing the client imports from the server
export type AppRouter = typeof appRouter;
3. React Client Integration
// client/trpc.ts
import { createTRPCReact } from "@trpc/react-query";
import type { AppRouter } from "../server/routers/_app";
export const trpc = createTRPCReact<AppRouter>();
// client/components/UserProfile.tsx
function UserProfile({ userId }: { userId: string }) {
// Full autocompletion and type safety -- input and output are inferred
const { data, isLoading } = trpc.user.getById.useQuery({ id: userId });
const updateMutation = trpc.user.update.useMutation({
onSuccess: () => {
utils.user.getById.invalidate({ id: userId });
},
});
if (isLoading) return <div>Loading...</div>;
return (
<div>
<h1>{data?.name}</h1>
<button onClick={() => updateMutation.mutate({ name: "New Name", email: data!.email })}>
Update
</button>
</div>
);
}
Common Patterns
Next.js App Router Integration
// app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { appRouter } from "@/server/routers/_app";
import { createContext } from "@/server/context";
const handler = (req: Request) =>
fetchRequestHandler({
endpoint: "/api/trpc",
req,
router: appRouter,
createContext,
});
export { handler as GET, handler as POST };
Error Handling with Custom Error Formatter
const t = initTRPC.context<Context>().create({
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.cause instanceof z.ZodError ? error.cause.flatten() : null,
},
};
},
});
Optimistic Updates
const utils = trpc.useUtils();
const deleteMutation = trpc.post.delete.useMutation({
onMutate: async (deletedId) => {
await utils.post.list.cancel();
const previous = utils.post.list.getData();
utils.post.list.setData(undefined, (old) =>
old ? old.filter((p) => p.id !== deletedId) : []
);
return { previous };
},
onError: (_err, _id, context) => {
utils.post.list.setData(undefined, context?.previous);
},
onSettled: () => utils.post.list.invalidate(),
});
Anti-Patterns
- Importing server code on the client -- only import
type AppRouterfrom the server; importing actual server modules bundles server code (secrets, database clients) into the client build. - Skipping input validation -- always use
.input(z.object(...))even for simple inputs; unvalidated inputs bypass tRPC's type guarantees at runtime and create security holes. - Creating one giant router file -- split routers by domain (user, post, order) into separate files and merge them in the app router; monolithic routers become unnavigable quickly.
- Using tRPC for public APIs consumed by third parties -- tRPC is designed for TypeScript-to-TypeScript communication; use REST or GraphQL for APIs consumed by external clients who cannot import your types.
When to Use
- You are building a full-stack TypeScript application (Next.js, Remix, or similar) where server and client share a monorepo.
- You want compile-time type safety for API calls without maintaining separate API schemas or running code generation.
- You need React Query integration with automatic cache invalidation, optimistic updates, and infinite scroll patterns.
- You want to refactor API contracts and get immediate TypeScript errors everywhere a breaking change impacts the client.
- You are building an internal tool or product API where all consumers are TypeScript applications you control.
Install this skill directly: skilldb add api-gateway-services-skills
Related Skills
Apisix
Apache APISIX is a dynamic, real-time, high-performance API Gateway built on Nginx and LuaJIT, designed for managing
AWS API Gateway
Build and manage APIs with AWS API Gateway including REST, HTTP, and WebSocket APIs.
Cloudflare Workers
Build and deploy edge computing applications with Cloudflare Workers.
Express Gateway
Express Gateway is an API Gateway built on Express.js, offering powerful features for proxying,
Fastify
Fastify is a highly performant, low-overhead web framework for Node.js, designed to be as fast as possible in terms of both throughput and response time.
GRAPHQL Mesh
Unify multiple API sources into a single GraphQL endpoint with GraphQL Mesh.