Slide 1
-----------
Slide 2
-----------
Part 1: tRPC is a tool that connects your frontend and backend using TypeScript, making them talk like they’re best friends. Imagine building a blogging app—users browse posts, admins manage them. tRPC makes this smooth and type-safe, unlike the clunky old REST way.
Part 2: It’s fast, modern, and cuts development time. Plus, it’s perfect for our blogging app, where we need safety and speed. You’ll see how it beats REST in action today!
Part 3: REST is the classic way: you send requests like GET, POST and get JSON back. It’s everywhere, but it’s manual—endpoints, status codes, and type mismatches galore.
Part 4: tRPC flips that. It’s one system, no HTTP juggling, just TypeScript functions. For our blog, we’d call getPostsByTag function instead of fussing with URLs.
Part 5: REST is universal but tedious. tRPC is fast and safe but needs TypeScript. For our app, tRPC wins on simplicity—though it’s not ideal for public APIs.
Slide 3
-----------
Part 1: Let's check out how easy it is to get tRPC up and to run!
Part 2: To set up tRPC we will use the initTRPC function.
Part 3: We will also use ZOD for validating data coming to tRPC.
Part 4: tRPC defines the shared context, which is then passed to every route. Here you can add information about your user or add Prisma access.
Part 5: tRPC defines a router, which uses object parameters to form different routes, such as defining subroute posts.
Part 6: In the posts route, we have defined a new procedure called getPosts.
Part 7: We define the shape of the expected parameters of this route, in our case, expecting the optional string parameter named tag.
Part 8: Last, we define either a query or a mutation, which returns json data. Note that queries use REST GET method, while mutations use POST with JSON body.
Slide 4
-----------
Part 1: On the client, the situation is... beautiful. Instead of handling routes, we handle type-safe function calls.
Part 2: We create a helper library where we initialise the client-side router and read the type shape of the server router into the AppRouter variable.
Part 3: Then, in the react component, we simply find the related procedure in the client router and call the useQuery function, which is built on top of well-known tanstack query. Magic!
Part 4: The mutation is handled in a similar way, finding the function in a client side router and calling the mutation from tanstack query.
Slide 5
-----------
Part 1: Calling tRPC procedures is even easier on server, where they become just asynchronous function calls.
Part 2: First, we create a helper object which creates access to tRPC router and expose it through trpc variable.
Part 3: Then, on the server, we simply await the asynchronous function call, removing the need for HTTP API calls.
Slide 6
-----------
Part 1: tRPC also comes with a handy middleware option, allowing you to create protected routes, which assure that the implemented procedure will have access to the user object
Part 2: We can then use this protected procedure to define routes accessible only to authenticated users.
Slide 7
-----------
Part 1: tRPC improves development experience with type safety and speed. It’s perfect for our blog—public queries for users, protected mutations for admins, and server-side reuse.
Part 2: tRPC imposes the use of validation libraries such as Zod for validation. This keeps routes modular and tests your context logic. It’s all about safety and structure.
Part 3: Don’t skip auth checks in protected routes, and don’t use tRPC without TypeScript—it’s the secret sauce!
Part 4: If you want to learn more, please check out the official documentation of the tRPC router.
Part 5: Also, if you are starting a new project, please consider using the T3 stack, that comes pre-packages with Prisma, tRCP and many other useful libraries!
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.
1. 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 context lets us pass user info (e.g., is this an admin?) to our routes.
2. 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.
3. 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.
1. 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' };
},
});
2. 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!
3. 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!
Maggie is a generative AI that can help you understand the course content better. You can ask her questions about the lecture, and she will try to answer them. You can also see the questions asked by other students and her responses.
Join the discussion to ask questions, share your thoughts, and discuss with other learners