This guide documents the backend API endpoints for PrepPilot. It includes endpoint paths, request and response examples, authentication requirements, and error cases.
Ground truth: Route registration in
backend/server.jsis the authoritative source for all paths and prefixes. Runnode scripts/check-routes.jsto verify this document stays in sync.
Most protected routes require a Bearer token in the Authorization header:
Authorization: Bearer <JWT_TOKEN>
- JSON body:
application/json - File upload:
multipart/form-data
| Limiter | Applied to | Window | Max requests |
|---|---|---|---|
authLimiter |
/api/auth/register, /api/auth/login |
15 min | 50 |
aiLimiter |
/api/generate, /api/ai/generate, /api/resume/compile, /api/resume/analyze |
1 hour | 20 |
generalLimiter |
All other routes | 15 min | 100 |
POST /api/auth/register- Public (rate-limited: 50 req / 15 min per IP)
Password requirements: minimum 8 characters, at least one uppercase letter, one lowercase letter, one digit, and one special character from @$!%*?&.
Request Body:
{
"name": "Jane Doe",
"email": "jane@example.com",
"password": "SecurePass1@",
"profileImageUrl": "https://example.com/avatar.png"
}Response 201:
{
"_id": "6426c5a5...",
"name": "Jane Doe",
"email": "jane@example.com",
"profileImageUrl": "https://example.com/avatar.png",
"firstName": "Jane",
"lastName": "Doe",
"prepPilotId": "jane1234",
"token": "eyJhb..."
}Errors:
400password does not meet requirements400user already exists500server error
POST /api/auth/login- Public (rate-limited: 50 req / 15 min per IP)
Request Body:
{
"email": "jane@example.com",
"password": "SecurePass1@"
}Response 200:
{
"_id": "6426c5a5...",
"name": "Jane Doe",
"email": "jane@example.com",
"profileImageUrl": "https://example.com/avatar.png",
"token": "eyJhb..."
}Errors:
401invalid email or password500server error
GET /api/auth/profile- Private
Headers:
Authorization: Bearer <JWT_TOKEN>
Response 200: Full user object (password excluded).
Errors:
401not authorized / token failed404user not found500server error
PUT /api/auth/profile- Private
All fields are optional — only supplied fields are updated.
Headers:
Authorization: Bearer <JWT_TOKEN>
Request Body:
{
"firstName": "Jane",
"lastName": "Doe",
"bio": "Software Engineer",
"country": "India",
"profileImageUrl": "https://example.com/new-avatar.png",
"visibility": "Public",
"prepPilotId": "jane_dev",
"educationDetails": {
"school": "IIT Delhi",
"degree": "B.Tech",
"branch": "Computer Science",
"graduationYear": "2024"
},
"profileDetails": {
"aboutMe": "Passionate about open source.",
"education": "B.Tech CSE",
"achievements": "GSSoC contributor",
"workExperience": "SDE Intern at XYZ",
"socials": {
"github": "https://github.com/jane",
"linkedin": "https://linkedin.com/in/jane",
"twitter": "https://twitter.com/jane",
"portfolio": "https://jane.dev"
}
},
"platformPreferences": {
"theme": "dark",
"notificationsEnabled": false
}
}Response 200: Updated user object (password excluded).
Errors:
400PrepPilot ID already taken401not authorized / token failed404user not found500server error
PUT /api/auth/change-password- Private
Headers:
Authorization: Bearer <JWT_TOKEN>
Request Body:
{
"originalPassword": "OldPass1@",
"newPassword": "NewPass2#"
}Response 200:
{
"success": true,
"message": "Password updated successfully"
}Errors:
400missing fields400incorrect original password401not authorized / token failed404user not found500server error
DELETE /api/auth/delete-account- Private
Permanently deletes the authenticated user's account. This action is irreversible.
Headers:
Authorization: Bearer <JWT_TOKEN>
Response 200:
{
"success": true,
"message": "Account deleted successfully"
}Errors:
401not authorized / token failed404user not found500server error
POST /api/auth/upload-image- Public — no authentication or rate limiter applied
⚠️ Note for contributors: This endpoint has noprotectguard and no rate limiter. See issue #128 for the tracked remediation. Do not add features that depend onreq.userin this handler until auth is added.
Form field:
image: image file (multipart/form-data)
Response 200:
{
"imageUrl": "http://localhost:5000/uploads/abc123.png"
}Errors:
400no file uploaded
POST /api/ai/generate- Public (rate-limited: 20 req / hour per IP)
⚠️ Note: The path is/api/ai/generate. An alias/api/generatealso exists (same handler, same rate limit). Both are registered viaapp.use("/api", aiRoutes)inserver.js. Prefer/api/ai/generatefor clarity.
Request Body:
{
"prompt": "Explain event delegation in JavaScript."
}Response 200:
{
"text": "Event delegation is...",
"model": "models/gemini-2.5-flash"
}Errors:
400missing prompt500generation failed
GET /api/models- Public
⚠️ Note: Registered viaapp.use("/api", aiRoutes)— the effective path is/api/models. Availability depends on theGEMINI_API_KEYin use and the caller's region.
Response 200:
{
"availableModels": ["gemini-2.5-flash", "gemini-flash-latest"],
"configured": "models/gemini-2.5-flash",
"note": "Actual availability depends on your API key & region. Set GEMINI_MODEL in .env to force a specific one."
}Errors:
500failed to list models
POST /api/ai/generate-questions- Private
Request Body:
{
"role": "Frontend Engineer",
"experience": "2 years",
"topicsToFocus": ["React", "JavaScript"],
"numberOfQuestions": 5
}Response 200:
{
"model": "models/gemini-2.5-flash",
"question": [
{"question": "Explain the virtual DOM.", "answer": "..."}
]
}Errors:
400missing required fields500Gemini generation failed
POST /api/ai/generate-explanation- Private
Request Body:
{
"question": "What is a closure in JavaScript?"
}Response 200:
{
"model": "models/gemini-2.5-flash",
"explanation": "..."
}Errors:
400missing question500Gemini generation failed
POST /api/sessions/create- Private
Request Body:
{
"role": "Backend Engineer",
"experience": "3 years",
"topicsToFocus": ["Node.js", "Databases"],
"description": "Prepare for backend interview",
"question": [{"question": "Explain ACID properties", "answer": "..."}]
}Response 201:
{
"success": true,
"session": {
"_id": "6426c5a5...",
"role": "Backend Engineer",
"experience": "3 years",
"description": "Prepare for backend interview",
"questions": ["..."]
}
}Errors:
403session limit reached (default max: 50)500server error
GET /api/sessions/my-sessions- Private
Response 200:
[
{"_id": "...", "role": "...", "questions": [...]}
]Errors:
500server error
GET /api/sessions/:id- Private
Response 200:
{
"success": true,
"session": {
"_id": "6426c5a5...",
"questions": []
}
}Errors:
404session not found500server error
DELETE /api/sessions/:id- Private
Response 200:
{
"message": "Session delete sucessfully"
}Errors:
401not authorized to delete this session404session not found500server error
POST /api/question/add- Private
Request Body:
{
"sessionId": "6426c5a5...",
"questions": [
{"question": "What is polymorphism?", "answer": "..."}
]
}Response 200:
[
{"_id": "...", "session": "...", "question": "...", "answer": "..."}
]Errors:
400invalid input data404session not found500server error
POST /api/question/:id/pin- Private
Response 200:
{
"success": true,
"question": {"_id": "...", "isPinned": true}
}Errors:
404question not found500server error
POST /api/question/:id/note- Private
Request Body:
{
"note": "Add more detail about the answer flow."
}Response 200:
{
"success": true,
"question": {"_id": "...", "note": "..."}
}Errors:
404question not found500server error
POST /api/resume/compile- Private (rate-limited: 20 req / hour per IP)
⚠️ External dependency: This endpoint proxies to texlive.net, a public LaTeX compilation service. Network failures or texlive.net downtime will surface as 500 errors. Authentication and rate limiting are applied to prevent billing abuse.
Request Body:
{
"code": "\\documentclass{article}..."
}Response 200: PDF binary (Content-Type: application/pdf)
Errors:
400no LaTeX code provided400LaTeX syntax error (includes partial compiler log)500compilation failed
POST /api/resume/analyze- Private (rate-limited: 20 req / hour per IP)
⚠️ External dependency: This endpoint sends the uploaded PDF to the Gemini API (paid quota). Authentication and rate limiting are applied to prevent quota exhaustion.
Form fields (multipart/form-data):
resume: PDF file (required)targetRole: string (optional, default:"General Professional")
Response 200:
{
"resumeScore": 85,
"roleMatch": 90,
"missingSkills": ["Docker"],
"missingProjects": ["Open Source Contributions"],
"atsCompatibility": {"status": "Good", "remarks": "Document structure is parseable."},
"suggestions": ["Add a summary section."]
}Errors:
400no resume file uploaded500AI analysis failed
POST /api/resume/save- Private
Request Body:
{
"title": "Senior Engineer Resume",
"latexCode": "\\documentclass{article}...",
"resumeId": "6426c5a5..."
}
resumeIdis optional. If provided, the matching resume is updated. If omitted, a new resume is created.
Response 200:
{
"success": true,
"resume": {"_id": "...", "title": "...", "latexCode": "..."}
}Errors:
400title or LaTeX code missing500server error
GET /api/resume/my-resumes- Private
Response 200:
{
"success": true,
"resumes": [
{"_id": "...", "title": "...", "latexCode": "..."}
]
}Errors:
500server error
GET /api/books/- Public
Response 200:
{
"categories": [
{
"id": "algorithms",
"title": "Algorithms",
"count": 10,
"items": [{"id": "...", "name": "...", "size": 1234, "url": "..."}]
}
],
"warnings": []
}Errors:
500failed to load books
GET /api/books/download?url=<raw_file_url>- Public
Response: Redirect to the GitHub raw file URL
Errors:
400url query is required
POST /api/user/sheet-progress- Private
Request Body:
{
"sheetId": "arrays",
"followed": true,
"completedTopics": ["Two Pointers", "Sliding Window"],
"percentage": 60
}Response 200:
{
"success": true,
"progress": {"sheetId": "arrays", "percentage": 60}
}Errors:
500server error
GET /api/user/sheet-progress/:sheetId- Private
Response 200:
{
"success": true,
"progress": {"sheetId": "arrays", "percentage": 60}
}Errors:
500server error
GET /api/user/sheet-progress- Private
Response 200:
{
"success": true,
"progressList": [
{"sheetId": "arrays", "percentage": 60}
]
}GET /api/user/achievements- Private
Response 200:
{
"success": true,
"unlockedAchievements": ["first_session", "streak_7"]
}Errors:
500server error
POST /api/user/achievements- Private
Request Body:
{
"unlockedAchievements": ["first_session", "streak_7"]
}Response 200:
{
"success": true
}Errors:
500server error
GET /api/test- Public
Response 200:
{
"message": "API is working!"
}