DEV Community

Spencer Lepine
Spencer Lepine

Posted on

GET, POST, PUT & DELETE with Next.js App Router

This article presents boilerplate code for API routes using Next.js App Router (docs), for a todo-list CRUD (Create, Read, Update, Delete) application. The App Router supports both client-side and server-side rendering, allowing for complex routing and dynamic site content.

Getting Started

Prerequisites:

  • NodeJS >= v18

Find the example code on GitHub: https://github.com/spencerlepine/nextjs-app-router-todo

Alternatively, you can clone and bootstrap a Next.js project directly with this command:

npx create-next-app@latest --example nextjs-app-router "https://github.com/spencerlepine/nextjs-app-router-todo"
Enter fullscreen mode Exit fullscreen mode

Endpoints

We’ll implement the following endpoints:

GET /users/:userId/todos
POST /users/:userId/todos
PUT /users/:userId/todos/:itemId
DELETE /users/:userId/todos/:itemId
Enter fullscreen mode Exit fullscreen mode

GET

// Next.js v14 App Router
// GET /users/:userId/todos
// app/api/users/[userId]/todos/route.ts
import { NextRequest, NextResponse } from "next/server"; // v14.x.x
import { todoItemDb } from "@/lib/db";

type TodoItem = {
  id: string;
  title: string;
  completed: boolean;
};

export const GET = async (
  req: NextRequest,
  { params, query }: { params: { userId: string }; query: {} }
) => {
  try {
    const { userId } = params;
    // const { limit, page } = query; // optional

    // Query the database
    const todoItems: TodoItem[] = await todoItemDb.getAll(userId);

    return NextResponse.json({ todoItems: todoItems }, { status: 200 });
  } catch (error) {
    console.error("Error processing request:", error);
    return NextResponse.json(
      { error: "Internal server error" },
      { status: 500 }
    );
  }
};
Enter fullscreen mode Exit fullscreen mode

POST

// Next.js v14 App Router
// POST /users/:userId/todos
// app/api/users/[userId]/todos/route.ts
import { NextRequest, NextResponse } from "next/server"; // v14.x.x
import { todoItemDb } from "@/lib/db";

type TodoItem = {
  id: string;
  title: string;
  completed: boolean;
};

export const POST = async (
  req: NextRequest,
  { params }: { params: { userId: string } }
) => {
  try {
    const { userId } = params;
    const { title } = await req.json(); // request body

    if (!title) {
      return NextResponse.json(
        { error: "Invalid request body" },
        { status: 400 }
      );
    }

    // Handle your business logic here (e.g., update the database)
    const newTodoItem = await todoItemDb.createOne(userId, title);

    return NextResponse.json(newTodoItem, { status: 201 });
  } catch (error) {
    console.error("Error processing request:", error);
    return NextResponse.json(
      { error: "Internal server error" },
      { status: 500 }
    );
  }
};
Enter fullscreen mode Exit fullscreen mode

PUT

// Next.js v14 App Router
// PUT /users/:userId/todos/:itemId
// app/api/users/[userId]/todos/[itemId]/route.ts
import { NextRequest, NextResponse } from "next/server"; // v14.x.x
import { todoItemDb } from "@/lib/db";

type TodoItem = {
  id: string;
  title: string;
  completed: boolean;
};

export const PUT = async (
  req: NextRequest,
  { params }: { params: { userId: string; itemId: string } }
) => {
  try {
    const { userId, itemId } = params;
    const todoItem = await req.json(); // request body

    if (!todoItem || typeof todoItem !== "object") {
      return NextResponse.json(
        { error: "Invalid request body" },
        { status: 400 }
      );
    }

    // Handle your business logic here (e.g., update the database)
    await todoItemDb.updateOne(userId, itemId, todoItem);

    return NextResponse.json({ success: true }, { status: 200 });
  } catch (error) {
    console.error("Error processing request:", error);
    return NextResponse.json(
      { error: "Internal server error" },
      { status: 500 }
    );
  }
};
Enter fullscreen mode Exit fullscreen mode

DELETE

// Next.js v14 App Router
// DELETE /users/:userId/todos/:itemId
// app/api/users/[userId]/todos/[itemId]/route.ts
import { NextRequest, NextResponse } from "next/server"; // v14.x.x
import { todoItemDb } from "@/lib/db";

export const DELETE = async (
  req: NextRequest,
  { params }: { params: { userId: string; itemId: string } }
) => {
  try {
    const { userId, itemId } = params;

    await todoItemDb.deleteOne(userId, itemId);

    return NextResponse.json(
      { message: "Todo item deleted successfully" },
      { status: 200 }
    );
  } catch (error) {
    console.error("Error processing request:", error);
    return NextResponse.json(
      { error: "Internal server error" },
      { status: 500 }
    );
  }
};
Enter fullscreen mode Exit fullscreen mode

Data Fetching & Rendering

Now with the endpoints built, we're able to make requests to our backend. App Router will use server-side rendering by default.

Client-side Rendering

If you have dynamic content or additional API requests from the frontend, add "use client"; to pages (or components) and use fetch or axios .

// Client-side rendering (CSR) - Next.js v14 App Router
// src/app/page.tsx

"use client";

export default async function HomePage() {
  const apiUrl =
    process.env.NEXT_PUBLIC_API_URL || "<http://localhost:3000/api>";
  const data = await fetch(`${apiUrl}/v1/user/mockUser123/todos`);
  const allTodoItems = await data.json();
  return (
    <ul>
      {allTodoItems.map((item) => (
        <li key={item.id}>{item.title}</li>
      ))}
    </ul>
  );
}
Enter fullscreen mode Exit fullscreen mode

Static Server-side Rendering

For static pages, use the default Next.js server-side rendering. This will be generated at build-time.

// Server-side rendering (SSR) - Next.js v14 App Router
// src/app/page.tsx

import { db, todoItems } from "../../lib/db";

export default async function HomePage() {
  const allTodoItems = await db.select().from(todoItems);
  return (
    <ul>
      {allTodoItems.map((item) => (
        <li key={item.id}>{item.title}</li>
      ))}
    </ul>
  );
}
Enter fullscreen mode Exit fullscreen mode

Dynamic Server-side Rendering

To dynamically render pages (or components) at request time, set and export the dynamic configuration option.

// Server-side rendering (SSR) - Next.js v14 App Router
// src/app/page.tsx

import { db, todoItems } from "../../lib/db";

// Render page request time
export const dynamic = "force-dynamic";

export default async function HomePage() {
  const allTodoItems = await db.select().from(todoItems);
  return (
    <ul>
      {allTodoItems.map((item) => (
        <li key={item.id}>{item.title}</li>
      ))}
    </ul>
  );
}
Enter fullscreen mode Exit fullscreen mode

Authentication

To secure page routes and endpoints in your application, consider using NextAuth.js (repository). You can either create your own custom login system and database or integrate with popular auth providers like Auth0 or Google.

Conclusion

In this guide we covered the basic endpoints for CRUD (Create, Read, Update, Delete) API routes, along with data fetching for the frontend. This will be the bread and butter for most applications. To started today, find the example code over on GitHub: https://github.com/spencerlepine/nextjs-app-router-todo

Thanks for reading. If you found the article useful don’t forget to clap. If you have any questions, feel free to reach out to me. Connect with me and follow my journey on 👉 👉 LinkedInGitHub

Top comments (0)