Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 

Repository files navigation

Prepify AI

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.


Table of Contents

  1. Features
  2. Tech Stack
  3. Architecture Overview
  4. Project Structure
  5. Data Models
  6. API Reference
  7. Feature Flows
  8. Environment Variables
  9. Local Setup
  10. Production Deployment
  11. Development Phases

Features

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

Tech Stack

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

Architecture Overview

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)

Backend Layer Responsibilities

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

Authentication Flow

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.


Project Structure

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

Data Models

User

{
  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.compare
  • generateAuthToken() — signs JWT with { id, role }, expires in JWT_EXPIRES_IN

Pre-save hook: auto-hashes password when modified.


Session

{
  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
}

Question (embedded in Session)

{
  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
}

API Reference

All protected routes require Authorization: Bearer <token> header.

Base URL: http://localhost:5000/api


Health

GET /health

Response 200:

{ "success": true, "status": "ok", "timestamp": "2025-05-09T..." }

Auth

Register

POST /auth/signup

Body:

{ "name": "Jane Doe", "email": "jane@example.com", "password": "Secret123" }

Response 201:

{ "success": true, "user": { "_id": "...", "name": "Jane Doe", "email": "..." }, "token": "..." }

Login

POST /auth/login

Body:

{ "email": "jane@example.com", "password": "Secret123" }

Response 200:

{ "success": true, "user": { ... }, "token": "..." }

Logout

POST /auth/logout   [protected]

Response 200:

{ "success": true, "message": "Logged out" }

Get Current User

GET /auth/me   [protected]

Response 200:

{ "success": true, "user": { "_id": "...", "name": "...", "email": "...", "role": "user" } }

Update Profile

PATCH /auth/profile   [protected]

Body:

{ "name": "Jane Smith" }

Change Password

PATCH /auth/password   [protected]

Body:

{ "currentPassword": "Secret123", "newPassword": "NewSecret456" }

Sessions

Create Session (Resume Upload)

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": "..."
  }
}

List Sessions

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 Session

GET /sessions/:id   [protected]

Returns full session document including resumeText, jobDescription, questions, and analysis results.

Delete Session

DELETE /sessions/:id   [protected]

Ownership-checked. Hard delete.


Resume-JD Match Analysis

POST /sessions/:id/analyze?force=true   [protected]
  • Without ?force=true: returns cached result if matchScore already 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| × 100
  • semanticScore = Gemini rates overall candidate-role fit (0–100)
  • matchScore = 0.5 × jaccardScore + 0.5 × semanticScore (clamped 0–100)

Interview Questions

Generate Questions

POST /sessions/:id/questions/generate   [protected]

Body:

{
  "count": 10,
  "difficulty": "mixed",
  "distribution": { "behavioral": 3, "technical": 5, "situational": 2 }
}
  • count: 5–20
  • difficulty: easy | medium | hard | mixed
  • distribution: must sum to count; 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 Questions

GET /sessions/:id/questions   [protected]

Returns all questions for the session sorted by creation date.

Submit and Evaluate Answer

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..."
  }
}

ATS Analysis

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

Analytics

All endpoints accept optional ?period=7d|30d|all query parameter.

Overview

GET /analytics/overview   [protected]

Response 200:

{
  "success": true,
  "overview": {
    "totalSessions": 12,
    "avgMatchScore": 71,
    "avgAtsScore": 65,
    "questionsAnswered": 48,
    "avgQuestionScore": 7.4,
    "streakDays": 5
  }
}

Trends

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 }
    ]
  }
}

Topic Mastery

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": { ... } }
  }
}

Weak Areas (AI Coaching)

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."
    }
  ]
}

Feature Flows

Resume Upload and Match Analysis

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'

Interview Session

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

ATS Scoring

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

Environment Variables

Server (server/.env)

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

Client (client/.env)

Variable Required Default Description
VITE_API_URL No http://localhost:5000/api Backend base URL

Local Setup

Prerequisites

  • Node.js v18+
  • npm v9+
  • MongoDB (local or Atlas)
  • Google Gemini API key (AI Studio)

1. Clone

git clone <repo-url>
cd Prepify-AI

2. Backend

cd 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:5000

3. Frontend

cd 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:5173

4. Verify

curl http://localhost:5000/api/health
# { "success": true, "status": "ok" }

Production Deployment

Build

# Frontend — outputs to client/dist/
cd client && npm run build

# Backend — no build step; runs directly with Node
cd server && npm start

Option A — Single VPS (nginx + PM2)

# 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

Option B — Separate Services

  • Frontend: Vercel or Netlify — connect repo or drop client/dist
    • Set VITE_API_URL to your backend URL in the platform env settings
  • Backend: Railway, Render, or Fly.io
    • Set all server env vars in the platform dashboard
    • Set CLIENT_URL to your exact frontend domain

Production Checklist

  • NODE_ENV=production
  • JWT_SECRET is a strong random value (not a placeholder)
  • CLIENT_URL matches 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)

Development Phases

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

About

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.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages