Skip to content

Comments Domain

The comments domain provides threaded commenting on any resource with soft-delete and author-based moderation for TypeScript backends. It is structured in strict Clean Architecture layers with zero npm dependencies in the domain and application layers.

Terminal window
npx @backcap/cli add comments

The Comment entity is the aggregate root. It attaches to any resource via generic resourceId/resourceType strings and supports threaded replies via parentId.

import { Comment } from "./domains/comments/domain/entities/comment.entity";
const result = Comment.create({
id: crypto.randomUUID(),
content: "Great article!",
authorId: "user-1",
resourceId: "post-123",
resourceType: "post",
parentId: undefined, // root comment
});
if (result.isOk()) {
const comment = result.unwrap();
const deleted = comment.softDelete().unwrap();
console.log(deleted.deletedAt); // Date
}
FieldTypeDescription
idstringUnique identifier (UUID)
contentCommentContentValidated content value object (1–10,000 chars)
authorIdstringComment author identifier
resourceIdstringGeneric resource identifier
resourceTypestringGeneric resource type (e.g., “post”, “product”)
parentIdstring | undefinedParent comment for threading
createdAtDateTimestamp of creation
deletedAtDate | undefinedSoft-delete timestamp

State transitions:

  • softDelete() — sets deletedAt to current date. Fails if already deleted.
import { CommentContent } from "./domains/comments/domain/value-objects/comment-content.vo";
const result = CommentContent.create("Great article!");
// Result<CommentContent, Error>

Validates: content must be between 1 and 10,000 characters after trimming.

Error ClassConditionMessage
CommentNotFoundNo comment found for the given IDComment not found with id: "<id>"
UnauthorizedDeleteRequester is not the comment authorUser "<requesterId>" is not authorized to delete comment "<id>"
EventEmitted ByPayload
CommentPostedPostComment use casecommentId, authorId, resourceId, resourceType, occurredAt

Creates a new comment on a resource. When parentId is provided for threading, the parent comment must exist or the operation fails with CommentNotFound.

const result = await commentsService.postComment({
content: "Great article!",
authorId: "user-1",
resourceId: "post-123",
resourceType: "post",
parentId: "comment-456", // optional for threading — parent must exist
});
// Result<{ commentId: string; createdAt: Date }, Error>

Lists comments for a resource with pagination. Excludes deleted comments by default.

const result = await commentsService.listComments({
resourceId: "post-123",
resourceType: "post",
includeDeleted: false, // default
limit: 50,
offset: 0,
});
// Result<{ comments: Array<...>; total: number }, Error>

Soft-deletes a comment. Verifies that requesterId === comment.authorId.

const result = await commentsService.deleteComment({
commentId: "comment-123",
requesterId: "user-1",
});
// Result<{ deletedAt: Date }, Error>
export interface ICommentRepository {
save(comment: Comment): Promise<void>;
findById(id: string): Promise<Comment | undefined>;
findByResource(resourceId: string, resourceType: string, filters: CommentFilters): Promise<{ comments: Comment[]; total: number }>;
}
import { createCommentsService, ICommentsService } from "./domains/comments/contracts";
const commentsService: ICommentsService = createCommentsService({ commentRepository });
domains/comments/
domain/
entities/comment.entity.ts
value-objects/comment-content.vo.ts
events/comment-posted.event.ts
errors/comment-not-found.error.ts
errors/unauthorized-delete.error.ts
application/
use-cases/post-comment.use-case.ts
use-cases/list-comments.use-case.ts
use-cases/delete-comment.use-case.ts
ports/comment-repository.port.ts
dto/post-comment.dto.ts
dto/list-comments.dto.ts
dto/delete-comment.dto.ts
contracts/
comments.contract.ts
comments.factory.ts
index.ts
shared/
result.ts