tRPC gives a TypeScript client end-to-end types without generating a schema or a client. It is a strong fit when one team owns the frontend and backend; it is a poor fit for public APIs or clients you do not control.
Key takeaways
- Routers group procedures; queries read, mutations write.
- Context carries the database client, session and request-scoped data.
- Middleware handles auth, logging, rate limits and validation.
- Prefer the simplest boundary that keeps data and authorisation on the server.
What matters in practice
- Input schemas validate at runtime; the inferred type flows to the client.
- For Next.js App Router, call the server caller on the server and use the client only for interactive leaves.
How to implement it
- Define the context. Create the request-scoped dependencies once.
- Create the router. Start with one feature and a small procedure set.
- Add input schemas. Validate every input with Zod.
- Protect procedures. Use middleware for authentication and authorisation.
- Wire the client. Use the recommended Next.js integration and a single provider.
- Test the procedure. Call it directly with a test context and cover permission failures.
A working example
import { initTRPC, TRPCError } from "@trpc/server";
const t = initTRPC.context<Context>().create();
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.session) throw new TRPCError({ code: "UNAUTHORIZED" });
return next({ ctx: { ...ctx, session: ctx.session } });
});
export const postRouter = t.router({
bySlug: protectedProcedure
.input(z.object({ slug: z.string().min(1) }))
.query(({ ctx, input }) => ctx.db.post.findUnique({ where: { slug: input.slug } })),
});
Production checklist
| Area | Check |
|---|---|
| Context | Database, session and request metadata created once |
| Validation | Zod input on every procedure |
| Auth | Protected procedures and object-level checks |
| Errors | Typed TRPC errors, no leaking internals |
| Caching | Server-side caller and query keys where appropriate |
| Observability | Logging and tracing around slow procedures |
Common mistakes
- Using tRPC for a public API that third parties must call.
- Putting authorisation only in the UI.
- Creating a new database client per procedure call.
- Exposing a procedure that returns more fields than the client needs.
When this is the wrong tool
Every tool has a boundary. If the team is small, the use case is simple and the operational cost is high, a simpler stack may be better. The right choice is the one you can debug, secure and hand over without the original author.
Rollout plan
- Prove one slice. Use tRPC for one real route or feature, not the whole product.
- Set the boundary. Write down what stays on the server, what runs on the client and how data crosses the boundary.
- Add the safety net. Types, validation, tests and error handling around the new slice.
- Measure. Check bundle size, latency, error rate and the developer experience before expanding.
- Document and hand over. Record the pattern, the pitfalls and the version constraints.
Testing strategy
| Layer | What to test | Why |
|---|---|---|
| Types | Strict compile and no unjustified any | Catches contract drift early |
| Unit | Pure logic, validation and state transitions | Fast feedback |
| Integration | API, database and auth boundaries | Most production failures live here |
| End-to-end | The critical user journey | Proves the wiring |
| Operations | Logs, errors, retries and rollback | A release is not finished at deploy |
Operational checklist
- Pin the runtime and dependency versions; test upgrades in a branch.
- Keep secrets on the server and validate every external payload.
- Add structured logs with a request ID, but never log tokens or personal data.
- Set a performance or bundle budget and check it in CI.
- Write a short runbook for the most likely failure.
Frequently asked questions
For an internal TypeScript client, often yes. For public APIs, mobile clients, partners or webhooks, REST or GraphQL is usually a better boundary.
Yes, but the integration details matter. Use the current recommended setup and avoid calling client hooks from server components.
tRPC is not ideal for large binary uploads. Use a presigned upload URL or a dedicated route, then store the resulting asset ID.
Use React Query or Next.js caching for reads, and invalidate after mutations. tRPC itself is a transport and procedure layer, not a cache.
Sources and further reading
- tRPC documentation (trpc.io)
- Zod documentation (zod.dev)
- Next.js server and client components (nextjs.org)
Related reading
Need an implementation review?
Send the repository or the design. I will review the boundary, the failure modes and the parts that will be expensive to change later.
How to cite this guide: Nur Hossain Repon, “tRPC setup: complete developer guide (2026)”, Nhrepon Portfolio, reviewed October 2026, https://nhrepon.com/posts/trpc-setup-complete-developer-guide-2026.





Comments
No comments yet.