How to Build a Resume Analysis Web App with Spring Boot, React, and Gemini API: A Step-by-Step Guide

In today's competitive job market, recruiters and hiring managers are often inundated with hundreds of resumes for a single position. Manually screening each one is a time-consuming and tedious task. What if you could build a smart application to automate this process, extracting key information from the resume and providing a concise analysis of the resume by providing an ATS score, pros, cons etc.

This is where the power of modern technology comes in. By combining a robust backend build with Java, Spring Boot, a dynamic frontend build with React.js, and a cutting-edge AI model, you can create a powerful Resume Analysis web application.

In this comprehensive article, we will walk you through building a full-stack resume analysis app from the scratch. We will use Spring Boot for the backend REST API, ReactJS for the interactive frontend, and Google's powerful Gemini API for the intelligent text analysis.

This project is not only an addition to your portfolio but also a practical knowledge of integrating generative AI into web applications.

Why This Tech Stack?

Before we dive into the code, let's understand why this combination of technologies is an excellent choice for our project.

Spring Boot

Spring Boot is a leading framework for building enterprise-grade Java applications. Its key advantages include:

  • Rapid Development: Auto-configuration and an opinionated setup drastically reduce boilerplate code.
  • Robust Ecosystem: Seamless integration with a vast library of tools for security, data access, and more.
  • Scalability: Built for creating scalable and maintainable microservices and REST APIs.

ReactJS

React is a declarative and component-based JavaScript library for building user interfaces.

  • Component-Based: Allows you to build encapsulated components that manage their own state, making complex UIs manageable.
  • Rich User Experience: Enables the creation of fast, responsive, and dynamic single-page applications (SPAs).
  • Strong Community: Backed by Facebook and a massive community, offering a wealth of resources and libraries.

Google Gemini API

Gemini is Google's latest and most capable family of multimodal AI models. For our app, it's the engine that provides the intelligence.

  • Advanced Understanding: Gemini can process and understand large contexts of text, making it perfect for analyzing a full resume.
  • Generative Power: We can instruct it with a specific prompt to extract skills, summarize experience, and check for specific qualifications.
  • Easy Integration: Provides a straightforward REST API that can be easily called from our Spring Boot backend.

Project Architecture Overview

Our application will have a simple but effective architecture:

ReactJS Frontend: The user will upload a resume in a PDF format through a simple web interface. And file clicking, upload the result will show like ATS score, pros, cons, and recommendations for improvement.

Spring Boot Backend: The frontend sends the resume to a REST endpoint on our Spring Boot server, and then it parses the PDF file and fetches its content it and sends that content with a prompt to the Gemini API.

Gemini API Integration: The backend takes the resume text, wraps it in a carefully crafted prompt, and sends it to the Google Gemini API.

Response Flow: Gemini processes the request and returns a structured JSON analysis. Our backend then forwards this analysis back to the React frontend to be displayed to the user.

Prerequisites

Before you begin, ensure you have the following installed:

  • Java Development Kit (JDK) 17 or later
  • Apache Maven
  • Node.js and npm
  • An IDE of your choice (e.g., IntelliJ IDEA, VS Code)
  • A Google AI Studio API Key for the Gemini API. You can get one for free from the Google AI for Developers website.

Part 1: Building the Spring Boot Backend

Let's start by creating the backend service that will communicate with the Gemini API and frontend.

Step 1: Initialize the Spring Boot Project

Go to start.spring.io and configure your project with the following settings:

Project: Maven

Language: Java 17 or later

Spring Boot: The latest stable version

Group: com.resumeanalyzer

Artifact: resume-analyzer

Dependencies: Add Spring Web

Download the generated ZIP file, extract it, and open it in your IDE.

Step 2: Configure the Gemini API

First, let's add your Gemini API key to the configuration file. Never hardcode API keys in your source code.

Open src/main/resources/application.properties and add the following lines:

properties

# Gemini API Configuration
gemini.api.key=YOUR_API_KEY
gemini.project.id=resume-analyzer
gemini.location=us-central1

Replace YOUR_API_KEY_HERE with the key you obtained from Google AI Studio.

Step 3: Create the DTOs (Data Transfer Objects)

We need to create a class that represents the structure of the result that comes from the Google Gemini api.

Create a DTO package. Inside it, create an AnalysisResult.java record to hold the incoming resume text.

Java

package com.resumeanalyzer.model;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;
import java.util.List;

@Data
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "resume_analyses")
public class AnalysisResult {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String fileName;

    @Column(nullable = false)
    private String overallScore;

    @Column(columnDefinition = "TEXT")
    private String summary;

    @ElementCollection
    @CollectionTable(name = "analysis_pros", joinColumns = @JoinColumn(name = "analysis_id"))
    @Column(name = "pro", columnDefinition = "TEXT")
    private List<String> pros;

    @ElementCollection
    @CollectionTable(name = "analysis_cons", joinColumns = @JoinColumn(name = "analysis_id"))
    @Column(name = "con", columnDefinition = "TEXT")
    private List<String> cons;

    @ElementCollection
    @CollectionTable(name = "analysis_recommendations", joinColumns = @JoinColumn(name = "analysis_id"))
    @Column(name = "recommendation", columnDefinition = "TEXT")
    private List<String> recommendations;

    @Column(nullable = false)
    private LocalDateTime analyzedAt;

    public AnalysisResult(String fileName, String overallScore, String summary, 
                         List<String> pros, List<String> cons, List<String> recommendations, 
                         LocalDateTime analyzedAt) {
        this.fileName = fileName;
        this.overallScore = overallScore;
        this.summary = summary;
        this.pros = pros;
        this.cons = cons;
        this.recommendations = recommendations;
        this.analyzedAt = analyzedAt;
    }
} 

Step 4: Build the Service Layer

The service layer will contain the core logic for parsing the content from PDF and communicating with the Gemini API and getting a response, and converting it into a proper DTO structure.

Create a service package and add a ResumeAnalysisService.java class.

Java

package com.resumeanalyzer.service;

import com.resumeanalyzer.model.AnalysisResult;
import com.resumeanalyzer.model.ResumeAnalysis;
import com.resumeanalyzer.repository.ResumeAnalysisRepository;
import lombok.extern.slf4j.Slf4j;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Service
@Slf4j
public class ResumeAnalysisService {

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

    @Autowired
    private ResumeAnalysisRepository resumeAnalysisRepository;

    private final RestTemplate restTemplate = new RestTemplate();
    private static final String UPLOAD_DIR = "uploads/resumes";
    private static final String GEMINI_API_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent";

    public AnalysisResult analyzeResume(MultipartFile file) throws IOException {
        createUploadDirectory();
        String fileName = UUID.randomUUID() + "_" + file.getOriginalFilename();
        Path filePath = Paths.get(UPLOAD_DIR, fileName);
        
        try {
            Files.copy(file.getInputStream(), filePath);
            String resumeText = extractTextFromPdf(filePath.toFile());
            AnalysisResult result = analyzeWithGemini(resumeText, fileName);
            
            ResumeAnalysis analysis = new ResumeAnalysis(
                file.getOriginalFilename(),
                result.getOverallScore(),
                result.getSummary()
            );
            resumeAnalysisRepository.save(analysis);
            
            return result;
        } finally {
            Files.deleteIfExists(filePath);
        }
    }

    private void createUploadDirectory() throws IOException {
        Path path = Paths.get(UPLOAD_DIR);
        if (!Files.exists(path)) {
            Files.createDirectories(path);
        }
    }

    private String extractTextFromPdf(File file) throws IOException {
        try (PDDocument document = PDDocument.load(file)) {
            PDFTextStripper stripper = new PDFTextStripper();
            return stripper.getText(document);
        }
    }

    private AnalysisResult analyzeWithGemini(String resumeText, String fileName) {
        String prompt = String.format(
            "Analyze this resume and provide a detailed analysis in EXACTLY this format:\n" +
            "1. Overall Score: [score]/100\n" +
            "2. Summary: [2-3 sentence summary]\n" +
            "3. Pros:\n- [Strength 1]\n- [Strength 2]\n- [Strength 3]\n" +
            "4. Cons:\n- [Weakness 1]\n- [Weakness 2]\n- [Weakness 3]\n" +
            "5. Recommendations:\n- [Suggestion 1]\n- [Suggestion 2]\n- [Suggestion 3]\n\n" +
            "Resume Content:\n%s", 
            resumeText.substring(0, Math.min(resumeText.length(), 15000))
        );


        Map<String, Object> requestBody = new HashMap<>();
        Map<String, Object> content = new HashMap<>();
        Map<String, String> textPart = new HashMap<>();
        textPart.put("text", prompt);
        
        content.put("parts", Collections.singletonList(textPart));
        requestBody.put("contents", Collections.singletonList(content));

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.set("x-goog-api-key", apiKey);

        try {
            ResponseEntity<Map> response = restTemplate.exchange(
                GEMINI_API_URL,
                HttpMethod.POST,
                new HttpEntity<>(requestBody, headers),
                Map.class
            );

            return parseGeminiResponse(response.getBody(), fileName);
        } catch (Exception e) {
            log.error("Gemini API error: {}", e.getMessage());
            return getFallbackAnalysis(fileName);
        }
    }

    private AnalysisResult parseGeminiResponse(Map<String, Object> response, String fileName) {
        if (response == null || !response.containsKey("candidates")) {
            log.warn("Invalid Gemini response structure");
            return getFallbackAnalysis(fileName);
        }

        try {
            Map<String, Object> candidate = ((List<Map<String, Object>>) response.get("candidates")).get(0);
            Map<String, Object> content = (Map<String, Object>) candidate.get("content");
            String analysisText = (String) ((Map<String, Object>) ((List<?>) content.get("parts")).get(0)).get("text");
            
            log.debug("Gemini raw analysis+++++++++++++++++++++++++++++++:\n{}", response);
            return parseAnalysisText(analysisText, fileName);
        } catch (Exception e) {
            log.error("Response parsing failed: {}", e.getMessage());
            return getFallbackAnalysis(fileName);
        }
    }

    private AnalysisResult parseAnalysisText(String analysisText, String fileName) {
        
        String[] sections = analysisText.split("\\d+\\.\\s");
        
      
        if (sections.length < 6) {
            log.warn("Unexpected section count: {}", sections.length);
            return parseAnalysisTextFallback(analysisText, fileName);
        }
        
        String score = extractScore(sections[1]);
        String summary = extractSummary(sections[2]);
        List<String> pros = extractListItems(sections[3]);
        List<String> cons = extractListItems(sections[4]);
        List<String> recommendations = extractListItems(sections[5]);
        
        return new AnalysisResult(
            fileName,
            score,
            summary,
            pros,
            cons,
            recommendations,
            LocalDateTime.now()
        );
    }
    
    private String extractScore(String section) {
       
        Pattern pattern = Pattern.compile("(\\d+/100)");
        Matcher matcher = pattern.matcher(section);
        return matcher.find() ? matcher.group(1) : "N/A";
    }
    
    private String extractSummary(String section) {
        String summary = section.replaceFirst("Summary[:\\s]*", "");
        return summary.trim().replaceAll("\n", " ");
    }
    
    private List<String> extractListItems(String section) {
        List<String> items = new ArrayList<>();
        String[] lines = section.split("\n");
        
        for (String line : lines) {
            line = line.trim();
            
            if (line.isEmpty() || 
                line.startsWith("Pros:") || 
                line.startsWith("Cons:") || 
                line.startsWith("Recommendations:")) {
                continue;
            }
            
          
            if (line.startsWith("- ") || line.startsWith("* ")) {
                items.add(line.substring(2).trim());
            } 
        
            else if (!line.contains(":") && line.length() > 10) {
                items.add(line);
            }
        }
        return items;
    }
    
    private AnalysisResult parseAnalysisTextFallback(String analysisText, String fileName) {
        log.warn("Using fallback parser for analysis text");
      
        String score = "N/A";
        Pattern pattern = Pattern.compile("(\\d+/100)");
        Matcher matcher = pattern.matcher(analysisText);
        if (matcher.find()) {
            score = matcher.group(1);
        }
        
        String summary = "";
        String[] parts = analysisText.split("2\\.\\s*Summary[:\\s]*", 2);
        if (parts.length > 1) {
            summary = parts[1].split("3\\.\\s*Pros[:\\s]*")[0].trim();
        }
        
        return new AnalysisResult(
            fileName,
            score,
            summary,
            extractListItemsFallback(analysisText, "Pros"),
            extractListItemsFallback(analysisText, "Cons"),
            extractListItemsFallback(analysisText, "Recommendations"),
            LocalDateTime.now()
        );
    }
    
    private List<String> extractListItemsFallback(String text, String sectionName) {
        List<String> items = new ArrayList<>();
        Pattern pattern = Pattern.compile(
            Pattern.quote(sectionName) + "[\\s:]*\\n(-[^\\n]+)"
        );
        Matcher matcher = pattern.matcher(text);
        
        while (matcher.find()) {
            items.add(matcher.group(1).substring(1).trim());
        }
        return items.isEmpty() ? Collections.emptyList() : items;
    }

    private AnalysisResult getFallbackAnalysis(String fileName) {
        return new AnalysisResult(
            fileName,
            "75/100",
            "Generated fallback analysis due to service issues",
            Arrays.asList("Strong technical background", "Good education history"),
            Arrays.asList("Limited work experience", "Could use more quantifiable achievements"),
            Arrays.asList("Add more metrics to experience", "Include relevant certifications"),
            LocalDateTime.now()
        );
    }

    public List<ResumeAnalysis> getUserAnalyses() {
        return resumeAnalysisRepository.findAll();
    }
}

Step 5: Create the REST Controller

Finally, create an endpoint to expose this service. Create a controller package with a ResumeAnalysisController.java class.

Java

package com.resumeanalyzer.controller;

import com.resumeanalyzer.model.AnalysisResult;
import com.resumeanalyzer.model.ResumeAnalysis;
import com.resumeanalyzer.service.ResumeAnalysisService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.List;

@RestController
@RequestMapping("/api")
@CrossOrigin(origins = "http://localhost:3000")
public class ResumeAnalysisController {

    @Autowired
    private ResumeAnalysisService resumeAnalysisService;

    @PostMapping("/analyze")
    public ResponseEntity<AnalysisResult> analyzeResume(@RequestParam("file") MultipartFile file) {
        try {
            AnalysisResult result = resumeAnalysisService.analyzeResume(file);
            return ResponseEntity.ok(result);
        } catch (IOException e) {
            return ResponseEntity.badRequest().build();
        }
    }

    @GetMapping("/analyses")
    public ResponseEntity<List<ResumeAnalysis>> getUserAnalyses() {
        List<ResumeAnalysis> analyses = resumeAnalysisService.getUserAnalyses();
        return ResponseEntity.ok(analyses);
    }
} 

The @CrossOrigin annotation is crucial for allowing our React app (which will run on port 3000) to communicate with our backend (running on port 8080).

Your backend is now ready! You can run it from your IDE or by using the command mvn spring-boot:run.

Part 2: Building the ReactJS Frontend

Now, let's create the user interface where users can upload their resumes and make sure the design can be varied according to your choices.

Step 1: Initialize the React App

Open your terminal, navigate to your desired project directory (outside the Spring Boot project), and run:

Bash

npx create-react-app resume-analyzer-ui
cd resume-analyzer-ui

Step 2: Install Axios

We'll use Axios to make HTTP requests to our backend.

Bash

npm install axios

Step 3: Create ResumeAnalysis.jsx for handling the resume and there response.

React

import { useState, useCallback } from "react"
import { useDropzone } from "react-dropzone"
import axios from "axios"
import {
    FiUpload,
    FiFile,
    FiAlertTriangle,
    FiClipboard,
    FiCheckCircle,
    FiXCircle,
    FiZap,
    FiRefreshCw,
    FiDownload,
} from "react-icons/fi"

const ResumeAnalysis = () => {
  const [file, setFile] = useState(null)
  const [loading, setLoading] = useState(false)
  const [analysis, setAnalysis] = useState(null)
  const [error, setError] = useState(null)

  const onDrop = useCallback(async (acceptedFiles) => {
    const selectedFile = acceptedFiles[0]
    if (selectedFile) {
      setFile(selectedFile)
      setLoading(true)
      setError(null)

      const formData = new FormData()
      formData.append("file", selectedFile)

      try {
        const response = await axios.post("http://localhost:8080/api/analyze", formData, {
          headers: {
            "Content-Type": "multipart/form-data",
          },
        })
        setAnalysis(response.data)
      } catch (err) {
        setError(err.response?.data?.message || "An error occurred while analyzing the resume")
      } finally {
        setLoading(false)
      }
    }
  }, [])

  const { getRootProps, getInputProps, isDragActive } = useDropzone({
    onDrop,
    accept: {
      "application/pdf": [".pdf"],
    },
    multiple: false,
  })

  const resetAnalysis = () => {
    setFile(null)
    setAnalysis(null)
    setError(null)
  }

  return (
    <div className="min-h-screen bg-gradient-primary px-12 [all:unset]">
      <div className="max-w-6xl  px-4 sm:px-6 lg:px-8 py-8">
         
        <div className="text-center mb-12">
          <div className="inline-flex items-center justify-center w-20 h-20 bg-white/20 rounded-full mb-6 backdrop-blur-glass">
            <FiFile size={40} className="text-white" />
          </div>
          <h1 className="text-5xl sm:text-6xl font-bold text-white mb-4 drop-shadow-lg">AI Resume Analyzer</h1>
          <p className="text-xl text-white/90 max-w-2xl mx-auto">
            Get instant, AI-powered insights to optimize your resume and land your dream job
          </p>
        </div>

        {!analysis && (
          <div className="glass-effect rounded-3xl p-8 mb-8 shadow-card animate-fade-in">
            <div
              {...getRootProps()}
              className={`relative transition-all duration-300 ease-in-out border-2 border-dashed rounded-2xl p-12 text-center cursor-pointer group ${
                isDragActive
                  ? "border-primary-500 bg-primary-50 scale-[1.02] shadow-card-hover"
                  : "border-gray-300 hover:border-primary-400 hover:bg-gray-50/50"
              }`}
            >
              <input {...getInputProps()} />
              <div className="flex flex-col items-center">
                <div
                  className={`w-20 h-20 rounded-full flex items-center justify-center mb-6 transition-all duration-300 ${
                    isDragActive
                      ? "bg-primary-100 scale-110"
                      : "bg-gray-100 group-hover:bg-primary-50 group-hover:scale-105"
                  }`}
                >
                  <FiUpload
                    size={32}
                    className={`transition-colors duration-300 ${
                      isDragActive ? "text-primary-600" : "text-gray-400 group-hover:text-primary-500"
                    }`}
                  />
                </div>
                <h3 className="text-2xl font-semibold text-gray-900 mb-2">
                  {isDragActive ? "Drop your resume here!" : "Upload your resume"}
                </h3>
                <p className="text-gray-600 mb-6 text-lg">Drag and drop your PDF file here, or click to browse</p>
                <button className="inline-flex items-center px-8 py-4 bg-gradient-primary text-white font-semibold rounded-xl hover:shadow-lg transform hover:scale-105 transition-all duration-200 focus:outline-none focus:ring-4 focus:ring-primary-200">
                  <FiUpload size={20} className="mr-2" />
                  Choose File
                </button>
                <p className="text-sm text-gray-500 mt-4">Only PDF files are supported • Max 10MB</p>
              </div>
            </div>

            {file && (
              <div className="mt-6 p-4 bg-success-50 border border-success-200 rounded-xl animate-slide-up ">
                <div className="flex items-center">
                  <FiCheckCircle size={24} className="text-success-500 mr-3" />
                  <div>
                    <p className="text-success-800 font-semibold">File uploaded successfully</p>
                    <p className="text-success-600 text-sm">{file.name}</p>
                  </div>
                </div>
              </div>
            )}
          </div>
        )}

    
        {loading && (
          <div className="glass-effect rounded-3xl p-12 text-center mx-12 mb-8 shadow-card animate-fade-in">
            <div className="w-16 h-16 border-4 border-primary-200 border-t-primary-600 rounded-full animate-spin mx-auto mb-6"></div>
            <h3 className="text-2xl font-semibold text-gray-900 mb-2">Analyzing Your Resume</h3>
            <p className="text-gray-600 text-lg">Our AI is reviewing your resume for optimization opportunities...</p>
          </div>
        )}

        {error && (
          <div className="glass-effect rounded-3xl p-8 mb-8 shadow-card animate-fade-in">
            <div className="bg-danger-50 border border-danger-200 rounded-xl p-6">
              <div className="flex items-center">
                <FiAlertTriangle size={24} className="text-danger-500 mr-3" />
                <div>
                  <h4 className="text-danger-800 font-semibold">Analysis Failed</h4>
                  <p className="text-danger-600">{error}</p>
                </div>
              </div>
            </div>
          </div>
        )}

        {analysis && (
          <div className="glass-effect rounded-3xl overflow-hidden shadow-card animate-fade-in" style={{margin:'40px'}}>
 
            <div className="bg-gradient-success p-8 text-white">
              <div className="flex justify-between items-center flex-wrap gap-4">
                <div>
                  <h2 className="text-3xl font-bold mb-2">Analysis Complete</h2>
                  <p className="text-green-100 text-lg">Here's your detailed resume breakdown</p>
                </div>
                <div className="text-right">
                  <div className="text-5xl font-bold">{analysis.overallScore}</div>
                  <div className="text-green-100">Overall Score</div>
                </div>
              </div>
            </div>

            <div className="p-8">
     
              {analysis.summary && (
                <div className="mb-8">
                  <h3 className="text-xl font-semibold text-gray-900 mb-4 flex items-center">
                    <FiClipboard size={24} className="text-info-500 mr-3" />
                    Executive Summary
                  </h3>
                  <div className="bg-info-50 border-l-4 border-info-400 p-6 rounded-r-xl">
                    <p className="text-gray-700 leading-relaxed text-lg">{analysis.summary}</p>
                  </div>
                </div>
              )}

     
              <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
   
                <div className="bg-success-50 border border-success-200 rounded-2xl p-6 hover:shadow-lg transition-shadow duration-300">
                  <div className="flex items-center mb-4">
                    <FiCheckCircle size={24} className="text-success-500 mr-3" />
                    <h3 className="text-xl font-semibold text-success-800">Strengths</h3>
                  </div>
                  <ul className="space-y-3">
                    {analysis.pros?.length > 0 ? (
                      analysis.pros.map((item, index) => (
                        <li key={index} className="flex items-start text-success-700">
                          <span className="text-success-500 mr-3 mt-1 text-lg">•</span>
                          <span className="text-sm leading-relaxed">{item}</span>
                        </li>
                      ))
                    ) : (
                      <li className="text-success-600 italic text-sm">No strengths identified</li>
                    )}
                  </ul>
                </div>

             
                <div className="bg-danger-50 border border-danger-200 rounded-2xl p-6 hover:shadow-lg transition-shadow duration-300">
                  <div className="flex items-center mb-4">
                    <FiXCircle size={24} className="text-danger-500 mr-3" />
                    <h3 className="text-xl font-semibold text-danger-800">Areas to Improve</h3>
                  </div>
                  <ul className="space-y-3">
                    {analysis.cons?.length > 0 ? (
                      analysis.cons.map((item, index) => (
                        <li key={index} className="flex items-start text-danger-700">
                          <span className="text-danger-500 mr-3 mt-1 text-lg">•</span>
                          <span className="text-sm leading-relaxed">{item}</span>
                        </li>
                      ))
                    ) : (
                      <li className="text-danger-600 italic text-sm">No issues identified</li>
                    )}
                  </ul>
                </div>

                <div className="bg-info-50 border border-info-200 rounded-2xl p-6 hover:shadow-lg transition-shadow duration-300">
                  <div className="flex items-center mb-4">
                    <FiZap size={24} className="text-info-500 mr-3" />
                    <h3 className="text-xl font-semibold text-info-800">Recommendations</h3>
                  </div>
                  <ul className="space-y-3">
                    {analysis.recommendations?.length > 0 ? (
                      analysis.recommendations.map((item, index) => (
                        <li key={index} className="flex items-start text-info-700">
                          <span className="text-info-500 mr-3 mt-1 text-lg">•</span>
                          <span className="text-sm leading-relaxed">{item}</span>
                        </li>
                      ))
                    ) : (
                      <li className="text-info-600 italic text-sm">No recommendations available</li>
                    )}
                  </ul>
                </div>
              </div>

              <div className="mt-8 flex justify-center gap-4 flex-wrap">
                
                <button
                  onClick={resetAnalysis}
                  className="inline-flex items-center px-8 py-4 bg-white border-2 border-gray-300 text-gray-700 font-semibold rounded-xl hover:bg-gray-50 hover:shadow-lg transform hover:scale-105 transition-all duration-200"
                >
                  <FiRefreshCw size={20} className="mr-2" />
                  Analyze Another
                </button>
              </div>
            </div>
          </div>
        )}
      </div>
    </div>
  )
}

export default ResumeAnalysis

Running the Full Application

Start the Backend: Run your Spring Boot application. It will start on http://localhost:8080.

Start the Frontend: In your resume-analyzer-ui directory, run the command npm start. Your browser will open to http://localhost:3000.

Now, you can upload a PDF resume file, and within seconds, you will see an analysis generated by the Gemini API.

Output:

As we already discussed, the design should be changed according to your requirements.



Potential Enhancements

This is a fantastic starting point. Here are some ways you can enhance this project:

Database Integration: Save the analysis results to a database like PostgreSQL.

User Accounts: Implement Spring Security to allow users to sign up and view their history of analyzed resumes.

Improved UI: Use a component library like Material-UI or Ant Design to create a more sophisticated user interface.

Conclusion

Congratulations! You have successfully built a full-stack resume analysis web application using Spring Boot, ReactJS, and the Google Gemini API. This project demonstrates how you can integrate powerful generative AI capabilities into traditional web applications to create truly intelligent and useful tools. By mastering this stack, you are well-equipped to build the next generation of smart software.

Next Post Previous Post
No Comment
Add Comment
comment url