What Is tRPC — and Why It Matters for TypeScript Developers
If you've ever built a full-stack TypeScript application, you know the pain of keeping types synchronized between your server and client. REST APIs require manual type definitions on both sides. GraphQL needs code generation tools to produce client types from schemas. tRPC takes a fundamentally different approach: types defined on the server automatically propagate to the client through TypeScript's built-in inference system.
The key innovation is zero code generation. There are no schema files to maintain, no codegen commands to run, and no generated artifacts to keep in sync. You define your API procedures in TypeScript, and the types flow to your client calls automatically. Antigravity's AI agents understand this architecture deeply, making them particularly effective at generating tRPC code that maintains type safety across the entire stack.
// How tRPC type inference works (conceptual overview)
// Types defined on the server automatically flow to the client
//
// Server: router.user.getById({ id: string }) => { name: string, email: string }
// ↓ TypeScript type inference
// Client: trpc.user.getById.useQuery({ id: "123" })
// → Return type automatically inferred as { name: string, email: string }Setting Up a tRPC Project with Antigravity
Antigravity's AI agents can scaffold a tRPC project in seconds. Start by describing the architecture you want in the chat panel:
Create a Next.js 15 App Router + tRPC v11 + Zod project with:
- src/server/trpc.ts for router configuration
- src/server/routers/ for router definitions
- src/lib/trpc.ts for client configuration
- Zod validation on all inputs
The agent will generate a well-structured base configuration like this:
// src/server/trpc.ts — tRPC initialization
import { initTRPC, TRPCError } from "@trpc/server";
import { ZodError } from "zod";
const t = initTRPC.create({
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.cause instanceof ZodError ? error.cause.flatten() : null,
},
};
},
});
// Base router and procedure exports
export const router = t.router;
export const publicProcedure = t.procedure;// src/server/routers/user.ts — Example user router
import { z } from "zod";
import { router, publicProcedure } from "../trpc";
export const userRouter = router({
getById: publicProcedure
.input(z.object({ id: z.string().uuid() }))
.query(async ({ input }) => {
// Fetch user from database (e.g., Prisma)
const user = await db.user.findUnique({
where: { id: input.id },
});
if (!user) {
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found",
});
}
return user;
// Expected output: { id: string, name: string, email: string, createdAt: Date }
}),
create: publicProcedure
.input(
z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
})
)
.mutation(async ({ input }) => {
const newUser = await db.user.create({ data: input });
return newUser;
// Expected output: { id: string, name: string, email: string, createdAt: Date }
}),
});Zod Validation Patterns That Scale
The combination of tRPC and Zod is where type safety truly shines. When you ask Antigravity to generate validation schemas, request that they be defined as reusable shared modules — this dramatically improves maintainability as your project grows.
// src/shared/schemas/user.ts — Shared validation schemas
import { z } from "zod";
// Base schemas (used on both server and client)
export const createUserSchema = z.object({
name: z
.string()
.min(1, "Name is required")
.max(100, "Name must be 100 characters or less"),
email: z
.string()
.email("Please enter a valid email address"),
role: z.enum(["admin", "user", "viewer"]).default("user"),
});
export const updateUserSchema = createUserSchema.partial().extend({
id: z.string().uuid(),
});
// Auto-export inferred types for client use
export type CreateUserInput = z.infer<typeof createUserSchema>;
export type UpdateUserInput = z.infer<typeof updateUserSchema>;Reference these schemas directly in your tRPC router for simultaneous validation and type safety:
// src/server/routers/user.ts — Using shared schemas
import { createUserSchema, updateUserSchema } from "@/shared/schemas/user";
export const userRouter = router({
create: publicProcedure
.input(createUserSchema) // Pass the Zod schema directly
.mutation(async ({ input }) => {
// input is automatically inferred as CreateUserInput
// input.name: string, input.email: string, input.role: "admin" | "user" | "viewer"
return await db.user.create({ data: input });
}),
update: publicProcedure
.input(updateUserSchema)
.mutation(async ({ input }) => {
// input.id is required, all other fields are optional
const { id, ...data } = input;
return await db.user.update({ where: { id }, data });
}),
});A helpful prompt pattern for Antigravity: "Define Zod schemas in a shared directory so both tRPC routers and form components can import them." This produces the most maintainable code structure.
Client-Side Implementation with React Query
tRPC v11 integrates seamlessly with React Query (TanStack Query), giving you powerful caching, background refetching, and optimistic updates out of the box. Antigravity generates client components that leverage these hooks automatically.
// src/lib/trpc.ts — Client configuration
import { createTRPCReact } from "@trpc/react-query";
import type { AppRouter } from "@/server/routers/_app";
export const trpc = createTRPCReact<AppRouter>();// src/components/UserList.tsx — tRPC + React Query in action
"use client";
import { trpc } from "@/lib/trpc";
export function UserList() {
// useQuery: type-safe data fetching
const { data: users, isLoading, error } = trpc.user.list.useQuery(
{ limit: 20, offset: 0 },
{
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
retry: 2,
}
);
// useMutation: type-safe data mutations
const utils = trpc.useUtils();
const createUser = trpc.user.create.useMutation({
onSuccess: () => {
// Invalidate and refetch user list on success
utils.user.list.invalidate();
},
onError: (err) => {
// Display Zod validation errors
if (err.data?.zodError) {
const fieldErrors = err.data.zodError.fieldErrors;
console.error("Validation errors:", fieldErrors);
}
},
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<ul>
{users?.map((user) => (
// user type is automatically inferred
<li key={user.id}>{user.name} ({user.email})</li>
))}
</ul>
<button
onClick={() =>
createUser.mutate({
name: "New User",
email: "new@example.com",
})
}
>
Add User
</button>
</div>
);
}
// Expected output: User list displayed with an "Add User" button that creates new entriesError Handling and Middleware Patterns
Production applications need robust authentication and error handling. tRPC's middleware system lets you implement these concerns declaratively, and Antigravity excels at generating well-structured middleware chains.
// src/server/trpc.ts — Adding authentication middleware
import { initTRPC, TRPCError } from "@trpc/server";
import type { Context } from "./context";
const t = initTRPC.context<Context>().create();
// Authentication middleware
const isAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.session?.user) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You must be logged in to access this resource",
});
}
return next({
ctx: {
session: ctx.session,
user: ctx.session.user, // user is now available in all downstream procedures
},
});
});
// Rate limiting middleware
const rateLimit = t.middleware(async ({ ctx, next, path }) => {
const key = `ratelimit:${ctx.ip}:${path}`;
const allowed = await checkRateLimit(key, { max: 100, window: 60 });
if (!allowed) {
throw new TRPCError({
code: "TOO_MANY_REQUESTS",
message: "Rate limit exceeded. Please try again later",
});
}
return next();
});
export const router = t.router;
export const publicProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(isAuthed);
export const rateLimitedProcedure = t.procedure.use(rateLimit);Tell Antigravity "create an authenticated tRPC router," and it will automatically include middleware like this. Adding middleware is as simple as chaining .use() calls, so the agent can augment existing routers with minimal code changes.
Getting the Most Out of Antigravity for tRPC Development
Here are the scenarios where Antigravity's AI agents add the most value to tRPC workflows.
Automated router generation: Describe your data model, and the agent produces a complete CRUD router with Zod schemas. Try: "Create a tRPC router for a Post model with title, content, and published fields. Include cursor-based pagination for the list query."
Bulk error handling: Ask the agent to "add error handling to all queries in this router — handle NOT_FOUND and INTERNAL_SERVER_ERROR with appropriate messages," and it will update every procedure consistently.
Test generation: tRPC routers are highly testable. Request "write Vitest tests for every procedure in the userRouter," and the agent will generate tests with mock data, covering both success and error paths.
For deeper dives into ORM integration, check out Antigravity + Prisma ORM for Type-Safe Database Operations and Antigravity + Drizzle ORM. If you're evaluating tRPC against GraphQL, our GraphQL + Apollo guide provides a useful comparison point.
Wrapping Up — Type-Safe Development with tRPC and Antigravity
tRPC fundamentally solves the type synchronization problem in TypeScript full-stack development. Paired with Antigravity's AI agents, you can generate router definitions, validation schemas, and client components with full end-to-end type safety — all without writing a single line of type declaration boilerplate.
Start with a small project to experience the tRPC + Antigravity workflow firsthand. Once you feel how effortless it is to have types flow automatically from server to client, you won't want to go back to manually maintaining API type definitions.