How to Implement CRUD Operations for Article Comments in the RealWorld API
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 articleGET /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, 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. 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, 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.
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.
// 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.
// 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.
// 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 linkingapps/api/server/routes/api/articles/[slug]/comments/index.get.ts– Handles public comment retrieval with author enrichmentapps/api/server/routes/api/articles/[slug]/comments/[id].delete.ts– Handles secure deletion with authorization checksapps/api/server/utils/prisma.ts– Provides the singletonusePrisma()clientapps/api/server/models/comment.model.ts– TypeScript interfaces for comment data structuresapps/api/server/models/http-exception.model.ts– Standardized error response helperapps/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
definePrivateEventHandlersecurely 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. 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. 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →