Angular & tRPC
Maximum type safety across the entire stack. How to setup a fullstack app with Angular and tRPC.
Kevin Kreuzer
@kreuzercode
24.01.2023
6 min read
On Twitter and Youtube, I heard many React developers talk about tRPC. I became curious and wanted to try it in Angular. Here’s how to do it.
What is tRPC?
tRPC is a lightweight, high-performance RPC (Remote Procedure Call) framework designed to be simple, fast, and easy to use.
It allows you to make calls to a server from a client, as if the server was a local object, and enables you to build distributed systems using a variety of programming languages.
The main benefit of using tRPC is type safety without code generation.
This blog post is also available as a Youtube video on my Youtube channel.
Let’s set up a server with tRPC
To set up a tRPC server, we first have to initiate a fresh npm project and install the required packages.
tRPC can be combined with different node backend frameworks such as express or fastify. Throughout this blog post, we will be using fastify. Furthermore, we will use zod to verify incoming request data.
npm init
npm i @trpc/server fastify fastify-cors zod
Once we installed the packages. we can go ahead and generate a server.ts
file.
import fastify from 'fastify';
import cors from '@fastify/cors';
import { fastifyTRPCPlugin } from '@trpc/server/adapters/fastify';
import { todosRouter } from './todo/todo.route';
const dev = true;
const port = 3000;
function createServer() {
const server = fastify({ logger: dev });
server.register(cors, {
origin: true,
});
server.register(fastifyTRPCPlugin, {
trpcOptions: { router: todosRouter },
});
server.get('/', async () => {
return { hello: 'wait-on 💨' };
});
const stop = () => server.close();
const start = async () => {
try {
await server.listen(port);
console.log('listening on port', port);
} catch (err) {
server.log.error(err);
process.exit(1);
}
};
return { server, start, stop };
}
createServer()
.start()
.then(() => console.log(`server starter on port ${port}`));
Okay, if you have ever used fastify the code here looks very familiar. We spin up a fastify server on port 3000
and enable CORS. However, there are a few very interesting tRPC-specific lines here. Let’s talk about them.
First, we import the fastifyTRPCPlugin
from @trpc/server/adapters/fastify
. We then use this plugin to register a tRPC router on our fastify server.
The router doesn’t yet exist; let’s go ahead and create one.
tRPC router
In a tRPC system, a router routes incoming requests to the appropriate server or service. The router acts as a central point of communication, forwarding client requests to the correct server and returning responses to the client.
A tRPC router typically does the following:
Accepts incoming requests from clients.
Matches the request to the appropriate server or service based on the method and service name.
Forwards the request to the correct server or service.
Receives the response from the server or service.
Returns the response to the client.
The main benefit of using a tRPC router is that it allows you to abstract away the underlying infrastructure of your distributed system, making it easier to add, remove, and scale servers and services. A tRPC router can also provide additional features such as load balancing, routing based on request data, and service discovery.
In our case, we want to set up a router with CRUD operations for a TODO app. Let’s go through it step by step.
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
const t = initTRPC.create();
const publicProcedure = t.procedure;
const router = t.router;
let id = 1;
let todos = [
{
id: 0,
todo: 'Clean the kitchen',
done: false,
},
{
id: 1,
todo: 'Bring out the trash',
done: false,
},
];
export const todosRouter = router({});
export type TodosRouter = typeof todosRouter;
We first import initTRPC
from @trpc/server
and z
from zod
. We then use the initTRPC.create()
method to create an instance of tRPC. We then use this instance to create a publicProcedure
and a router.
After creating those constants, we added some todos as the initial state. Then we create a router (more on that in the next step), and last but not least, we export the type of our todosRouter
. This later becomes very important once we start calling methods from our client.
Let’s write some routes. Let’s first start with a route that returns our todos.
export const todosRouter = router({
todos: publicProcedure.query((_) => todos),
});
That looks very simple, right? and it is. For classic GET
calls we use .query
a method that is available on the publicProcedure
object. We pass a simple callback function to the .query
function that returns our todos
.
Great! Let’s take a look at methods that update our todos. Let’s say a function that adds a todo
.
export const todosRouter = router({
todos: publicProcedure.query((_) => todos),
addTodo: publicProcedure
.input(
z.object({
todo: z.string(),
done: z.boolean(),
}),
)
.mutation(({ input }) => {
const newTodo = {
id: ++id,
...input,
};
todos.push(newTodo);
return newTodo;
}),
});
We added a new key named addTodo
on the object, we pass to the router function. This time we used the .input
and the .mutation
calls on the publicProcedure
.
The .input
method allows us to verify the parameters of the function. To do so, we use zod
.
Next, we can use the .mutation
function in combination with a resolver to implement the logic to add a new Todo
. It’s important to know that the function parameter is available as a property named input on the object passed to our resolver. We can grasp it via restructuring ({input})
.
That’s it. We can complete our router by adding the missing CRUD operations.
export const todosRouter = router({
todos: publicProcedure.query((_) => todos),
addTodo: publicProcedure
.input(
z.object({
todo: z.string(),
done: z.boolean(),
}),
)
.mutation(({ input }) => {
const newTodo = {
id: ++id,
...input,
};
todos.push(newTodo);
return newTodo;
}),
updateTodo: publicProcedure
.input(
z.object({
id: z.number(),
todo: z.string(),
done: z.boolean(),
}),
)
.mutation(({ input }) => {
todos = todos.map((t) => (t.id === input.id ? input : t));
return input;
}),
deleteTodo: publicProcedure.input(z.number()).mutation(({ input }) => {
const todoToDelete = todos.find((todo) => todo.id === input);
todos = todos.filter((todo) => todo.id !== input);
return todoToDelete;
}),
});
Awesome. We successfully implemented a fastify server with a tRPC router. Let’s now switch to the Angular side and call our remote functions.
Everything discribed in this post was developed live on my Twitch stream. If you are interested in modern web development or just want to chat, you should definitely subscribe to my Channel to not miss future broadcasts.
tRPC client
Angular backend calls are done inside a service using Angular’s HTTPClient
. Here’s a sample service for a Todo app that calls a bunch of REST endpoints.
@Injectable({
providedIn: 'root',
})
export class TodoService {
constructor(private http: HttpClient) {}
public getAllTodos(): Observable<Todo[]> {
return this.http.get<Todo[]>(ENDPOINT);
}
public addTodo(todo: CreateAndUpdateTodo): Observable<Todo> {
return this.http.post<Todo>(ENDPOINT, todo);
}
public updateTodo(todo: Todo): Observable<Todo> {
return this.http.patch<Todo>(`${ENDPOINT}/${todo.id}`, {
todo: todo.todo,
done: todo.done,
});
}
public deleteTodo(id: number): Observable<Todo> {
return this.http.delete<Todo>(`${ENDPOINT}/${id}`);
}
}
Nothing special. But we don’t want to invoke REST endpoints right; we want to call some remote functions using tRPC. So let's install the @trpc/client
package and refactor our service.
npm i @trpc/client
To get started, we have to create a client instance. To do so, we will use two helper functions named createTRPCProxyClient
and httpBatchLink
provided by @trpc/client
.
private client = createTRPCProxyClient<TodosRouter>({
links: [
httpBatchLink({
url: 'http://localhost:3000',
}),
],
});
The most important thing here is the generic we pass to createTRPCProxyClient
. But where is the TodosRouter
coming from?
Remember how I mentioned previously that this line in our backend is very important?
export type TodosRouter = typeof todosRouter;
Those are the types we want to import in our front end. So let’s add the following import to our client service.
import type {TodosRouter} from '../../../todo-backend/todo/todo.route';
private client = createTRPCProxyClient<TodosRouter>({
links: [
httpBatchLink({
url: 'http://localhost:3000',
}),
],
});
With this setup, tRPC will perform its magic and provide maximal type safety.
Okay, time to call some functions! Let’s start by fetching our Todos!
Call query functions
public getAllTodos(): Observable<Todo[]> {
return fromPromise(this.client.todos.query());
}
We can use our client to call the .todos.query
function. Invoking this function returns a Promise
. We can use fromProise
to convert it to an Observable
so that it fits nicely into Angular and feels similar to using the HTTPClient
.
The best thing is that we get epic IDE support due to tRPC magic!
The IDEA is aware of all the available remote functions inside our frontend code. Nice. The todos function only returns todos
and doesn’t accept any input. Let’s see if type-safety also works for mutations such as addTodo
.
Calling mutations
Calling mutations is similar to calling .query
functions. To add a new Todo, we call the .addTodo.mutate
function and pass in our new todo.
public addTodo(todo: CreateAndUpdateTodo): Observable<Todo> {
return fromPromise(this.client.addTodo.mutate(todo));
}
What if we pass in a string
instead of a todo object?
You guessed it. We get a friendly warning since the type of string is not assignable to an object of type {todo: string, done: boolean}
. Full type-safety!
Summary
tRPC is an outstanding piece of technology. It allows you to call backend functions from your front end. Using tRPC, you leverage the full power of TypeScript across the stack.
tRPC guarantees that your back and front end are never out of sync. The contract is made with TypeScript, with no code generation, just TypeScript.
Of course, you can only use tRPC if your backend is written in TypeScript and the backend code is in the same repository as your frontend code.
Do you enjoy the theme of the code preview? Explore our brand new theme plugin
Skol - the ultimate IDE theme
Northern lights feeling straight to your IDE. A simple but powerful dark theme that looks great and relaxes your eyes.
Do you enjoy the content and would like to learn more about how to ensure long term maintainability of your Angular application?
Angular Enterprise Architecture eBook
Learn how to architect a new or existing enterprise grade Angular application with a bulletproof tooling based automated architecture validation.
This will ensure that Your project stays maintainable, extendable and therefore with high delivery velocity over the whole project lifetime!
Get notified
about new blog posts
Sign up for Angular Experts Content Updates & News and you'll get notified whenever I release a new article about Angular, Ngrx, RxJs or other interesting Frontend topics!
We will never share your email with anyone else and you can unsubscribe at any time!
Responses & comments
Do not hesitate to ask questions and share your own experience and perspective with the topic
Nivek
GDE for Angular & Web Technologies
Trainer, Berater und Senior Front-End Engineer mit Schwerpunkt auf dem modernen Web. Er ist sehr erfahren in der Implementierung, Wartung und Verbesserung von Anwendungen und Kernbibliotheken für große Unternehmen.
Kevin ist ständig dabei, sein Wissen zu erweitern und zu teilen. Er unterhält mehrere Open-Source-Projekte, unterrichtet moderne Webtechnologie in Workshops, Podcasts, Videos und Artikeln. Weiter ist er ein beliebter Referent auf Konferenzen. Er schreibt für verschiedene Tech-Publikationen und war 2019 der aktivste Autor der beliebten Angular In-Depth Publikation.
58
Blog posts
2M+
Blog views
39
NPM packages
4M+
Downloaded packages
100+
Videos
15
Celebrated Champions League titles
You might also like
Check out following blog posts from Angular Experts to learn even more about related topics like Angular or TypeScript !
Top 10 Angular Architecture Mistakes You Really Want To Avoid
In 2024, Angular keeps changing for better with ever increasing pace, but the big picture remains the same which makes architecture know-how timeless and well worth your time!
Tomas Trajan
@tomastrajan
10.09.2024
15 min read
Angular Signal Inputs
Revolutionize Your Angular Components with the brand new Reactive Signal Inputs.
Kevin Kreuzer
@kreuzercode
24.01.2024
6 min read
Improving DX with new Angular @Input Value Transform
Embrace the Future: Moving Beyond Getters and Setters! Learn how to leverage the power of custom transformers or the build in booleanAttribute and numberAttribute transformers.
Kevin Kreuzer
@kreuzercode
18.11.2023
3 min read
Stärken Sie Ihr Team mit unserer umfassenden Erfahrung
Unsere Angular Experten haben viele Jahre damit verbracht, Unternehmen und Startups zu beraten, Workshops und Tutorials zu leiten und umfangreiche Open-Source-Ressourcen zu pflegen. Wir sind sehr stolz auf unsere Erfahrung im Bereich des modernen Frontends und würden uns freuen auch Ihrem Unternehmen zum Aufschwung zu verhelfen.