tRPC
Complete guide to tRPC - type-safe APIs without REST, routers and procedures, context vs middleware, and when to use tRPC vs REST/GraphQL
What is tRPC?
tRPC is a tool that helps you connect your frontend (React, Next.js, etc.) with your backend (Node.js, Express, etc.) easily without writing REST APIs or GraphQL.
tRPC = directly call backend functions from frontend Think of it like this:
- You write a function on the backend
- and directly call that same function on the frontend
- Like you are calling a normal JavaScript function.
Main tRPC Keywords
| Keyword | Simple Meaning | Compare with REST |
|---|---|---|
Router | A group of functions (like your API endpoints) | Like a collection of routes /user, /post |
Procedure | A single function that does something | Like one REST endpoint /api/user |
Query | Used to get (read) data | Like GET request |
Mutation | Used to change (write) data | Like POST, PUT, DELETE request |
Input | What data you send to the backend | Like req.body in REST |
Output | What data backend returns | Like res.json() in REST |
Zod | A library to check and validate data | Like checking data before saving in REST |
When to Use tRPC
Use tRPC when:
- You are building a TypeScript project (it works best with TS)
- You want automatic type safety between frontend and backend
- You want to use monorepo
- Your frontend and backend are in the same codebase (like Next.js)
- You are tired of writing extra REST endpoints for simple things
Do not use tRPC when:
- You need to connect with external apps or mobile apps
- You want to make public APIs (use REST or GraphQL for that)
- Your frontend and backend are in different servers or languages
Why use tRPC?
Let us compare it with REST API.
REST API way
You make endpoints like /api/users, /api/posts, etc.
You must send and receive JSON manually.
Example:
Frontend:
const res = await fetch('/api/user', {
method: 'POST',
body: JSON.stringify({ name: "John" })
});
const data = await res.json();Backend:
app.post('/api/user', (req, res) => {
const { name } = req.body;
res.json({ message: "Hello " + name });
});You have to write types twice (once for frontend and once for backend).
tRPC way
You just write one function on the server and call it from the client. Types are shared automatically.
Backend:
const appRouter = createTRPCRouter({
sayHello: publicProcedure
.input(z.string())
.query(({ input }) => {
return `Hello ${input}`;
}),
});Frontend:
const { data } = trpc.sayHello.useQuery("John");
console.log(data); // "Hello John"No need to fetch manually No need to parse JSON No need to write types twice
Visual Comparison Diagram
REST API
+-------------+ +-------------+
| Frontend | HTTP Req | Backend |
| fetch(url) | --------> | app.get(...)|
| parse JSON | <-------- | res.json() |
+-------------+ +-------------+
^ manual coding both sides
tRPC
+-------------+ +-------------+
| Frontend | direct | Backend |
| trpc.call() | --------> | procedure() |
| auto typed | <-------- | return data |
+-------------+ +-------------+
^ types shared automaticallyContext
What is context in tRPC?
- tRPC
context= RESTreq - In tRPC, context is an object that is created for each request.
- This object holds data that all your procedures (queries, mutations) can access.
- It is a place to put shared things you need everywhere: user info, database client, authorization data, etc.
Diagram
HTTP Request --> tRPC Handler --> createContext(req) --> ctx object
|
+------+------+
| |
passed into each |
procedure / middleware |
as opts.ctx |
|
procedure logic (resolver)tRPC Context vs REST Pattern
Overview
| Concept | REST Pattern | tRPC Context |
|---|---|---|
| Shared Data | Stored in req (e.g. req.user, req.db) | Stored in ctx (context object) |
| Creation | Middleware runs before each route | createContext() runs before each procedure |
| Access | Access via req.user or req.db | Access via ctx.user or ctx.db |
| Typing | Manual TypeScript types or none | Fully typed automatically |
| Usage Scope | Per request | Per request |
| Where Used | Route handlers (app.get, app.post) | Procedures (query, mutation) |
| Setup Example | app.use(authMiddleware) | initTRPC.context<Context>().create() |
| Best For | Public APIs or mixed tech stacks | TypeScript full stack apps (Next.js etc.) |
Code Comparison Example
REST Example
// Express server
app.use((req, res, next) => {
const user = getUserFromToken(req.headers.authorization);
req.user = user;
next();
});
app.get("/api/secret", (req, res) => {
if (!req.user) return res.status(401).send("Not logged in");
res.send(`Hello ${req.user.name}`);
});tRPC Example
// server/context.ts
export async function createContext({ req }) {
const user = getUserFromToken(req.headers.authorization);
return { user };
}
// router
const t = initTRPC.context<typeof createContext>().create();
export const appRouter = t.router({
secretData: t.procedure.query(({ ctx }) => {
if (!ctx.user) throw new Error("Not logged in");
return `Hello ${ctx.user.name}`;
}),
});Note: Middleware is block the request but Context will not block user request. because context is used to only get the user data,db connection,authorization etc.
Databases
Learn about relational databases, ACID properties, transactions, and how databases ensure data consistency and reliability in system design
My Development Bookmarks
Curated collection of development resources - UI component libraries, design tools, icons, animation libraries, and productivity tools for modern web development