Home / Side Quests / Interview Prep Mode

Interview Prep Mode

Estimated time: 3–4 hours | Difficulty: Intermediate

Side Quest

What You Will Build

  • An LLM-powered service that generates targeted interview questions from any job description
  • A practice UI that presents questions one at a time, simulating a real interview flow
  • A self-assessment system with star ratings and session summaries
  • A persistent question bank with filtering and search across all practiced jobs
  • An optional mock interview mode with timers and audio recording

1. Why Targeted Practice?

You have probably heard the advice before: "practice interview questions." And it is good advice. But here is the problem — when you practice with generic questions, you are preparing for an interview that does not exist. No company asks you generic questions. They ask you questions about their stack, their domain, their challenges. The difference between generic prep and targeted prep is the difference between studying "math" and studying the exact chapters that will be on the exam.

Think about it this way. If you are interviewing for a role that says "build and maintain RESTful microservices in Java using Spring Boot," the interviewer is not going to ask you about Python data science. They are going to ask you about dependency injection, REST endpoint design, service layers, error handling, and Spring Boot configuration. The job description tells you exactly what they care about. It is a roadmap to the interview.

Generic prep helps. But job-specific questions are far more effective. The job description contains everything you need: the required skills, the day-to-day responsibilities, and the context of the team you would be joining. An LLM can read that description and generate realistic, targeted interview questions that mirror what you would actually face in that room. That is what you are going to build.

Why this matters for Resumator Your Resumator already saves jobs with their full descriptions. That means you are sitting on a goldmine of interview prep data. Every saved job is an opportunity to practice before you apply. By the end of this side quest, clicking any saved job will give you a personalized interview prep session — questions tailored to that exact role, that exact company, that exact tech stack.

This is not a hypothetical exercise. This is exactly how professional career coaches work. They read the job posting, identify what the company is looking for, and generate practice questions around those themes. You are automating that process with an LLM.

2. The LLM Integration

The core of this feature is a service that takes a job description and returns a set of structured interview questions. You already know how to call an LLM from Java — you did it when building the resume tailor features. This follows the same pattern, but with a prompt specifically designed to generate interview questions.

The key insight is in the prompt engineering. You do not just ask the LLM to "generate some interview questions." You give it a specific structure: three technical questions that test the skills listed in the description, three behavioral questions that probe the candidate's experience with the responsibilities mentioned, and two company-and-role questions that test whether the candidate has thought about why this particular job at this particular company is the right fit.

Create the InterviewPrepService class:

package com.example.resumator.service;

import com.example.resumator.model.InterviewQuestion;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.util.*;

@Service
public class InterviewPrepService {

    @Value("${openai.api.key}")
    private String apiKey;

    @Value("${openai.api.url:https://api.openai.com/v1/chat/completions}")
    private String apiUrl;

    private final RestTemplate restTemplate = new RestTemplate();

    public List<InterviewQuestion> generateQuestions(String jobTitle,
                                                       String company,
                                                       String jobDescription) {

        String prompt = buildPrompt(jobTitle, company, jobDescription);

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.setBearerAuth(apiKey);

        Map<String, Object> message = Map.of(
            "role", "user",
            "content", prompt
        );

        Map<String, Object> body = Map.of(
            "model", "gpt-4o-mini",
            "messages", List.of(message),
            "temperature", 0.7
        );

        HttpEntity<Map<String, Object>> request =
            new HttpEntity<>(body, headers);

        ResponseEntity<Map> response =
            restTemplate.exchange(apiUrl, HttpMethod.POST, request, Map.class);

        String content = extractContent(response.getBody());
        return parseQuestions(content);
    }

    private String buildPrompt(String jobTitle, String company,
                                String jobDescription) {
        return """
            You are an expert technical interviewer. Based on the following
            job posting, generate exactly 8 interview questions.

            Job Title: %s
            Company: %s
            Description: %s

            Generate:
            - 3 TECHNICAL questions testing specific skills from the description
            - 3 BEHAVIORAL questions about responsibilities and teamwork
            - 2 COMPANY/ROLE questions about culture fit and motivation

            For each question, respond in this exact JSON format:
            [
              {
                "category": "TECHNICAL" | "BEHAVIORAL" | "COMPANY_ROLE",
                "question": "the interview question",
                "tip": "what the interviewer is really looking for",
                "keyPoints": ["point 1", "point 2", "point 3"]
              }
            ]

            Return ONLY the JSON array. No other text.
            """.formatted(jobTitle, company, jobDescription);
    }

    @SuppressWarnings("unchecked")
    private String extractContent(Map<String, Object> responseBody) {
        List<Map<String, Object>> choices =
            (List<Map<String, Object>>) responseBody.get("choices");
        Map<String, Object> firstChoice = choices.get(0);
        Map<String, Object> message =
            (Map<String, Object>) firstChoice.get("message");
        return (String) message.get("content");
    }
}

Notice the prompt structure. You are being extremely specific about what you want: exactly eight questions, broken into three categories, with a defined JSON format that includes not just the question but also a tip explaining what the interviewer is looking for and key points for a strong answer. The more structured your prompt, the more structured — and useful — the response will be.

The temperature parameter is set to 0.7. This controls how creative or random the LLM's output is. A value of 0 would make it completely deterministic (always the same output for the same input). A value of 1.0 would make it very creative. 0.7 gives you variety between sessions while keeping responses focused and professional.

Now you need to parse the LLM's JSON response into structured Java objects. Create the InterviewQuestion model and the parsing logic:

package com.example.resumator.model;

import java.util.List;

public class InterviewQuestion {

    public enum Category {
        TECHNICAL, BEHAVIORAL, COMPANY_ROLE
    }

    private Category category;
    private String question;
    private String tip;
    private List<String> keyPoints;

    // Default constructor for JSON parsing
    public InterviewQuestion() {}

    public InterviewQuestion(Category category, String question,
                              String tip, List<String> keyPoints) {
        this.category = category;
        this.question = question;
        this.tip = tip;
        this.keyPoints = keyPoints;
    }

    public Category getCategory() { return category; }
    public void setCategory(Category category) { this.category = category; }

    public String getQuestion() { return question; }
    public void setQuestion(String question) { this.question = question; }

    public String getTip() { return tip; }
    public void setTip(String tip) { this.tip = tip; }

    public List<String> getKeyPoints() { return keyPoints; }
    public void setKeyPoints(List<String> keyPoints) {
        this.keyPoints = keyPoints;
    }
}

// --- Add this method to InterviewPrepService ---

private List<InterviewQuestion> parseQuestions(String jsonContent) {
    try {
        ObjectMapper mapper = new ObjectMapper();
        // Clean the response — LLMs sometimes wrap JSON in markdown
        String cleaned = jsonContent.trim();
        if (cleaned.startsWith("```")) {
            cleaned = cleaned.replaceAll("```json?\\s*", "")
                             .replaceAll("```\\s*$", "")
                             .trim();
        }

        List<Map<String, Object>> rawList =
            mapper.readValue(cleaned, new TypeReference<>() {});

        List<InterviewQuestion> questions = new ArrayList<>();
        for (Map<String, Object> raw : rawList) {
            InterviewQuestion q = new InterviewQuestion();
            q.setCategory(
                InterviewQuestion.Category.valueOf(
                    (String) raw.get("category")
                )
            );
            q.setQuestion((String) raw.get("question"));
            q.setTip((String) raw.get("tip"));

            @SuppressWarnings("unchecked")
            List<String> points = (List<String>) raw.get("keyPoints");
            q.setKeyPoints(points != null ? points : List.of());

            questions.add(q);
        }
        return questions;

    } catch (Exception e) {
        throw new RuntimeException(
            "Failed to parse interview questions from LLM response", e
        );
    }
}
Always clean LLM output Notice the code that strips markdown formatting from the response. Even when you ask an LLM to return "ONLY the JSON array," it will sometimes wrap the response in ```json code blocks. Defensive parsing like this is not optional — it is a requirement when working with LLM output. Never assume the format will be perfect. Always sanitize.

This pattern — structured prompt, JSON response, defensive parsing — is the foundation of every LLM integration you will build in your career. The specific use case changes, but the approach stays the same: tell the model exactly what you want, specify the output format, and handle the response carefully.

3. The Practice UI

Now comes the part the user actually sees. The practice interface needs to feel like a real interview, not like a quiz. That means presenting one question at a time, giving the user space to think and outline their answer, and revealing tips only after they have attempted a response. The goal is to build muscle memory for the real thing.

You will add a "Practice Interview" button to each saved job card. When clicked, it opens a dedicated practice view. Here is the HTML and CSS for that view:

<!-- Interview Practice View -->
<div id="interview-practice" class="practice-container" style="display:none;">

  <!-- Header with job context -->
  <div class="practice-header">
    <button id="exit-practice" class="btn btn-outline">&larr; Back to Job</button>
    <div class="practice-job-info">
      <h2 id="practice-job-title"></h2>
      <p id="practice-company" class="text-muted"></p>
    </div>
    <div class="practice-progress">
      <span id="question-counter">Question 1 of 8</span>
      <div class="progress-bar">
        <div id="progress-fill" class="progress-fill" style="width:12.5%"></div>
      </div>
    </div>
  </div>

  <!-- Question card -->
  <div class="question-card">
    <div class="question-category">
      <span id="category-badge" class="category-badge"></span>
    </div>
    <p id="question-text" class="question-text"></p>

    <!-- Answer area -->
    <label for="answer-area" class="answer-label">
      Outline your answer:
    </label>
    <textarea id="answer-area" class="answer-textarea"
              rows="8"
              placeholder="Think out loud. Jot down key points you would mention. This is your scratch pad &mdash; no one grades this but you."></textarea>

    <!-- Tip reveal -->
    <div class="tip-section">
      <button id="show-tip-btn" class="btn btn-secondary">
        Show Tip &mdash; What the Interviewer Looks For
      </button>
      <div id="tip-content" class="tip-content" style="display:none;">
        <p id="tip-text"></p>
        <h4>Key points for a strong answer:</h4>
        <ul id="key-points-list"></ul>
      </div>
    </div>

    <!-- Self-rating -->
    <div class="rating-section">
      <p>How confident are you in your answer?</p>
      <div id="star-rating" class="star-rating">
        <button class="star" data-value="1" aria-label="1 star">&#9734;</button>
        <button class="star" data-value="2" aria-label="2 stars">&#9734;</button>
        <button class="star" data-value="3" aria-label="3 stars">&#9734;</button>
        <button class="star" data-value="4" aria-label="4 stars">&#9734;</button>
        <button class="star" data-value="5" aria-label="5 stars">&#9734;</button>
      </div>
    </div>
  </div>

  <!-- Navigation -->
  <div class="practice-nav">
    <button id="prev-question" class="btn btn-outline" disabled>
      &larr; Previous
    </button>
    <button id="next-question" class="btn btn-primary">
      Next Question &rarr;
    </button>
  </div>

  <!-- Session summary (shown after last question) -->
  <div id="session-summary" class="session-summary" style="display:none;">
    <h2>Session Complete!</h2>
    <div class="summary-stats">
      <div class="stat-card">
        <span class="stat-number" id="avg-rating">0</span>
        <span class="stat-label">Average Confidence</span>
      </div>
      <div class="stat-card">
        <span class="stat-number" id="questions-answered">0</span>
        <span class="stat-label">Questions Answered</span>
      </div>
      <div class="stat-card">
        <span class="stat-number" id="tips-viewed">0</span>
        <span class="stat-label">Tips Viewed</span>
      </div>
    </div>
    <div id="weak-areas" class="weak-areas"></div>
    <div class="summary-actions">
      <button id="restart-session" class="btn btn-primary">
        Practice Again
      </button>
      <button id="save-to-bank" class="btn btn-secondary">
        Save Questions to Bank
      </button>
    </div>
  </div>
</div>
/* Interview Practice Styles */
.practice-container {
  max-width: 800px;
  margin: 0 auto;
  padding: 2rem;
}

.practice-header {
  margin-bottom: 2rem;
  border-bottom: 1px solid var(--border-color);
  padding-bottom: 1.5rem;
}

.practice-job-info h2 {
  margin: 0.5rem 0 0.25rem;
  font-size: 1.5rem;
}

.practice-progress {
  margin-top: 1rem;
}

.progress-bar {
  height: 6px;
  background: var(--bg-muted);
  border-radius: 3px;
  margin-top: 0.5rem;
  overflow: hidden;
}

.progress-fill {
  height: 100%;
  background: var(--color-primary);
  border-radius: 3px;
  transition: width 0.3s ease;
}

.question-card {
  background: var(--bg-card);
  border: 1px solid var(--border-color);
  border-radius: 12px;
  padding: 2rem;
  margin-bottom: 1.5rem;
}

.category-badge {
  display: inline-block;
  padding: 0.25rem 0.75rem;
  border-radius: 999px;
  font-size: 0.85rem;
  font-weight: 600;
}

.category-badge.technical { background: #dbeafe; color: #1e40af; }
.category-badge.behavioral { background: #dcfce7; color: #166534; }
.category-badge.company-role { background: #fef3c7; color: #92400e; }

.question-text {
  font-size: 1.25rem;
  line-height: 1.6;
  margin: 1.25rem 0;
}

.answer-textarea {
  width: 100%;
  padding: 1rem;
  border: 1px solid var(--border-color);
  border-radius: 8px;
  background: var(--bg-main);
  color: var(--text-main);
  font-family: inherit;
  font-size: 1rem;
  line-height: 1.6;
  resize: vertical;
}

.answer-textarea:focus {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
}

.tip-section { margin-top: 1.5rem; }

.tip-content {
  margin-top: 1rem;
  padding: 1.25rem;
  background: var(--bg-muted);
  border-left: 4px solid var(--color-primary);
  border-radius: 0 8px 8px 0;
}

.star-rating {
  display: flex;
  gap: 0.5rem;
  margin-top: 0.5rem;
}

.star {
  font-size: 2rem;
  background: none;
  border: none;
  cursor: pointer;
  color: var(--text-muted);
  transition: color 0.15s, transform 0.15s;
  padding: 0;
}

.star:hover, .star.active {
  color: #f59e0b;
  transform: scale(1.15);
}

.practice-nav {
  display: flex;
  justify-content: space-between;
  margin-top: 1.5rem;
}

.session-summary {
  text-align: center;
  padding: 3rem 2rem;
}

.summary-stats {
  display: flex;
  justify-content: center;
  gap: 2rem;
  margin: 2rem 0;
  flex-wrap: wrap;
}

.stat-card {
  background: var(--bg-card);
  border: 1px solid var(--border-color);
  border-radius: 12px;
  padding: 1.5rem 2rem;
  min-width: 140px;
}

.stat-number {
  display: block;
  font-size: 2rem;
  font-weight: 700;
  color: var(--color-primary);
}

.stat-label {
  display: block;
  font-size: 0.85rem;
  color: var(--text-muted);
  margin-top: 0.25rem;
}

.weak-areas {
  margin: 2rem 0;
  text-align: left;
}

.summary-actions {
  display: flex;
  justify-content: center;
  gap: 1rem;
  margin-top: 2rem;
}

Look at how the interface is structured. The job title and company appear at the top so the user always knows what role they are preparing for. The progress bar shows how far they are through the session. The question card is the centerpiece — large, readable, with a category badge so the user knows what type of question they are facing. The text area is generous because you want people to actually write, not just skim. And the tip is hidden behind a button because the point is to try first, then check.

The star rating is a critical piece. Self-assessment is one of the most effective study techniques. When you rate yourself honestly, you are forced to confront what you actually know versus what you think you know. A question you rate as a 2 out of 5 tells you exactly where to focus your next study session.

Now for the JavaScript that drives the practice session. This handles navigation between questions, tracks answers and ratings, and generates the summary at the end:

class InterviewPractice {
  constructor(questions, jobTitle, company) {
    this.questions = questions;
    this.jobTitle = jobTitle;
    this.company = company;
    this.currentIndex = 0;
    this.answers = new Array(questions.length).fill('');
    this.ratings = new Array(questions.length).fill(0);
    this.tipsViewed = new Set();
    this.sessionId = Date.now().toString(36);

    this.init();
  }

  init() {
    document.getElementById('practice-job-title').textContent =
      this.jobTitle;
    document.getElementById('practice-company').textContent =
      this.company;
    document.getElementById('interview-practice').style.display = 'block';

    // Star rating listeners
    document.querySelectorAll('#star-rating .star').forEach(star => {
      star.addEventListener('click', () => {
        const value = parseInt(star.dataset.value);
        this.ratings[this.currentIndex] = value;
        this.updateStars(value);
      });
    });

    // Tip button
    document.getElementById('show-tip-btn')
      .addEventListener('click', () => this.showTip());

    // Navigation
    document.getElementById('next-question')
      .addEventListener('click', () => this.next());
    document.getElementById('prev-question')
      .addEventListener('click', () => this.prev());
    document.getElementById('exit-practice')
      .addEventListener('click', () => this.exit());

    this.renderQuestion();
  }

  renderQuestion() {
    const q = this.questions[this.currentIndex];
    const total = this.questions.length;
    const num = this.currentIndex + 1;

    // Update counter and progress
    document.getElementById('question-counter').textContent =
      `Question ${num} of ${total}`;
    document.getElementById('progress-fill').style.width =
      `${(num / total) * 100}%`;

    // Category badge
    const badge = document.getElementById('category-badge');
    badge.className = 'category-badge';
    const categoryMap = {
      'TECHNICAL':    { label: '\uD83D\uDCBB Technical',  css: 'technical' },
      'BEHAVIORAL':   { label: '\uD83E\uDD1D Behavioral', css: 'behavioral' },
      'COMPANY_ROLE': { label: '\uD83C\uDFE2 Role/Company', css: 'company-role' }
    };
    const cat = categoryMap[q.category] || categoryMap['TECHNICAL'];
    badge.classList.add(cat.css);
    badge.textContent = cat.label;

    // Question text
    document.getElementById('question-text').textContent = q.question;

    // Restore saved answer for this question
    const textarea = document.getElementById('answer-area');
    textarea.value = this.answers[this.currentIndex];
    textarea.addEventListener('input', () => {
      this.answers[this.currentIndex] = textarea.value;
    });

    // Reset tip visibility
    document.getElementById('tip-content').style.display = 'none';
    document.getElementById('show-tip-btn').style.display = 'inline-block';

    // Restore stars
    this.updateStars(this.ratings[this.currentIndex]);

    // Navigation buttons
    document.getElementById('prev-question').disabled =
      this.currentIndex === 0;
    const nextBtn = document.getElementById('next-question');
    nextBtn.textContent = (num === total)
      ? 'Finish Session'
      : 'Next Question \u2192';
  }

  showTip() {
    const q = this.questions[this.currentIndex];
    this.tipsViewed.add(this.currentIndex);

    document.getElementById('tip-text').textContent = q.tip;
    const pointsList = document.getElementById('key-points-list');
    pointsList.innerHTML = '';
    (q.keyPoints || []).forEach(point => {
      const li = document.createElement('li');
      li.textContent = point;
      pointsList.appendChild(li);
    });

    document.getElementById('tip-content').style.display = 'block';
    document.getElementById('show-tip-btn').style.display = 'none';
  }

  updateStars(value) {
    document.querySelectorAll('#star-rating .star').forEach(star => {
      const starVal = parseInt(star.dataset.value);
      star.classList.toggle('active', starVal <= value);
      star.innerHTML = starVal <= value ? '&#9733;' : '&#9734;';
    });
  }

  next() {
    if (this.currentIndex < this.questions.length - 1) {
      this.currentIndex++;
      this.renderQuestion();
    } else {
      this.showSummary();
    }
  }

  prev() {
    if (this.currentIndex > 0) {
      this.currentIndex--;
      this.renderQuestion();
    }
  }

  showSummary() {
    document.querySelector('.question-card').style.display = 'none';
    document.querySelector('.practice-nav').style.display = 'none';

    const answered = this.answers.filter(a => a.trim().length > 0).length;
    const rated = this.ratings.filter(r => r > 0);
    const avgRating = rated.length > 0
      ? (rated.reduce((a, b) => a + b, 0) / rated.length).toFixed(1)
      : '0';

    document.getElementById('avg-rating').textContent = avgRating;
    document.getElementById('questions-answered').textContent = answered;
    document.getElementById('tips-viewed').textContent =
      this.tipsViewed.size;

    // Identify weak areas (rated 1-2)
    const weakAreas = document.getElementById('weak-areas');
    const weakQuestions = this.questions.filter((q, i) =>
      this.ratings[i] > 0 && this.ratings[i] <= 2
    );

    if (weakQuestions.length > 0) {
      weakAreas.innerHTML = '<h3>Focus Areas</h3><p>You rated ' +
        'these questions lowest. Review them before your interview:</p>' +
        '<ul>' + weakQuestions.map(q =>
          `<li><strong>${q.category}</strong>: ${q.question}</li>`
        ).join('') + '</ul>';
    }

    // Save session to LocalStorage
    this.saveSession(avgRating, answered);

    document.getElementById('session-summary').style.display = 'block';
  }

  saveSession(avgRating, answeredCount) {
    const session = {
      id: this.sessionId,
      jobTitle: this.jobTitle,
      company: this.company,
      date: new Date().toISOString(),
      questions: this.questions.map((q, i) => ({
        ...q,
        answer: this.answers[i],
        rating: this.ratings[i],
        tipViewed: this.tipsViewed.has(i)
      })),
      avgRating: parseFloat(avgRating),
      answeredCount
    };

    const sessions = JSON.parse(
      localStorage.getItem('interviewSessions') || '[]'
    );
    sessions.push(session);
    localStorage.setItem('interviewSessions', JSON.stringify(sessions));
  }

  exit() {
    document.getElementById('interview-practice').style.display = 'none';
  }
}

// Launch from a saved job card
function startInterviewPrep(jobId) {
  const job = getSavedJobById(jobId); // from your existing code
  fetch(`/api/interview-prep?jobId=${jobId}`)
    .then(res => res.json())
    .then(questions => {
      new InterviewPractice(
        questions,
        job.jobTitle,
        job.employerName
      );
    })
    .catch(err => {
      console.error('Failed to generate questions:', err);
      alert('Could not generate interview questions. Please try again.');
    });
}

There is a lot happening in this class, so let us walk through the design decisions. The constructor takes the questions array, the job title, and the company name. It initializes parallel arrays for answers and ratings — one slot per question — so the user can navigate back and forth without losing their work. The sessionId is generated from the current timestamp converted to base-36, which gives a compact, unique string for identifying this session later.

The renderQuestion method is the workhorse. Every time the user navigates to a new question, it updates the counter, the progress bar, the category badge, the question text, restores any previously typed answer, and resets the tip and star state. This is the classic "render from state" pattern — the data lives in the arrays, and the DOM is just a projection of that data.

Saving Sessions to LocalStorage

The saveSession method stores the entire practice session — questions, answers, ratings, and metadata — in the browser's LocalStorage. This means even without a database, users can review past sessions, track their progress over time, and see which areas they consistently struggle with. LocalStorage persists even after the browser is closed, so this data survives between visits. The interviewSessions key holds a JSON array of all past sessions.

The showSummary method calculates statistics from the session: how many questions were answered, the average confidence rating, how many tips were viewed. It also identifies "weak areas" — any question the user rated as a 1 or 2 out of 5. These are surfaced prominently because they represent the highest-value study targets. If you consistently rate behavioral questions low, that tells you something important about where to focus.

4. Question Categories

Every question in the practice session belongs to one of three categories, each with a visual indicator that helps the user instantly recognize what type of thinking is required:

Many people make the mistake of preparing only for technical questions. They memorize data structure algorithms and system design patterns, walk into the interview, and freeze when the interviewer asks "Tell me about a time you handled a difficult stakeholder." Interviews are not just technical. Behavioral and cultural fit matter equally. Companies want to know that you can do the work and that you can work well with the existing team.

The hidden evaluation Here is something most candidates do not realize: behavioral questions are often weighted more heavily than technical questions, especially at larger companies. A candidate who scores 8 out of 10 on technical but 10 out of 10 on behavioral will often beat a candidate who scores 10 on technical but 5 on behavioral. The reasoning is simple — technical skills can be taught on the job, but communication habits and teamwork instincts are much harder to change. By practicing all three categories equally, you are giving yourself a significant advantage over candidates who only practice coding questions.

The color-coded badges in the UI serve a psychological purpose as well. When a user sees the green "Behavioral" badge, their brain immediately shifts from "think about code" to "think about experiences." This mental context-switching is exactly what happens in a real interview, where the interviewer might jump from a technical question to a behavioral one without warning. Practicing that transition is part of the training.

5. Building a Question Bank

After a few practice sessions, something interesting happens. The user starts to notice patterns. Certain questions keep coming up across different jobs: "Tell me about a time you worked under a tight deadline." "How would you design a REST API for this feature?" "Why are you interested in this role?" These recurring themes are not a coincidence — they represent the core competencies that the industry values.

A question bank captures every question generated across all practice sessions and makes them searchable, filterable, and reviewable. Instead of losing questions after a session ends, they accumulate into a personal study resource that grows with every job the user practices for.

On the backend, you need a table to persist these questions. Here is the schema:

CREATE TABLE practice_questions (
    id            BIGINT AUTO_INCREMENT PRIMARY KEY,
    job_title     VARCHAR(255) NOT NULL,
    company       VARCHAR(255) NOT NULL,
    category      ENUM('TECHNICAL', 'BEHAVIORAL', 'COMPANY_ROLE') NOT NULL,
    question      TEXT NOT NULL,
    tip           TEXT,
    key_points    JSON,
    best_answer   TEXT,
    best_rating   INT DEFAULT 0,
    times_seen    INT DEFAULT 1,
    last_practiced DATETIME,
    created_at    DATETIME DEFAULT CURRENT_TIMESTAMP,

    INDEX idx_category (category),
    INDEX idx_company (company),
    FULLTEXT INDEX idx_question_search (question, tip)
);

There are several thoughtful design choices in this schema. The category column uses MySQL's ENUM type, which restricts the value to one of the three valid categories. This is a database-level constraint — even if a bug in your code tries to insert an invalid category, the database will reject it. Defense in depth.

The key_points column uses the JSON type. MySQL natively supports JSON columns, which means you can store the list of key points as a JSON array without needing a separate table. For a small, bounded list like this, JSON is simpler and faster than a normalized design with a separate key_points table and a foreign key relationship.

The best_answer and best_rating columns track the user's top performance on each question. When the same question appears in a future session (because multiple jobs might generate similar questions), you update these fields if the user improves. This creates a personal record that motivates improvement.

The times_seen column counts how many times this question has appeared across practice sessions. A question that appears five times across different jobs is clearly a core competency question worth mastering. The FULLTEXT index on question and tip enables fast keyword search, so the user can type "REST APIs" and instantly find every question related to that topic.

Why a Question Bank Changes Everything

Without a question bank, each practice session exists in isolation. You practice, you feel prepared for a day, and then you forget. With a question bank, patterns emerge. You can see that across five different jobs, you were asked about REST API design four times. You can see that your average rating on behavioral questions is 2.5 while technical questions average 4.0. You can search for "microservices" and find every question that mentioned it. The question bank turns scattered practice into systematic preparation. It is the difference between studying randomly and studying strategically.

The filtering and search capabilities turn this from a simple list into a powerful study tool. Filter by category to focus a session entirely on behavioral questions if that is your weak area. Filter by company to review all the questions that were generated for a specific employer. Search by keyword when you realize "I keep getting asked about REST APIs — I need to practice more on that." The data is already there; you just need to surface it effectively.

6. Mock Interview Mode (Stretch)

This section is a stretch goal. It is intentionally optional and brief. If you have completed everything above and want to push further, this will add two powerful features: timed responses and audio recording.

In a real interview, you do not get unlimited time to think. Most interviewers expect an answer to start within 10–15 seconds and wrap up within two minutes. Adding a timer to your practice sessions trains this discipline. Set a two-minute countdown per question. When the timer hits zero, do not cut the user off — just flash the timer red to signal that they are over time. The goal is awareness, not punishment.

The second feature is more powerful: recording your answers using the browser's MediaRecorder API. Hearing yourself answer interview questions is one of the best prep techniques available. Most people have no idea how they sound until they hear a recording. Do you trail off at the end of sentences? Do you use filler words? Do you actually answer the question that was asked, or do you wander? A recording reveals all of this.

Privacy first Audio recordings should stay entirely on the user's device. Store the audio blobs in memory or offer a download — never send recordings to a server. The MediaRecorder API requests microphone permission through the browser's built-in permission dialog, and users can deny access at any time. Always check that navigator.mediaDevices is available before attempting to use it, as not all browsers or environments support it.

The implementation is straightforward. Use navigator.mediaDevices.getUserMedia({ audio: true }) to request microphone access. Create a new MediaRecorder(stream) from the resulting stream. Call start() when the user clicks a "Record" button. Collect audio chunks in the ondataavailable handler. Call stop() when they click "Stop" or the timer runs out. Create a Blob from the chunks and display it in an <audio> element so the user can play it back immediately.

These two features — timers and recording — transform practice from "thinking about answers" to "performing answers." That difference matters. Interview performance is a skill, not just knowledge, and skills are built through realistic practice.

Knowledge Check

1. Why does the InterviewPrepService prompt ask for exactly three categories of questions (Technical, Behavioral, Company/Role) instead of just generating generic interview questions?

Correct! Real interviews test candidates across multiple dimensions: technical competence, teamwork and communication (behavioral), and motivation and cultural fit (company/role). By generating questions in all three categories, the practice session mirrors the structure of an actual interview. This is especially important because many candidates only practice technical questions and are caught off guard by behavioral ones — which are often weighted equally or even more heavily in hiring decisions.

2. What is the purpose of the FULLTEXT index on the practice_questions table?

That's right! A FULLTEXT index tells MySQL to build a searchable word index on the specified columns. This allows efficient MATCH ... AGAINST queries that find rows containing specific keywords, even within long text. Without a fulltext index, keyword searches would require a LIKE '%keyword%' query, which scans every row in the table — extremely slow as the question bank grows. The fulltext index enables the "search by keyword" feature that lets users say "I keep getting asked about microservices" and instantly find every related question.

3. Why does the JavaScript class save each session's answers and ratings to localStorage instead of only keeping them in memory?

Correct! Data stored in JavaScript variables disappears as soon as the page is closed or refreshed. localStorage persists in the browser even after it is closed and reopened. This means a user can complete a practice session on Monday, close the browser, and come back on Wednesday to review their answers and ratings. Over time, the accumulated session data reveals patterns — which categories are consistently rated low, which types of questions keep appearing — turning isolated practice into strategic preparation. Note that localStorage does not sync to any server; it stays entirely in the user's browser.

Deliverable

By the end of this side quest, you should have a working interview prep feature integrated into your Resumator application. Specifically:

If you completed the stretch goal, you also have timed responses and audio recording for the most realistic practice experience possible.

This feature turns your Resumator from a job search tool into a complete interview preparation system. Every saved job becomes a practice opportunity. Every practice session builds your question bank. And every time you sit down for a real interview, you will have already faced questions tailored to that exact role. That is a genuine competitive advantage.

Finished this side quest?

← Back to Side Quests