AI-powered interview preparation platform built on the MERN stack with Google Gemini. Upload your resume, paste a job description, and get a match score, tailored interview questions with real-time answer evaluation, ATS optimization feedback, and an analytics dashboard that tracks progress over time.
- Features
- Tech Stack
- Architecture Overview
- Project Structure
- Data Models
- API Reference
- Feature Flows
- Environment Variables
- Local Setup
- Production Deployment
- Development Phases
| Feature | Description |
|---|---|
| Resume Analysis | Upload PDF/DOCX resume + paste job description → AI match score (0–100) with matched/missing skills and recommendations |
| Interview Q&A | Generate 5–20 tailored questions by category and difficulty; submit answers and get AI-evaluated scores with feedback |
| ATS Optimizer | Heuristic + AI scoring across 6 categories (sections, action verbs, quantification, keywords, contact, length) |
| Analytics Dashboard | Score trends over time, topic mastery radar, weekly activity chart, AI coaching for weak areas |
| Session History | Searchable, filterable, paginated session list with status badges and score summaries |
| Profile Management | Edit name, change password, view personal stats and streak |
| Dark / Light Mode | Toggle with smooth transition, persisted in localStorage |
| Layer | Technology | Purpose |
|---|---|---|
| Frontend | React 19, Vite | UI framework and build tool |
| Styling | Tailwind CSS | Utility-first CSS |
| State | Zustand | Auth state management |
| Forms | react-hook-form + Zod | Form handling and client-side validation |
| Charts | Recharts | Analytics visualizations |
| HTTP | Axios | API requests with interceptors |
| Backend | Express.js, Node.js | Server and routing |
| Database | MongoDB, Mongoose | Data persistence and schemas |
| Auth | JWT, bcryptjs | Token-based auth, password hashing |
| File Upload | Multer | Multipart form handling (memory storage) |
| Resume Parsing | pdf-parse, mammoth | Text extraction from PDF and DOCX |
| AI | Google Gemini 2.5 Flash | Resume analysis, question generation, answer evaluation, coaching |
| Validation | Zod | Schema validation on client and server |
| Security | Helmet, express-rate-limit | HTTP headers, rate limiting |
| Icons | lucide-react | UI icons |
| Notifications | react-hot-toast | User feedback toasts |
Client (React/Vite)
│
│ HTTP/REST over JSON
▼
Express Server (port 5000)
│
├── Middleware: CORS, Helmet, Rate Limit, Morgan, Cookie Parser
│
├── Routes → Controllers → Services → Models
│ │
│ └── Google Gemini API (AI features)
│
└── MongoDB (via Mongoose)
| Layer | Responsibility |
|---|---|
| Routes | URL mapping, middleware chaining |
| Controllers | Request parsing, response formatting, ownership checks |
| Services | Business logic, Gemini API calls, scoring algorithms |
| Models | Mongoose schemas, pre-save hooks, instance methods |
| Middleware | Auth guard, file upload, error normalization |
| Validators | Zod schemas for incoming request bodies |
| Config | Env validation, DB connection, Gemini client initialization |
Client sends Bearer token (localStorage)
│
▼
authMiddleware.protect()
├── Extracts token from Authorization header or httpOnly cookie
├── Verifies JWT signature
└── Attaches req.user for downstream handlers
Token is issued on login/signup, stored in both an httpOnly cookie and localStorage, and attached to every request by the Axios request interceptor.
Prepify-AI/
├── server/
│ ├── server.js # Entry point — Express app, middleware, route mounting
│ ├── config/
│ │ ├── env.js # Zod env validation, exits on missing required vars
│ │ ├── db.js # MongoDB connection with exponential-backoff retry
│ │ └── gemini.js # Lazy Gemini client init (fails on first use if key missing)
│ ├── models/
│ │ ├── User.js # User schema — bcrypt hash, JWT generation, comparePassword
│ │ ├── Session.js # Session schema — resume text, JD, match/ATS results, questions
│ │ └── Question.js # Question schema — text, category, difficulty, answer, evaluation
│ ├── middleware/
│ │ ├── authMiddleware.js # protect (JWT verify), authorize (RBAC)
│ │ ├── uploadMiddleware.js # Multer memory storage, MIME + extension validation, 5 MB limit
│ │ └── errorHandler.js # Global error normalizer → consistent JSON responses
│ ├── controllers/
│ │ ├── authController.js # signup, login, logout, getMe, updateProfile, changePassword
│ │ ├── sessionController.js# createSession, getSessions, getSessionById, deleteSession
│ │ ├── matchController.js # analyzeSession (resume-JD match)
│ │ ├── questionController.js# generateQuestions, getQuestions, submitAnswer
│ │ ├── atsController.js # atsAnalyzeSession
│ │ └── analyticsController.js# overview, trends, topicMastery, weakAreas
│ ├── services/
│ │ ├── geminiService.js # generateJSON (structured output), generateText
│ │ ├── matchingService.js # skill extraction, Jaccard similarity, semantic fit, recommendations
│ │ ├── questionService.js # question generation, answer evaluation
│ │ └── atsService.js # heuristic scoring, Gemini issue/suggestion analysis
│ ├── routes/
│ │ ├── healthRoute.js # GET /api/health
│ │ ├── authRoute.js # /api/auth/*
│ │ ├── sessionRoute.js # /api/sessions/*
│ │ ├── questionRoute.js # /api/sessions/:id/questions/*
│ │ └── analyticsRoute.js # /api/analytics/*
│ ├── utils/
│ │ └── parseResume.js # parsePDF (pdf-parse), parseDOCX (mammoth), cleanText
│ └── validators/
│ ├── authValidator.js # signupSchema, loginSchema (Zod)
│ └── questionValidator.js# generateSchema, answerSchema (Zod)
│
└── client/
├── index.html
├── vite.config.js
├── tailwind.config.js
└── src/
├── main.jsx # React root — StrictMode, ThemeProvider, ErrorBoundary
├── App.jsx # Router setup, checkAuth on mount, route definitions
├── api/
│ ├── axios.js # Axios instance, request/response interceptors
│ ├── authApi.js # signup, login, logout, getMe, updateProfile, changePassword
│ ├── sessionApi.js # createSession (FormData), getSessions, getSessionById, deleteSession
│ ├── matchApi.js # analyzeSessionApi
│ ├── questionApi.js # generateQuestionsApi, getQuestionsApi, submitAnswerApi
│ ├── atsApi.js # atsAnalyzeSessionApi
│ └── analyticsApi.js # overview, trends, topicMastery, weakAreas
├── store/
│ └── authStore.js # Zustand — user, isAuthenticated, isLoading, auth actions
├── context/
│ └── ThemeContext.jsx # Dark/light mode toggle, localStorage persistence
├── routes/
│ ├── ProtectedRoute.jsx # Shows loader → redirects to /login if not authed
│ └── PublicRoute.jsx # Shows loader → redirects to /dashboard if authed
├── components/
│ ├── common/
│ │ ├── LoadingScreen.jsx # Full-screen spinner shown during auth check
│ │ └── ThemeToggle.jsx # Sun/Moon icon button
│ ├── layouts/
│ │ ├── MainLayout.jsx # Sidebar + topbar + scrollable content area
│ │ ├── AuthLayout.jsx # Centered card for login/signup pages
│ │ └── Sidebar.jsx # Nav links, collapse toggle, user info, logout
│ ├── ErrorBoundary.jsx # React error boundary with reload button
│ ├── FileDropzone.jsx # Drag-drop + click resume upload with client validation
│ ├── MatchScoreCard.jsx # Animated SVG circular progress ring
│ ├── SkillsBreakdown.jsx # Matched vs missing skills grid
│ ├── Recommendations.jsx # Bulleted AI recommendations list
│ ├── QuestionCard.jsx # Question display with category/difficulty badges
│ ├── EvaluationCard.jsx # Score ring + progress bars + feedback
│ └── VoiceInputButton.jsx # Web Speech API with graceful degradation
└── pages/
├── Home.jsx # Landing page with hero and feature cards
├── Login.jsx # Login form (react-hook-form + Zod)
├── Signup.jsx # Signup form
├── Dashboard.jsx # Overview stats + charts + weak areas
├── NewSession.jsx # Resume upload + job details form
├── Sessions.jsx # Paginated session list with search/filter
├── SessionDetail.jsx # Full session view with match results
├── Interview.jsx # Session picker for starting interview
├── InterviewSession.jsx # Q&A interface with voice input and evaluation
├── ATSOptimizer.jsx # ATS session list
├── ATSDetail.jsx # ATS score breakdown + issues + suggestions
├── Analytics.jsx # Trends + topic mastery charts
├── History.jsx # Historical session view
└── Profile.jsx # User profile, stats, name/password update
{
name: String, // 2–50 chars, required
email: String, // valid email, unique, lowercase
password: String, // bcrypt hash (salt 16), never returned in API responses
avatar: String, // optional URL
role: String, // 'user' | 'admin', default 'user'
createdAt: Date,
updatedAt: Date
}Instance methods:
comparePassword(candidate)— bcrypt.comparegenerateAuthToken()— signs JWT with{ id, role }, expires inJWT_EXPIRES_IN
Pre-save hook: auto-hashes password when modified.
{
user: ObjectId, // ref: User, indexed
resumeText: String, // extracted plain text from uploaded file
jobDescription: String, // pasted by user
jobTitle: String, // optional
company: String, // optional
status: String, // 'draft' | 'in-progress' | 'completed'
// Match Analysis (populated by /analyze)
matchScore: Number, // 0–100 weighted average
matchedSkills: [String],
missingSkills: [String],
recommendations: [String],
// ATS Analysis (populated by /ats-analyze)
atsScore: Number, // 0–100 heuristic score
atsBreakdown: Object, // per-category scores and evidence
atsIssues: [Object], // { issue, severity, suggestion }
atsSuggestions: [String], // ordered by impact
// Questions (embedded)
questions: [Question],
createdAt: Date,
updatedAt: Date
}{
questionText: String, // the question
category: String, // 'behavioral' | 'technical' | 'situational'
difficulty: String, // 'easy' | 'medium' | 'hard'
expectedTopics: [String], // hints used during evaluation
userAnswer: String, // null until submitted
evaluation: {
clarity: Number, // 1–10
relevance: Number, // 1–10
depth: Number, // 1–10
overall: Number, // 1–10
feedback: String, // 2–3 sentence narrative
improvementSuggestions: [String] // 2–4 actionable tips
},
answeredAt: Date,
createdAt: Date
}All protected routes require Authorization: Bearer <token> header.
Base URL: http://localhost:5000/api
GET /health
Response 200:
{ "success": true, "status": "ok", "timestamp": "2025-05-09T..." }POST /auth/signup
Body:
{ "name": "Jane Doe", "email": "jane@example.com", "password": "Secret123" }Response 201:
{ "success": true, "user": { "_id": "...", "name": "Jane Doe", "email": "..." }, "token": "..." }POST /auth/login
Body:
{ "email": "jane@example.com", "password": "Secret123" }Response 200:
{ "success": true, "user": { ... }, "token": "..." }POST /auth/logout [protected]
Response 200:
{ "success": true, "message": "Logged out" }GET /auth/me [protected]
Response 200:
{ "success": true, "user": { "_id": "...", "name": "...", "email": "...", "role": "user" } }PATCH /auth/profile [protected]
Body:
{ "name": "Jane Smith" }PATCH /auth/password [protected]
Body:
{ "currentPassword": "Secret123", "newPassword": "NewSecret456" }POST /sessions [protected]
Content-Type: multipart/form-data
Fields:
resume— PDF or DOCX file (max 5 MB)jobDescription— string (required, min 20 chars)jobTitle— string (optional)company— string (optional)
Response 201:
{
"success": true,
"session": {
"_id": "...",
"status": "draft",
"jobTitle": "Backend Engineer",
"company": "Acme Corp",
"createdAt": "..."
}
}GET /sessions?page=1&limit=10&status=completed [protected]
Query params: page, limit (max 50), status (all|draft|in-progress|completed)
Response 200:
{
"success": true,
"sessions": [ { "_id": "...", "jobTitle": "...", "status": "...", "matchScore": 78, ... } ],
"pagination": { "page": 1, "limit": 10, "total": 24, "pages": 3 }
}Note: resumeText and jobDescription are excluded from list responses for performance.
GET /sessions/:id [protected]
Returns full session document including resumeText, jobDescription, questions, and analysis results.
DELETE /sessions/:id [protected]
Ownership-checked. Hard delete.
POST /sessions/:id/analyze?force=true [protected]
- Without
?force=true: returns cached result ifmatchScorealready exists. - With
?force=true: re-runs the full Gemini pipeline.
Response 200:
{
"success": true,
"analysis": {
"matchScore": 72,
"matchedSkills": ["Node.js", "MongoDB", "REST APIs"],
"missingSkills": ["Docker", "Kubernetes", "Redis"],
"recommendations": [
"Complete a Docker fundamentals course to address the containerization gap.",
"Build a small project using Redis for caching to demonstrate practical experience."
]
}
}Scoring algorithm:
- Extract skills from resume and JD separately (10–40 skills each, parallel Gemini calls)
jaccardScore = |intersection| / |union| × 100semanticScore= Gemini rates overall candidate-role fit (0–100)matchScore = 0.5 × jaccardScore + 0.5 × semanticScore(clamped 0–100)
POST /sessions/:id/questions/generate [protected]
Body:
{
"count": 10,
"difficulty": "mixed",
"distribution": { "behavioral": 3, "technical": 5, "situational": 2 }
}count: 5–20difficulty:easy|medium|hard|mixeddistribution: must sum tocount; all three categories required
Clears existing questions before generating new ones.
Response 201:
{
"success": true,
"questions": [
{
"_id": "...",
"questionText": "Describe a time you optimized a slow database query.",
"category": "technical",
"difficulty": "medium",
"expectedTopics": ["indexing", "query plans", "profiling"],
"userAnswer": null
}
]
}GET /sessions/:id/questions [protected]
Returns all questions for the session sorted by creation date.
POST /sessions/:id/questions/:qid/answer [protected]
Body:
{ "answer": "In my last role I identified a slow N+1 query..." }answer: 10–5000 characters
Response 200:
{
"success": true,
"question": {
"_id": "...",
"userAnswer": "In my last role...",
"evaluation": {
"clarity": 8,
"relevance": 9,
"depth": 7,
"overall": 8,
"feedback": "Strong answer with a concrete example. The STAR structure was clear.",
"improvementSuggestions": [
"Quantify the performance improvement (e.g., reduced query time by 80%).",
"Mention how you identified the bottleneck using profiling tools."
]
},
"answeredAt": "2025-05-09T..."
}
}POST /sessions/:id/ats-analyze?force=true [protected]
Same caching logic as /analyze.
Response 200:
{
"success": true,
"ats": {
"atsScore": 68,
"atsBreakdown": {
"sections": { "score": 20, "max": 20, "found": ["Experience", "Education", "Skills"] },
"actionVerbs": { "score": 14, "max": 20, "percentage": 62, "examples": ["Developed", "Led"] },
"quantification":{ "score": 10, "max": 20, "count": 4 },
"keywords": { "score": 14, "max": 20, "overlap": 0.58 },
"contactInfo": { "score": 8, "max": 10, "hasEmail": true, "hasPhone": true, "hasLinkedIn": false },
"length": { "score": 10, "max": 10, "wordCount": 512 }
},
"atsIssues": [
{ "issue": "Missing LinkedIn URL", "severity": "medium", "suggestion": "Add a LinkedIn profile link to your contact section." }
],
"atsSuggestions": [
"Start 70%+ of bullet points with strong action verbs such as Designed, Architected, or Delivered.",
"Add measurable outcomes to at least 10 bullet points (e.g., reduced latency by 40%)."
]
}
}ATS Scoring Breakdown (100 points total):
| Category | Max | Criteria |
|---|---|---|
| Section Headers | 20 | Finds Experience, Education, Skills, Summary |
| Action Verbs | 20 | ≥70% of bullet points start with strong verbs |
| Quantification | 20 | ≥10 metrics/numbers detected |
| Keywords | 20 | Jaccard overlap with JD (0–1 scaled to 0–20) |
| Contact Info | 10 | Email + phone + LinkedIn = 10 pts |
| Length | 10 | Ideal 300–700 words |
All endpoints accept optional ?period=7d|30d|all query parameter.
GET /analytics/overview [protected]
Response 200:
{
"success": true,
"overview": {
"totalSessions": 12,
"avgMatchScore": 71,
"avgAtsScore": 65,
"questionsAnswered": 48,
"avgQuestionScore": 7.4,
"streakDays": 5
}
}GET /analytics/trends [protected]
Response 200:
{
"success": true,
"trends": {
"scores": [
{ "sessionId": "...", "date": "2025-05-01", "matchScore": 60, "atsScore": 55 }
],
"weeklyActivity": [
{ "week": "2025-W18", "sessions": 3, "questionsAnswered": 14 }
]
}
}GET /analytics/topic-mastery [protected]
Response 200:
{
"success": true,
"mastery": {
"behavioral": { "easy": { "count": 5, "avgScore": 8.2 }, "medium": { ... }, "hard": { ... } },
"technical": { "easy": { ... }, "medium": { ... }, "hard": { ... } },
"situational": { "easy": { ... }, "medium": { ... }, "hard": { ... } }
}
}GET /analytics/weak-areas [protected]
Requires ≥3 answered questions. Gemini identifies 2–4 lowest-performing areas with concrete improvement tips.
Response 200:
{
"success": true,
"weakAreas": [
{
"area": "Technical — Hard",
"avgScore": 5.1,
"coaching": "Focus on system design fundamentals. Practice drawing architecture diagrams and explaining trade-offs."
}
]
}User selects PDF/DOCX + pastes job description
│
▼
FileDropzone validates: MIME type, extension, ≤5 MB
│
▼
POST /sessions (multipart/form-data)
│
├── Multer streams file to memory buffer
├── parseResume() extracts plain text (pdf-parse or mammoth)
├── Guards against blank/scanned PDFs (text length check)
└── Persists Session with status 'draft'
│
▼
POST /sessions/:id/analyze
│
├── Parallel: extractSkills(resumeText), extractSkills(jobDescription)
├── jaccardScore = overlap / union × 100
├── Parallel: semanticScore (Gemini), recommendations (Gemini)
├── matchScore = (jaccardScore + semanticScore) / 2
└── Persists matchScore, matchedSkills, missingSkills, recommendations
status → 'in-progress' or 'completed'
User picks session → configures count/difficulty/distribution
│
▼
POST /sessions/:id/questions/generate
│
├── Clears existing questions
├── Gemini generates questions from resume + JD excerpt
└── Bulk inserts validated Question documents
│
▼
User reads question → types/speaks answer → submits
│
▼
POST /sessions/:id/questions/:qid/answer
│
├── Gemini evaluates answer against question + expectedTopics
├── Returns clarity, relevance, depth (1–10 each) + overall + feedback
└── Persists evaluation + answeredAt timestamp
POST /sessions/:id/ats-analyze
│
├── Heuristic checks (synchronous):
│ ├── Section headers regex scan
│ ├── Bullet-point action verb detection
│ ├── Number/metric count
│ ├── Keyword Jaccard with JD
│ ├── Contact info pattern matching
│ └── Word count range check
│
├── Parallel Gemini calls:
│ ├── Identify specific issues (severity: high/medium/low)
│ └── Generate ordered improvement suggestions
│
└── Persists atsScore, atsBreakdown, atsIssues, atsSuggestions
| Variable | Required | Default | Description |
|---|---|---|---|
MONGO_URI |
Yes | — | MongoDB connection string (local or Atlas) |
JWT_SECRET |
Yes | — | Random secret ≥32 chars — openssl rand -base64 32 |
JWT_EXPIRES_IN |
No | 7d |
Token lifetime |
GEMINI_API_KEY |
Yes | — | From Google AI Studio |
CLIENT_URL |
Yes | — | Frontend origin, comma-separated for multiple |
PORT |
No | 5000 |
Server port |
NODE_ENV |
No | development |
development | production | test |
| Variable | Required | Default | Description |
|---|---|---|---|
VITE_API_URL |
No | http://localhost:5000/api |
Backend base URL |
git clone <repo-url>
cd Prepify-AIcd server
npm install
# Create and fill .env
cp .env.example .env # if example exists, otherwise create manually
# Required: MONGO_URI, JWT_SECRET, GEMINI_API_KEY, CLIENT_URL
npm run dev # nodemon — runs on http://localhost:5000cd client
npm install
# .env only needed if changing the API URL
# VITE_API_URL=http://localhost:5000/api (this is the default)
npm run dev # Vite — runs on http://localhost:5173curl http://localhost:5000/api/health
# { "success": true, "status": "ok" }# Frontend — outputs to client/dist/
cd client && npm run build
# Backend — no build step; runs directly with Node
cd server && npm start# Nginx config
# location /api { proxy_pass http://localhost:5000; }
# location / { root /var/www/prepify/dist; try_files $uri /index.html; }
npm install -g pm2
cd server && pm2 start server.js --name prepify-api
pm2 save && pm2 startup- Frontend: Vercel or Netlify — connect repo or drop
client/dist- Set
VITE_API_URLto your backend URL in the platform env settings
- Set
- Backend: Railway, Render, or Fly.io
- Set all server env vars in the platform dashboard
- Set
CLIENT_URLto your exact frontend domain
-
NODE_ENV=production -
JWT_SECRETis a strong random value (not a placeholder) -
CLIENT_URLmatches the exact frontend origin - MongoDB Atlas IP whitelist includes your server IP
- HTTPS enabled on frontend and backend
- Rate limits reviewed (
server.js— default 100 req/15 min)
| Phase | Feature | Status |
|---|---|---|
| 1 | Project scaffolding (Express + React + MongoDB) | ✅ |
| 2 | Authentication — JWT, bcrypt, protected routes | ✅ |
| 3 | Resume upload — PDF/DOCX parsing, session creation | ✅ |
| 4 | Gemini integration — resume-JD match scoring | ✅ |
| 5 | Interview Q&A — question generation + answer evaluation | ✅ |
| 6 | ATS Optimizer — heuristic + AI scoring | ✅ |
| 7 | Analytics dashboard — trends, mastery, weak areas | ✅ |
| 8 | Polish — error handling, dark mode, responsive UI | ✅ |