End-to-end type-safe APIs with tRPC including routers, procedures, subscriptions, and error handling
# tRPC API Patterns for Google Antigravity
Build end-to-end type-safe APIs with tRPC using Google Antigravity's Gemini 3 engine. This guide covers router setup, procedures, middleware, subscriptions, and client integration patterns.
## Router Setup
```typescript
// server/trpc.ts
import { initTRPC, TRPCError } from '@trpc/server';
import { ZodError } from 'zod';
import superjson from 'superjson';
import { db } from '@/lib/db';
import { verifyToken } from '@/lib/auth';
export interface Context {
db: typeof db;
user: { id: string; email: string; role: string } | null;
}
export async function createContext(opts: { headers: Headers }): Promise<Context> {
const token = opts.headers.get('authorization')?.replace('Bearer ', '');
const user = token ? await verifyToken(token) : null;
return { db, user };
}
const t = initTRPC.context<Context>().create({
transformer: superjson,
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.cause instanceof ZodError ? error.cause.flatten() : null,
},
};
},
});
export const router = t.router;
export const publicProcedure = t.procedure;
export const middleware = t.middleware;
// Auth middleware
const isAuthenticated = middleware(async ({ ctx, next }) => {
if (!ctx.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return next({ ctx: { ...ctx, user: ctx.user } });
});
const isAdmin = middleware(async ({ ctx, next }) => {
if (!ctx.user || ctx.user.role !== 'admin') {
throw new TRPCError({ code: 'FORBIDDEN' });
}
return next({ ctx });
});
export const protectedProcedure = publicProcedure.use(isAuthenticated);
export const adminProcedure = protectedProcedure.use(isAdmin);
```
## Router Implementation
```typescript
// server/routers/product.ts
import { z } from 'zod';
import { router, publicProcedure, adminProcedure } from '../trpc';
import { TRPCError } from '@trpc/server';
const productSchema = z.object({
name: z.string().min(1).max(200),
description: z.string().optional(),
price: z.number().positive(),
categoryId: z.string().uuid().optional(),
stock: z.number().int().min(0).default(0),
});
export const productRouter = router({
list: publicProcedure
.input(z.object({
page: z.number().int().positive().default(1),
limit: z.number().int().min(1).max(100).default(20),
categoryId: z.string().uuid().optional(),
search: z.string().optional(),
sortBy: z.enum(['name', 'price', 'createdAt']).default('createdAt'),
sortOrder: z.enum(['asc', 'desc']).default('desc'),
}))
.query(async ({ ctx, input }) => {
const { page, limit, categoryId, search, sortBy, sortOrder } = input;
const offset = (page - 1) * limit;
const where = {
...(categoryId && { categoryId }),
...(search && {
OR: [
{ name: { contains: search, mode: 'insensitive' } },
{ description: { contains: search, mode: 'insensitive' } },
],
}),
};
const [products, total] = await Promise.all([
ctx.db.product.findMany({
where,
include: { category: true },
orderBy: { [sortBy]: sortOrder },
skip: offset,
take: limit,
}),
ctx.db.product.count({ where }),
]);
return {
items: products,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit),
},
};
}),
byId: publicProcedure
.input(z.object({ id: z.string().uuid() }))
.query(async ({ ctx, input }) => {
const product = await ctx.db.product.findUnique({
where: { id: input.id },
include: { category: true, reviews: { take: 10 } },
});
if (!product) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Product not found',
});
}
return product;
}),
create: adminProcedure
.input(productSchema)
.mutation(async ({ ctx, input }) => {
return ctx.db.product.create({ data: input });
}),
update: adminProcedure
.input(z.object({
id: z.string().uuid(),
data: productSchema.partial(),
}))
.mutation(async ({ ctx, input }) => {
const existing = await ctx.db.product.findUnique({
where: { id: input.id },
});
if (!existing) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Product not found',
});
}
return ctx.db.product.update({
where: { id: input.id },
data: input.data,
});
}),
delete: adminProcedure
.input(z.object({ id: z.string().uuid() }))
.mutation(async ({ ctx, input }) => {
await ctx.db.product.delete({ where: { id: input.id } });
return { success: true };
}),
});
```
## Subscriptions
```typescript
// server/routers/notifications.ts
import { z } from 'zod';
import { observable } from '@trpc/server/observable';
import { router, protectedProcedure } from '../trpc';
import { EventEmitter } from 'events';
const ee = new EventEmitter();
interface Notification {
id: string;
userId: string;
message: string;
type: 'info' | 'success' | 'warning' | 'error';
createdAt: Date;
}
export const notificationRouter = router({
onNotification: protectedProcedure.subscription(({ ctx }) => {
return observable<Notification>((emit) => {
const handler = (notification: Notification) => {
if (notification.userId === ctx.user.id) {
emit.next(notification);
}
};
ee.on('notification', handler);
return () => {
ee.off('notification', handler);
};
});
}),
send: protectedProcedure
.input(z.object({
userId: z.string().uuid(),
message: z.string(),
type: z.enum(['info', 'success', 'warning', 'error']),
}))
.mutation(async ({ input }) => {
const notification: Notification = {
id: crypto.randomUUID(),
...input,
createdAt: new Date(),
};
ee.emit('notification', notification);
return notification;
}),
});
```
## Client Integration
```typescript
// lib/trpc/client.ts
import { createTRPCReact } from '@trpc/react-query';
import { httpBatchLink, wsLink, splitLink } from '@trpc/client';
import superjson from 'superjson';
import type { AppRouter } from '@/server/routers';
export const trpc = createTRPCReact<AppRouter>();
function getBaseUrl() {
if (typeof window !== 'undefined') return '';
return process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}`
: 'http://localhost:3000';
}
export function createTRPCClient() {
return trpc.createClient({
transformer: superjson,
links: [
splitLink({
condition: (op) => op.type === 'subscription',
true: wsLink({ url: `${getBaseUrl().replace('http', 'ws')}/api/trpc` }),
false: httpBatchLink({
url: `${getBaseUrl()}/api/trpc`,
headers: () => {
const token = localStorage.getItem('token');
return token ? { authorization: `Bearer ${token}` } : {};
},
}),
}),
],
});
}
```
## Best Practices
Google Antigravity's Gemini 3 engine recommends these tRPC patterns: Use Zod schemas for input validation. Implement proper error handling with TRPCError. Create middleware for cross-cutting concerns. Use batching for multiple concurrent requests. Leverage subscriptions for real-time updates instead of polling.This tRPC prompt is ideal for developers working on:
By using this prompt, you can save hours of manual coding and ensure best practices are followed from the start. It's particularly valuable for teams looking to maintain consistency across their trpc implementations.
Yes! All prompts on Antigravity AI Directory are free to use for both personal and commercial projects. No attribution required, though it's always appreciated.
This prompt works excellently with Claude, ChatGPT, Cursor, GitHub Copilot, and other modern AI coding assistants. For best results, use models with large context windows.
You can modify the prompt by adding specific requirements, constraints, or preferences. For tRPC projects, consider mentioning your framework version, coding style, and any specific libraries you're using.