ProNextJS
    lesson

    The API Route Variant of Local Systems

    Jack HerringtonJack Herrington

    Let's look at the variation of the local systems architecture where our Next.js application communicates with a local API route instead of relying on server actions.

    This approach is particularly useful when you have multiple clients, such as a web browser, mobile app, or desktop app, that need to access the same API.

    the local api architecture

    The to-do data is stored in an in-memory database, and we'll use Next.js API routes to handle client requests.

    Client-Side Implementation

    Let's start by examining the client-side code for our to-do list inside of the local-api directory.

    page.tsx

    In the homepage component, page.tsx, we use React Server Components (RSC) to fetch data. First, we check if the user is logged in using getAuthorization. We then retrieve a hardcoded list of priorities. If the user is logged in, we fetch the to-dos:

    // inside of app/page.tsx
    import { auth } from "@/auth";
    import { Todo, PRIORITIES, getTodos } from "@repo/todos";
    
    import AuthButton from "@/components/AuthButton.server";
    
    import TodoList from "./TodoList";
    
    export default async function Home() {
      const session = await auth();
    
      const priorities: string[] = PRIORITIES;
    
      const todos: Todo[] = session?.user?.id
        ? await getTodos(session?.user?.id)
        : [];
    
      return (
        <main>
          <AuthButton />
          {session?.user && <TodoList todos={todos} priorities={priorities} />}
        </main>
      );
    }
    

    The homepage then renders the TodoList component, passing in the fetched to-dos and priorities:

    TodoList.tsx

    The ToDoList component handles displaying and managing the to-dos. It retrieves the todos and priorities from its props and maintains a local copy of the to-dos.

    "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>
      );
    }
    

    The TodoList component interacts with the various API routes using fetch to add, update, and delete to-dos. After each mutation, it calls updateTodos to re-fetch the to-dos and update the UI.

    APIs

    Now, let's shift our attention to the API routes responsible for handling these requests. These routes are located in the app/api directory.

    Getting All To-Dos

    The /api/todos route handles retrieving the list of to-dos. It first checks if the user is authenticated. If authenticated, it returns the list of to-dos. If not, it returns an error:

    // inside of app/api/todos/route.ts
    
    import { auth } from "@/auth";
    
    import { NextResponse } from "next/server";
    
    import { getTodos } from "@repo/todos";
    
    export async function GET() {
      const session = await auth();
    
      if (!session?.user?.id) return NextResponse.error();
    
      return NextResponse.json(await getTodos(session?.user?.id));
    }
    

    Creating a New To-Do

    The /api/todo route with a POST request creates a new to-do. First, it verifies if the user is logged in. If so, it adds the new to-do to the database, revalidates the / route to reflect the changes, and returns the newly created to-do.

    // inside of app/api/todo/route.ts
    
    import { revalidatePath } from "next/cache";
    import { NextResponse } from "next/server";
    
    import { auth } from "@/auth";
    import { getTodoById, deleteTodo, updateTodoCompletion } from "@repo/todos";
    
    export async function GET(
      req: Request,
      { params: { id } }: { params: { id: string } }
    ) {
      const session = await auth();
    
      if (!session?.user?.id) return NextResponse.error();
    
      return NextResponse.json(await getTodoById(session?.user?.id, id));
    }
    
    export async function PUT(
      req: Request,
      { params: { id } }: { params: { id: string } }
    ) {
      const session = await auth();
    
      if (!session?.user?.id) return NextResponse.error();
    
      const body = await req.json();
      const out = await updateTodoCompletion(session?.user?.id, id, body.completed);
      revalidatePath("/");
      return NextResponse.json(out);
    }
    
    export async function DELETE(
      req: Request,
      { params: { id } }: { params: { id: string } }
    ) {
      const session = await auth();
    
      if (!session?.user?.id) return NextResponse.error();
    
      const out = await deleteTodo(session?.user?.id, id);
      revalidatePath("/");
      return NextResponse.json(out);
    }
    

    Similarly, we have routes for getting a single to-do (/api/todo/:id with GET), updating a to-do (/api/todo/:id with PUT), and deleting a to-do (/api/todo/:id with DELETE).

    When to Use This Architecture

    By separating the client-side logic from the API endpoints, this approach provides flexibility and scalability, especially when dealing with multiple client applications.

    While server actions excel in scenarios with a single client (the web browser), the API route approach shines when you need to share your API with other clients or platforms.

    Transcript

    This is the API variant of the local systems architecture. It is very simple. You have a single Next.js application. It would talk directly to a database. Of course, in this case, it's an in-memory database of Todos.

    When it gets a page request, it makes a request to that Todos in-memory database, and then sends that data to a client component. That client component then makes any mutations to the to-dos using REST APIs as opposed to server actions. Those APIs are implemented using AppWriter or RouteHandler. Let's go take a look at the code. In the home page RSC we're first going to get our authorization, find out if we're logged in.

    We're then going to get our priorities. Those are hard-coded and if we are logged in we're gonna then go and get our to-dos. In the page itself we're gonna render the Auth button and then if we're logged in we're gonna render the to-dos. Let's take a look at our to-do list client component. We take the priorities and the to-dos as properties.

    We then keep a local copy of the to-dos, initialize to that initial set of to-dos, as well as two mutable values for the title of the new to-do as well as its priority. Next up we create some event handlers. Those are going to talk to our local slash API slash to do routes. So to update the list of to do's we do a fetch against API to do's and then we set the to do's to the result. On adding a new to do we post against API to do.

    We await that and then when that's done we update our list of to do's. To set our unset completed, we put against API todo with the given ID, and then to delete a todo, we simply just run the delete method against that particular todo and of course, again, update the list of todos. The JSX then formats all those to-dos as well as having the input section allows you to input a new to-do. Let's go take a look at the APIs. Under the API directory we have the auth directory that's handling next auth but we have our APIs under slash API to-dos and slash API to-do.

    I'm not going to go through all of these. We'll take a look at a select few. To get the list of to do's, we're first going to get the off. That of course is going to turn us into a dynamic route because we're taking a look at the request. If we don't see any session, then we're going to return error.

    Otherwise, we're just going to return the list of all the to-dos. Over in the API to-do route, we handle the post to go and create a new to-do. So in this case, we're going to export a post function. We're going to find out who you are, get your session, make sure that you are logged in because you can't create a to-do without being logged in. Then we're going to use that in an add to-do and that will revalidate the slash path and output the new to-do.

    You can get a single to-do by doing a get on API to-do with the ID. Now to toggle the to-do We sit on the put verb, we do our usual session lookup, and then we make that mutation change and revalidate path, and that's about it. This API variant of the local setup is particularly good if you have clients other than just the web browser. If you say have a mobile client or a desktop client or something else that needs to access that API in addition to the client then this would be a good variant for that. If I didn't have that I would probably stick with the server action version.