Search code examples
expressprisma

How should I handle ownership/existence checks using PrismaORM for nested REST api resources?


I'm working on a REST API with express to keep track of climbs you've completed. A user has a one to many relation with Climbs, and climb has a one to many relationship with notes.

I've chosen to make notes a nested resource within the climb router, as a note can't exist without a climb. This leads to making multiple checks at each level that a) when trying to update/delete a note the climb belongs to the current user, and b) that the note belongs to the climb, as well as them existing.

const deleteNote = expressAsyncHandler(async (req, res) => {

  const climb = await PrismaClient.climb.findUnique({
    where: {
      id: climbId,
    },
  });

  if (climb.userId !== req.user.id) {
    res.status(403);
    throw new ApiException(
      'You are not authorized to view notes for this climb',
      403
    );
  }

  const note = await PrismaClient.note.findUnique({
    where: {
      id: noteId,
    },
  });

  if (!note) {
    throw new ApiException('Note not found', 404);
  }

  if (note.climbId !== climb.id) {
    throw new ApiException(
      'You are not authorized to view notes for this climb',
      403
    );
  }

  await PrismaClient.note.delete({
    where: {
      id: noteId
    },
  });

  return res.status(204).end();
});

Making these checks gives me more granular control over the HTTP response codes I send back to the client, but there's also another way to do it like so:

  const note = await PrismaClient.note.findFirst({
    where: {
      id: parseInt(noteId),
      climb: {
        id: parseInt(climbId),
        userId: req.user.id, // Ensure the climb belongs to the authenticated user
      },
    },
  });

  if (!note) {
    res.status(404);
    throw new ApiException('Note not found or unauthorized', 404);
  }

This approach leverages joins on the database level, and is slightly more complicated query as opposed to many small ones. This way though, I lose the capability to respond with a 403/404/etc depending on the scenario.

My question is, which of the two is better, or is it on a case by case basis?


Solution

  • I believe there is no one objectively correct way.

    To me, the question comes down to whether the API consumer cares about the different error codes. Will there be a different error message shown to the user depending on the result? If yes, then your first example is probably the way to go even if it is very verbose.

    If there's no need for a detailed error message, then the second, concise solution would be my choice. I would return code 400 "Bad request" (MDN).

    One suggestion regarding the first option, though: you can use a relation query (Prisma documentation) and fetch all the data you need at once. If I got the relations right, you can do something like:

    const climb = await PrismaClient.note.findUnique({
      where: { id: noteId },
      include: { climb: true, user: true },
    });
    
    if (!note) {
      throw new ApiException('Note not found', 404);
    }
    
    if (note.user.id !== userId) {
      throw new ApiException('You are not authorized to view notes for this climb', 403);
    }
    
    // Go on and delete the note ...