← All @molecule/* packages · App templates
@molecule/api-resource-reviewAPI resource · reviews · API (Node) · v1.0.1 · Apache-2.0
Reviews with ratings on any resource — star ratings, helpfulness voting, and aggregate statistics
npm install @molecule/api-resource-review@molecule/api-resource-review is an API resource: the routes, validation and storage for reviews, built on the database and auth cores so it runs on whichever providers your app has bonded.
import { routes, requestHandlerMap } from '@molecule/api-resource-review'
// Wire routes into your Express app via mlcl inject
// POST /:resourceType/:resourceId/reviews
// GET /:resourceType/:resourceId/reviews
// GET /:resourceType/:resourceId/reviews/rating
// GET /reviews/:reviewId
// PUT /reviews/:reviewId
// DELETE /reviews/:reviewId
// POST /reviews/:reviewId/helpfulAuto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Reviews with ratings resource for molecule.dev.
Polymorphic reviews that attach to any resource type. Supports star ratings (1–5), title/body text, helpfulness voting, and aggregate rating statistics.
import { routes, requestHandlerMap } from '@molecule/api-resource-review'
// Wire routes into your Express app via mlcl inject
// POST /:resourceType/:resourceId/reviews
// GET /:resourceType/:resourceId/reviews
// GET /:resourceType/:resourceId/reviews/rating
// GET /reviews/:reviewId
// PUT /reviews/:reviewId
// DELETE /reviews/:reviewId
// POST /reviews/:reviewId/helpful
resource
npm install @molecule/api-resource-review @molecule/api-database @molecule/api-i18n @molecule/api-logger @molecule/api-resource zod
CreateReviewInputInput for creating a new review.
interface CreateReviewInput {
/** Numeric rating (1–5). */
rating: number
/** Short review title/summary. */
title: string
/** Detailed review body text. */
body: string
}
PaginatedResultA paginated result set.
interface PaginatedResult<T> {
/** The result items for the current page. */
data: T[]
/** Total number of matching items across all pages. */
total: number
/** Maximum number of results per page. */
limit: number
/** Number of results skipped. */
offset: number
}
PaginationOptionsOptions for paginated queries.
interface PaginationOptions {
/** Maximum number of results to return. */
limit?: number
/** Number of results to skip. */
offset?: number
}
RatingStatsAggregate rating statistics for a resource.
interface RatingStats {
/** Average rating across all reviews. */
average: number
/** Total number of reviews. */
count: number
/** Breakdown of reviews by star rating (1–5). */
distribution: Record<number, number>
}
ReviewA review attached to a resource, with a numeric rating and optional body.
interface Review {
/** Unique review identifier. */
id: string
/** The type of resource this review is attached to (e.g. 'product', 'course'). */
resourceType: string
/** The ID of the resource this review is attached to. */
resourceId: string
/** The ID of the user who created this review. */
userId: string
/** Numeric rating (1–5). */
rating: number
/** Short review title/summary. */
title: string
/** Detailed review body text. */
body: string
/** Number of users who marked this review as helpful. */
helpful: number
/** When the review was created (ISO 8601). */
createdAt: string
/** When the review was last updated (ISO 8601). */
updatedAt: string
}
ReviewHelpfulA record of a user marking a review as helpful.
interface ReviewHelpful {
/** The review that was marked helpful. */
reviewId: string
/** The user who marked it helpful. */
userId: string
/** When it was marked helpful (ISO 8601). */
createdAt: string
}
ReviewQueryQuery options for listing reviews with optional sorting.
interface ReviewQuery extends PaginationOptions {
/** Sort field. Defaults to 'createdAt'. */
sortBy?: 'createdAt' | 'rating' | 'helpful'
/** Sort direction. Defaults to 'desc'. */
sortDirection?: 'asc' | 'desc'
}
UpdateReviewInputInput for updating an existing review.
interface UpdateReviewInput {
/** Updated numeric rating (1–5). */
rating?: number
/** Updated review title. */
title?: string
/** Updated review body text. */
body?: string
}
averageRating(req, res)Returns aggregate rating statistics for a resource.
function averageRating(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
req — The request with resourceType and resourceId params.res — The response object.create(req, res)Creates a new review on a resource.
function create(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
req — The request with resourceType and resourceId params and review body.res — The response object.createReview(resourceType, resourceId, userId, data)Creates a new review on a resource. One review per user per resource.
function createReview(
resourceType: string,
resourceId: string,
userId: string,
data: CreateReviewInput,
): Promise<Review>
resourceType — The type of resource being reviewed.resourceId — The ID of the resource being reviewed.userId — The ID of the reviewing user.data — The review creation input.Returns: The created review.
del(req, res)Deletes a review. Only the review owner can delete.
function del(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
req — The request with reviewId param.res — The response object.deleteReview(reviewId, userId)Deletes a review. Only the review owner can delete.
function deleteReview(reviewId: string, userId: string): Promise<boolean>
reviewId — The review ID to delete.userId — The requesting user's ID (must match review owner).Returns: true if deleted, false if not found or unauthorized.
getAverageRating(resourceType, resourceId)Computes aggregate rating statistics for a resource.
function getAverageRating(resourceType: string, resourceId: string): Promise<RatingStats>
resourceType — The type of resource.resourceId — The ID of the resource.Returns: Rating statistics including average, count, and distribution.
getReviewById(reviewId)Retrieves a single review by ID.
function getReviewById(reviewId: string): Promise<Review | null>
reviewId — The review ID to look up.Returns: The review or null if not found.
getReviewsByResource(resourceType, resourceId, options)Retrieves paginated reviews for a resource with optional sorting.
function getReviewsByResource(
resourceType: string,
resourceId: string,
options?: ReviewQuery,
): Promise<PaginatedResult<Review>>
resourceType — The type of resource.resourceId — The ID of the resource.options — Query options (pagination and sorting).Returns: A paginated result of reviews.
helpful(req, res)Marks a review as helpful. Idempotent — duplicate votes are silently ignored.
function helpful(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
req — The request with reviewId param.res — The response object.list(req, res)Lists paginated reviews for a resource with optional sorting.
function list(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
req — The request with resourceType and resourceId params.res — The response object.markHelpful(reviewId, userId)Marks a review as helpful by a user. Idempotent — duplicate votes are ignored.
function markHelpful(reviewId: string, userId: string): Promise<void>
reviewId — The review to mark as helpful.userId — The user marking it helpful.read(req, res)Retrieves a single review by ID.
function read(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
req — The request with reviewId param.res — The response object.update(req, res)Updates an existing review. Only the review owner can update.
function update(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
req — The request with reviewId param and update body.res — The response object.updateReview(reviewId, userId, data)Updates a review. Only the review owner can update.
function updateReview(
reviewId: string,
userId: string,
data: UpdateReviewInput,
): Promise<Review | null>
reviewId — The review ID to update.userId — The requesting user's ID (must match review owner).data — The update input.Returns: The updated review or null if not found or unauthorized.
createReviewSchemaSchema for validating review creation input.
const createReviewSchema: z.ZodObject<
{ rating: z.ZodNumber; title: z.ZodString; body: z.ZodString },
z.core.$strip
>
requestHandlerMapHandler map for review routes.
const requestHandlerMap: {
readonly create: typeof create
readonly list: typeof list
readonly averageRating: typeof averageRating
readonly read: typeof read
readonly update: typeof update
readonly del: typeof del
readonly helpful: typeof helpful
}
routesRoutes for review CRUD, rating statistics, and helpfulness voting.
const routes: readonly [
{
readonly method: 'post'
readonly path: '/:resourceType/:resourceId/reviews'
readonly handler: 'create'
readonly middlewares: readonly ['authenticate']
},
{
readonly method: 'get'
readonly path: '/:resourceType/:resourceId/reviews'
readonly handler: 'list'
},
{
readonly method: 'get'
readonly path: '/:resourceType/:resourceId/reviews/rating'
readonly handler: 'averageRating'
},
{ readonly method: 'get'; readonly path: '/reviews/:reviewId'; readonly handler: 'read' },
{
readonly method: 'put'
readonly path: '/reviews/:reviewId'
readonly handler: 'update'
readonly middlewares: readonly ['authenticate']
},
{
readonly method: 'delete'
readonly path: '/reviews/:reviewId'
readonly handler: 'del'
readonly middlewares: readonly ['authenticate']
},
{
readonly method: 'post'
readonly path: '/reviews/:reviewId/helpful'
readonly handler: 'helpful'
readonly middlewares: readonly ['authenticate']
},
]
updateReviewSchemaSchema for validating review update input. All fields are optional.
const updateReviewSchema: z.ZodObject<
{
rating: z.ZodOptional<z.ZodNumber>
title: z.ZodOptional<z.ZodString>
body: z.ZodOptional<z.ZodString>
},
z.core.$strip
>
Peer dependencies:
@molecule/api-database ^1.0.1@molecule/api-i18n ^1.0.1@molecule/api-logger ^1.0.1@molecule/api-resource ^1.0.1zod ^4.0.0@molecule/api-database
@molecule/api-i18n
@molecule/api-logger
@molecule/api-resource
zod
List endpoints return a PAGINATED envelope { data, total, limit, offset }, not a
bare array — read the rows off result.data (server). On the client, unwrapList(res)
from @molecule/app-http normalizes this envelope (pass it the whole HttpResponse), so
the rows come back; reading the response as a bare array — or res.data alone (which is
the envelope) — yields an EMPTY list.
Session-auth prerequisite: the mutating handlers (create, update, del,
helpful) read the caller from res.locals.session.userId and fail closed
with 401 — mount the routes behind your global auth middleware (the declared
authenticate middleware string). Ownership is enforced in the service
layer: update/del act only when the review's userId matches the
AUTHENTICATED caller (a non-owner gets 404, not 403 — existence is not
leaked). Never pass a client-supplied user id.
One review per user per resource: reviews is UNIQUE on
(resourceType, resourceId, userId) — a second create for the same
resource fails at the database; use update to change an existing review.
Helpful votes are idempotent (review_helpful is keyed by
(reviewId, userId); duplicates are silently ignored).
The read routes (list, read, averageRating) are PUBLIC by default so
anonymous visitors can browse ratings. This package does NOT validate that
the reviewed resource exists or is visible to the caller — if reviews
attach to private resources in your app, gate these routes behind the
parent resource's own access check.
Tables: src/__setup__/reviews.sql creates reviews and review_helpful.
An mlcl-scaffolded API replays __setup__/*.sql automatically on migrate;
anywhere else run it once — nothing at runtime creates them.
Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual review screens/flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip. Verify BEHAVIOR and the business rules, not just that CRUD compiles: