ProNextJS
    Loading
    lesson

    BFF Architecture with GraphQL

    Jack HerringtonJack Herrington

    This time we'll look at how to build a Backend for Frontend (BFF) using GraphQL within a Next.js application. This architecture is particularly useful when you have a GraphQL-based microservice architecture but prefer to avoid exposing GraphQL directly to the client.

    Our setup involves a client, a BFF GraphQL Next.js App Router application, and the GraphQL backend. The client interacts with the BFF via REST, which then uses GraphQL to communicate with the api-gql backend. This separation offers a layer of security, mitigating vulnerabilities associated with exposing GraphQL endpoints publicly.

    the bff-gql architecture

    Code Walkthrough

    Let's break down the code and see how this architecture comes together.

    Homepage

    On our homepage, we begin by initializing a client and a context for our Todo API. We retrieve the session from NextAuth and make a single GraphQL request to fetch both priorities and ToDos:

    // inside bff-gql/src/app/page.tsx
    
    import { gql } from "@apollo/client";
    
    import { auth } from "@/auth";
    import { Todo } from "@repo/todos";
    
    import AuthButton from "@/components/AuthButton.server";
    
    import TodoList from "./TodoList";
    
    import { client, getContext } from "@/todo-api";
    
    export default async function Home() {
      const session = await auth();
    
      const { data } = await client.query({
        query: gql`
          query {
            getPriorities
            getTodos {
              id
              title
              priority
              completed
            }
          }
        `,
        context: getContext(),
      });
      const priorities: string[] = data.getPriorities;
      const todos: Todo[] = data.getTodos;
    
      return (
        <main>
          <AuthButton />
          {session?.user && <TodoList todos={todos} priorities={priorities} />}
        </main>
      );
    }
    

    The Todo API

    The Todo API code is inside of todo-api.ts. We initialize an Apollo client, which connects to our API GraphQL backend at the /graphql endpoint. We're using a context to inject the cookie containing the NextAuth authorization token:

    // inside todo-api.ts
    
    import { headers } from "next/headers";
    
    import { ApolloClient, InMemoryCache } from "@apollo/client";
    
    export const client = new ApolloClient({
      uri: `${process.env.API_SERVER}/graphql`,
      cache: new InMemoryCache({
        resultCaching: false,
      }),
    });
    
    export function getContext() {
      return {
        headers: {
          Cookie: headers().get("Cookie")!,
        },
      };
    }
    

    Remember, for this example app the JWT is unencrypted for illustrative purposes. In a production environment, you should use a more secure JWT implementation!

    The TodoList Component

    Our ToDo list component is a client-side component that tracks internal state as well as interacting with REST API routes on the Next.js server to fetch, create, update, and delete ToDos:

    // inside TodoList.tsx
    
    "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: initialTodos,
    }: {
      priorities: string[];
      todos: Todo[];
    }) {
      const [todos, setTodos] = useState<Todo[]>(initialTodos);
      const [priority, setPriority] = useState<string>(priorities[0]);
      const [title, setTitle] = useState<string>("");
    
      const updateTodos = async () => {
        const res = await fetch("/api/todos", {
          cache: "no-cache",
        });
        setTodos(await res.json());
      };
    
      const onSubmit = async () => {
        await fetch("/api/todo", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            title,
            priority,
            completed: false,
          }),
        });
        updateTodos();
      };
    
      const onSetCompleted = async (id: string, completed: boolean) => {
        await fetch(`/api/todo/${id}`, {
          method: "PUT",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify({ completed }),
        });
        updateTodos();
      };
    
      const onDelete = async (id: string) => {
        await fetch(`/api/todo/${id}`, {
          method: "DELETE",
        });
        updateTodos();
      };
    
      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>
      );
    }
    
    

    API Routes

    The API routes handle communication between the client-side component and the API GraphQL backend.

    The todos Route

    We'll start by looking at the todos route that fetches the list of ToDos. The Apollo client is used to execute a GraphQL query to fetch the ToDos for the current user that's stored in the context:

    // inside api/todos/route.ts
    
    import { NextResponse } from "next/server";
    
    import { gql } from "@apollo/client";
    
    import { client, getContext } from "@/todo-api";
    
    export async function GET() {
      const { data } = await client.query({
        query: gql`
          query {
            getTodos {
              id
              title
              priority
              completed
            }
          }
        `,
        context: getContext(),
      });
    
      return NextResponse.json(data.getTodos);
    }
    

    The todo Route

    Inside of api/todo/route.ts, we have the todo route that handles creating, updating, and deleting ToDos. The route uses the Apollo client to execute GraphQL mutations to interact with the API GraphQL backend:

    import { revalidatePath } from "next/cache";
    import { NextResponse } from "next/server";
    import { gql } from "@apollo/client";
    
    import { client, getContext } from "@/todo-api";
    
    export async function POST(req: Request) {
      const body = await req.json();
      const { data } = await client.mutate({
        mutation: gql`
          mutation ($title: String!, $priority: String!) {
            addTodo(title: $title, priority: $priority) {
              id
              title
              priority
              completed
            }
          }
        `,
        variables: { title: body.title, priority: body.priority },
        context: getContext(),
      });
      revalidatePath("/");
      return NextResponse.json(data.addTodo);
    }
    

    The todo/[id] Route

    The API route for working with specific ToDos is defined in api/todo/[id]/route.ts. It handles updating the completion status and deleting a specific ToDo:

    import { revalidatePath } from "next/cache";
    import { NextResponse } from "next/server";
    import { gql } from "@apollo/client";
    
    import { client, getContext } from "@/todo-api";
    
    export async function GET(
      req: Request,
      { params: { id } }: { params: { id: string } }
    ) {
      const { data } = await client.query({
        query: gql`
          query ($id: ID!) {
            getTodoById(id: $id) {
              id
              title
              priority
              completed
            }
          }
        `,
        variables: { id },
        context: getContext(),
      });
    
      return NextResponse.json(data.getTodoById);
    }
    
    export async function PUT(
      req: Request,
      { params: { id } }: { params: { id: string } }
    ) {
      const body = await req.json();
      const { data } = await client.mutate({
        mutation: gql`
          mutation ($id: ID!, $completed: Boolean!) {
            updateTodoCompletion(id: $id, completed: $completed) {
              id
              title
              priority
              completed
            }
          }
        `,
        variables: { id, completed: body.completed },
        context: getContext(),
      });
      revalidatePath("/");
      return NextResponse.json(data.updateTodoCompletion);
    }
    
    export async function DELETE(
      req: Request,
      { params: { id } }: { params: { id: string } }
    ) {
      await client.mutate({
        mutation: gql`
          mutation ($id: ID!) {
            removeTodoById(id: $id)
          }
        `,
        variables: { id },
        context: getContext(),
      });
      revalidatePath("/");
      return NextResponse.json({ success: true });
    }
    

    Notice how we use revalidatePath("/") after each mutation to invalidate the Next.js cache, ensuring data consistency.

    This is the architecture to use if you have a GraphQL backend.

    Transcript

    This is a GraphQL variant in the backend for frontend systems architecture. The idea here is that the client is going to talk to our BFF GQL Next.js AppRouter application, which is in turn going to service requests from the client using GraphQL to the API GQL backend. This is the architecture you're going to want to use if your microservice architecture is based on GraphQL, but you don't want to use GraphQL on the front end from the client to the server. Now, that's actually really handy because GraphQL has some known vulnerabilities when it comes to putting GraphQL servers out on the open internet. GraphQL can be exploited in terms of making arbitrary requests.

    So in this case you have a controlled REST API between the client and the server, but you have a GraphQL API between the Next.js server and its microservices. Let's go take a look at how to do this in the code. We'll again start on our homepage where we first start off by getting a client and a context from our To Do API. We'll take a look at that in just a second. We get our session from NextAuth, and then we make a single GraphQL request that both gets the priorities as well as the to-dos.

    We then deconstruct the priorities and the to-dos out of that response, and then we render it. So we're going to create the off button first, and then render the to-do list with the to-dos and the priorities that we just got. Let's go take a look at our to-do API. So we're using the Apollo client for GraphQL in this case. We are initializing an Apollo client with our GQL API server and its slash GraphQL endpoint and then we have a getContactHelper function which just allows us to add our headers in this case we're just adding on that cookie so We're going to pass the next auth authorization, which is an HTTP only cookie, back to the API GQL.

    The API GQL is then going to get that JWT in that cookie. It's going to use the local auth function to decode that unencrypted JWT and then use that as a way to understand who it's talking to and get the right set of to-dos for that person. Again, I emphasize unencrypted JWT because you do not want to have this system running in production. In that configuration, we're using an unencrypted JWT at this point in this example simply because it's just easier to do than to have NextAuth in all of these applications and let NextAuth do that JWT work. We're going to use a much simplified Jot that is unencrypted.

    Now that we've got our data from our backend GQL service, let's go and write that in our To-Do List. Because it's interactive, our To-Do List component is of course a client component that takes the priorities as well as the initial set of To-Do's. Now, it's going to manage that list of to-dos as state internally. It's also going to have some extra state to manage the new to-do, including the title and the priority. To get the list of to-dos, it's going to use its own REST API internally.

    So it's going to fetch from API to-dos. That's on the Next.js server itself. That's an API route. We'll take a look at that in a second. It's going to get the list of to-dos and then set the to-dos.

    To create a new to-do, it fetches again against a local endpoint API to-do with a method post and the JSON encoded body pleaded, it then updates the list of to dos. We have an on set completed that handles clicking on the completed checkbox and an on delete that handles handling the delete button. And then we format the list of to-dos as well as the input fields. Let's go take a look at the API because that's where it really gets interesting. So we go in the source API directory, we've got the auth directory that handles the next auth authorization.

    Let's go take a look at how we get the list of to-dos. So over in our route handler for API to-dos, this is where we're going to get the list of to-dos for the given user. To do that, we use our Apollo client to execute a query against our API GQL backend, and we use that context to send along the cookie that is our identity. So we're proxying through the cookie to that API GQL backend. Let's go take a look at how to do a mutation.

    That would be over in API to do, in the route where we create a new to do. In this case, and in all cases, we're using the Apollo GQL client to talk between the Next.js server and the API GQL. We're going to get the body of our request. We're then going to run a GQL mutation against that API GQL backend. We're going to send along the title on a priority, and we're going to get back a completed to do.

    Again, we're going to proxy through that cookie using the context. We're going to do the exact same things when it comes to updating the to do completion, as well as deleting a specific to do. The important part in all the mutation cases is because Next.js has its own caching layer. We want to make sure that when we make any mutations against the Todoist database, that we invalidate the Next.js cache. So we're going to use revalidate path slash to invalidate the Next.js cache, so that the next time we ask for to-dos, we will go and send the request all the way back to the API GQL backend.

    This kind of systems architecture is really good, obviously, if you have a GraphQL microservice backend.