AI Mock Interview with Spring Boot

Build an AI interview platform with Spring Boot and Google Gemini.

Introduction

30 Second Summary

Technical interviews are one of the most stressful parts of job hunting, but finding someone to practice with is surprisingly hard to arrange. What if you could get instant, structured feedback on your answers any time you wanted?

In this project, you will build a full-stack AI Mock Interview Platform that generates tailored technical questions for any job role and evaluates your answers with detailed scores and improvement tips.

What You'll Build

A Spring Boot REST API paired with a React frontend that uses Google Gemini to generate interview questions and evaluate answers with multi-dimensional scoring.

By the end of this project, you'll have:

  • A Spring Boot backend with REST endpoints that call Google Gemini to generate role-specific interview questions and evaluate candidate answers.
  • A React frontend where users select a job role and difficulty level, answer AI-generated questions, and receive structured performance feedback with scores.
  • Hands-on experience with prompt engineering and JSON schema responses to get structured, predictable output from an AI model.
  • Secret Mission: Add interview history persistence with an H2 database so users can track their performance over time.

Are there any prerequisites?

This project assumes you have basic familiarity with Spring Boot and React. You will also need a Google account to get a free Gemini API key.

You should already have IntelliJ IDEA, JDK 21, Maven, Node.js v22, and Postman installed on your machine.

Set Up the Project and Get Your API Key

To build an AI-powered interview platform, you need a backend that can talk to an AI model and a frontend that users interact with. In this step, you will create both projects from scratch and connect them to the Gemini API.

You will use Spring Initializr to scaffold the backend, Google AI Studio to get a free API key, and Vite to scaffold the React frontend.

In this step, get ready to:
  • Create a Spring Boot backend project with the Google GenAI SDK dependency.
  • Get a free Gemini API key and configure it in IntelliJ IDEA.
  • Scaffold a React frontend and verify both servers run.
Create the Spring Boot backend project

Spring Initializr generates a ready-to-run Spring Boot project with your chosen dependencies pre-configured.

  • Open your browser and go to start.spring.io.
  • Under **Project**, select Maven.
  • Under Language, select Java.
  • Under Spring Boot, select 3.5.16.
  • Under Project Metadata, set Group to com.mockinterview.
  • Set Artifact to ai-mock-interview.
  • Set Java version to 21.
  • Click Add Dependencies, search for Spring Web, and select it.
  • Click Generate to download the project zip file.
  • Extract the zip file to a location on your computer (your Desktop works well).
  • In IntelliJ IDEA, click File, then Open.
  • Select the extracted ai-mock-interview folder and click OK.
  • If IntelliJ asks to trust the project, click Trust Project.

What did Spring Initializr generate?

The downloaded project contains a pom.xml with your Maven configuration, a main class called AiMockInterviewApplication.java, and an application.properties file for settings.

IntelliJ will automatically detect the Maven project and start downloading dependencies. Wait for the indexing to finish before continuing.

The Google GenAI SDK is not available as a Spring Initializr dependency, so you need to add it manually to pom.xml.

  • In IntelliJ, open pom.xml from the project root.
  • Find the <dependencies> section and add the following dependency below the existing spring-boot-starter-web dependency:
<dependency>
    <groupId>com.google.genai</groupId>
    <artifactId>google-genai</artifactId>
    <version>1.60.0</version>
</dependency>

What does this dependency do?

This is Google's official GenAI Java SDK. It provides the Client class and configuration builders you will use to call the Gemini API for generating questions and evaluating answers.

  • After adding the dependency, click the Maven reload icon that appears in the top-right corner of the editor (or press Ctrl+Shift+O on Windows / Cmd+Shift+O on macOS) to sync the project.
  • Wait for IntelliJ to finish downloading the new dependency.

Next, verify your application.properties has the correct configuration.

  • Open src/main/resources/application.properties.
  • Make sure it contains the following two lines:
server.port=8080
spring.application.name=ai-mock-interview

What do these properties do?

The server.port property explicitly sets your backend to listen on port 8080. The spring.application.name identifies your application in logs and monitoring.

Don't see application.properties?

Make sure you are looking in src/main/resources/. If the file is missing, create it by right-clicking the resources folder and selecting New then File. Name it application.properties.

help me find or create application.properties in my Spring Boot project

✔️ Awesome, I've got everything!

Great. Make sure you have saved pom.xml and that Maven finished syncing with no errors.

ⓧ I'd like to double check the full code

Here is what your pom.xml should look like at this point:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.5.16</version>
        <relativePath/>
    </parent>
    <groupId>com.mockinterview</groupId>
    <artifactId>ai-mock-interview</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>ai-mock-interview</name>
    <description>AI Mock Interview Platform</description>
    <properties>
        <java.version>21</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.google.genai</groupId>
            <artifactId>google-genai</artifactId>
            <version>1.60.0</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

And your main class AiMockInterviewApplication.java in src/main/java/com/mockinterview/:

package com.mockinterview;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class AiMockInterviewApplication {
    public static void main(String[] args) {
        SpringApplication.run(AiMockInterviewApplication.class, args);
    }
}

And application.properties:

server.port=8080
spring.application.name=ai-mock-interview
Get a free Gemini API key

The Google GenAI SDK reads your API key from an environment variable called GOOGLE_API_KEY. You will get this key for free from Google AI Studio.

  • Open your browser and go to aistudio.google.com.
  • Sign in with your Google account.
  • For new users, AI Studio automatically creates a project and an API key after you accept the Terms of Service. You can copy it from the API keys page.
  • If you need a new key, click Create API key and follow the dialog.
  • Copy the API key value and store it somewhere safe temporarily.

Is this really free?

Yes. Gemini 2.5 Flash on the free tier has no charges for input and output tokens. No credit card is required.

Now configure IntelliJ to pass this key to your application as an environment variable.

  • In IntelliJ, go to Run in the top menu bar.
  • Click Edit Configurations.
  • Select your Spring Boot run configuration (AiMockInterviewApplication). If none exists yet, click the + icon, select Spring Boot, and set the main class to com.mockinterview.AiMockInterviewApplication.
  • Find the Environment variables field.
  • Type GOOGLE_API_KEY= followed by your actual API key (e.g. GOOGLE_API_KEY=your-api-key-here).
  • Click Apply, then OK.

Why GOOGLE_API_KEY?

The Google GenAI Java SDK automatically reads from the GOOGLE_API_KEY environment variable when you create a Client with no arguments. This means your code never needs to hardcode the key.

Verify the backend starts successfully with the new dependency and API key configured.

  • Press Shift+F10 (Windows) or Ctrl+R (macOS) to run the application.
  • In the Run tool window at the bottom, you should see Spring Boot start up with a message like Started AiMockInterviewApplication and Tomcat started on port 8080.

Seeing a build error?

Make sure Maven finished syncing after you added the google-genai dependency. Click the Maven reload icon in pom.xml or run mvn clean install in the IntelliJ terminal.

If you see a connection error mentioning the API key, double-check that the GOOGLE_API_KEY environment variable is set correctly in your run configuration.

help me troubleshoot my Spring Boot application not starting

Scaffold the React frontend

Your backend is running. Now you will create the frontend project that users will interact with.

  • Open a terminal in IntelliJ by clicking Terminal at the bottom of the window.
  • Make sure you are in the root of your ai-mock-interview project folder.
  • Create the React frontend by running this command:
npm create vite@latest frontend -- --template react

What does this command do?

This uses Vite's scaffolding tool to create a new React project inside a frontend/ directory within your project. The -- --template react part tells Vite to use the React template without prompting you interactively.

  • Move into the frontend folder and install dependencies by running:
cd frontend
npm install
  • Install axios for making HTTP requests to your backend by running:
npm install axios
  • Start the React development server by running:
npm run dev
  • Open your browser and go to http://localhost:5173. You should see the default Vite + React welcome page with a spinning logo.

Frontend not starting on port 5173?

Make sure you ran npm install first. If port 5173 is already in use, Vite will pick another port and display it in the terminal output.

Also confirm you are running the command from inside the frontend/ directory, not the project root.

help me get the Vite React dev server running

You now have two servers running. The Spring Boot backend is on http://localhost:8080 and the React frontend is on http://localhost:5173.

Your environment is ready. Next up, you will build the Gemini AI service that generates interview questions and evaluates answers.

Build the Gemini AI Service

Your project skeleton is running and your API key is configured. Now you need the core intelligence layer that talks to Gemini.

In this step, you will create a service class that sends structured prompts to Gemini and receives predictable JSON responses. This is where prompt engineering meets code. You will also configure CORS so the React frontend can communicate with the backend without being blocked by the browser.

In this step, get ready to:
  • Build a GeminiService that generates tailored interview questions using JSON schema responses.
  • Add an answer evaluation method that scores responses across multiple dimensions.
  • Configure CORS to allow the React frontend to call the backend.
Create GeminiService with the question generation method

The GeminiService class is where all AI communication happens. It initializes a Client that reads your GOOGLE_API_KEY environment variable automatically, then uses that client to send prompts with strict JSON output schemas.

  • In IntelliJ, right-click the src/main/java/com/mockinterview package in the **Project** tool window.
  • Select **New**, then **Package**, and name it service.
  • Right-click the new service package, select **New**, then **Java Class**, and name it GeminiService.
  • Replace the entire file contents with the following code:
package com.mockinterview.service;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.genai.Client;
import com.google.genai.types.Content;
import com.google.genai.types.GenerateContentConfig;
import com.google.genai.types.GenerateContentResponse;
import com.google.genai.types.Part;
import com.google.genai.types.ThinkingConfig;
import org.springframework.stereotype.Service;

@Service
public class GeminiService {

    private final Client client;

    // The Client reads GOOGLE_API_KEY from the environment automatically
    public GeminiService() {
        this.client = new Client();
    }

    public String generateQuestions(String role, String difficulty) {
        // Build the user prompt with the role and difficulty
        String prompt = "Generate exactly 5 technical interview questions for a " + difficulty
                + " level " + role + " position. The questions should test core concepts, "
                + "problem-solving ability, and practical knowledge relevant to the role.";

        // System instruction tells Gemini how to behave
        Content systemInstruction = Content.fromParts(
                Part.fromText("You are a senior technical interviewer at a top tech company. "
                        + "Generate challenging but fair interview questions. "
                        + "Return your response as a JSON object with a 'questions' array "
                        + "where each item has 'id' (number 1-5), 'question' (the question text), "
                        + "and 'topic' (the technical topic being tested).")
        );

        // Define the JSON schema so Gemini returns structured data
        ImmutableMap<String, Object> schema = ImmutableMap.of(
                "type", "object",
                "properties", ImmutableMap.of(
                        "questions", ImmutableMap.of(
                                "type", "array",
                                "items", ImmutableMap.of(
                                        "type", "object",
                                        "properties", ImmutableMap.of(
                                                "id", ImmutableMap.of("type", "integer"),
                                                "question", ImmutableMap.of("type", "string"),
                                                "topic", ImmutableMap.of("type", "string")
                                        ),
                                        "required", ImmutableList.of("id", "question", "topic")
                                )
                        )
                ),
                "required", ImmutableList.of("questions")
        );

        // Configure the request with system instruction, JSON mode, and no thinking
        GenerateContentConfig config = GenerateContentConfig.builder()
                .thinkingConfig(ThinkingConfig.builder().thinkingBudget(0))
                .systemInstruction(systemInstruction)
                .responseMimeType("application/json")
                .responseSchema(schema)
                .build();

        // Call Gemini 2.5 Flash and return the JSON text
        GenerateContentResponse response = client.models.generateContent(
                "gemini-2.5-flash", prompt, config);

        return response.text();
    }
}

What does this code do?

This class is annotated with @Service so Spring Boot automatically creates a single instance of it and makes it available for injection into controllers.

  • new Client() creates a Gemini API client that reads your GOOGLE_API_KEY environment variable automatically.
  • The systemInstruction tells Gemini to act as a senior technical interviewer and return structured JSON.
  • The responseSchema enforces that Gemini returns data in exactly the shape you specify. No manual JSON parsing needed.
  • Setting thinkingBudget(0) disables Gemini's extended reasoning mode for faster responses within free tier limits.

Why JSON schema responses instead of free-text parsing?

Without a schema, Gemini might return JSON with inconsistent keys, extra commentary, or markdown formatting. The responseMimeType and responseSchema combination guarantees the output shape at the API level. Your frontend can trust the response structure without defensive parsing.

  • Save GeminiService.java.

You won't test this method directly yet. The next substep adds the evaluation method, and then you will restart the application to confirm everything compiles.

Add the answer evaluation method

The second method in your service evaluates a candidate's answer against the original question. It uses a different system instruction and schema to return numerical scores plus written feedback.

  • In GeminiService.java, find the closing brace } of the generateQuestions method.
  • Add the following method below it, before the final class closing brace:
    public String evaluateAnswer(String question, String answer, String role) {
        // Build a prompt with the question and the candidate's answer
        String prompt = "Question: " + question + "\n\nCandidate's Answer: " + answer
                + "\n\nEvaluate this answer for a " + role + " interview.";

        // System instruction for evaluation behavior
        Content systemInstruction = Content.fromParts(
                Part.fromText("You are a senior technical interviewer evaluating a candidate's answer. "
                        + "Provide a detailed evaluation as JSON with: "
                        + "'technicalAccuracy' (score 1-10), "
                        + "'depth' (score 1-10), "
                        + "'clarity' (score 1-10), "
                        + "'overallScore' (average of the three scores, rounded), "
                        + "'strengths' (array of strings listing what was good), "
                        + "'improvements' (array of strings listing what could be better), "
                        + "'suggestion' (a brief tip for improvement).")
        );

        // Schema for the evaluation response
        ImmutableMap<String, Object> schema = ImmutableMap.of(
                "type", "object",
                "properties", ImmutableMap.of(
                        "technicalAccuracy", ImmutableMap.of("type", "integer"),
                        "depth", ImmutableMap.of("type", "integer"),
                        "clarity", ImmutableMap.of("type", "integer"),
                        "overallScore", ImmutableMap.of("type", "integer"),
                        "strengths", ImmutableMap.of("type", "array", "items", ImmutableMap.of("type", "string")),
                        "improvements", ImmutableMap.of("type", "array", "items", ImmutableMap.of("type", "string")),
                        "suggestion", ImmutableMap.of("type", "string")
                ),
                "required", ImmutableList.of("technicalAccuracy", "depth", "clarity", "overallScore", "strengths", "improvements", "suggestion")
        );

        // Configure with JSON mode and no thinking
        GenerateContentConfig config = GenerateContentConfig.builder()
                .thinkingConfig(ThinkingConfig.builder().thinkingBudget(0))
                .systemInstruction(systemInstruction)
                .responseMimeType("application/json")
                .responseSchema(schema)
                .build();

        // Call Gemini and return the evaluation JSON
        GenerateContentResponse response = client.models.generateContent(
                "gemini-2.5-flash", prompt, config);

        return response.text();
    }

What does this method do?

This method takes the original interview question, the candidate's answer, and the role context. It asks Gemini to score the answer on three dimensions (technical accuracy, depth, clarity) and provide actionable feedback.

  • The schema enforces integer scores plus string arrays for strengths and improvements. This means your frontend can render score badges and bullet lists without guessing the data shape.
  • The overallScore is calculated by Gemini as an average of the three individual scores.
  • Save GeminiService.java.
  • Restart your Spring Boot application by clicking the **Rerun** button in IntelliJ's **Run** tool window (or press **Ctrl+F5**).
  • Check the console output. You should see Started AiMockInterviewApplication with no errors.

Seeing a compilation error?

  • If IntelliJ shows red underlines on ImmutableMap or ImmutableList, make sure your pom.xml has the google-genai dependency. These classes come from Google Guava, which is included as a transitive dependency of the GenAI SDK.
  • If you see "package does not exist" errors, click the **Maven** tool window on the right side of IntelliJ and press the **Reload All Maven Projects** button (circular arrows icon).
  • Make sure the evaluateAnswer method is placed inside the class braces but outside the generateQuestions method.

Help me fix a compilation error in my GeminiService class

✔️ Awesome, I've got everything!

Great. Double check you have saved GeminiService.java and your application restarted without errors.

ⓧ I'd like to double check the full code

package com.mockinterview.service;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.genai.Client;
import com.google.genai.types.Content;
import com.google.genai.types.GenerateContentConfig;
import com.google.genai.types.GenerateContentResponse;
import com.google.genai.types.Part;
import com.google.genai.types.ThinkingConfig;
import org.springframework.stereotype.Service;

@Service
public class GeminiService {

    private final Client client;

    public GeminiService() {
        this.client = new Client();
    }

    public String generateQuestions(String role, String difficulty) {
        String prompt = "Generate exactly 5 technical interview questions for a " + difficulty
                + " level " + role + " position. The questions should test core concepts, "
                + "problem-solving ability, and practical knowledge relevant to the role.";

        Content systemInstruction = Content.fromParts(
                Part.fromText("You are a senior technical interviewer at a top tech company. "
                        + "Generate challenging but fair interview questions. "
                        + "Return your response as a JSON object with a 'questions' array "
                        + "where each item has 'id' (number 1-5), 'question' (the question text), "
                        + "and 'topic' (the technical topic being tested).")
        );

        ImmutableMap<String, Object> schema = ImmutableMap.of(
                "type", "object",
                "properties", ImmutableMap.of(
                        "questions", ImmutableMap.of(
                                "type", "array",
                                "items", ImmutableMap.of(
                                        "type", "object",
                                        "properties", ImmutableMap.of(
                                                "id", ImmutableMap.of("type", "integer"),
                                                "question", ImmutableMap.of("type", "string"),
                                                "topic", ImmutableMap.of("type", "string")
                                        ),
                                        "required", ImmutableList.of("id", "question", "topic")
                                )
                        )
                ),
                "required", ImmutableList.of("questions")
        );

        GenerateContentConfig config = GenerateContentConfig.builder()
                .thinkingConfig(ThinkingConfig.builder().thinkingBudget(0))
                .systemInstruction(systemInstruction)
                .responseMimeType("application/json")
                .responseSchema(schema)
                .build();

        GenerateContentResponse response = client.models.generateContent(
                "gemini-2.5-flash", prompt, config);

        return response.text();
    }

    public String evaluateAnswer(String question, String answer, String role) {
        String prompt = "Question: " + question + "\n\nCandidate's Answer: " + answer
                + "\n\nEvaluate this answer for a " + role + " interview.";

        Content systemInstruction = Content.fromParts(
                Part.fromText("You are a senior technical interviewer evaluating a candidate's answer. "
                        + "Provide a detailed evaluation as JSON with: "
                        + "'technicalAccuracy' (score 1-10), "
                        + "'depth' (score 1-10), "
                        + "'clarity' (score 1-10), "
                        + "'overallScore' (average of the three scores, rounded), "
                        + "'strengths' (array of strings listing what was good), "
                        + "'improvements' (array of strings listing what could be better), "
                        + "'suggestion' (a brief tip for improvement).")
        );

        ImmutableMap<String, Object> schema = ImmutableMap.of(
                "type", "object",
                "properties", ImmutableMap.of(
                        "technicalAccuracy", ImmutableMap.of("type", "integer"),
                        "depth", ImmutableMap.of("type", "integer"),
                        "clarity", ImmutableMap.of("type", "integer"),
                        "overallScore", ImmutableMap.of("type", "integer"),
                        "strengths", ImmutableMap.of("type", "array", "items", ImmutableMap.of("type", "string")),
                        "improvements", ImmutableMap.of("type", "array", "items", ImmutableMap.of("type", "string")),
                        "suggestion", ImmutableMap.of("type", "string")
                ),
                "required", ImmutableList.of("technicalAccuracy", "depth", "clarity", "overallScore", "strengths", "improvements", "suggestion")
        );

        GenerateContentConfig config = GenerateContentConfig.builder()
                .thinkingConfig(ThinkingConfig.builder().thinkingBudget(0))
                .systemInstruction(systemInstruction)
                .responseMimeType("application/json")
                .responseSchema(schema)
                .build();

        GenerateContentResponse response = client.models.generateContent(
                "gemini-2.5-flash", prompt, config);

        return response.text();
    }
}
Add CORS configuration for the frontend

Your React dev server runs on port 5173 while Spring Boot runs on port 8080. Browsers block cross-origin requests by default for security. A CORS configuration tells Spring Boot to allow requests from the frontend's origin.

  • In IntelliJ, right-click the src/main/java/com/mockinterview package in the **Project** tool window.
  • Select **New**, then **Package**, and name it config.
  • Right-click the new config package, select **New**, then **Java Class**, and name it CorsConfig.
  • Replace the entire file contents with the following code:
package com.mockinterview.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

import java.util.List;

@Configuration
public class CorsConfig {

    // Allow the React dev server to call our API
    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(List.of("http://localhost:5173"));
        config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
        config.setAllowedHeaders(List.of("*"));
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }
}

What does this configuration do?

  • setAllowedOrigins permits requests from http://localhost:5173 (your Vite dev server).
  • setAllowedMethods specifies which HTTP methods the frontend can use.
  • The /** path pattern applies this configuration to all endpoints in your application.
  • Save CorsConfig.java.
  • Restart your Spring Boot application by clicking the **Rerun** button (or press **Ctrl+F5**).
  • Check the console output. You should see Started AiMockInterviewApplication with no errors, confirming both the service and CORS config loaded correctly.

Application won't start after adding CorsConfig?

  • Verify the package declaration at the top of the file is package com.mockinterview.config; and the file is inside the config folder.
  • Make sure you have the @Configuration annotation on the class and @Bean on the method.
  • Check that the List import is java.util.List, not another List class.

Help me fix my CorsConfig not loading

✔️ Awesome, I've got everything!

Great. Double check you have saved CorsConfig.java and your application is running.

ⓧ I'd like to double check the full code

package com.mockinterview.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

import java.util.List;

@Configuration
public class CorsConfig {

    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(List.of("http://localhost:5173"));
        config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
        config.setAllowedHeaders(List.of("*"));
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }
}

Your AI service is ready. It can generate tailored interview questions and evaluate answers with structured scores. Next up, you will expose these capabilities through REST API endpoints that the frontend can call.

Create the REST API Endpoints

Your GeminiService can generate interview questions and evaluate answers. However, the React frontend has no way to reach those methods yet.

In this step, you will create a REST API controller that exposes two HTTP endpoints. The frontend will send POST requests to these endpoints, and the controller will pass the data to GeminiService and return the AI-generated JSON.

In this step, get ready to:
  • Create the /api/interview/start endpoint that generates interview questions.
  • Create the /api/interview/evaluate endpoint that scores answers.
  • Test both endpoints with Postman to verify correct JSON responses.
Create the InterviewController with the start endpoint

Spring Boot uses @RestController classes to map incoming HTTP requests to Java methods. Each method handles a specific URL path and HTTP verb.

  • In IntelliJ, right-click the com.mockinterview package in the Project sidebar.
  • Select New then Package.
  • Name the new package controller and press Enter.
  • Right-click the new controller package, select New then Java Class.
  • Name it InterviewController and press Enter.
  • Replace the contents of InterviewController.java with the following code:
package com.mockinterview.controller;

import com.mockinterview.service.GeminiService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

// Marks this class as a REST controller that handles HTTP requests
@RestController
// All endpoints in this controller start with /api/interview
@RequestMapping("/api/interview")
public class InterviewController {

    // Spring automatically injects the GeminiService bean
    private final GeminiService geminiService;

    public InterviewController(GeminiService geminiService) {
        this.geminiService = geminiService;
    }

    // POST /api/interview/start - generates interview questions
    @PostMapping("/start")
    public ResponseEntity<String> startInterview(@RequestBody Map<String, String> request) {
        String role = request.get("role");
        String difficulty = request.get("difficulty");
        String questions = geminiService.generateQuestions(role, difficulty);
        return ResponseEntity.ok(questions);
    }
}

What does this code do?

This controller uses @RequestMapping("/api/interview") to group all interview endpoints under a common base path.

  • @RestController tells Spring this class handles HTTP requests and returns JSON responses directly (no HTML templates).
  • @RequestBody Map<String, String> automatically parses the incoming JSON body into a Java Map, so you can pull values by key name.
  • Constructor injection wires in GeminiService automatically because Spring manages it as a bean (thanks to the @Service annotation you added earlier).
  • Save InterviewController.java.

Seeing red underlines on imports?

Make sure your file is in the com.mockinterview.controller package (check the first line matches package com.mockinterview.controller;). If IntelliJ shows unresolved imports, click File then Invalidate Caches / Restart or wait for Maven to re-index.

help me fix import errors in my InterviewController

Add the evaluate endpoint

The second endpoint accepts a question, the user's answer, and the role. It passes these to GeminiService for AI evaluation.

  • In InterviewController.java, find the closing brace } of the startInterview method.
  • Add the following method below it, before the final class closing brace:
    // POST /api/interview/evaluate - scores a candidate's answer
    @PostMapping("/evaluate")
    public ResponseEntity<String> evaluateAnswer(@RequestBody Map<String, String> request) {
        String question = request.get("question");
        String answer = request.get("answer");
        String role = request.get("role");
        String evaluation = geminiService.evaluateAnswer(question, answer, role);
        return ResponseEntity.ok(evaluation);
    }

What does this endpoint do?

This method extracts three fields from the request body: the interview question, the candidate's answer, and the role for context. It delegates the evaluation to GeminiService and returns the structured JSON scores directly to the caller.

The ResponseEntity.ok() wrapper sends a 200 OK status code along with the response body. Since GeminiService already returns a JSON string (enforced by the response schema), it passes through to the frontend unchanged.

  • Save InterviewController.java.
  • Restart your Spring Boot application by clicking the green Run button in IntelliJ (or press Shift+F10 on Windows / Ctrl+R on macOS).
  • Confirm the console shows Started AiMockInterviewApplication with no errors.

Application fails to start?

Check for a missing closing brace at the end of the class. The file should have exactly two } at the bottom: one closing the evaluateAnswer method and one closing the class.

help me fix my Spring Boot application startup error in InterviewController

✔️ Awesome, I've got everything!

Great. Double check you have saved InterviewController.java and that the application restarted successfully.

ⓧ I'd like to double check the full code

package com.mockinterview.controller;

import com.mockinterview.service.GeminiService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
@RequestMapping("/api/interview")
public class InterviewController {

    private final GeminiService geminiService;

    public InterviewController(GeminiService geminiService) {
        this.geminiService = geminiService;
    }

    @PostMapping("/start")
    public ResponseEntity<String> startInterview(@RequestBody Map<String, String> request) {
        String role = request.get("role");
        String difficulty = request.get("difficulty");
        String questions = geminiService.generateQuestions(role, difficulty);
        return ResponseEntity.ok(questions);
    }

    @PostMapping("/evaluate")
    public ResponseEntity<String> evaluateAnswer(@RequestBody Map<String, String> request) {
        String question = request.get("question");
        String answer = request.get("answer");
        String role = request.get("role");
        String evaluation = geminiService.evaluateAnswer(question, answer, role);
        return ResponseEntity.ok(evaluation);
    }
}
Test both endpoints with Postman

Before wiring up the React frontend, it is important to verify that the API works correctly in isolation. Postman lets you send HTTP requests manually so you can inspect the raw JSON responses.

  • Open Postman.
  • Click the + button to create a new request tab.
  • Change the method dropdown from GET to POST.
  • Enter this URL in the address bar:
http://localhost:8080/api/interview/start
  • Click the Body tab below the URL bar.
  • Select raw, then choose JSON from the dropdown to the right of the radio buttons.
  • Paste the following JSON into the body editor:
{
    "role": "Java Backend Developer",
    "difficulty": "Junior"
}
  • Click Send.

You should see a 200 OK response containing a JSON object with a questions array. Each item has an id, question, and topic field.

Why test with Postman first?

Testing the API independently from the frontend isolates issues. If Postman gets a correct response but the frontend does not, you know the problem is in the React code or CORS configuration, not in the controller logic.

  • Now test the evaluate endpoint. Create another new request tab in Postman.
  • Set the method to POST and enter this URL:
http://localhost:8080/api/interview/evaluate
  • Click Body, select raw and JSON, then paste:
{
    "question": "What is the difference between an abstract class and an interface in Java?",
    "answer": "An abstract class can have method implementations and state, while an interface only defines method signatures. Since Java 8, interfaces can have default methods too.",
    "role": "Java Backend Developer"
}
  • Click Send.

You should see a 200 OK response with a JSON object containing technicalAccuracy, depth, clarity, overallScore, strengths, improvements, and suggestion fields.

Getting a 404 or connection refused?

Make sure your Spring Boot application is still running. Check the IntelliJ console for the Started AiMockInterviewApplication message.

Verify the URL is exactly http://localhost:8080/api/interview/start (no trailing slash, no typos). Also confirm the method is set to POST, not GET.

help me troubleshoot why my Postman request to localhost:8080 is failing

Both endpoints are working. Next up, you will build the React interview UI that sends requests to these endpoints and displays the AI-generated questions and feedback to users.

Build the React Interview UI

Your backend API is tested and returning structured interview questions and evaluations. Now you need a way for users to actually interact with it.

In this step, you will build the full React frontend that guides users through the interview experience. The UI has three distinct views: a setup form, a question-answer session, and a final report with scores.

In this step, get ready to:
  • Build the InterviewSetup view with role and difficulty selection.
  • Build the InterviewSession view that displays questions and submits answers.
  • Build the InterviewReport view showing scores and feedback.
Build the InterviewSetup component

The setup form is the first thing users see. It lets them pick a job role and difficulty level before the AI generates tailored questions.

You will replace the Vite boilerplate in App.jsx with a single-component architecture that uses React state to switch between views.

  • In IntelliJ IDEA, open the frontend/src/App.jsx file.
  • Select all the existing boilerplate code and delete it.
  • Add the imports, state declarations, and the startInterview function by pasting the following code:
import { useState } from 'react';
import './App.css';

function App() {
  // Track which view is active: setup, session, or report
  const [view, setView] = useState('setup');
  const [role, setRole] = useState('');
  const [difficulty, setDifficulty] = useState('');
  const [questions, setQuestions] = useState([]);
  const [currentIndex, setCurrentIndex] = useState(0);
  const [answer, setAnswer] = useState('');
  const [evaluations, setEvaluations] = useState([]);
  const [loading, setLoading] = useState(false);

  // Send role and difficulty to the backend, receive 5 questions
  const startInterview = async (e) => {
    e.preventDefault();
    setLoading(true);
    try {
      const res = await fetch('http://localhost:8080/api/interview/start', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ role, difficulty })
      });
      const data = await res.json();
      setQuestions(data.questions);
      setView('session');
    } catch (err) {
      alert('Error starting interview. Is the backend running?');
    }
    setLoading(false);
  };

What does this code do?

The eight useState hooks track all the data the app needs: which view to show, what role and difficulty the user picked, the list of AI-generated questions, which question the user is on, their current typed answer, all evaluations collected so far, and whether a request is in progress.

The startInterview function fires when the form submits. It POSTs the user's role and difficulty to your Spring Boot backend, stores the returned questions in state, and switches the view to the session screen.

  • You won't see a change yet. The next chunk adds the visible form that uses these hooks.
  • Directly below the startInterview function's closing brace, add the setup view and component export by pasting:
  // Setup view: role and difficulty selection form
  if (view === 'setup') {
    return (
      <div className="container">
        <h1>AI Mock Interview</h1>
        <p className="subtitle">Practice technical interviews with AI-powered feedback</p>
        <form onSubmit={startInterview} className="setup-form">
          <label>Job Role</label>
          <select value={role} onChange={(e) => setRole(e.target.value)} required>
            <option value="">Select a role...</option>
            <option value="Java Backend Developer">Java Backend Developer</option>
            <option value="Full-Stack Developer">Full-Stack Developer</option>
            <option value="React Frontend Developer">React Frontend Developer</option>
            <option value="Spring Boot Developer">Spring Boot Developer</option>
          </select>
          <label>Difficulty Level</label>
          <select value={difficulty} onChange={(e) => setDifficulty(e.target.value)} required>
            <option value="">Select difficulty...</option>
            <option value="Junior">Junior</option>
            <option value="Mid-Level">Mid-Level</option>
            <option value="Senior">Senior</option>
          </select>
          <button type="submit" disabled={loading}>
            {loading ? 'Generating Questions...' : 'Start Interview'}
          </button>
        </form>
      </div>
    );
  }
}

export default App;

What does this code do?

The if (view === 'setup') block renders the form only when the app is in its initial state. The two <select> dropdowns are controlled components bound to role and difficulty state.

When the user clicks Start Interview, the form's onSubmit calls startInterview. The button disables itself while loading to prevent duplicate requests.

  • Save App.jsx.
  • Switch to your browser at http://localhost:5173 and refresh. You should see the heading "AI Mock Interview", a Job Role dropdown, a Difficulty Level dropdown, and a Start Interview button.

Don't see the form?

  • Make sure your Vite dev server is still running in the frontend/ directory. If it stopped, restart it with npm run dev.
  • Check the browser console for syntax errors. A missing closing brace or parenthesis will prevent the component from rendering.
  • Confirm you deleted all the old Vite boilerplate before pasting the new code.

Still stuck? Help me debug why my React setup form isn't rendering.

Build the InterviewSession component

Once the user starts an interview, the session view takes over. It shows one question at a time, collects the user's answer, and sends it to Gemini for evaluation before advancing to the next question.

  • In App.jsx, find the startInterview function's closing }; (the line right before if (view === 'setup')).
  • Add the submitAnswer function below it by pasting:
  // Send the user's answer to the backend for AI evaluation
  const submitAnswer = async () => {
    if (!answer.trim()) return;
    setLoading(true);
    try {
      const res = await fetch('http://localhost:8080/api/interview/evaluate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          question: questions[currentIndex].question,
          answer: answer,
          role: role
        })
      });
      const evaluation = await res.json();
      setEvaluations([...evaluations, {
        question: questions[currentIndex].question,
        topic: questions[currentIndex].topic,
        answer: answer,
        ...evaluation
      }]);
      setAnswer('');
      if (currentIndex < questions.length - 1) {
        setCurrentIndex(currentIndex + 1);
      } else {
        setView('report');
      }
    } catch (err) {
      alert('Error evaluating answer.');
    }
    setLoading(false);
  };

What does this code do?

The submitAnswer function sends the current question text and the user's typed answer to /api/interview/evaluate. It includes the role so Gemini can evaluate the answer in the right context.

After receiving the evaluation, it appends the full result (question, topic, answer, and all scores) to the evaluations array. If there are more questions, it advances currentIndex. If this was the last question, it switches to the report view.

  • Now find the closing brace of the if (view === 'setup') block (the } on its own line, right before the final } of the App function).
  • Add the session view between the setup block's closing brace and the component's closing brace by pasting:
  // Session view: display one question at a time with answer input
  if (view === 'session') {
    return (
      <div className="container">
        <div className="progress">Question {currentIndex + 1} of {questions.length}</div>
        <div className="topic-badge">{questions[currentIndex].topic}</div>
        <h2 className="question">{questions[currentIndex].question}</h2>
        <textarea
          value={answer}
          onChange={(e) => setAnswer(e.target.value)}
          placeholder="Type your answer here..."
          rows={8}
        />
        <button onClick={submitAnswer} disabled={loading || !answer.trim()}>
          {loading ? 'Evaluating...' : currentIndex < questions.length - 1 ? 'Submit & Next' : 'Submit & Finish'}
        </button>
      </div>
    );
  }

What does this code do?

The progress indicator shows "Question 1 of 5" to orient the user. The topic badge displays the technical topic being tested (e.g., "Concurrency", "REST APIs") so the user knows what area to focus on.

The button label changes dynamically. It shows Submit & Next for questions 1-4 and Submit & Finish for the final question. It also disables when loading or when the textarea is empty.

  • Save App.jsx.
  • Refresh your browser. Select a role and difficulty, then click Start Interview. After a few seconds, you should see the first question with a topic badge, a progress indicator, a textarea, and a submit button.

Seeing an error when you click Start Interview?

  • Make sure your Spring Boot backend is still running on port 8080. The frontend calls http://localhost:8080/api/interview/start directly.
  • Check the browser console for CORS errors. If you see one, confirm your CorsConfig.java allows http://localhost:5173.
  • If the page shows "Error starting interview. Is the backend running?", the backend may have crashed. Check the IntelliJ console for exceptions.

Need help? Help me debug why my interview questions aren't loading from the backend.

Build the InterviewReport component

After answering all five questions, the user sees a summary report. This view calculates an average score across all evaluations and displays detailed feedback for each question.

  • In App.jsx, find the closing brace of the if (view === 'session') block.
  • Add the report view below it (before the component's final closing brace and export default App) by pasting:
  // Report view: display scores and feedback for all questions
  if (view === 'report') {
    const avgScore = evaluations.length > 0
      ? Math.round(evaluations.reduce((sum, e) => sum + e.overallScore, 0) / evaluations.length)
      : 0;

    return (
      <div className="container">
        <h1>Interview Report</h1>
        <div className="overall-score">
          <span className="score-number">{avgScore}</span>/10
          <p>Overall Performance</p>
        </div>
        {evaluations.map((eval, i) => (
          <div key={i} className="evaluation-card">
            <h3>Q{i + 1}: {eval.question}</h3>
            <p className="your-answer"><strong>Your answer:</strong> {eval.answer}</p>
            <div className="scores">
              <span>Technical: {eval.technicalAccuracy}/10</span>
              <span>Depth: {eval.depth}/10</span>
              <span>Clarity: {eval.clarity}/10</span>
            </div>
            <div className="feedback">
              <p><strong>Strengths:</strong> {eval.strengths.join(', ')}</p>
              <p><strong>Improvements:</strong> {eval.improvements.join(', ')}</p>
              <p><strong>Tip:</strong> {eval.suggestion}</p>
            </div>
          </div>
        ))}
        <button onClick={() => { setView('setup'); setEvaluations([]); setCurrentIndex(0); setQuestions([]); }}>
          Start New Interview
        </button>
      </div>
    );
  }

What does this code do?

The avgScore calculation reduces all evaluations' overallScore values into a single average, rounded to a whole number. This gives the user a quick sense of their performance at a glance.

Each evaluation card shows the question, the user's answer, three individual scores (technical accuracy, depth, and clarity), plus strengths, areas for improvement, and a tip. The Start New Interview button resets all state back to the beginning.

  • Save App.jsx. You will verify the report view works after adding styling.

Now add the dark-theme styling to make the UI presentable.

  • Open frontend/src/App.css.
  • Select all the existing Vite boilerplate CSS and delete it.
  • Replace it with the full application styles by pasting:
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background: #0f172a;
  color: #e2e8f0;
  min-height: 100vh;
}

.container {
  max-width: 720px;
  margin: 0 auto;
  padding: 40px 20px;
}

h1 {
  font-size: 2rem;
  margin-bottom: 8px;
  color: #f8fafc;
}

.subtitle {
  color: #94a3b8;
  margin-bottom: 32px;
}

.setup-form {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

label {
  font-weight: 600;
  color: #cbd5e1;
}

select, textarea {
  padding: 12px;
  border-radius: 8px;
  border: 1px solid #334155;
  background: #1e293b;
  color: #e2e8f0;
  font-size: 1rem;
  width: 100%;
}

textarea {
  resize: vertical;
  min-height: 160px;
}

button {
  padding: 14px 24px;
  background: #3b82f6;
  color: white;
  border: none;
  border-radius: 8px;
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;
  margin-top: 8px;
}

button:hover {
  background: #2563eb;
}

button:disabled {
  background: #475569;
  cursor: not-allowed;
}

.progress {
  color: #94a3b8;
  margin-bottom: 8px;
}

.topic-badge {
  display: inline-block;
  background: #1e3a5f;
  color: #60a5fa;
  padding: 4px 12px;
  border-radius: 16px;
  font-size: 0.85rem;
  margin-bottom: 16px;
}

.question {
  font-size: 1.3rem;
  margin-bottom: 24px;
  line-height: 1.5;
}

.overall-score {
  text-align: center;
  margin: 24px 0;
  padding: 24px;
  background: #1e293b;
  border-radius: 12px;
}

.score-number {
  font-size: 3rem;
  font-weight: 700;
  color: #3b82f6;
}

.evaluation-card {
  background: #1e293b;
  border-radius: 12px;
  padding: 20px;
  margin-bottom: 16px;
}

.evaluation-card h3 {
  color: #f8fafc;
  margin-bottom: 8px;
  font-size: 1rem;
}

.your-answer {
  color: #94a3b8;
  margin-bottom: 12px;
  font-size: 0.9rem;
}

.scores {
  display: flex;
  gap: 16px;
  margin-bottom: 12px;
}

.scores span {
  background: #0f172a;
  padding: 4px 10px;
  border-radius: 6px;
  font-size: 0.85rem;
  color: #60a5fa;
}

.feedback p {
  margin-bottom: 6px;
  font-size: 0.9rem;
  color: #cbd5e1;
}

What do these styles do?

The CSS uses a dark color scheme (#0f172a background, #e2e8f0 text) to create a professional look. The .container class centers content at a max width of 720px.

Key visual elements include the .topic-badge (a small blue pill showing the topic), .evaluation-card (rounded cards for each question's feedback), and .score-number (large blue text for the overall score).

  • Save App.css.
  • Refresh your browser. The setup form should now have a dark background, styled dropdowns, and a blue Start Interview button.

Styles not applying?

  • Make sure you saved both App.jsx and App.css. Vite hot-reloads on save, so unsaved changes won't appear.
  • Confirm the import line import './App.css' is at the top of your App.jsx. Without it, the stylesheet won't load.
  • If you see the old Vite spinning logos, you still have leftover boilerplate CSS. Make sure App.css only contains the new styles.

Still stuck? Help me debug why my CSS styles aren't showing in the React app.

✔️ Awesome, I've got everything!

Great. Double check you have saved both files before moving on.

ⓧ I'd like to double check the full code

Here is the complete App.jsx:

import { useState } from 'react';
import './App.css';

function App() {
  const [view, setView] = useState('setup');
  const [role, setRole] = useState('');
  const [difficulty, setDifficulty] = useState('');
  const [questions, setQuestions] = useState([]);
  const [currentIndex, setCurrentIndex] = useState(0);
  const [answer, setAnswer] = useState('');
  const [evaluations, setEvaluations] = useState([]);
  const [loading, setLoading] = useState(false);

  const startInterview = async (e) => {
    e.preventDefault();
    setLoading(true);
    try {
      const res = await fetch('http://localhost:8080/api/interview/start', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ role, difficulty })
      });
      const data = await res.json();
      setQuestions(data.questions);
      setView('session');
    } catch (err) {
      alert('Error starting interview. Is the backend running?');
    }
    setLoading(false);
  };

  const submitAnswer = async () => {
    if (!answer.trim()) return;
    setLoading(true);
    try {
      const res = await fetch('http://localhost:8080/api/interview/evaluate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          question: questions[currentIndex].question,
          answer: answer,
          role: role
        })
      });
      const evaluation = await res.json();
      setEvaluations([...evaluations, {
        question: questions[currentIndex].question,
        topic: questions[currentIndex].topic,
        answer: answer,
        ...evaluation
      }]);
      setAnswer('');
      if (currentIndex < questions.length - 1) {
        setCurrentIndex(currentIndex + 1);
      } else {
        setView('report');
      }
    } catch (err) {
      alert('Error evaluating answer.');
    }
    setLoading(false);
  };

  if (view === 'setup') {
    return (
      <div className="container">
        <h1>AI Mock Interview</h1>
        <p className="subtitle">Practice technical interviews with AI-powered feedback</p>
        <form onSubmit={startInterview} className="setup-form">
          <label>Job Role</label>
          <select value={role} onChange={(e) => setRole(e.target.value)} required>
            <option value="">Select a role...</option>
            <option value="Java Backend Developer">Java Backend Developer</option>
            <option value="Full-Stack Developer">Full-Stack Developer</option>
            <option value="React Frontend Developer">React Frontend Developer</option>
            <option value="Spring Boot Developer">Spring Boot Developer</option>
          </select>
          <label>Difficulty Level</label>
          <select value={difficulty} onChange={(e) => setDifficulty(e.target.value)} required>
            <option value="">Select difficulty...</option>
            <option value="Junior">Junior</option>
            <option value="Mid-Level">Mid-Level</option>
            <option value="Senior">Senior</option>
          </select>
          <button type="submit" disabled={loading}>
            {loading ? 'Generating Questions...' : 'Start Interview'}
          </button>
        </form>
      </div>
    );
  }

  if (view === 'session') {
    return (
      <div className="container">
        <div className="progress">Question {currentIndex + 1} of {questions.length}</div>
        <div className="topic-badge">{questions[currentIndex].topic}</div>
        <h2 className="question">{questions[currentIndex].question}</h2>
        <textarea
          value={answer}
          onChange={(e) => setAnswer(e.target.value)}
          placeholder="Type your answer here..."
          rows={8}
        />
        <button onClick={submitAnswer} disabled={loading || !answer.trim()}>
          {loading ? 'Evaluating...' : currentIndex < questions.length - 1 ? 'Submit & Next' : 'Submit & Finish'}
        </button>
      </div>
    );
  }

  if (view === 'report') {
    const avgScore = evaluations.length > 0
      ? Math.round(evaluations.reduce((sum, e) => sum + e.overallScore, 0) / evaluations.length)
      : 0;

    return (
      <div className="container">
        <h1>Interview Report</h1>
        <div className="overall-score">
          <span className="score-number">{avgScore}</span>/10
          <p>Overall Performance</p>
        </div>
        {evaluations.map((eval, i) => (
          <div key={i} className="evaluation-card">
            <h3>Q{i + 1}: {eval.question}</h3>
            <p className="your-answer"><strong>Your answer:</strong> {eval.answer}</p>
            <div className="scores">
              <span>Technical: {eval.technicalAccuracy}/10</span>
              <span>Depth: {eval.depth}/10</span>
              <span>Clarity: {eval.clarity}/10</span>
            </div>
            <div className="feedback">
              <p><strong>Strengths:</strong> {eval.strengths.join(', ')}</p>
              <p><strong>Improvements:</strong> {eval.improvements.join(', ')}</p>
              <p><strong>Tip:</strong> {eval.suggestion}</p>
            </div>
          </div>
        ))}
        <button onClick={() => { setView('setup'); setEvaluations([]); setCurrentIndex(0); setQuestions([]); }}>
          Start New Interview
        </button>
      </div>
    );
  }
}

export default App;

And here is the complete App.css:

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background: #0f172a;
  color: #e2e8f0;
  min-height: 100vh;
}

.container {
  max-width: 720px;
  margin: 0 auto;
  padding: 40px 20px;
}

h1 {
  font-size: 2rem;
  margin-bottom: 8px;
  color: #f8fafc;
}

.subtitle {
  color: #94a3b8;
  margin-bottom: 32px;
}

.setup-form {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

label {
  font-weight: 600;
  color: #cbd5e1;
}

select, textarea {
  padding: 12px;
  border-radius: 8px;
  border: 1px solid #334155;
  background: #1e293b;
  color: #e2e8f0;
  font-size: 1rem;
  width: 100%;
}

textarea {
  resize: vertical;
  min-height: 160px;
}

button {
  padding: 14px 24px;
  background: #3b82f6;
  color: white;
  border: none;
  border-radius: 8px;
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;
  margin-top: 8px;
}

button:hover {
  background: #2563eb;
}

button:disabled {
  background: #475569;
  cursor: not-allowed;
}

.progress {
  color: #94a3b8;
  margin-bottom: 8px;
}

.topic-badge {
  display: inline-block;
  background: #1e3a5f;
  color: #60a5fa;
  padding: 4px 12px;
  border-radius: 16px;
  font-size: 0.85rem;
  margin-bottom: 16px;
}

.question {
  font-size: 1.3rem;
  margin-bottom: 24px;
  line-height: 1.5;
}

.overall-score {
  text-align: center;
  margin: 24px 0;
  padding: 24px;
  background: #1e293b;
  border-radius: 12px;
}

.score-number {
  font-size: 3rem;
  font-weight: 700;
  color: #3b82f6;
}

.evaluation-card {
  background: #1e293b;
  border-radius: 12px;
  padding: 20px;
  margin-bottom: 16px;
}

.evaluation-card h3 {
  color: #f8fafc;
  margin-bottom: 8px;
  font-size: 1rem;
}

.your-answer {
  color: #94a3b8;
  margin-bottom: 12px;
  font-size: 0.9rem;
}

.scores {
  display: flex;
  gap: 16px;
  margin-bottom: 12px;
}

.scores span {
  background: #0f172a;
  padding: 4px 10px;
  border-radius: 6px;
  font-size: 0.85rem;
  color: #60a5fa;
}

.feedback p {
  margin-bottom: 6px;
  font-size: 0.9rem;
  color: #cbd5e1;
}

Your frontend is fully built. Next up, you will wire everything together for a complete end-to-end test of the full interview flow.

Connect and Test End-to-End

You now have a complete backend with AI-powered endpoints and a React frontend with all three views built out. The final piece is verifying that everything works together as one seamless application.

In this step, you will trace how React state management drives your interview flow from setup through questions to the final report. Then you will run a complete mock interview end-to-end to confirm scores and feedback render correctly.

In this step, get ready to:
  • Understand how state management navigates between the Setup, Session, and Report views.
  • Run a full mock interview from role selection through all 5 questions to the final report.
Review the state management flow

Your App.jsx already contains all the logic to navigate between views. Before testing, it helps to understand how state drives the entire interview lifecycle.

  • Switch back to App.jsx in your editor and look at the state hooks at the top of the App function.
const [view, setView] = useState('setup');
const [role, setRole] = useState('');
const [difficulty, setDifficulty] = useState('');
const [questions, setQuestions] = useState([]);
const [currentIndex, setCurrentIndex] = useState(0);
const [answer, setAnswer] = useState('');
const [evaluations, setEvaluations] = useState([]);
const [loading, setLoading] = useState(false);

How does state drive the interview flow?

The view state variable controls which screen the user sees. It starts at 'setup', changes to 'session' after questions are generated, and finally becomes 'report' after the last answer is submitted.

  • questions stores the 5 questions from the /api/interview/start response.
  • currentIndex tracks which question the user is on (0 through 4).
  • evaluations accumulates each answer's scores from /api/interview/evaluate, building the data for the final report.

The flow works like a pipeline: setup collects user preferences, session iterates through questions one at a time, and report computes averages from the accumulated evaluations.

  • Look at the submitAnswer function to see the view-transition logic.
setAnswer('');
if (currentIndex < questions.length - 1) {
  setCurrentIndex(currentIndex + 1);
} else {
  setView('report');
}

Why does this conditional matter?

After each answer submission, the code checks whether there are more questions remaining. If yes, it increments currentIndex to show the next question. If the user just answered the final question, it switches the view to 'report' instead.

Run the full end-to-end interview

Now it is time to run through the entire interview flow in your browser. You need both the backend and frontend servers running simultaneously.

  • Make sure your Spring Boot backend is still running on port 8080. If it stopped, run the application again from IntelliJ by clicking the green play button.
  • Make sure your React frontend is still running on port 5173. If it stopped, return to the frontend terminal and start it by running:
npm run dev
  • Open your browser and navigate to http://localhost:5173.

You should see the AI Mock Interview setup screen with the dark theme, a Job Role dropdown, a Difficulty Level dropdown, and a blue Start Interview button.

  • Select Java Backend Developer from the Job Role dropdown.
  • Select Junior from the Difficulty Level dropdown.
  • Click Start Interview.

What should I see?

The button text changes to "Generating Questions..." while the AI processes your request. After a few seconds, the view transitions to the Session screen showing "Question 1 of 5", a topic badge, and the first interview question.

  • Type a short answer in the text area. Even a one-sentence answer works for testing purposes.
  • Click Submit & Next.

The button text changes to "Evaluating..." while Gemini scores your answer. After a few seconds, the next question appears. The progress indicator updates to "Question 2 of 5".

  • Repeat for all 5 questions. On the last question, the button says Submit & Finish instead.

Speed up testing

You do not need to write perfect answers for testing. A sentence or two per question is enough to verify the full flow works correctly. The AI will still return scored feedback on short answers.

After submitting the fifth answer, the view automatically transitions to the Interview Report. You should see a large overall score (out of 10) at the top, followed by evaluation cards for each question.

  • Verify that each evaluation card shows the question text, your answer, three individual scores (Technical, Depth, Clarity), strengths, improvements, and a tip.
  • Verify that the overall score at the top is the average of all five overallScore values.
  • Click Start New Interview at the bottom to confirm it resets back to the setup screen.

Seeing "Error starting interview. Is the backend running?"

  • Check that your Spring Boot application is still running in IntelliJ. Look for the green indicator in the Run panel.
  • Confirm your GOOGLE_API_KEY environment variable is set in your IntelliJ run configuration. The backend will start but API calls will fail if the key is missing.
  • Check the IntelliJ console for stack traces. A CorsConfig issue will show as a CORS error in the browser DevTools console.

Help me debug my Spring Boot and React connection issue

You have a fully working AI mock interview platform. The frontend collects user preferences, the backend generates tailored questions with Gemini, evaluates each answer with structured scoring, and the report gives the user clear feedback on their performance.

Secret mission

Add Interview History with H2 Database

Your mock interview platform works great for a single session. But what if you could track your progress over time? In this secret mission, you'll add database persistence so every completed interview is saved and you can review past performance from the setup screen.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally with a free-tier API key, so there are no ongoing costs.

Resources you used:

  • Spring Boot backend application running on localhost:8080 (free, local only).
  • React development server running on localhost:5173 (free, local only).
  • Google Gemini API key (free tier, no charges).
  • Project files in the ai-mock-interview directory on your Desktop.

Keep everything running

No action needed. Choose this if you're still actively building or plan to continue practicing interviews.

  • Your Spring Boot backend continues serving on port 8080.
  • Your React frontend continues serving on port 5173.
  • Your Gemini API key remains active. It is free-tier with no charges.
  • The H2 database is in-memory, so interview history resets each time you restart the backend.

Pause - I'll come back to this later

Shut down running processes to free up memory. Your project files stay on disk so you can restart anytime.

  • In the IntelliJ terminal running your Spring Boot backend, press Ctrl+C to stop the server.
  • In the terminal running your React dev server, press Ctrl+C to stop it.
  • To restart later, run this in your backend project directory:
mvn spring-boot:run
  • To restart the frontend, run this in your frontend directory:
npm run dev

Delete - I don't want to use this again

Remove all project resources and start fresh. Follow each step below to clean up completely.

  • Stop both servers if they are still running. Press Ctrl+C in each terminal.
  • Delete the project directory by running this on macOS/Linux:
cd ~/Desktop
rm -rf ai-mock-interview
  • On Windows, run this in PowerShell:
cd ~\Desktop
Remove-Item -Recurse -Force ai-mock-interview
  • Optionally, revoke your Gemini API key if you no longer need it. Visit aistudio.google.com, find your API key in the keys list, and delete it. This is optional since the free tier incurs no charges.

Nice Work!

Nice Work!

You just built a full-stack AI Mock Interview Platform from scratch. Your application generates tailored technical interview questions, evaluates answers with multi-dimensional scoring, and presents structured feedback through a polished React interface.

You've learned how to:

  • Build a Spring Boot REST API with a clean service layer and integrate Google's official GenAI Java SDK to call Gemini 2.5 Flash for AI-powered features.
  • Engineer prompts with system instructions and JSON schema responses to produce structured, predictable AI output without manual parsing.
  • Create a React/Vite frontend with multi-view state management that connects end-to-end with a backend API for a complete interview experience.
  • Secret Mission: Added database persistence with Spring Data JPA and H2 to track interview history over time.

Ready to quiz yourself?