Angular & tRPC

Maximum type safety across the entire stack. How to setup a fullstack app with Angular and tRPC.

emoji_objects emoji_objects emoji_objects
Kevin Kreuzer

Kevin Kreuzer

@kreuzercode

24.01.2023

6 min read

Angular & tRPC
share

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

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

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!

Emails may include additional promotional content, for more details see our Privacy policy.

Responses & comments

Do not hesitate to ask questions and share your own experience and perspective with the topic

Nivek - GDE for Angular & Web Technologies

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 !

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.

or