ProNextJS
    Loading
    lesson

    BFF Pattern with gRPC (TwirpScript)

    Jack HerringtonJack Herrington

    In this example of the BFF pattern, the client is talking to the Next.js server through either server actions or REST, but the Next.js server is in turn talking to its microservice backend through gRPC. We've chosen to use TwirpScript, a gRPC equivalent, to simplify the communication between the frontend and backend services.

    TwirpScript provides a more developer-friendly way to work with gRPC, while having essentially the same architecture. Let's explore how we can set up our Next.js server to communicate with the microservice backend using TwirpScript.

    bff with grpc

    Code Walkthrough

    The code for this example is in the bff-twirp directory.

    Homepage

    There are two different gRPC services, one for priorities and one for ToDos. Both services are reside on the api-twirp server. The associated functions are in the repo/twirp-protos package, which is shared between the Next.js application and the API for Twirp.

    Like before, client headers are set, and authentication is handled. When authorized, we can make tRPC calls to interact with our microservices.

    import { auth } from "@/auth";
    import { Todo } from "@repo/todos";
    import { revalidatePath } from "next/cache";
    import { client } from "twirpscript";
    import { headers } from "next/headers";
    
    import { GetPriorities } from "@repo/twirp-protos/priorities";
    import {
      GetTodos,
      AddTodo,
      DeleteTodo,
      UpdateTodo,
    } from "@repo/twirp-protos/todos";
    
    import AuthButton from "@/components/AuthButton.server";
    
    import TodoList from "./TodoList";
    
    const API_SERVER = process.env.API_SERVER;
    
    client.baseURL = API_SERVER;
    
    function setClientHeaders() {
      client.headers = {
        Cookie: headers().get("Cookie")!,
      };
    }
    
    export default async function Home() {
      const session = await auth();
    
      setClientHeaders();
    
      const prioritiesReq = await GetPriorities({});
      const priorities: string[] = prioritiesReq.priorities;
    
      const todosReq = await GetTodos({});
      const todos: Todo[] = todosReq.todos;
    
      async function addTodoAction(
        title: string,
        priority: string,
        completed: boolean = false
      ) {
        "use server";
        const session = await auth();
        if (!session?.user?.id) throw new Error("User not authenticated");
    
        setClientHeaders();
        await AddTodo({
          title,
          priority,
          completed,
        });
    
        revalidatePath("/");
      }
    
      async function updateTodoCompletionAction(
        todoId: string,
        completed: boolean
      ) {
        "use server";
        const session = await auth();
        if (!session?.user?.id) throw new Error("User not authenticated");
    
        setClientHeaders();
        await UpdateTodo({
          id: todoId,
          completed,
        });
    
        revalidatePath("/");
      }
    
      async function deleteTodoAction(todoId: string) {
        "use server";
        const session = await auth();
        if (!session?.user?.id) throw new Error("User not authenticated");
    
        setClientHeaders();
        await DeleteTodo({
          id: todoId,
        });
    
        revalidatePath("/");
      }
    
      return (
        <main>
          <AuthButton />
          {session?.user && (
            <TodoList
              todos={todos}
              priorities={priorities}
              addTodoAction={addTodoAction}
              updateTodoCompletionAction={updateTodoCompletionAction}
              deleteTodoAction={deleteTodoAction}
            />
          )}
        </main>
      );
    }
    

    The TodoList.tsx Component

    The TodoList component is agnostic about gRPC or tRPC. It receives the priorities, to-dos, and a set of functions representing server actions as props:

    "use client";
    import { useState } from "react";
    import {
      Select,
      SelectContent,
      SelectItem,
      SelectTrigger,
      SelectValue,
    } from "@/components/ui/select";
    import { Input } from "@/components/ui/input";
    import { Checkbox } from "@/components/ui/checkbox";
    
    import { Todo } from "@repo/todos";
    import { Button } from "@/components/ui/button";
    
    export default function TodoList({
      priorities,
      todos,
      addTodoAction,
      updateTodoCompletionAction,
      deleteTodoAction,
    }: {
      priorities: string[];
      todos: Todo[];
      addTodoAction: (title: string, priority: string, completed?: boolean) => void;
      updateTodoCompletionAction: (todoId: string, completed: boolean) => void;
      deleteTodoAction: (todoId: string) => void;
    }) {
      const [priority, setPriority] = useState<string>(priorities[0]);
      const [title, setTitle] = useState<string>("");
    
      const onSubmit = async () => {
        await addTodoAction(title, priority);
      };
    
      const onSetCompleted = async (id: string, completed: boolean) => {
        await updateTodoCompletionAction(id, completed);
      };
    
      const onDelete = async (id: string) => {
        await deleteTodoAction(id);
      };
    
      return (
        <div className="mt-5">
          {todos && (
            <>
              <ul>
                {todos?.map((todo) => (
                  <li key={todo.id} className="flex gap-2 items-center mb-3">
                    <Checkbox
                      checked={todo.completed}
                      onClick={() => onSetCompleted(todo.id, !todo.completed)}
                    />
                    <div className="flex-grow">{todo.title}</div>
                    <Button variant="destructive" onClick={() => onDelete(todo.id)}>
                      Delete
                    </Button>
                  </li>
                ))}
              </ul>
            </>
          )}
          <div className="flex gap-2">
            <Select value={priority} onValueChange={(v) => setPriority(v)}>
              <SelectTrigger className="w-[180px]">
                <SelectValue placeholder="Priority" />
              </SelectTrigger>
              <SelectContent>
                {priorities.map((priority) => (
                  <SelectItem value={priority} key={priority}>
                    {priority}
                  </SelectItem>
                ))}
              </SelectContent>
            </Select>
            <Input
              placeholder="Todo"
              value={title}
              onChange={(evt) => setTitle(evt.target.value)}
            />
            <Button onClick={onSubmit}>Submit</Button>
          </div>
        </div>
      );
    }
    

    This is one of the cleanest approaches to building a BFF with a gRPC architecture, and is highly recommended if you're planning to use gRPC in your future projects.

    Transcript

    Say common backend for friend systems architecture scenario for the app router, is that the client is talking to the Next.js server through either server actions or rest, but the Next.js server is in turn talking to its microservice backend through gRPC. Now, we've chosen in this case to use a gRPC equivalent which is Torbscript. The reason being that gRPC is honestly just a hassle to set up and maintain and Torbscript which essentially has the same proto architecture and RPC architecture. It's just a lot easier to work with. Let's go check out our code and see how to have our Next.js server talk in Torbscript or gRPC to the microservice backend.

    Let's start off on our BFF twerp homepage. We have two different gRPC services, one for priorities and then one for the to-dos. That's just how you manage things in gRPC world. You tend to have multiple services that you talk to. In this case, both services are actually on the API twerp server.

    Now both this Next.js application and also the API twerp connect to the same backend package and that's repo twerp protos. So that's how you synchronize the protos between both the Next.js application as well as the API for TORP. So in the Next.js case, we import those protos, those have been converted into TypeScript using TORP script. We also get our client from TORP script. Now, let's see how we mutate our client.

    So we get our API server from our local environment, then set the base URL of our client. Then when we're about to make a request, we set the client headers in this set client headers. This is how we're going to get our next auth jot from our client all the way to our Twerp backend. What we're gonna do is just set the headers, I'm gonna set our cookie in our headers, and that's gonna send that JWT to our API Twerp, and that's gonna let our API on the backend, our Microsoft API, know who is talking to it. Speaking of that, in our homepage RSC, we're gonna get our session, that's gonna be our next auth session, and then we're gonna start making requests.

    First thing we wanna do is set our client headers. Once we've done that, we can call getPriorities to get our list of priorities. That's a gRPC request, and then we're going to do that getTodo's with no arguments, and that's going to get the todo's for the user that is specified by that cookie that's in the client headers. Then we're going to set up our server actions. Let's take a look at the addTodoServerAction.

    It's going to take title, priority, and completed. It's going to be a server action because we use user. First thing it's going to do is go and make sure that we're logged in, so we have off. Then it's going to go and use that set client headers to set the cookie properly from the incoming request. Then it's going to use the gRPC call to add the to-do.

    Now because Next.js has its idea of what's in the to-dos and the backend service has its idea of what is in the to-dos, which of course is the correct one, anytime we make a mutation we want to invalidate Next.js' cache, so that's why we use revalidatePath slash. That's going to make sure that we don't get any caching when it comes to this page. We also add server actions for updating the to-do completion. It follows the exact same pattern as well as deleting a to-do action. Once we have our server action set up we can render our JSX which includes adding the off button as well as the to-do list with all of the existing to-dos, the priorities, as well as the server actions.

    Let's take a look at that client code. The to-do list of course knows nothing about gRPC. It just simply gets given the priorities and the to-dos as well as a set of functions. Those functions happen to be things that run on the server as server actions. Those are async functions that can call to add, update, and delete the to-dos.

    In terms of state, it just holds the priority as well as the title of the new to-do that it's creating. And then all the event handlers simply just call those server actions that were sent in. It is that simple. To be honest, I've done more than my fair share of gRPC work. This is actually the cleanest architecture that I've seen when it comes to gRPC.

    So I strongly recommend that if gRPC is in your future that you use this as a template to get there.