Generated: November 3, 2025
All routes are RESTful and return JSON responses in the following format:
{
"success": true/false,
"message": "Description of result",
"data": { ... }, // Present on success
"errors": [ ... ] // Present on validation failures
}- POST
/api/auth/signup - Rate Limited: 5 requests per 15 minutes
- Body:
{ "username": "string (3-50 chars, alphanumeric + underscore)", "email": "string (valid email)", "password": "string (min 8 chars, 1 uppercase, 1 lowercase, 1 number)" } - Response:
201 Created{ "user": { "id", "username", "email", "isAdmin", "createdAt" }, "token": "JWT token" }
- POST
/api/auth/login - Rate Limited: 5 requests per 15 minutes
- Body:
{ "email": "string", "password": "string" } - Response:
200 OK{ "user": { "id", "username", "email", "isAdmin", "createdAt", "lastLoginAt" }, "token": "JWT token" }
- GET
/api/auth/profile - Auth: Required (Bearer token)
- Response:
200 OK- User object with profile relation
All routes require authentication + admin privileges.
- GET
/api/users - Response:
200 OK- Array of users with profiles
- GET
/api/users/:id - Params:
id(UUID) - Response:
200 OK- User object with profile
- POST
/api/users - Rate Limited: 10 requests per hour
- Body:
{ "username": "string", "email": "string", "passwordHash": "string", "isAdmin": "boolean (optional)" } - Response:
201 Created
- PUT
/api/users/:id - Params:
id(UUID) - Body: Partial user object
- Response:
200 OK
- DELETE
/api/users/:id - Params:
id(UUID) - Response:
204 No Content
All routes require authentication + admin privileges.
- GET
/api/categories - Response:
200 OK- Array of categories with article counts
- GET
/api/categories/:id - Params:
id(UUID) - Response:
200 OK- Category object
- POST
/api/categories - Rate Limited: 10 requests per hour
- Body:
{ "name": "string (1-100 chars, unique)", "description": "string (optional, max 500 chars)", "color": "string (optional, hex color e.g., #FF5733)" } - Response:
201 Created
- PUT
/api/categories/:id - Params:
id(UUID) - Body: Partial category object
- Response:
200 OK
- DELETE
/api/categories/:id - Params:
id(UUID) - Note: Cannot delete category with articles
- Response:
204 No Content
- GET
/api/articles - Auth: Required
- Query Params:
page(integer, min 1, default 1)limit(integer, 1-100, default 20)sortBy(createdAt|title|publishedDate|viewCount|likeCount, default createdAt)sortOrder(asc|desc, default desc)
- Response:
200 OK{ "articles": [ ... ], "pagination": { "page": 1, "limit": 20, "total": 100, "totalPages": 5 } }
- GET
/api/articles/:id - Auth: Required
- Params:
id(UUID) - Response:
200 OK- Article with category
- POST
/api/articles/:id/view - Auth: Required
- Params:
id(UUID) - Response:
200 OK- Updated article
- POST
/api/articles - Auth: Required + Admin
- Rate Limited: 10 requests per hour
- Body:
{ "title": "string (1-500 chars)", "wikipediaUrl": "string (valid URL, unique)", "aiSummary": "string", "audioUrl": "string (optional, valid URL)", "tags": ["string", "string"] (optional), "publishedDate": "ISO 8601 date", "categoryId": "UUID" } - Response:
201 Created
- PUT
/api/articles/:id - Auth: Required + Admin
- Params:
id(UUID) - Body: Partial article object (including isActive, isProcessed)
- Response:
200 OK
- DELETE
/api/articles/:id - Auth: Required + Admin
- Params:
id(UUID) - Response:
204 No Content
- GET
/api/profiles/me - Auth: Required
- Response:
200 OK- Profile with user data
- POST
/api/profiles/me - Auth: Required
- Rate Limited: 10 requests per hour
- Body:
{ "displayName": "string (optional, 1-100 chars)", "bio": "string (optional, max 500 chars)", "interests": ["string", "string"] (optional, each 1-50 chars) } - Response:
201 Created
- PUT
/api/profiles/me - Auth: Required
- Body: Partial profile object
- Response:
200 OK
- DELETE
/api/profiles/me - Auth: Required
- Response:
204 No Content
- GET
/api/profiles - Auth: Required + Admin
- Response:
200 OK- Array of profiles with user data
- GET
/api/profiles/:userId - Auth: Required (own profile or admin)
- Params:
userId(UUID) - Response:
200 OK
- PUT
/api/profiles/:userId - Auth: Required + Admin
- Params:
userId(UUID) - Body: Partial profile object
- Response:
200 OK
- DELETE
/api/profiles/:userId - Auth: Required + Admin
- Params:
userId(UUID) - Response:
204 No Content
- POST
/api/interactions - Auth: Required
- Body:
{ "articleId": "UUID", "interactionType": "LIKE | VIEW | SAVE" } - Note: Automatically updates article counts (viewCount, likeCount, saveCount)
- Response:
201 Created
- GET
/api/interactions/me - Auth: Required
- Query Params:
type(optional: LIKE|VIEW|SAVE)
- Response:
200 OK- Array of interactions with article details
- GET
/api/interactions/check/:articleId - Auth: Required
- Params:
articleId(UUID) - Query Params:
type(required: LIKE|VIEW|SAVE)
- Response:
200 OK{ "hasInteraction": true/false }
- DELETE
/api/interactions - Auth: Required
- Body:
{ "articleId": "UUID", "interactionType": "LIKE | SAVE" (VIEW cannot be deleted) } - Note: Automatically decrements article counts
- Response:
204 No Content
- GET
/api/interactions/article/:articleId - Auth: Required + Admin
- Params:
articleId(UUID) - Response:
200 OK- Array of interactions with user data
- GET
/api/feeds/me - Auth: Required
- Note: Auto-creates empty feed if doesn't exist
- Response:
200 OK{ "id": "UUID", "userId": "UUID", "articleIds": ["UUID", "UUID", ...], "currentPosition": 0, "updatedAt": "ISO 8601" }
- POST
/api/feeds/me - Auth: Required
- Rate Limited: 10 requests per hour
- Body:
{ "articleIds": ["UUID", "UUID"] (optional) } - Response:
201 Created
- PUT
/api/feeds/me - Auth: Required
- Body:
{ "articleIds": ["UUID", "UUID"] (optional), "currentPosition": 5 (optional, non-negative integer) } - Response:
200 OK
- PUT
/api/feeds/me/position - Auth: Required
- Body:
{ "position": 10 (non-negative integer) } - Response:
200 OK
- POST
/api/feeds/me/regenerate - Auth: Required
- Body:
{ "articleIds": ["UUID", "UUID"] (required) } - Note: Resets currentPosition to 0
- Response:
200 OK
- DELETE
/api/feeds/me - Auth: Required
- Response:
204 No Content
- GET
/api/feeds - Auth: Required + Admin
- Response:
200 OK- Array of feeds with user data
- GET
/api/feeds/:userId - Auth: Required (own feed or admin)
- Params:
userId(UUID) - Response:
200 OK
- PUT
/api/feeds/:userId - Auth: Required + Admin
- Params:
userId(UUID) - Body: Partial feed object
- Response:
200 OK
- DELETE
/api/feeds/:userId - Auth: Required + Admin
- Params:
userId(UUID) - Response:
204 No Content
- GET
/health - Auth: Not required
- Response:
200 OK{ "status": "ok", "timestamp": "ISO 8601" }
All protected routes require a JWT token in the Authorization header:
Authorization: Bearer <your-jwt-token>
- Expiration: 7 days (configurable via
JWT_EXPIRES_IN) - Secret: Set via
JWT_SECRETenvironment variable - Payload:
{ id, username, email, isAdmin }
- Public: No authentication required (health check)
- Authenticated: Valid JWT token required
- Admin: Valid JWT token +
isAdmin: truerequired
- General API: 100 requests per 15 minutes per IP
- Auth Endpoints: 5 requests per 15 minutes per IP
- Create Endpoints: 10 requests per hour per IP
{
"success": false,
"message": "Validation failed",
"errors": [
{
"field": "email",
"message": "Must be a valid email address"
}
]
}{
"success": false,
"message": "Authentication required",
"errors": [
{
"field": "token",
"message": "No token provided"
}
]
}{
"success": false,
"message": "Authorization failed",
"errors": [
{
"field": "auth",
"message": "Admin privileges required"
}
]
}{
"success": false,
"message": "Article with id xxx not found"
}{
"success": false,
"message": "Email already registered"
}{
"success": false,
"message": "Internal server error"
}-
Register a new user:
POST /api/auth/signup
-
Login and get token:
POST /api/auth/login
-
Use token for subsequent requests:
GET /api/articles Header: Authorization: Bearer <token>
-
Create profile:
POST /api/profiles/me
-
Interact with articles:
POST /api/interactions Body: { "articleId": "...", "interactionType": "LIKE" } -
Get personalized feed:
GET /api/feeds/me
- All UUIDs are PostgreSQL UUID v4 format
- All dates are ISO 8601 format
- Category deletion is blocked if articles exist
- Interaction deletion automatically updates denormalized counts
- Feed auto-creates on first access if missing
- VIEW interactions can be created multiple times
- LIKE/SAVE interactions are unique per user-article pair