# How to Implement CRUD Operations for Article Comments in the RealWorld API

> Learn how to implement article comment CRUD operations in the RealWorld API. Discover best practices for REST handlers authentication authorization using Prisma.

- Repository: [Thinkster/realworld](https://github.com/gothinkster/realworld)
- Tags: how-to-guide
- Published: 2026-02-28

---

**The RealWorld API implements article comment CRUD through REST-style route handlers using Prisma for persistence, `definePrivateEventHandler` for authentication, and explicit authorization checks to ensure only authors can delete their comments.**

The gothinkster/realworld repository provides a production-ready backend implementation demonstrating how to handle nested resources like comments on articles. This guide explains the recommended approach for implementing CRUD operations for article comments in the RealWorld API, based on the actual Nuxt 3 and Prisma implementation that powers the specification reference backend.

## Architecture Overview

The RealWorld API organizes comment functionality into distinct layers that separate routing, business logic, authorization, and data persistence. Understanding this structure is essential for extending or modifying the comment system.

### Route Layer and Endpoints

The API exposes three REST endpoints for comment management, implemented as Nitro server routes in the `apps/api/server/routes/api/articles/[slug]/comments/` directory:

- `POST /api/articles/:slug/comments` – Creates a new comment on the specified article
- `GET /api/articles/:slug/comments` – Retrieves all comments for an article (publicly accessible)
- `DELETE /api/articles/:slug/comments/:id` – Removes a specific comment (author-only)

These file-based routes map directly to handler files: [`index.post.ts`](https://github.com/gothinkster/realworld/blob/main/index.post.ts), [`index.get.ts`](https://github.com/gothinkster/realworld/blob/main/index.get.ts), and `[id].delete.ts` respectively.

### Handler Middleware and Authentication

All mutation handlers wrap their logic with `definePrivateEventHandler` imported from `~/auth-event-handler`. This middleware validates JWT tokens and injects an `auth` object containing the authenticated user's ID into the handler context. The GET handler explicitly disables authentication requirements by passing `{ requireAuth: false }` as the second argument, allowing public access to comment threads.

### Authorization Logic

The deletion endpoint implements resource-level authorization by querying the comment with the author's ID and comparing it against the authenticated user's ID from the `auth` object. When the IDs do not match, the handler throws a `403` `HttpException`, preventing users from deleting comments they did not create.

### Persistence with Prisma

The codebase uses a singleton `PrismaClient` accessed via `usePrisma()` defined in [`apps/api/server/utils/prisma.ts`](https://github.com/gothinkster/realworld/blob/main/apps/api/server/utils/prisma.ts). Handlers perform CRUD operations using Prisma's type-safe `create`, `findUnique`, `findFirst`, and `delete` methods. The `Comment` model, declared in [`apps/api/server/models/comment.model.ts`](https://github.com/gothinkster/realworld/blob/main/apps/api/server/models/comment.model.ts), includes relations to both `Article` and `Author`, enabling efficient nested queries.

## Implementing Comment Endpoints

The recommended implementation follows a consistent pattern across all operations: validate input, query or mutate via Prisma, map the result to the API specification format, and return. This ensures type safety and consistent error handling using `HttpException` from [`apps/api/server/models/http-exception.model.ts`](https://github.com/gothinkster/realworld/blob/main/apps/api/server/models/http-exception.model.ts).

### Creating Comments (POST)

The creation handler validates the request body, links the comment to both the article (via slug lookup) and the authenticated author, then returns the enriched comment object with author details.

```typescript
// apps/api/server/routes/api/articles/[slug]/comments/index.post.ts
import HttpException from "~/models/http-exception.model";
import { definePrivateEventHandler } from "~/auth-event-handler";

export default definePrivateEventHandler(async (event, { auth }) => {
  const { comment } = await readBody(event);
  const slug = getRouterParam(event, "slug");

  if (!comment.body) {
    throw new HttpException(422, { errors: { body: ["can't be blank"] } });
  }

  const article = await usePrisma().article.findUnique({
    where: { slug },
    select: { id: true },
  });

  const createdComment = await usePrisma().comment.create({
    data: {
      body: comment.body,
      article: { connect: { id: article?.id } },
      author: { connect: { id: auth.id } },
    },
    include: {
      author: {
        select: {
          username: true,
          bio: true,
          image: true,
          followedBy: true,
        },
      },
    },
  });

  return {
    comment: {
      id: createdComment.id,
      createdAt: createdComment.createdAt,
      updatedAt: createdComment.updatedAt,
      body: createdComment.body,
      author: {
        username: createdComment.author.username,
        bio: createdComment.author.bio,
        image: createdComment.author.image,
        following: createdComment.author.followedBy.some(
          (follow: any) => follow.id === auth.id
        ),
      },
    },
  };
});

```

### Retrieving Comments (GET)

The retrieval handler queries comments through the article relation and filters results based on authentication status. It returns an array of comments enriched with author profiles and computed `following` status.

```typescript
// apps/api/server/routes/api/articles/[slug]/comments/index.get.ts
import { definePrivateEventHandler } from "~/auth-event-handler";

export default definePrivateEventHandler(
  async (event, { auth }) => {
    const slug = getRouterParam(event, "slug");

    const queries = [{ author: { demo: true } }];
    if (auth?.id) {
      queries.push({ author: { id: auth.id } });
    }

    const comments = await usePrisma().article.findUnique({
      where: { slug },
      include: {
        comments: {
          where: { OR: queries },
          select: {
            id: true,
            createdAt: true,
            updatedAt: true,
            body: true,
            author: {
              select: {
                username: true,
                bio: true,
                image: true,
                followedBy: true,
              },
            },
          },
        },
      },
    });

    const result = comments?.comments.map((comment: any) => ({
      ...comment,
      author: {
        username: comment.author.username,
        bio: comment.author.bio,
        image: comment.author.image,
        following: comment.author.followedBy.some(
          (follow: any) => follow.id === auth.id
        ),
      },
    }));

    return { comments: result };
  },
  { requireAuth: false }
);

```

### Deleting Comments (DELETE)

The deletion handler performs an explicit ownership check before executing the Prisma `delete` operation, ensuring users can only remove their own contributions.

```typescript
// apps/api/server/routes/api/articles/[slug]/comments/[id].delete.ts
import HttpException from "~/models/http-exception.model";
import { definePrivateEventHandler } from "~/auth-event-handler";

export default definePrivateEventHandler(async (event, { auth }) => {
  const id = Number(getRouterParam(event, "id"));

  const comment = await usePrisma().comment.findFirst({
    where: {
      id,
      author: { id: auth.id },
    },
    select: {
      author: {
        select: {
          id: true,
          username: true,
        },
      },
    },
  });

  if (!comment) {
    throw new HttpException(404, {});
  }

  if (comment.author.id !== auth.id) {
    throw new HttpException(403, {
      message: "You are not authorized to delete this comment",
    });
  }

  await usePrisma().comment.delete({ where: { id } });
});

```

## Key Implementation Files

These source files define the core infrastructure for comment CRUD operations in the RealWorld API:

- `apps/api/server/routes/api/articles/[slug]/comments/index.post.ts` – Handles comment creation with body validation and author linking
- `apps/api/server/routes/api/articles/[slug]/comments/index.get.ts` – Handles public comment retrieval with author enrichment
- `apps/api/server/routes/api/articles/[slug]/comments/[id].delete.ts` – Handles secure deletion with authorization checks
- [`apps/api/server/utils/prisma.ts`](https://github.com/gothinkster/realworld/blob/main/apps/api/server/utils/prisma.ts) – Provides the singleton `usePrisma()` client
- [`apps/api/server/models/comment.model.ts`](https://github.com/gothinkster/realworld/blob/main/apps/api/server/models/comment.model.ts) – TypeScript interfaces for comment data structures
- [`apps/api/server/models/http-exception.model.ts`](https://github.com/gothinkster/realworld/blob/main/apps/api/server/models/http-exception.model.ts) – Standardized error response helper
- [`apps/api/server/auth-event-handler.ts`](https://github.com/gothinkster/realworld/blob/main/apps/api/server/auth-event-handler.ts) – Middleware for JWT validation and auth context injection

## Summary

The RealWorld API demonstrates a clean, modular approach to implementing CRUD operations for article comments:

- **Route-specific handlers** use Nitro's file-based routing for clear API organization
- **Authentication middleware** via `definePrivateEventHandler` securely injects user context only where required
- **Prisma ORM** provides type-safe database operations through the `usePrisma()` singleton pattern
- **Explicit authorization** in the DELETE handler ensures resource-level security by verifying comment ownership
- **Consistent response shaping** maps database entities to specification-compliant DTOs including author metadata and follow status

## Frequently Asked Questions

### How does the RealWorld API authorize comment deletion?

The API authorizes comment deletion by querying the comment with the author's ID and comparing it against the authenticated user's ID from the `auth` object injected by `definePrivateEventHandler`. If the IDs do not match in `apps/api/server/routes/api/articles/[slug]/comments/[id].delete.ts`, the handler throws a `403` `HttpException` with the message "You are not authorized to delete this comment".

### What ORM pattern does the RealWorld API use for database operations?

The RealWorld API uses Prisma with a singleton client pattern accessed via `usePrisma()` from [`apps/api/server/utils/prisma.ts`](https://github.com/gothinkster/realworld/blob/main/apps/api/server/utils/prisma.ts). This ensures a single database connection instance is reused across requests, preventing connection pool exhaustion while providing type-safe CRUD operations for comments and related entities.

### Can unauthenticated users view article comments?

Yes, unauthenticated users can view article comments. The GET handler in `apps/api/server/routes/api/articles/[slug]/comments/index.get.ts` explicitly sets `{ requireAuth: false }` when calling `definePrivateEventHandler`, making the comment retrieval endpoint publicly accessible while still supporting optional authentication for personalized `following` status calculation.

### How are validation errors handled in the comment endpoints?

Validation errors are handled using the `HttpException` class from [`apps/api/server/models/http-exception.model.ts`](https://github.com/gothinkster/realworld/blob/main/apps/api/server/models/http-exception.model.ts). When validation fails, such as a missing comment body in the POST handler, the code throws `new HttpException(422, { errors: { body: ["can't be blank"] } })`, which automatically formats the response according to the RealWorld API specification with the appropriate HTTP status code and error payload.