Learn and Teach Coding
N

Next.js API Route CRUD

Create a full CRUD API using Next.js API Routes and Prisma.

Next.jsBeginnerapicrudprismaroute-handler

Source code

import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";

// GET /api/todos — list
export async function GET() {
  const todos = await prisma.todo.findMany({ orderBy: { id: "desc" } });
  return NextResponse.json(todos);
}

// POST /api/todos — create
export async function POST(request: Request) {
  const { title } = await request.json();
  if (!title) {
    return NextResponse.json({ error: "title is required" }, { status: 400 });
  }
  const todo = await prisma.todo.create({ data: { title } });
  return NextResponse.json(todo, { status: 201 });
}

Walkthrough

Route handlers in the App Router let you build a REST API with the same file-based routing you use for pages: each exported HTTP method becomes an endpoint. Pair them with Prisma for a fully typed database client. Both route files are on the left.

1. The collection route

app/api/todos/route.ts handles the collection. Each exported function name is the HTTP verb. GET lists todos; POST reads the JSON body, validates, and creates.

// GET /api/todos
export async function GET() {
  const todos = await prisma.todo.findMany({ orderBy: { id: "desc" } });
  return NextResponse.json(todos);
}

2. Validation and status codes

POST rejects a missing title with 400, and returns 201 Created on success — proper status codes are what make it a real REST API.

export async function POST(request: Request) {
  const { title } = await request.json();
  if (!title) {
    return NextResponse.json({ error: "title is required" }, { status: 400 });
  }
  const todo = await prisma.todo.create({ data: { title } });
  return NextResponse.json(todo, { status: 201 });
}

3. The dynamic item route

app/api/todos/[id]/route.ts handles a single record. In the App Router params is a Promise, so you await it to read the id.

type Params = { params: Promise<{ id: string }> };

export async function PATCH(request: Request, { params }: Params) {
  const { id } = await params;
  const data = await request.json();
  const todo = await prisma.todo.update({ where: { id: Number(id) }, data });
  return NextResponse.json(todo);
}

4. Delete with 204

A successful delete returns 204 No Content — an empty body with the right status.

export async function DELETE(_request: Request, { params }: Params) {
  const { id } = await params;
  await prisma.todo.delete({ where: { id: Number(id) } });
  return new NextResponse(null, { status: 204 });
}

Splitting collection (/todos) from item (/todos/[id]) routes mirrors REST conventions and keeps each file focused. The complete files are in the panel.

Related examples

.NETIntermediate

.NET Repository Pattern

Implement the repository pattern with Entity Framework Core.

View example