Spec Decision Record — Comments
Feature spec for the comment vertical slice. Derives from
docs/PRD.md(comments) and reuses the ports & adapters decision ofdocs/adrs/ADR-0002-auth.mdand the soft-delete decision ofdocs/specs/articles.md. Conforms to the RealWorld Conduit API comment contract.
Endpoints
| Method | Path | Auth | Success |
|---|---|---|---|
| GET | /api/articles/:slug/comments | optional | 200 |
| POST | /api/articles/:slug/comments | required | 201 |
| DELETE | /api/articles/:slug/comments/:id | required (author) | 200 ({}) |
Authorization: Token <jwt> is the RealWorld scheme; Bearer is also accepted.
Behaviour
- Comments are scoped to an article. Every endpoint first resolves the
:slugto a live (non-soft-deleted) article; an unknown slug →404. - List order is newest first, matching the recency convention used across the API. The list carries each comment’s embedded author (
username,bio,image,following), wherefollowingreflects the viewing user’s follow edge (falsewhen anonymous), reusing the sameIProfileRepository.isFollowingthe profile and article slices use. - Create wraps the payload in a top-level
commentobject ({ comment: { body } }); a blank/whitespacebody→422with a field error. The created comment echoes back with its author and timestamps. - Delete is author-only. The service compares the comment’s
authorIdto the caller, so the rule holds regardless of transport; a non-author →403. The comment must also belong to the named article, otherwise404.
Status code & authorization decisions
Following the codebase’s error taxonomy (src/errors/AppError.ts) and the precedent in docs/specs/articles.md:
- Create →
201; list →200; delete →200 {}. - Unknown
:slugor:id→404(NotFoundError, resourceArticle/Comment). A comment whose article does not match the slug is also404(it is not addressable under that path). - Auth at the router seam:
authenticateon create/delete;optionalAuthenticateon the public list so its embeddedfollowingflag can reflect a known caller. Auth is never checked inside handlers (.claude/standards/api.md). - Author-only delete →
403(ForbiddenError): enforced in the service. - Validation →
422with field errors, via Zod at the boundary (commentSchemas.ts+ the sharedvalidation.tshelper).
The soft-delete decision (operation-classification Tier 3)
docs/operation-classification.md classes hard delete of domain entities as Tier 3 (“use soft delete + audit log instead”). The PRD requires a working DELETE whose comment disappears from subsequent reads. As with articles, we reconcile the two with a soft delete: delete sets deletedAt, and every read path (findById, listByArticle) filters deletedAt: null. The observable RealWorld behaviour (gone on re-list) is identical while the row is retained — no hard delete, scoped by a specific id (never an unscoped DELETE). The in-memory fake mirrors this exclusion contract.
Architecture (ports & adapters)
HTTP (commentRoutes/CommentController) → CommentService → ports
· optionalAuthenticate (list) ├─ ICommentRepository → Prisma / InMemory
· authenticate (create, delete) ├─ IArticleRepository → resolve slug → article id (404)
├─ IUserRepository → embed author profile
└─ IProfileRepository → author `following`
No new ADR is raised: this slice reuses ADR-0002’s layering and injection pattern and the article slice’s soft-delete decision. The pre-scaffolded ICommentRepository port was used as-is; the comment adapter references the author by id and stays ignorant of users and the follow graph, exactly as the article adapter does. The shared boundary-validation helper parseOrThrow (src/http/validation.ts) is reused.
Test strategy
- Unit:
InMemoryCommentRepository(per-article scoping, newest-first ordering, find-by-id, soft-delete exclusion);CommentService(article resolution incl. unknown-slug 404, listing order, author-only delete, article/comment ownership mismatch, viewer-relativefollowing). - Integration (subcutaneous): Supertest against the real Express app over the in-memory fakes — all three endpoints, 401/403/404/422 paths, ordering, and the viewer-relative
followingflag. - Integration (real DB):
PrismaCommentRepositoryis verified by the gated real-database suite (RUN_DB_TESTS=1) and excluded from the unit-coverage denominator, exactly as the other Prisma adapters are.