Next.js API Route CRUD
Create a full CRUD API using Next.js API Routes and Prisma.
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
.NET Repository Pattern
Implement the repository pattern with Entity Framework Core.
View example