GET /tasks/:id looks trivial, but doing it right covers existence, authorization, and shaping.
1export async function GET(
2 _req: NextRequest,
3 { params }: { params: Promise<{ id: string }> },
4) {
5 const { id } = await params;
6 const session = await getSession();
7 if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
8
9 const task = await prisma.task.findUnique({
10 where: { id },
11 include: { owner: { select: { id: true, name: true } }, tags: true },
12 });
13
14 // Exists?
15 if (!task) return NextResponse.json({ error: "Not found" }, { status: 404 });
16
17 // Allowed?
18 if (task.ownerId !== session.userId) {
19 return NextResponse.json({ error: "Not found" }, { status: 404 });
20 }
21
22 return NextResponse.json({ data: task });
23}404.403 or, to hide existence, 404.select away anything sensitive.include adds whole related records.select picks exact fields and excludes the rest.Use select on anything with secrets (users, payment methods) so a password hash never rides along.
When you later list tasks with their owners, fetch the relation in one query (include), not one query per row. The ORM batches it into a single JOIN-style fetch. We tune this in Chapter 8.