ProNextJS
    Loading
    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