PresentationInteractive, narrated presentation of the section content.
ContentDetailed description of the section content.
Motivation
Welcome, everyone! Today, we’re diving into tRPC, a powerful tool for building modern web applications. Why should you care about tRPC? Well, imagine you’re creating a blogging app where users can browse posts, filter them by tags or dates, and admins can add or edit content. Normally, you’d use something like REST to connect the frontend and backend, right? But REST can get messy—lots of endpoints, manual type definitions, and endless back-and-forth. tRPC promises to simplify all that by letting your frontend and backend “talk” seamlessly using TypeScript. It’s like giving your app a superpower: type safety and speed, all in one.
Let’s start with a real-world problem. Say you’re building this blogging app, and you want the frontend to fetch posts tagged “tech” while the admin adds a new post. With REST, you’d define separate endpoints, handle HTTP requests, and pray your types match. With tRPC, it’s a single, type-safe system that feels like magic. Intrigued? Good—because it’s not as complicated as you might think!
Now, some of you might assume REST is the only way to go—it’s been around forever, after all. Or maybe you think tRPC is just “hype” with no real benefits. Stick with me, and I’ll show you why that’s not true. We’ll bust some myths and build our blogging app example step-by-step.
Section 1: tRPC vs. REST – What’s the Big Deal?
Let’s compare tRPC to REST. REST is like sending letters: you write a request (GET /posts), wait for the server to reply, and hope the response matches what you expect. tRPC, on the other hand, is like a phone call—direct, instant, and you both speak the same language (TypeScript!).
- REST Pros: Works everywhere, widely understood, great for public APIs.
- REST Cons: You need to define endpoints manually, handle status codes (404, 500, etc.), and keep frontend/backend types in sync—often by hand or with extra tools like OpenAPI.
- tRPC Pros: End-to-end type safety, no need for separate endpoint docs, faster development since it’s all in one codebase.
- tRPC Cons: Ties you to TypeScript and might not suit huge, distributed teams or public APIs.
Here’s a surprise: many think REST is simpler because it’s familiar, but tRPC can cut your setup time in half once you get it. For our blogging app, REST would mean writing /posts, /posts/:id, /posts?tag=tech, etc. With tRPC, we define procedures once and call them like functions—getPostsByTag("tech"). No HTTP juggling!
Section 2: Setting Up tRPC – The Foundation
Let’s build our blogging app with tRPC. First, we need a context and some routes. Imagine our app has users browsing posts and an admin managing them. We’ll use TypeScript for everything.
Initialize tRPC: We set up a tRPC server in a Next.js app.
import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.context<{ user?: { id: string } }>().create();This
contextlets us pass user info (e.g., is this an admin?) to our routes.Public Route: Anyone can fetch posts.
const postRouter = t.router({ getPosts: t.procedure .input(z.object({ tag: z.string().optional() })) .query(({ input }) => { const posts = [ { id: 1, title: "Hello tRPC", tag: "tech" }, { id: 2, title: "Why TypeScript?", tag: "code" }, ]; return posts.filter((p) => !input.tag || p.tag === input.tag); }), });Users can call this to get all posts or filter by tag.
Protected Route: Only admins can add posts.
const adminRouter = t.router({ addPost: t.procedure .input(z.object({ title: z.string(), tag: z.string() })) .mutation(({ input, ctx }) => { if (!ctx.user?.id) throw new Error("Unauthorized"); const newPost = { id: Date.now(), ...input }; return newPost; // In reality, save to a DB }), });You will be using protected routes so often you will want to create a helper function to create protected routes such as this:
/** * Public (unauthenticated) procedure * * This is the base piece you use to build new queries and mutations on your tRPC API. * It does not guarantee that a user querying is authorised */ export const publicProcedure = t.procedure; /** * Protected (authenticated) procedure * * If you want a query or mutation to ONLY be accessible to logged in users, use this. * It verifies the session is valid and guarantees `ctx.session.user` is not null. * * @see https://trpc.io/docs/procedures */ export const protectedProcedure = t.procedure.use(({ ctx, next }) => { if (!ctx.session || !ctx.session.user) { throw new TRPCError({ code: "UNAUTHORIZED" }); } return next({ ctx: { // infers the `session` as non-nullable user: ctx.session.user // session: { ...ctx.session, user: ctx.session.user }, } }); });With the helper procedure, you can re-define your route as:
const adminRouter = t.router({ addPost: protectedProcedure .input(z.object({ title: z.string(), tag: z.string() })) .mutation(({ input, ctx }) => { const newPost = { id: Date.now(), ...input }; return newPost; // In reality, save to a DB }), });We combine these into one router:
export const appRouter = t.router({ post: postRouter, admin: adminRouter, }); export type AppRouter = typeof appRouter;
This is our foundation. Public routes are open, protected ones check the context. Simple, right? Now let’s use it.
Section 3: Client-Side Magic with Queries and Mutations
On the client (Next.js frontend), tRPC feels like calling functions. Let’s fetch posts and add one.
Setup Client: In
_app.tsx:import { trpc } from '../utils/trpc'; export default function App({ Component, pageProps }) { return <Component {...pageProps} />; }In
utils/trpc.ts:import { createTRPCNext } from '@trpc/next'; import type { AppRouter } from '../server/router'; export const trpc = createTRPCNext<AppRouter>({ config() { return { url: '/api/trpc' }; }, });Query Posts: Display posts filtered by “tech”:
import { trpc } from '../utils/trpc'; export default function Posts() { const { data: posts } = trpc.post.getPosts.useQuery({ tag: "tech" }); return ( <ul> {posts?.map((post) => ( <li key={post.id}>{post.title} ({post.tag})</li> ))} </ul> ); }This fetches posts instantly, with full type safety!
Mutation (Add Post): Admin adds a post:
import { trpc } from '../utils/trpc'; export default function AdminPanel() { const mutation = trpc.admin.addPost.useMutation(); const handleAdd = () => { mutation.mutate({ title: "New Post", tag: "news" }); }; return <button onClick={handleAdd}>Add Post</button>; }If the user isn’t logged in as an admin, it fails—safe and secure.
See how clean that is? No fetch, no JSON parsing—just functions.
Section 4: Server-Side Calls
What if we need tRPC in a server route (e.g., API route in Next.js)? Say we want to fetch posts from /api/latest:
import { createContext } from '../../server/context';
import { appRouter } from '../../server/router';
export default async function handler(req, res) {
const caller = appRouter.createCaller(await createContext({ req, res }));
const posts = await caller.post.getPosts({ tag: "news" });
res.json(posts);
}
This runs tRPC server-side, reusing our logic. It’s like having one brain for the whole app!
Conclusion
So, what did we learn? tRPC is a game-changer for TypeScript apps like our blogging platform. It beats REST in simplicity and type safety, though it’s not perfect for every scenario (e.g., public APIs). We set up a context, built public and protected routes, used queries and mutations on the client, and even called it server-side—all with one example that grew from browsing posts to managing them.
Key Takeaways:
- Use tRPC for type-safe, fast development.
- Public routes for all, protected ones for admins—context is your friend.
- Queries fetch data, mutations change it, and server calls reuse it all.
Pitfalls to Avoid:
- Don’t skip input validation (use Zod!).
- Don’t assume tRPC fits non-TypeScript projects—it won’t.
Next time you’re building an app, give tRPC a shot. It’ll save you headaches and make coding feel like a breeze! Questions? Let’s dig in!
No hints available.