Let's build POST /tasks properly — not just "it works", but production-shaped.
1// app/api/v1/tasks/route.ts
2import { NextRequest, NextResponse } from "next/server";
3import { z } from "zod";
4import { prisma } from "@/lib/prisma";
5import { getSession } from "@/lib/auth";
6
7const CreateTask = z.object({
8 title: z.string().min(1).max(200),
9 description: z.string().max(2000).optional(),
10 dueDate: z.coerce.date().optional(),
11});
12
13export async function POST(req: NextRequest) {
14 // 1. Authenticate
15 const session = await getSession();
16 if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
17
18 // 2. Validate
19 const parsed = CreateTask.safeParse(await req.json());
20 if (!parsed.success) {
21 return NextResponse.json(
22 { error: "Validation failed", issues: parsed.error.flatten() },
23 { status: 400 },
24 );
25 }
26
27 // 3. Create — owner comes from the session, never the body
28 const task = await prisma.task.create({
29 data: { ...parsed.data, ownerId: session.userId },
30 });
31
32 // 4. Respond 201 with a Location header
33 return NextResponse.json(
34 { data: task },
35 { status: 201, headers: { Location: `/api/v1/tasks/${task.id}` } },
36 );
37}201, the created resource, and a Location header.1// ❌ ownerId from the body — a user could create tasks for someone else
2data: { ...body }
3
4// ✅ ownerId from the authenticated session
5data: { ...parsed.data, ownerId: session.userId }This single rule prevents a whole class of "mass assignment" vulnerabilities, covered fully in Chapter 7.
Always return the created row (including its generated id and timestamps) so the client doesn't need a follow-up read.