Let's stand up the routes. We'll show two popular stacks; the shape is identical.
1import express from "express";
2const app = express();
3app.use(express.json());
4
5// CREATE
6app.post("/api/v1/tasks", createTask);
7// READ (list)
8app.get("/api/v1/tasks", listTasks);
9// READ (one)
10app.get("/api/v1/tasks/:id", getTask);
11// UPDATE (partial)
12app.patch("/api/v1/tasks/:id", updateTask);
13// DELETE
14app.delete("/api/v1/tasks/:id", deleteTask);
15
16app.listen(3000);A handler signature:
1async function getTask(req, res) {
2 const task = await prisma.task.findUnique({ where: { id: req.params.id } });
3 if (!task) return res.status(404).json({ error: "Task not found" });
4 res.json({ data: task });
5}The App Router maps files to routes. A route.ts exports functions named after HTTP verbs.
1// app/api/v1/tasks/route.ts
2import { NextRequest, NextResponse } from "next/server";
3import { prisma } from "@/lib/prisma";
4
5export async function GET() {
6 const tasks = await prisma.task.findMany();
7 return NextResponse.json({ data: tasks });
8}
9
10export async function POST(req: NextRequest) {
11 const body = await req.json();
12 const task = await prisma.task.create({ data: body });
13 return NextResponse.json({ data: task }, { status: 201 });
14}1// app/api/v1/tasks/[id]/route.ts
2export async function GET(_req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
3 const { id } = await params;
4 const task = await prisma.task.findUnique({ where: { id } });
5 if (!task) return NextResponse.json({ error: "Not found" }, { status: 404 });
6 return NextResponse.json({ data: task });
7}Keep handlers thin. Push database logic into a service layer:
Route handler → parse & validate request, choose status code
Service → business logic + database queries
ORM / SQL → data access
This separation makes each piece testable and keeps the same logic reusable from a CLI, a job, or another route.