Skip to main content

Favorites API Endpoints

The Favorites API allows authenticated users to manage their personal list of favorite items. Each favorite stores item metadata (name, icon, category) for quick display without requiring a join to the content layer.

Source files:

  • template/app/api/favorites/route.ts
  • template/app/api/favorites/[itemSlug]/route.ts

Endpoint Summary

MethodPathAuthDescription
GET/api/favoritesSessionList all favorites for the current user
POST/api/favoritesSessionAdd an item to favorites
DELETE/api/favorites/{itemSlug}SessionRemove an item from favorites

All endpoints require an authenticated user session and a working database connection (checked via checkDatabaseAvailability).


GET /api/favorites

Returns all items favorited by the authenticated user, ordered by creation date (oldest first).

Request

No query parameters or body required. Authentication is provided via session cookie.

Response Shape

200 -- Success

{
"success": true,
"favorites": [
{
"id": "fav_123abc",
"userId": "user_456def",
"itemSlug": "awesome-productivity-tool",
"itemName": "Awesome Productivity Tool",
"itemIconUrl": "https://example.com/icons/tool.png",
"itemCategory": "productivity",
"createdAt": "2024-01-20T10:30:00.000Z",
"updatedAt": "2024-01-20T10:30:00.000Z"
}
]
}

401 -- Unauthorized

{
"success": false,
"error": "Unauthorized"
}

500 -- Server Error

{
"success": false,
"error": "Failed to fetch favorites"
}

POST /api/favorites

Adds an item to the authenticated user's favorites. Includes duplicate checking to prevent adding the same item twice.

Request Body

FieldTypeRequiredDescription
itemSlugstringYesUnique item slug identifier
itemNamestringYesItem display name
itemIconUrlstringNoURL to the item's icon
itemCategorystringNoCategory name for the item

The request body is validated using a Zod schema:

const addFavoriteSchema = z.object({
itemSlug: z.string().min(1),
itemName: z.string().min(1),
itemIconUrl: z.string().optional(),
itemCategory: z.string().optional(),
});

Request Example

{
"itemSlug": "awesome-productivity-tool",
"itemName": "Awesome Productivity Tool",
"itemIconUrl": "https://example.com/icons/tool.png",
"itemCategory": "productivity"
}

Response Shape

201 -- Created

{
"success": true,
"favorite": {
"id": "fav_123abc",
"userId": "user_456def",
"itemSlug": "awesome-productivity-tool",
"itemName": "Awesome Productivity Tool",
"itemIconUrl": "https://example.com/icons/tool.png",
"itemCategory": "productivity",
"createdAt": "2024-01-20T10:30:00.000Z",
"updatedAt": "2024-01-20T10:30:00.000Z"
}
}

400 -- Validation Error

{
"success": false,
"error": "Invalid request data",
"details": "itemSlug is required and must be a non-empty string"
}

401 -- Unauthorized

{
"success": false,
"error": "Unauthorized"
}

409 -- Conflict (Duplicate)

{
"success": false,
"error": "Item is already in favorites"
}

Duplicate Detection

Before inserting, the handler checks for an existing favorite with the same user and item slug:

const existingFavorite = await db
.select()
.from(favorites)
.where(
and(
eq(favorites.userId, session.user.id),
eq(favorites.itemSlug, validatedData.itemSlug)
)
)
.limit(1);

if (existingFavorite.length > 0) {
return NextResponse.json(
{ success: false, error: "Item is already in favorites" },
{ status: 409 }
);
}

DELETE /api/favorites/{itemSlug}

Removes a specific item from the authenticated user's favorites list.

Path Parameters

ParameterTypeRequiredDescription
itemSlugstringYesThe slug of the item to remove

Response Shape

200 -- Successfully Removed

{
"success": true,
"message": "Favorite removed successfully"
}

401 -- Unauthorized

{
"success": false,
"error": "Unauthorized"
}

404 -- Not Found

Returned when the favorite does not exist or does not belong to the current user:

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

How It Works

The handler verifies ownership before deleting. It first queries for a matching favorite owned by the current user, then deletes only if found:

const existingFavorite = await db
.select()
.from(favorites)
.where(
and(
eq(favorites.userId, session.user.id),
eq(favorites.itemSlug, itemSlug)
)
)
.limit(1);

if (existingFavorite.length === 0) {
return NextResponse.json(
{ success: false, error: "Favorite not found" },
{ status: 404 }
);
}

await db
.delete(favorites)
.where(
and(
eq(favorites.userId, session.user.id),
eq(favorites.itemSlug, itemSlug)
)
);

Usage Example (Full Workflow)

// 1. List current favorites
const listRes = await fetch('/api/favorites');
const { favorites } = await listRes.json();

// 2. Add a new favorite
const addRes = await fetch('/api/favorites', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
itemSlug: 'new-tool',
itemName: 'New Tool',
itemCategory: 'utilities'
})
});
const { favorite } = await addRes.json();

// 3. Remove a favorite
const deleteRes = await fetch('/api/favorites/new-tool', {
method: 'DELETE'
});
const { message } = await deleteRes.json();

Database Requirements

  • Requires the favorites table to exist in the database schema.
  • checkDatabaseAvailability() is called at the start of each handler.
  • Error responses use safeErrorResponse to avoid leaking internal details.
FilePurpose
template/app/api/favorites/route.tsGET (list) and POST (add) handlers
template/app/api/favorites/[itemSlug]/route.tsDELETE handler
template/lib/db/schema.tsfavorites table definition
template/lib/utils/database-check.tsDatabase availability check
template/lib/utils/api-error.tsSafe error response utility