Validation asks "is the data well-formed?" Authorization asks "is this user allowed to do this to this row?" Update and Delete are where it bites hardest.
1// ❌ Anyone who knows the id can edit any task
2await prisma.task.update({ where: { id }, data });Knowing /tasks/42 exists is trivial. Without an ownership check, any logged-in user can edit anyone's data. This is IDOR (Insecure Direct Object Reference) — one of the most common real-world API bugs.
1const task = await prisma.task.findUnique({ where: { id } });
2if (!task || task.ownerId !== session.userId) {
3 return notFound(); // 404 hides existence; 403 also valid
4}
5// safe to update/deleteEven better, fold the ownership check into the write so there's no gap:
1const result = await prisma.task.updateMany({
2 where: { id, ownerId: session.userId },
3 data,
4});
5if (result.count === 0) return notFound();updateMany/deleteMany with an ownerId filter changes the row only if it belongs to the caller, and tells you whether anything matched.
Beyond ownership, real apps add roles:
1const canEdit =
2 task.ownerId === session.userId ||
3 session.role === "ADMIN";
4if (!canEdit) return forbidden();Centralize these rules in a can(user, action, resource) helper so authorization logic isn't scattered and inconsistent across endpoints.