Skip to main content

tRPC: End-to-End Type Safety without Code Generation

Complete guide and resources from the tRPC presentation for Fullstack architectures built on TypeScript (React, Node.js, Express, and MongoDB).

Team Members​

  • Alejandro Vargas - A00404840
  • Juan José Muñoz - A00450054
  • David Chicué - A00405613
  • Victoria Restrepo - A00405025

Slides and Resources​

tRPC
Project Links and Presentation

1. What is tRPC and What Problem Does it Solve?​

In traditional Fullstack development (REST or GraphQL), maintaining type consistency between client and backend requires either:

  • Writing and synchronizing duplicate interface contracts by hand.
  • Relying on code generators (code-gen) targeting OpenAPI or GraphQL schemas.

tRPC eliminates intermediate schema generation by leveraging TypeScript's native type inference: the backend router's TypeScript type is imported directly as a compile-time type in the frontend.

Clic para ampliar

Core Advantages​

  • Zero Code-Gen: No compiling .proto files or regenerating .graphql schemas.
  • Immediate Error Feedback: Changing an API property on the server causes instant compiler diagnostics across frontend callers.
  • Complete IntelliSense: Endpoints, request arguments, and response objects provide full autocomplete.
  • Runtime Validation via Zod: Ensures incoming request bodies strictly comply with expected TypeScript structures.

2. Queries vs. Mutations​

  • Query: Semantic equivalent to GET. Used for reading data without mutating server state.
  • Mutation: Semantic equivalent to POST, PUT, or DELETE. Used for creating, updating, or removing resources.

3. Practical Demo: Fullstack Task Manager​

1

Define Router on Server

server/src/trpc.ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';

const t = initTRPC.create();
export const router = t.router;
export const publicProcedure = t.procedure;

export const appRouter = router({
getTasks: publicProcedure.query(async () => {
return [{ id: '1', title: 'Learn tRPC', completed: false }];
}),
createTask: publicProcedure
.input(z.object({ title: z.string().min(3) }))
.mutation(async ({ input }) => {
return { id: Date.now().toString(), title: input.title, completed: false };
})
});

export type AppRouter = typeof appRouter;
2

Attach Router to Express

server/src/index.ts
import express from 'express';
import * as trpcExpress from '@trpc/server/adapters/express';
import { appRouter } from './trpc';

const app = express();
app.use(
'/trpc',
trpcExpress.createExpressMiddleware({
router: appRouter,
})
);

app.listen(4000, () => console.log('Server running on port 4000'));
3

Consume in React with End-to-End Type Safety

client/src/App.tsx
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../../server/src/trpc';

export const trpc = createTRPCReact<AppRouter>();

export function TaskList() {
const { data: tasks, isLoading } = trpc.getTasks.useQuery();
const createTask = trpc.createTask.useMutation();

if (isLoading) return <div>Loading tasks...</div>;

return (
<div>
<h2>Tasks</h2>
{tasks?.map(t => <p key={t.id}>{t.title}</p>)}
<button onClick={() => createTask.mutate({ title: 'New Task' })}>
Add Task
</button>
</div>
);
}