Reviews API Reference
Complete API contracts for the Reviews module, including routes, auth, DTOs, responses, errors, examples, and integration notes.
Reviews - API Reference
Audience: Frontend engineers, mobile engineers, backend engineers, QA, and API consumers. Scope: All 20 review endpoints — 2 public, 7 customer, 11 admin.
1. Documentation Evidence
| Area | Files Inspected | What Was Verified |
|---|---|---|
| Controllers | apps/api/src/modules/reviews/customer/{product-review,my-review}/*.controller.ts, admin/{review,report}/*.controller.ts | Routes, methods, guards, permissions, rate limits |
| DTOs | dto/*.ts | Validation |
| Services | shared + customer + admin services | Behavior, transitions |
| Schema | packages/db/src/schema/reviews/*.ts | Tables, GENERATED aggregate |
| Error registry | apps/api/src/common/types/error-codes.ts (// REVIEW) | REVIEW_* codes |
2. Module Summary
| Field | Value |
|---|---|
| Module name | reviews |
| Module slug | reviews |
| Primary actors | guest, customer, admin |
| API surfaces | mobile (public + customer), admin |
| Base route prefixes | /api/mobile/products/:productId/reviews, /api/mobile/reviews, /api/admin/reviews, /api/admin/review-reports |
| Auth model | @Public() (2 routes); JwtAuthGuard (customer); JwtAuthGuard + RoleGuard (admin) |
| Persistence | PostgreSQL (4 tables), Redis (product_review cache domain) |
| Runtime source of truth | product_review rows + the GENERATED aggregate |
| Sibling docs | Backend, Features and flows |
3. Concepts and Terminology
| Term | Meaning | Source File | Used By |
|---|---|---|---|
productId | The product public_id uuid — exposed as basic.id, not the slug | schema | Public routes |
status | pending / published / rejected / hidden / deleted | schema | All routes |
version | Optimistic lock — edit and every moderation action | schema | Edit, moderation, withdraw |
canCreate | Eligibility result — the key to key off | eligibility service | Eligibility |
existingReview.version | What the edit must send back | eligibility service | Edit |
recorded | false + 200 = already reported — a success | report service | Report |
moderationReason | The only explanation the customer gets | schema | Rejected/hidden |
verifiedPurchase | Derived — the order line FKs make it structural | response builder | Public reviews |
4. API Surface Map
4.1 Public — /api/mobile/products/{productId}/reviews
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | / | none | Published reviews (page/size/sort/rating/withTextOnly) |
GET | /summary | none | The aggregate |
{productId} is the public_id uuid — not the slug (a slug returns 400).
4.2 Customer — /api/mobile/reviews
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /eligibility | JWT | Can this customer review this product? |
GET | / | JWT | My reviews (every state without a filter) |
GET | /:id | JWT | One of my reviews |
POST | / | JWT | Create (201, idempotent) |
PATCH | /:id | JWT | Edit (→ pending) |
DELETE | /:id | JWT | Withdraw (version as query param) |
POST | /:id/report | JWT | Report |
4.3 Admin — /api/admin/reviews
| Method | Path | Permission | Purpose |
|---|---|---|---|
GET | / | Reviews_READ | List (two queues) |
GET | /:id | Reviews_READ | Detail |
GET | /:id/events | Reviews_READ | Timeline |
POST | /:id/approve | Reviews_UPDATE | Approve |
POST | /:id/reject | Reviews_UPDATE | Reject (reason required) |
POST | /:id/hide | Reviews_UPDATE | Hide (reason required; resolves reports) |
POST | /:id/restore | Reviews_RESTORE | Restore |
POST | /bulk/approve | Reviews_UPDATE | Bulk approve |
POST | /summary/:productId/recalculate | Reviews_UPDATE | Recalculate (202) |
4.4 Admin — /api/admin/review-reports
| Method | Path | Permission | Purpose |
|---|---|---|---|
GET | / | Reviews_READ | Report queue (default status=open) |
POST | /:id/dismiss | Reviews_UPDATE | Dismiss |
/api/admin/review-reports, never /api/admin/reviews/reports — the latter is matched against /api/admin/reviews/{id} and dies in ParseUUIDPipe.
5. Auth, Identity, and Permissions
| Surface | Guard/Decorator | Identity Shape | Permission | Guest Allowed | Notes |
|---|---|---|---|---|---|
| Public | @Public() + ParseUUIDPipe | None | — | Yes | A missing pipe surfaced a 500 on an unauthenticated route; a non-uuid is a clean 400 |
| Customer | JwtAuthGuard | req.user.id | — | No | Ownership-scoped: another customer's review is 404 |
| Admin | JwtAuthGuard, RoleGuard | req.user | Reviews_* | No | Reviews is already in the permission catalog — permissions:sync only |
Rate limits: create/edit CUSTOMER_REVIEW_SUBMIT 5/hour per account; report CUSTOMER_REVIEW_REPORT 10/hour per account; everything else the usual budgets. Five an hour is generous for a real shopper and tight for a form that retries on its own — do not auto-retry a failed submission without a backoff, and do not treat a 429 here as a bug.
6. DTO and Model Reference
6.1 CreateReviewDto
| Field | Type | Required | Validation | Notes |
|---|---|---|---|---|
productId | UUID | Yes | @IsUUID("7") | |
rating | number | Yes | int 1–5 | |
title | string | No | ≤150 | |
body | string | No | ≤4000 |
Create requires an Idempotency-Key header — a retry replays the original 201 instead of colliding.
6.2 UpdateReviewDto
| Field | Type | Required | Notes |
|---|---|---|---|
version | number | Yes | The concurrency guard |
rating / title / body | — | No | Asymmetry with create: an empty string clears the field, omitting it leaves it alone. On create the two mean the same thing; on update they mean opposite things |
6.3 Report DTO
reason ∈ spam · offensive · fake · irrelevant · personal_information · other (required); detail optional, ≤500, admin-only.
6.4 Moderation DTOs
approve/restore: { version }. reject/hide: { version, reason } — reason required. bulk/approve: { ids: string[] } (max 100). Recalculate: no body.
6.5 Query DTOs
Public list: page, size (max 50), sort (newest/oldest/highest/lowest), rating (1–5), withTextOnly. My list: page, size, optional status. Admin list: page, size, sort (oldestPending/newest/mostReported/recentlyModerated/recentlyEdited), status, productId, customerId, rating, reportedOnly, createdFrom, createdTo. Report queue: page, size, status (default open), reviewId.
7. Enum Reference
| Enum | Value | Meaning | Runtime Effect | Source |
|---|---|---|---|---|
review_status | pending / published / rejected / hidden / deleted | Lifecycle | Defaults published | enums.ts |
report_reason | spam / offensive / fake / irrelevant / personal_information / other | Why reported | Required |
8. Endpoint Reference
8.1 GET /api/mobile/products/:productId/reviews
Purpose
The product page's review list. Published reviews only.
Response
200 — { items: [{ id, rating, title, body, reviewer: { displayName, verifiedPurchase }, publishedAt, editedAt }], count, currentPage, totalPage }. title and body are independently nullable; editedAt is null unless edited — render both dates when not, so an edited review does not pass as an original. verifiedPurchase is derived from the order line FKs.
8.2 GET /api/mobile/products/:productId/reviews/summary
200 — { productId, averageRating, reviewCount, distribution: { "1": n, ..., "5": n } }. Zeroes, never nulls, for an unreviewed product; reviewCount tells you whether averageRating: 0 means "bad" or "nobody has said". The three numbers are guaranteed to agree — computed by the database from one set of counters.
8.3 GET /api/mobile/reviews/eligibility?productId=
200 — { eligible, orderId, existingReview: { id, status, rating, version }, canCreate }. Key off canCreate, not eligible — it already accounts for the delivered purchase and any existing review. existingReview.version is what the edit call must send back.
8.4 GET /api/mobile/reviews(/:id)
Purpose
The customer's own reviews — the only surface where non-published states are visible. Without a status filter the list returns every state including withdrawn.
Response
200 — { id, product: { id, name, thumbnail }, rating, title, body, status, moderationReason, version, publishedAt, editedAt, createdAt }. moderationReason is populated on rejected and hidden — show it; it is the only explanation the customer gets.
8.5 POST /api/mobile/reviews
Purpose
Create a review. rating required; title/body optional. Idempotency-Key required. Returns 201 with the MyReview shape.
Error Cases
| HTTP | Code | Condition |
|---|---|---|
| 400 | validation | Bad rating/uuid |
| 403 | REVIEW_NOT_ELIGIBLE | No delivered purchase of this product |
| 404 | REVIEW_PRODUCT_NOT_FOUND | Product missing or withdrawn |
| 409 | REVIEW_ALREADY_EXISTS | A live review exists — body carries existingReviewId |
| 429 | — | 5/hour budget |
8.6 PATCH /api/mobile/reviews/:id
Purpose
Edit. version required. The review returns to pending and leaves the product's rating until a moderator approves — the single most surprising behaviour; tell the customer before they submit.
Error Cases
| HTTP | Code | Condition |
|---|---|---|
| 404 | REVIEW_NOT_FOUND | No such review, or not this customer's |
| 409 | REVIEW_STALE_VERSION | Somebody changed it first |
| 409 | REVIEW_NOT_EDITABLE | hidden or deleted — offer withdraw instead |
8.7 DELETE /api/mobile/reviews/:id?version={n}
Purpose
Withdraw. The version is a query parameter, not a body — DELETE bodies are dropped by enough proxies that relying on one would silently disable the guard. Returns 200. The customer may write a new review afterwards — withdrawing frees the slot.
8.8 POST /api/mobile/reviews/:id/report
Purpose
Report somebody else's published review. recorded: false with a 200 is a success — already reported. Reporting never hides anything; it raises a moderator's priority and nothing more.
Error Cases
| HTTP | Code | Condition |
|---|---|---|
| 409 | REVIEW_REPORT_SELF | Reporting your own review |
| 404 | REVIEW_NOT_FOUND | Unknown review |
8.9 GET /api/admin/reviews
Two useful queues: ?status=pending&sort=oldestPending (edits awaiting approval) and ?reportedOnly=true&sort=mostReported (abuse). Row carries product/customer/order refs, reportCount, version, moderation fields.
8.10 Moderation — /{id}/approve|reject|hide|restore
| Route | Body |
|---|---|
approve | { version } |
reject | { version, reason } — reason required |
hide | { version, reason } — reason required; also resolves every open report on the review |
restore | { version } |
All return 200. Always send the version you fetched — it is what stops two moderators silently overwriting each other (409 REVIEW_STALE_VERSION). An approved review can be hidden; a rejected one stays rejected.
8.11 POST /api/admin/reviews/bulk/approve
{ ids } → { approved, skipped }. Max 100 distinct ids (409 REVIEW_BULK_LIMIT_EXCEEDED).
8.12 POST /api/admin/reviews/summary/:productId/recalculate
Queues a recount of one product's aggregate from its reviews. Returns 202. A repair path, not the write path — the route includes soft-deleted products deliberately (a withdrawn product's aggregate can still be wrong, and refusing to repair it because the product is off sale leaves bad data where nobody looks).
Error Cases
| HTTP | Code | Condition |
|---|---|---|
| 404 | REVIEW_SUMMARY_NOT_FOUND | The product does not exist. Not a product with no summary row — that is exactly the drift this endpoint repairs |
8.13 GET /api/admin/review-reports
Report queue (default status=open). Rows carry the review's own text so a moderator decides without a second call; the reporting customer is deliberately not identified and never will be.
8.14 POST /api/admin/review-reports/:id/dismiss
The no-action outcome; optional { note }. 404 REVIEW_REPORT_NOT_FOUND; 409 REVIEW_REPORT_ALREADY_RESOLVED when another moderator handled it.
9. Flow Diagrams
9.1 Route Ownership
9.2 Request Sequence (create)
9.3 Error Branch (edit)
10. Pagination, Sorting, Filtering, and Search
| Endpoint | Pagination Type | Default Size | Max Size | Sort Fields | Filters | Result Cap |
|---|---|---|---|---|---|---|
GET /api/mobile/products/:productId/reviews | offset page/size | 20 | 50 | newest/oldest/highest/lowest | rating, withTextOnly | — |
GET /api/mobile/reviews | offset page/size | 20 | 100 | standard | status | — |
GET /api/admin/reviews | offset page/size | 20 | 100 | 5 admin sorts | status, product, customer, rating, reportedOnly, dates | — |
GET /api/admin/review-reports | offset page/size | 20 | 100 | standard | status (open), reviewId | — |
11. Caching, Jobs, and External Integrations
| Integration | Used? | Details |
|---|---|---|
| Redis cache | Yes | product_review domain — public reviews + summary + the product detail rating block; invalidated on every write |
| BullMQ | Yes | REVIEW queue — recalculate + moderation emails via outbox (dedupe key carries the review version); hourly drift sweep logs repairs |
| Yes | Moderation notices via outbox inside their transactions | |
| External API | No | — |
13. Mandatory Deep API Documentation Pack
13.1 Route-by-Route Completeness Matrix
| Route | Controller Method | DTOs | Service Method | Guards | Permissions | Cache | Jobs | DB Touches | Errors | Documented? |
|---|---|---|---|---|---|---|---|---|---|---|
GET /api/mobile/products/:productId/reviews | list | query DTO | product review service | Public+UUID pipe | — | product_review | — | reviews | — | Yes |
GET .../reviews/summary | summary | params | summary service | Public+UUID pipe | — | product_review | — | summary | 404 | Yes |
GET /api/mobile/reviews/eligibility | eligibility | query | eligibility service | JWT | — | — | — | orders, reviews | — | Yes |
GET /api/mobile/reviews | findAll | query | my-review service | JWT | — | — | — | reviews | — | Yes |
GET /api/mobile/reviews/:id | findById | params | my-review service | JWT | — | — | — | review | 404 | Yes |
POST /api/mobile/reviews | create | CreateReviewDto | create service | JWT+Idempotency | — | invalidate | — | review + summary | 400/403/404/409 | Yes |
PATCH /api/mobile/reviews/:id | edit | UpdateReviewDto | edit service | JWT | — | invalidate | review + summary | 404/409 | Yes | |
DELETE /api/mobile/reviews/:id | withdraw | query version | withdraw service | JWT | — | invalidate | — | review + summary | 404/409 | Yes |
POST /api/mobile/reviews/:id/report | report | report DTO | report service | JWT | — | — | — | report + counter | 404/409 | Yes |
GET /api/admin/reviews | findAll | query | moderation list | JWT+Role | Reviews_READ | — | — | reviews | — | Yes |
GET /api/admin/reviews/:id | findById | params | moderation | JWT+Role | Reviews_READ | — | — | review | 404 | Yes |
GET /api/admin/reviews/:id/events | events | params | event service | JWT+Role | Reviews_READ | — | — | events | 404 | Yes |
POST /:id/approve | approve | version DTO | moderation | JWT+Role | Reviews_UPDATE | invalidate | review + summary | 404/409 | Yes | |
POST /:id/reject | reject | version+reason | moderation | JWT+Role | Reviews_UPDATE | invalidate | review | 404/409 | Yes | |
POST /:id/hide | hide | version+reason | moderation | JWT+Role | Reviews_UPDATE | invalidate | review + reports | 404/409 | Yes | |
POST /:id/restore | restore | version DTO | moderation | JWT+Role | Reviews_RESTORE | invalidate | review + summary | 404/409 | Yes | |
POST /bulk/approve | bulkApprove | ids DTO | moderation | JWT+Role | Reviews_UPDATE | invalidate | reviews | 409 | Yes | |
POST /summary/:productId/recalculate | recalculate | params | recalc service | JWT+Role | Reviews_UPDATE | invalidate | queue | summary | 202 / 404 REVIEW_SUMMARY_NOT_FOUND | Yes |
GET /api/admin/review-reports | findAll | query | report service | JWT+Role | Reviews_READ | — | — | reports | — | Yes |
POST /api/admin/review-reports/:id/dismiss | dismiss | note DTO | report service | JWT+Role | Reviews_UPDATE | — | — | report | 404/409 | Yes |
13.2 Request/Response Exhaustiveness
Covered in §8: minimal/full create payloads (§6.1/8.5), the update empty-string-vs-omit asymmetry (§6.2), the recorded:false success (§8.8), the pending-on-edit surprise (§8.6), zeroes-not-nulls summary (§8.2), domain errors per endpoint (§8 error tables), rate-limit behavior (5/hr, 10/hr), 202 recalculate.
13.3 API Diagram Pack
Route ownership (§9.1), sequence per endpoint family (§9.2, backend §7), error decision tree (§9.3), async flow (backend §7.2 — outbox email), cache flow (backend §8).
13.4 Consumer Integration Notes
| Consumer | Required Knowledge | Failure Handling | Contract Stability |
|---|---|---|---|
| Web frontend | Edit → pending surprise; canCreate is the key; recorded:false is success | 409 STALE_VERSION → re-fetch, show current, ask again | Stable |
| Mobile app | Idempotency-Key on create; 5/hr + 10/hr budgets; version as query param on DELETE | 429 → back off, never auto-retry without backoff | Stable |
| Admin panel | Two queues; version on every action; hide resolves reports; reporter never identified | 409s → refresh | Stable |
| QA | Aggregate invariants (428C9/23514), one-per-pair, pending semantics | Reproduce via exact codes | Stable |
| Products integration | rating block on detail only; listings use the summary endpoint | — | Stable |
13.5 API Tradeoffs and Rationale
| Decision | Chosen Behavior | Alternatives Considered | Why This Tradeoff | Risk | Mitigation |
|---|---|---|---|---|---|
| One review per pair | Edit, not second vote | Multiple reviews | One honest opinion | — | Partial unique |
| Auto-publish | Instant feedback | Pre-moderation | Trust new content | Bad content briefly live | Reports |
| Edit → pending | Honest edits | In-place | No bait-and-switch | Surprise | Documented |
| Aggregate GENERATED | Consistent numbers | App-computed | 428C9 structural proof | — | — |
| Version everywhere | No silent overwrite | Last-write-wins | Two moderators | 409 churn | Re-fetch |
| Detail-only rating | No join per card | Listing ratings | — | Second call | Summary endpoint |
| Report never hides | Free speech | Auto-hide | Counter orders queue | Abuse lingers | Priority queue |
13.6 API Change Impact
| Change | Affected Consumers | Backend Impact | Data Impact | Migration Needed? | Compatibility Plan |
|---|---|---|---|---|---|
Product detail rating block | Storefront | Projection | None | No | Additive optional field |
| Future verified-purchase rule change | None | Eligibility service | None | No | Structural today |
14. Zero-Omission API Checklist
- Every controller route is documented (§4, §8, §13.1).
- Every parent route prefix and runtime URL is documented (§2, §4).
- Every DTO field, enum, default, transform and validator is documented (§6, §7).
- Every response field and nullable field is documented (§8).
- Every auth, guard, permission and guest identity branch is documented (§5).
- Every success, validation, not-found, conflict, rate-limit and server-error branch is documented (§8).
- Every DB read/write, cache hit/miss/invalidation, queue job and external call is documented (§11, backend §8/§9).
- Every route has examples for minimal request, success response and representative failures (§8).
- Every endpoint family has route, sequence and error diagrams (§9, backend §7).
- Every tradeoff and compatibility risk is documented (§13.5, §13.6).
- The API doc links to backend and features/flows (§1, See Also).
15. Integration Checklist
- Every route from controllers is documented.
- Every DTO field is documented.
- Every enum value is documented.
- Every response envelope is documented.
- Every error code is documented.
- Every auth guard and permission is documented.
- Every cache key, queue job and external call is documented.
- Every diagram matches the current code.
- The API doc links to backend and features/flows.
See Also
- Backend doc: /docs/developer/reviews/backend
- Features and flows doc: /docs/developer/reviews/feature
- TDD: not yet published
Reviews Backend Documentation
Backend architecture, data model, services, and operational behavior for the Reviews module.
Reports & Analytics Module Overview
The admin analytics surface — dashboards and KPIs served from pre-aggregated MongoDB rollups, a 19-report registry, async exports, and the PostgreSQL-to-MongoDB ETL that feeds it all.