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

Jan 24, 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 think that your teammates or organization could benefit from more direct support?

Angular Enterprise Architecture Ebook

Angular Enterprise Architecture Ebook

Learn how to architect and scaffold a new enterprise grade Angular application with clean, maintainable and extendable architecture in almost no time!

Lots of actionable tips and pros & cons of specific decisions based on the extensive experience!

Get notified
about new blog posts

Sign up for Angular Experts Content Updates & News and you'll get notified whenever we 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.
Kevin Kreuzer - GDE for Angular & Web Technologies

Kevin Kreuzer

Google Developer Expert (GDE)
for Angular & Web Technologies

A trainer, consultant, and senior front-end engineer with a focus on the modern web, as well as a Google Developer Expert for Angular & Web technologies. He is deeply experienced in implementing, maintaining and improving applications and core libraries on behalf of big enterprises worldwide.

Kevin is forever expanding and sharing his knowledge. He maintains and contributes to multiple open-source projects and teaches modern web technologies on stage, in workshops, podcasts, videos and articles. He is a writer for various tech publications and was 2019’s most active Angular In-Depth publication writer.

58

Blog posts

2M

Blog views

23

NPM packages

3M+

Downloaded packages

39

Videos

14

Celebrated Champions League titles

Responses

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

You might also like

Check out following blog posts from Angular Experts to learn even more about related topics like Angular or TypeScript !

Empower your team with our extensive experience

Angular Experts have spent many years consulting with enterprises and startups alike, leading workshops and tutorials, and maintaining rich open source resources. We take great pride in our experience in modern front-end and would be thrilled to help your business boom