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
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /api/items/{slug}/comments | Public | List item comments |
POST | /api/items/{slug}/comments | Session | Create 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/rating | Public | Get rating statistics |
GET | /api/items/{slug}/comments/rating/{commentId} | Public | Get single comment rating |
PATCH | /api/items/{slug}/comments/rating/{commentId} | Public | Update comment rating |
GET | /api/items/{slug}/company | Admin | Get item's company |
POST | /api/items/{slug}/company | Admin | Assign company to item |
DELETE | /api/items/{slug}/company | Admin | Remove company from item |
POST | /api/items/{slug}/views | Public | Record item view |
GET | /api/items/{slug}/votes | Public | Get vote info + user status |
POST | /api/items/{slug}/votes | Session | Cast or update vote |
DELETE | /api/items/{slug}/votes | Session | Remove vote |
GET | /api/items/{slug}/votes/count | Public | Get vote count only |
GET | /api/items/{slug}/votes/status | Session | Get user's vote record |
GET | /api/items/engagement | Public | Batch engagement metrics |
GET | /api/items/popularity-scores | Public | Debug popularity scores |
Comments
List Comments
Returns all comments for a specific item, including user profile information.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/items/{slug}/comments |
| Auth | None (public) |
| Source | items/[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.
| Property | Value |
|---|---|
| Method | POST |
| Path | /api/items/{slug}/comments |
| Auth | Session (user with client profile) |
| Source | items/[slug]/comments/route.ts |
Request Body
{
"content": "This tool is excellent for team collaboration!",
"rating": 5
}
| Field | Type | Required | Description |
|---|---|---|---|
content | string | Yes | Comment text (must be non-empty) |
rating | integer | Yes | Rating from 1 to 5 |
Responses
| Status | Description |
|---|---|
| 200 | Comment created successfully |
| 400 | Invalid content or rating |
| 401 | Authentication required |
| 403 | User is blocked (suspended or banned) |
| 404 | Client profile not found |
| 500 | Server 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 }'
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.
| Property | Value |
|---|---|
| Method | PUT |
| Path | /api/items/{slug}/comments/{commentId} |
| Auth | Session (comment owner) |
| Source | items/[slug]/comments/[commentId]/route.ts |
Request Body
At least one field must be provided:
{
"content": "Updated review text.",
"rating": 4
}
| Field | Type | Required | Constraints |
|---|---|---|---|
content | string | No | 1-1000 characters |
rating | integer | No | 1-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.
| Property | Value |
|---|---|
| Method | DELETE |
| Path | /api/items/{slug}/comments/{commentId} |
| Auth | Session (comment owner) |
| Source | items/[slug]/comments/[commentId]/route.ts |
Response
Status 204 -- No content (comment deleted successfully).
| Status | Description |
|---|---|
| 204 | Comment deleted |
| 401 | Unauthorized |
| 404 | Comment 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.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/items/{slug}/comments/rating |
| Auth | None (public) |
| Source | items/[slug]/comments/rating/route.ts |
Response
Status 200
{
"averageRating": 4.2,
"totalRatings": 15
}
| Field | Type | Description |
|---|---|---|
averageRating | number | Average rating (0 if no ratings, max 5) |
totalRatings | number | Total 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
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/items/{slug}/comments/rating/{commentId} |
| Auth | None (public) |
Returns the full comment object for a specific comment ID.
Update Comment Rating
| Property | Value |
|---|---|
| Method | PATCH |
| Path | /api/items/{slug}/comments/rating/{commentId} |
| Auth | None |
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
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/items/{slug}/company |
| Auth | Admin |
| Source | items/[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.
| Property | Value |
|---|---|
| Method | POST |
| Path | /api/items/{slug}/company |
| Auth | Admin |
| Source | items/[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.
| Property | Value |
|---|---|
| Method | DELETE |
| Path | /api/items/{slug}/company |
| Auth | Admin |
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.
| Property | Value |
|---|---|
| Method | POST |
| Path | /api/items/{slug}/views |
| Auth | None (public) |
| Source | items/[slug]/views/route.ts |
Processing Flow
- Database check -- verifies database availability.
- Bot detection -- rejects known bot user agents.
- Item validation -- confirms the item exists (returns 404 if not found).
- Owner exclusion -- if authenticated, skips counting if the viewer is the item owner.
- Viewer ID -- reads or creates a viewer cookie (
VIEWER_COOKIE_NAME) for anonymous tracking. - Daily deduplication -- records the view only once per viewer per day.
Response
Status 200 -- View processed.
{ "success": true, "counted": true }
| Scenario | counted | reason |
|---|---|---|
| New view recorded | true | -- |
| Duplicate view (same day) | false | -- |
| Bot detected | false | "bot" |
| Owner viewing own item | false | "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,Securein production, and hasSameSite: lax. - View deduplication is based on
(itemId, viewerId, viewedDateUtc)where the date isYYYY-MM-DDin 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).
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/items/{slug}/votes |
| Auth | None (public; user status requires session) |
| Source | items/[slug]/votes/route.ts |
Response
Status 200
{
"success": true,
"count": 15,
"userVote": "up"
}
| Field | Type | Description |
|---|---|---|
count | number | Net vote count (upvotes - downvotes) |
userVote | "up" | "down" | null | User's vote (null if unauthenticated or no vote) |
Cast or Update Vote
Casts a new vote or replaces an existing vote.
| Property | Value |
|---|---|
| Method | POST |
| Path | /api/items/{slug}/votes |
| Auth | Session (user with client profile) |
| Source | items/[slug]/votes/route.ts |
Request Body
{
"type": "up"
}
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Vote type: "up" or "down" |
Response
Status 200
{
"success": true,
"count": 16,
"userVote": "up"
}
| Status | Description |
|---|---|
| 200 | Vote cast successfully |
| 400 | Invalid vote type |
| 401 | Unauthorized |
| 403 | User is blocked (suspended/banned) |
| 404 | Client 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.
| Property | Value |
|---|---|
| Method | DELETE |
| Path | /api/items/{slug}/votes |
| Auth | Session (user with client profile) |
| Source | items/[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).
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/items/{slug}/votes/count |
| Auth | None (public) |
| Source | items/[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.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/items/{slug}/votes/status |
| Auth | Session (user) |
| Source | items/[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.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/items/engagement |
| Auth | None (public) |
| Caching | force-dynamic |
| Source | items/engagement/route.ts |
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
slugs | string | Yes | Comma-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
| Status | Description |
|---|---|
| 400 | Missing 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.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/items/popularity-scores |
| Auth | None (public) |
| Caching | force-dynamic |
| Source | items/popularity-scores/route.ts |
Query Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
limit | integer | No | 20 | Number of items to return (max 100) |
locale | string | No | "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:
| Factor | Weight | Formula |
|---|---|---|
| Featured boost | 10000 | Flat bonus for featured items |
| Views | 1000 | log10(views + 1) * 1000 |
| Votes | 1200 | log10(max(votes, 0) + 1) * 1200 |
| Average rating | 500 | avgRating * 500 |
| Favorites | 1100 | log10(favorites + 1) * 1100 |
| Comments | 1000 | log10(comments + 1) * 1000 |
| Recency | up to 1000 | Decaying 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}/commentsendpoint checks if the user is blocked (suspended or banned) before allowing comment creation. - Voting: The
POST /api/items/{slug}/votesendpoint performs the same block check. - Blocked users receive a
403response with a human-readable message explaining their status.
The block check uses isUserBlocked() and getBlockReasonMessage() from @/lib/db/queries/moderation.queries.