Skip to main content

Items API Endpoints Deep Dive

The Items API provides public-facing endpoints for interacting with items, including comments, votes, views tracking, company associations, and engagement metrics. These endpoints power the core user-facing features of the directory website.

Source directory: template/app/api/items/


Route Map

MethodPathAuthDescription
GET/api/items/{slug}/commentsPublicList item comments
POST/api/items/{slug}/commentsSessionCreate a comment
PUT/api/items/{slug}/comments/{commentId}Session (owner)Update a comment
DELETE/api/items/{slug}/comments/{commentId}Session (owner)Delete a comment
GET/api/items/{slug}/comments/ratingPublicGet rating statistics
GET/api/items/{slug}/comments/rating/{commentId}PublicGet single comment rating
PATCH/api/items/{slug}/comments/rating/{commentId}PublicUpdate comment rating
GET/api/items/{slug}/companyAdminGet item's company
POST/api/items/{slug}/companyAdminAssign company to item
DELETE/api/items/{slug}/companyAdminRemove company from item
POST/api/items/{slug}/viewsPublicRecord item view
GET/api/items/{slug}/votesPublicGet vote info + user status
POST/api/items/{slug}/votesSessionCast or update vote
DELETE/api/items/{slug}/votesSessionRemove vote
GET/api/items/{slug}/votes/countPublicGet vote count only
GET/api/items/{slug}/votes/statusSessionGet user's vote record
GET/api/items/engagementPublicBatch engagement metrics
GET/api/items/popularity-scoresPublicDebug popularity scores

Comments

List Comments

Returns all comments for a specific item, including user profile information.

PropertyValue
MethodGET
Path/api/items/{slug}/comments
AuthNone (public)
Sourceitems/[slug]/comments/route.ts

Response

Status 200

{
"success": true,
"comments": [
{
"id": "comment_123abc",
"content": "This is an amazing tool! Really helped boost my productivity.",
"rating": 5,
"userId": "client_456def",
"itemId": "item_123abc",
"createdAt": "2024-01-20T10:30:00.000Z",
"updatedAt": "2024-01-20T10:30:00.000Z",
"deletedAt": null,
"user": {
"id": "client_456def",
"name": "John Doe",
"email": "john.doe@example.com",
"avatar": "https://example.com/avatars/john.jpg"
}
}
]
}

curl Example

curl -s http://localhost:3000/api/items/awesome-productivity-tool/comments

Create Comment

Creates a new comment with a rating for an item.

PropertyValue
MethodPOST
Path/api/items/{slug}/comments
AuthSession (user with client profile)
Sourceitems/[slug]/comments/route.ts

Request Body

{
"content": "This tool is excellent for team collaboration!",
"rating": 5
}
FieldTypeRequiredDescription
contentstringYesComment text (must be non-empty)
ratingintegerYesRating from 1 to 5

Responses

StatusDescription
200Comment created successfully
400Invalid content or rating
401Authentication required
403User is blocked (suspended or banned)
404Client profile not found
500Server error

Status 200

{
"success": true,
"comment": {
"id": "comment_new123",
"content": "This tool is excellent for team collaboration!",
"rating": 5,
"userId": "client_456def",
"itemId": "awesome-productivity-tool",
"createdAt": "2024-01-21T14:00:00.000Z",
"updatedAt": "2024-01-21T14:00:00.000Z",
"deletedAt": null,
"user": {
"id": "client_456def",
"name": "John Doe",
"email": "john.doe@example.com",
"avatar": "https://example.com/avatars/john.jpg"
}
}
}

curl Example

curl -s -X POST http://localhost:3000/api/items/awesome-productivity-tool/comments \
-H "Content-Type: application/json" \
-H "Cookie: next-auth.session-token=<session_token>" \
-d '{ "content": "Great tool!", "rating": 5 }'
Moderation

Blocked users (suspended or banned) receive a 403 response with a message explaining their block status. The isUserBlocked() check is performed using the client profile's status field.


Update Comment

Updates a comment's content and/or rating. Only the comment author can update their comment.

PropertyValue
MethodPUT
Path/api/items/{slug}/comments/{commentId}
AuthSession (comment owner)
Sourceitems/[slug]/comments/[commentId]/route.ts

Request Body

At least one field must be provided:

{
"content": "Updated review text.",
"rating": 4
}
FieldTypeRequiredConstraints
contentstringNo1-1000 characters
ratingintegerNo1-5

Response

Status 200 -- Returns the updated comment with user information and an editedAt timestamp.

{
"id": "comment_123abc",
"content": "Updated review text.",
"rating": 4,
"userId": "client_456def",
"itemId": "awesome-productivity-tool",
"createdAt": "2024-01-20T10:30:00.000Z",
"updatedAt": "2024-01-21T15:00:00.000Z",
"editedAt": "2024-01-21T15:00:00.000Z",
"deletedAt": null,
"user": {
"id": "client_456def",
"name": "John Doe",
"email": "john.doe@example.com",
"image": "https://example.com/avatars/john.jpg"
}
}

Delete Comment

Soft-deletes a comment. Only the comment author can delete their comment.

PropertyValue
MethodDELETE
Path/api/items/{slug}/comments/{commentId}
AuthSession (comment owner)
Sourceitems/[slug]/comments/[commentId]/route.ts

Response

Status 204 -- No content (comment deleted successfully).

StatusDescription
204Comment deleted
401Unauthorized
404Comment not found or not authorized

curl Example

curl -s -X DELETE http://localhost:3000/api/items/awesome-tool/comments/comment_123 \
-H "Cookie: next-auth.session-token=<session_token>"

Get Rating Statistics

Returns aggregated rating statistics for an item: average rating and total count.

PropertyValue
MethodGET
Path/api/items/{slug}/comments/rating
AuthNone (public)
Sourceitems/[slug]/comments/rating/route.ts

Response

Status 200

{
"averageRating": 4.2,
"totalRatings": 15
}
FieldTypeDescription
averageRatingnumberAverage rating (0 if no ratings, max 5)
totalRatingsnumberTotal number of non-deleted comments with ratings

curl Example

curl -s http://localhost:3000/api/items/awesome-productivity-tool/comments/rating

Get/Update Single Comment Rating

Get Comment Rating

PropertyValue
MethodGET
Path/api/items/{slug}/comments/rating/{commentId}
AuthNone (public)

Returns the full comment object for a specific comment ID.

Update Comment Rating

PropertyValue
MethodPATCH
Path/api/items/{slug}/comments/rating/{commentId}
AuthNone

Request Body:

{
"rating": 4
}

Returns the updated comment object.


Company Association

Admin-only endpoints to manage the relationship between items and companies.

Get Item Company

PropertyValue
MethodGET
Path/api/items/{slug}/company
AuthAdmin
Sourceitems/[slug]/company/route.ts

Response

Status 200 -- Company found.

{
"success": true,
"data": {
"id": "company_123",
"name": "Acme Corp",
"website": "https://acme.com"
}
}

Status 200 -- No company assigned.

{
"success": true,
"data": null
}

Assign Company to Item

Assigns a company to an item. This operation is idempotent.

PropertyValue
MethodPOST
Path/api/items/{slug}/company
AuthAdmin
Sourceitems/[slug]/company/route.ts

Request Body

{
"companyId": "company_123"
}

Responses

Status 201 -- New association created.

{
"success": true,
"data": { /* association object */ },
"created": true,
"updated": false
}

Status 200 -- Existing association updated.

{
"success": true,
"data": { /* association object */ },
"created": false,
"updated": true
}

Status 409 -- Item already linked to a different company.

{
"error": "Item is already linked to another company"
}

Remove Company from Item

Removes the company association from an item. This operation is idempotent.

PropertyValue
MethodDELETE
Path/api/items/{slug}/company
AuthAdmin

Response

Status 200

{
"success": true,
"deleted": true
}

curl Example

# Assign company
curl -s -X POST http://localhost:3000/api/items/awesome-tool/company \
-H "Content-Type: application/json" \
-H "Cookie: next-auth.session-token=<admin_session>" \
-d '{ "companyId": "company_123" }'

# Remove company
curl -s -X DELETE http://localhost:3000/api/items/awesome-tool/company \
-H "Cookie: next-auth.session-token=<admin_session>"

Views

Record Item View

Records a unique daily view for an item with built-in deduplication, bot detection, and owner exclusion.

PropertyValue
MethodPOST
Path/api/items/{slug}/views
AuthNone (public)
Sourceitems/[slug]/views/route.ts

Processing Flow

  1. Database check -- verifies database availability.
  2. Bot detection -- rejects known bot user agents.
  3. Item validation -- confirms the item exists (returns 404 if not found).
  4. Owner exclusion -- if authenticated, skips counting if the viewer is the item owner.
  5. Viewer ID -- reads or creates a viewer cookie (VIEWER_COOKIE_NAME) for anonymous tracking.
  6. Daily deduplication -- records the view only once per viewer per day.

Response

Status 200 -- View processed.

{ "success": true, "counted": true }
Scenariocountedreason
New view recordedtrue--
Duplicate view (same day)false--
Bot detectedfalse"bot"
Owner viewing own itemfalse"owner"

Status 404 -- Item not found.

{ "success": false, "error": "Item not found" }

curl Example

curl -s -X POST http://localhost:3000/api/items/awesome-productivity-tool/views \
-H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)"

Implementation Notes

  • The viewer cookie is HttpOnly, Secure in production, and has SameSite: lax.
  • View deduplication is based on (itemId, viewerId, viewedDateUtc) where the date is YYYY-MM-DD in UTC.
  • The isBot() utility checks the user agent against known bot patterns.

Votes

Get Vote Info

Returns the total vote count and the current user's vote status (if authenticated).

PropertyValue
MethodGET
Path/api/items/{slug}/votes
AuthNone (public; user status requires session)
Sourceitems/[slug]/votes/route.ts

Response

Status 200

{
"success": true,
"count": 15,
"userVote": "up"
}
FieldTypeDescription
countnumberNet vote count (upvotes - downvotes)
userVote"up" | "down" | nullUser's vote (null if unauthenticated or no vote)

Cast or Update Vote

Casts a new vote or replaces an existing vote.

PropertyValue
MethodPOST
Path/api/items/{slug}/votes
AuthSession (user with client profile)
Sourceitems/[slug]/votes/route.ts

Request Body

{
"type": "up"
}
FieldTypeRequiredDescription
typestringYesVote type: "up" or "down"

Response

Status 200

{
"success": true,
"count": 16,
"userVote": "up"
}
StatusDescription
200Vote cast successfully
400Invalid vote type
401Unauthorized
403User is blocked (suspended/banned)
404Client profile not found

curl Example

# Upvote
curl -s -X POST http://localhost:3000/api/items/awesome-tool/votes \
-H "Content-Type: application/json" \
-H "Cookie: next-auth.session-token=<session_token>" \
-d '{ "type": "up" }'

# Downvote
curl -s -X POST http://localhost:3000/api/items/awesome-tool/votes \
-H "Content-Type: application/json" \
-H "Cookie: next-auth.session-token=<session_token>" \
-d '{ "type": "down" }'

Remove Vote

Removes the current user's vote from an item.

PropertyValue
MethodDELETE
Path/api/items/{slug}/votes
AuthSession (user with client profile)
Sourceitems/[slug]/votes/route.ts

Response

Status 200

{
"success": true,
"count": 14,
"userVote": null
}

Get Vote Count

A lightweight endpoint that returns only the vote count (no user status).

PropertyValue
MethodGET
Path/api/items/{slug}/votes/count
AuthNone (public)
Sourceitems/[slug]/votes/count/route.ts

Response

Status 200

{
"success": true,
"count": 15
}

Get User Vote Status

Returns the full vote record for the authenticated user's vote on a specific item.

PropertyValue
MethodGET
Path/api/items/{slug}/votes/status
AuthSession (user)
Sourceitems/[slug]/votes/status/route.ts

Response

Status 200 -- User has voted.

{
"id": "vote_123abc",
"userId": "client_456def",
"itemId": "item_123abc",
"voteType": "UPVOTE",
"createdAt": "2024-01-20T10:30:00.000Z",
"updatedAt": "2024-01-20T10:30:00.000Z"
}

Status 200 -- User has not voted.

null

Engagement Metrics

Batch Engagement Metrics

Fetches engagement metrics (views, votes, ratings, favorites, comments) for multiple items in a single request.

PropertyValue
MethodGET
Path/api/items/engagement
AuthNone (public)
Cachingforce-dynamic
Sourceitems/engagement/route.ts

Query Parameters

ParameterTypeRequiredDescription
slugsstringYesComma-separated list of item slugs (max 200)

Response

Status 200

{
"metrics": {
"awesome-tool": {
"views": 1500,
"votes": 25,
"avgRating": 4.2,
"favorites": 12,
"comments": 8
},
"another-tool": {
"views": 800,
"votes": 10,
"avgRating": 3.8,
"favorites": 5,
"comments": 3
}
}
}

Error Responses

StatusDescription
400Missing slugs parameter or more than 200 slugs

curl Example

curl -s "http://localhost:3000/api/items/engagement?slugs=awesome-tool,another-tool,third-tool"

Popularity Scores (Debug)

A debug endpoint that returns items sorted by their calculated popularity score with a detailed breakdown of scoring factors.

PropertyValue
MethodGET
Path/api/items/popularity-scores
AuthNone (public)
Cachingforce-dynamic
Sourceitems/popularity-scores/route.ts

Query Parameters

ParameterTypeRequiredDefaultDescription
limitintegerNo20Number of items to return (max 100)
localestringNo"en"Language for items

Response

Status 200

{
"totalItems": 150,
"showing": 20,
"items": [
{
"rank": 1,
"name": "Top Tool",
"slug": "top-tool",
"featured": true,
"score": 15234,
"scoreBreakdown": {
"featured": 10000,
"views": 2500,
"votes": 1200,
"rating": 2100,
"favorites": 900,
"comments": 234,
"recency": 300
},
"engagement": {
"views": 5000,
"votes": 50,
"avgRating": 4.2,
"favorites": 30,
"comments": 15
},
"ageInDays": 15
}
]
}

Scoring Algorithm

The popularity score uses logarithmic scaling to prevent outliers from dominating:

FactorWeightFormula
Featured boost10000Flat bonus for featured items
Views1000log10(views + 1) * 1000
Votes1200log10(max(votes, 0) + 1) * 1200
Average rating500avgRating * 500
Favorites1100log10(favorites + 1) * 1100
Comments1000log10(comments + 1) * 1000
Recencyup to 1000Decaying bonus for items under 180 days old

Items without engagement data receive a small heuristic score based on metadata quality (tags count, name length, icon presence, promo code).

curl Example

curl -s "http://localhost:3000/api/items/popularity-scores?limit=10&locale=en"

TypeScript Usage

// Fetch comments for an item
const commentsRes = await fetch(`/api/items/${slug}/comments`);
const { comments } = await commentsRes.json();

// Post a comment
const newComment = await fetch(`/api/items/${slug}/comments`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: 'Great tool!', rating: 5 }),
}).then(r => r.json());

// Upvote an item
const voteRes = await fetch(`/api/items/${slug}/votes`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'up' }),
}).then(r => r.json());
console.log(`New vote count: ${voteRes.count}`);

// Record a view
await fetch(`/api/items/${slug}/views`, { method: 'POST' });

// Batch fetch engagement for multiple items
const slugList = ['tool-a', 'tool-b', 'tool-c'].join(',');
const { metrics } = await fetch(`/api/items/engagement?slugs=${slugList}`).then(r => r.json());

// Get rating stats
const { averageRating, totalRatings } = await fetch(
`/api/items/${slug}/comments/rating`
).then(r => r.json());

Moderation Integration

Several endpoints in the Items API integrate with the moderation system:

  • Commenting: The POST /api/items/{slug}/comments endpoint checks if the user is blocked (suspended or banned) before allowing comment creation.
  • Voting: The POST /api/items/{slug}/votes endpoint performs the same block check.
  • Blocked users receive a 403 response with a human-readable message explaining their status.

The block check uses isUserBlocked() and getBlockReasonMessage() from @/lib/db/queries/moderation.queries.