Prompt of the Day: Convert a REST API to Type-Safe tRPC
Series: Prompt of the Day — Part 8 of 30
The 2 AM Bug That Started This
It was a Friday night deploy. The backend team had quietly changed a user endpoint — role went from a string to an enum, and createdAt was renamed to created_at. The frontend had no idea. By the time anyone noticed, production was throwing 500s for every new signup.
I've seen this scenario play out at least a dozen times in my career. You've got a REST API, a separate TypeScript frontend, and a weekly ritual of "did you update the types?" standing meetings that nobody enjoys.
This is exactly the problem tRPC was built to eliminate — and with tRPC v11 releasing officially in March 2025, there has never been a better time to make the switch. Companies like Cal.com, Ping.gg, and Documenso have migrated production systems to tRPC, reporting 60–80% reductions in API-related bugs and 40% faster feature development. One developer's documented migration from REST to tRPC v11 showed development time per endpoint drop from ~45 minutes to ~20 minutes — and type mismatches dropped to zero, not reduced, zero.
The good news: you don't have to rewrite your app. You can migrate one endpoint at a time. And your AI assistant can do the heavy lifting if you give it the right prompt.
The Prompt
You are a TypeScript architect helping me migrate a REST API endpoint to tRPC v11.
Here is my existing REST endpoint (Express/Next.js API route):
[PASTE YOUR ROUTE CODE HERE]
Here is the current TypeScript interface I'm using on the frontend:
[PASTE YOUR INTERFACE HERE]
Please:
1. Create a tRPC v11 router procedure that replaces this endpoint (use initTRPC, publicProcedure or protectedProcedure as appropriate)
2. Define a Zod schema for all inputs and outputs — do not leave any field unvalidated
3. Show the updated frontend call using the tRPC React Query hook (useQuery or useMutation)
4. Mount the new tRPC router alongside the existing REST route so both run in parallel during migration
5. Flag any fields where the REST response shape doesn't match what the frontend interface expects
Use tRPC v11 conventions. Import from @trpc/server and use TanStack Query v5 integration.
Why It Works
This prompt succeeds because it gives the model a complete job description, not just a vague directive.
It sets the version explicitly. tRPC v11 — released officially on March 21, 2025 — has different import paths, TanStack Query v5 integration, and SSE-based subscriptions compared to v10. Without specifying the version, you'll get a mix of outdated patterns.
It asks for Zod validation on inputs and outputs. Most generated tRPC code validates inputs but skips output schemas. Validating outputs is what catches the role → enum and createdAt → created_at class of bugs at the server boundary before they ever reach the client.
It requests parallel running. The gradual migration strategy — mounting tRPC alongside existing REST routes — is the proven low-risk path. As noted in multiple 2025 migration guides, running both systems simultaneously means no big-bang rewrite, easy rollback, and continuous delivery while migrating.
It asks the model to audit the contract mismatch. This is the underrated gem. Before you even run any code, the model flags where your existing REST response shape diverges from what the frontend expects.
Before and After
Before: REST endpoint + manual type sync
// server/routes/users.ts (Express)
app.get('/api/users/:id', async (req, res) => {
const user = await db.users.findById(req.params.id);
if (!user) return res.status(404).json({ error: 'Not found' });
res.json(user);
});
// frontend/types.ts (manually maintained — and already drifting)
export interface User {
id: string;
name: string;
email: string;
role: string; // is this still a string? who knows
createdAt: string; // or was it created_at now?
}
// frontend/components/Profile.tsx
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
fetch(`/api/users/${id}`)
.then(res => res.json())
.then(data => setUser(data)); // any type sneaks in here
}, [id]);
After: tRPC procedure with full type inference
// server/routers/user.ts
import { z } from 'zod';
import { router, publicProcedure } from '../trpc';
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string(),
email: z.string().email(),
role: z.enum(['admin', 'member', 'viewer']), // enum, not string
createdAt: z.string().datetime(),
});
export const userRouter = router({
getById: publicProcedure
.input(z.object({ id: z.string().uuid() }))
.output(UserSchema)
.query(async ({ input }) => {
const user = await db.users.findById(input.id);
if (!user) throw new TRPCError({ code: 'NOT_FOUND' });
return user;
}),
});
// server/app.ts — parallel running during migration
import { createExpressMiddleware } from '@trpc/server/adapters/express';
app.use('/api', legacyRestRouter); // existing REST still live
app.use('/trpc', createExpressMiddleware({ router: appRouter })); // tRPC new
// frontend/components/Profile.tsx — type-safe, no manual interface
const { data: user, isLoading } = trpc.user.getById.useQuery({ id });
// user is fully typed — autocomplete works, mismatches are compile errors
The Anti-Prompt
Bad version:
Convert my REST API to tRPC.
Why it fails: This generates something, but it's a roulette wheel of tRPC versions. You might get v9 syntax (createRouter), v10 patterns, or v11 — and the model won't tell you which. It won't add Zod output validation (so type drift still sneaks through). It won't show you how to run the systems in parallel (so you're forced into a risky all-at-once swap). And it absolutely will not audit your existing type contract for mismatches.
Vague prompts produce plausible-looking code that breaks in production. Specific prompts produce code you can actually ship.
Variations
For a Next.js App Router project:
... same prompt as above, but add:
Use the Next.js App Router setup with a route handler at app/api/trpc/[trpc]/route.ts.
Show how to use React Server Components with tRPC v11 prefetch helpers to avoid
client-server waterfalls on initial load.
For migrating a mutation endpoint:
... same prompt as above, but add:
This is a POST/PUT/DELETE endpoint. Create a tRPC mutation procedure.
Include optimistic update logic in the frontend useMutation hook,
and add a protectedProcedure that checks req.session.userId before executing.
For a full router audit:
Here is my complete Express router file with 8 endpoints.
Group them into tRPC sub-routers by domain (users, billing, analytics).
Identify which endpoints can be public procedures and which require auth middleware.
Output the full appRouter with merged sub-routers.
Your Migration Checklist
- Install tRPC v11 and TanStack Query v5:
npm install @trpc/server@^11 @trpc/client@^11 @trpc/react-query@^11 @tanstack/react-query@^5 - Set up
initTRPCwith your context (auth session, DB client) - Pick one simple GET endpoint and run the prompt above
- Mount the tRPC router alongside your existing REST routes — don't remove anything yet
- Migrate the frontend call for that one endpoint and verify full type safety
- Check the model's contract mismatch report — fix any shape discrepancies before continuing
- Repeat for 2–3 more endpoints per sprint until legacy routes are empty
- Leave file upload endpoints as REST (tRPC handles FormData in v11, but REST is still simpler for large binaries)
Ask The Guild
This week's community prompt:
What's the hairiest REST-to-tRPC migration you've tackled — or are dreading? Did you have a big endpoint count, a tricky auth middleware pattern, or a team that needed convincing? Share your before/after or your war story in the Guild forum. Bonus points if you paste the exact prompt variation that saved you.