Introduction

You’ve spent hours perfecting your resume, tailoring it for every job application. Yet, you still don’t get the callbacks. It’s frustrating, right? You’re highly skilled, but your resume just doesn’t seem to capture the attention of recruiters.

Why? Because recruiters often skip resumes that don’t align with job requirements or fail to stand out. In a world where thousands of candidates apply for each position, making your resume catch attention is crucial.

Product Name

ResumeFixer AI — choose a name that reflects your brand’s mission: making resume improvement easy and accessible for developers.

Core Idea

Imagine a lightweight web tool that does all the hard work for you — an AI-powered resume fixer. You simply upload your resume and paste the job description. The AI does the rest.

  • Scores your resume against the job description.
  • Gives suggestions on how to improve the match.
  • Optionally, rewrites bullet points with stronger, results-oriented language that resonates with recruiters.

Target User Persona

  • Age Range: 25–35 years old.
  • Profession: Software developers (mid-level experience).
  • Frustration: Struggling to get callbacks despite being highly skilled.
  • Motivation: Wants to apply AI to land better job opportunities with top companies or global remote roles.
  • Tech-Savviness: Familiar with AI but needs practical help applying it to their career.

Problem Statement

“Skilled developers often don’t get interview calls because their resume doesn’t align well with job requirements or lacks impactful language. Recruiters have too many resumes to go through — if your resume isn’t instantly tailored to the job, you risk getting lost in the pile.”

Unique Value Proposition

ResumeFixer AI instantly analyzes your resume and tailors it to match the job description. You get:

  • AI-driven feedback on how well your resume fits the job.
  • Resume score that tells you how well your experience aligns.
  • Improvement suggestions to help you craft a more impactful resume.

All in just a few minutes, with suggestions tailored to your specific role.

MVP Features

FeatureDetails
Resume UploadAccept PDF or plain text formats.
Job Description InputPaste the job description or upload it.
AI Matching Score% match between resume and JD (based on skills, experience, tone).
Bullet Point SuggestionsAI rewrites selected resume bullet points to sound more impactful and results-oriented.
Keyword GapIdentifies missing keywords or skills compared to the job description.
Improvement TipsClear suggestions like “Add metrics” or “Show team size.”
Export OptionCopy the edited resume or download it in .docx/text format.

AI Usage Plan

TaskModelNotes
Resume & JD ParsingOpenAI GPT-4oToken-efficient, high-quality parsing for resumes and job descriptions.
Matching LogicGPT + Keyword Vector OverlapCombining structured logic and LLM models to match resumes to job descriptions.
RewritingGPT Prompt: “Rewrite this bullet to sound more result-oriented.”Adjust the tone (bold, humble, etc.) depending on the job description.

How Does It Work?

  1. Upload Your Resume – In any format you have (PDF, text).
  2. Paste the Job Description – This can be copied from the job post you’re applying to.
  3. AI Matching Score – Instantly get a match score showing how well your resume fits the job based on required skills and experience.
  4. Suggestions for Improvement – The AI provides feedback on how to tailor your resume, including:
    • Adjusting tone to match the company culture.
    • Adding metrics for more impact.
    • Rewriting weak bullet points to highlight results.
  5. Keyword Gap Analysis – See which key skills or keywords you’re missing that are crucial for the job.
  6. Download the Edited Resume – Once your resume is optimized, download it in your preferred format.

Why Developers Need This Tool

As a developer, you know how competitive the job market is. Companies receive hundreds of resumes for each job opening. If your resume isn’t aligned with the job’s keywords or doesn’t highlight your accomplishments clearly, it might never get seen by a recruiter.

RecruMatch AI (or ResumeFixer AI) solves this by providing you with an easy, efficient way to optimize your resume for every job you apply to — increasing your chances of getting noticed and getting those interview invites.

Code Structure Overview

📁 GitHub Repo Structure: resume-fixer-ai

resume-fixer-ai/

├── app/ # Laravel application logic (Controllers, Services, Jobs)
│ ├── Http/ # Handles HTTP requests, controllers, middleware
│ ├── Services/ # Custom services like interacting with OpenAI API
│ └── Models/ # Eloquent models to interact with the database (e.g., saving resumes)

├── bootstrap/ # Laravel bootstrap files, application setup

├── config/ # Configuration files, including API settings for OpenAI
│ └── openai.php # Store OpenAI API keys, configurations, and endpoints

├── database/
│ ├── factories/ # Factory classes for database seeding during testing
│ ├── migrations/ # Database migrations for tables like `resumes`, `job_descriptions`
│ └── seeders/ # Seeders to populate the database with example data

├── public/ # Public-facing assets (JS, CSS, images)

├── resources/
│ ├── views/ # Blade templates for rendering pages (e.g., resume upload page)
│ ├── js/ # JavaScript components (for interactivity if using Inertia/Vue)
│ └── lang/ # Language files for localization and multi-language support

├── routes/
│ └── web.php # Routes definition for web-based requests (upload, feedback)

├── storage/ # Storage for uploaded files, logs, and caches

├── tests/ # Unit and feature tests
│ ├── Feature/ # Tests for interacting with the application (e.g., upload, feedback)
│ └── Unit/ # Tests for specific components like resume parsing, AI logic

├── .env.example # Example environment file for setting API keys and other settings
├── .gitignore # Specifies which files/folders should be excluded from Git
├── artisan # Command-line utility for Laravel
├── composer.json # PHP dependencies, including Laravel and OpenAI SDK
├── package.json # JS dependencies (for frontend and interactivity)
├── README.md # Project documentation and setup guide
└── openai_prompts/ # Custom folder for storing prompt templates
├── match_score.txt # Template for generating the resume-job description match score
├── bullet_rewrite.txt # Template for rewriting resume bullet points
└── feedback_analysis.txt # Template for giving feedback on the resume's content

📘 README.md

To ensure other developers or collaborators can quickly understand the project and get started, the README.md file outlines the necessary information:

# ResumeFixer AI

AI-powered resume improvement tool for developers. Upload your resume, paste a job description, and get AI-based feedback, keyword gaps, and rewritten bullet points.

## Features
- Resume vs JD matching score
- AI suggestions for improvement
- Keyword gap analysis
- Bullet point rewriting
- Export improved resume

## Stack
- Laravel + Livewire
- OpenAI GPT-4o
- TailwindCSS

## Setup
1. Clone the repo
2. Run `composer install` and `npm install`
3. Set `.env` with your OpenAI API Key
4. Run migrations (`php artisan migrate`)
5. Start the local development server (`php artisan serve`) & explore!

## Folder Guide
- `openai_prompts/` – Prompt templates used for OpenAI API
- `app/Services/OpenAIService.php` – Core logic for calling LLMs

## Future Roadmap
- Add ATS scanner to match against Applicant Tracking Systems
- Role-switch resume transformation (e.g., Backend → Machine Learning role)

Step 1: Controller Code – ResumeController.php

The ResumeController.php is the first point of contact between the user and the backend. It’s responsible for handling the requests related to the resume upload, processing the job description, and interacting with the AI services.

Here’s a step-by-step breakdown of what the controller is doing:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Services\OpenAIService;

class ResumeController extends Controller
{
// Inject the OpenAI service
protected $openAI;

public function __construct(OpenAIService $openAI)
{
// Store the injected OpenAIService
$this->openAI = $openAI;
}

// Show the resume upload form
public function index()
{
// Return the view with the form where the user can upload their resume and enter the job description
return view('resume.index');
}

// Handle the resume and job description input, analyze with AI
public function analyze(Request $request)
{
// Validate incoming data
$request->validate([
'resume' => 'required|file|mimes:txt,pdf,doc,docx|max:5120', // Max 5MB file size
'job_description' => 'required|string', // Validate that job description is a string
]);

// Extract text from the uploaded resume
$resumeText = $this->extractTextFromFile($request->file('resume'));
$jobDescription = $request->input('job_description'); // Get the job description from the form

// Send the resume text and job description to the OpenAI service for analysis
$response = $this->openAI->analyzeResumeMatch($resumeText, $jobDescription);

// Return the result view with the AI response
return view('resume.result', compact('response'));
}

// Private method to extract text from uploaded file
private function extractTextFromFile($file)
{
// For now, extract text from supported files (txt, pdf, doc, docx)
// Basic implementation for MVP, you can extend this later to handle PDF or DOC parsing

// Check if the file is a valid type
if (!in_array($file->getClientOriginalExtension(), ['txt', 'pdf', 'doc', 'docx'])) {
throw new \Exception("Unsupported file type.");
}

try {
// Extract content for text-based files (txt, doc, docx)
if ($file->getClientOriginalExtension() === 'txt') {
return file_get_contents($file->getPathname());
}
// For PDFs (you can use a library like 'pdftotext' or 'smalot/pdfparser' to parse PDF content)
elseif ($file->getClientOriginalExtension() === 'pdf') {
// Example of extracting text from PDF, you can extend this later
$pdfText = shell_exec("pdftotext {$file->getPathname()} -");
return $pdfText;
}
// For DOCX files, you can use a library like 'phpoffice/phpword' to read the document content
elseif (in_array($file->getClientOriginalExtension(), ['doc', 'docx'])) {
// Extracting text for Word documents is possible using a package like PHPWord
$phpWord = \PhpOffice\PhpWord\IOFactory::load($file->getPathname());
$text = '';
foreach ($phpWord->getSections() as $section) {
foreach ($section->getElements() as $element) {
if (method_exists($element, 'getText')) {
$text .= $element->getText() . "\n";
}
}
}
return $text;
}

return ''; // Return empty if no format is found

} catch (\Exception $e) {
// Handle any errors during file extraction
throw new \Exception("Error extracting text from file: " . $e->getMessage());
}
}
}

Step 2: Service Code – OpenAIService.php

In this updated version of the OpenAIService.php file, we’ve added the ability to dynamically load prompt templates from external files and inject variables into them. This makes the prompt system more flexible and maintainable.

Here’s the updated version of the service with detailed explanations:

<?php

namespace App\Services;

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

class OpenAIService
{
protected $apiKey;
protected $apiUrl;
protected $promptPath;

public function __construct()
{
// Retrieve API key from the configuration file
$this->apiKey = config('services.openai.key');
$this->apiUrl = 'https://api.openai.com/v1/chat/completions'; // OpenAI API URL
// Set the path to the prompt templates folder
$this->promptPath = resource_path('openai_prompts');
}

// Main function to analyze resume and job description match
public function analyzeResumeMatch($resumeText, $jobDescription)
{
// Load the prompt template and inject resume and job description text
$prompt = $this->loadPrompt('match_score.txt', [
'resume_text' => $resumeText,
'job_description' => $jobDescription
]);

// Send the request to OpenAI API
try {
$response = Http::withToken($this->apiKey)
->post($this->apiUrl, [
'model' => 'gpt-4o', // Using GPT-4o model for analysis
'messages' => [
['role' => 'user', 'content' => $prompt]
],
'temperature' => 0.7 // Controls the randomness of the AI response
]);

// Check if the response was successful
if ($response->failed()) {
throw new \Exception("Failed to get a valid response from OpenAI API.");
}

// Extract content from the OpenAI response
$content = $response->json('choices.0.message.content');
if (empty($content)) {
throw new \Exception("Received empty content from OpenAI API.");
}

// Parse and return the AI response in a structured format
return $this->parseAIResponse($content);

} catch (\Exception $e) {
// Log the error and return default response
Log::error("OpenAI API request failed: " . $e->getMessage());
return [
'match_score' => 'N/A',
'missing_keywords' => [],
'improved_bullets' => ['Error processing resume and job description.'],
'summary_comment' => 'Could not analyze resume properly.',
];
}
}

/**
* Load a prompt from file and inject variables.
*
* @param string $filename The prompt file to load (e.g., 'match_score.txt')
* @param array $vars Variables to inject into the prompt template
* @return string The final prompt after variable injection
*/
protected function loadPrompt($filename, array $vars = [])
{
// Construct the file path for the prompt template
$path = "{$this->promptPath}/{$filename}";

// Check if the file exists and is readable
if (!file_exists($path) || !is_readable($path)) {
throw new \Exception("Prompt file {$filename} not found or not readable.");
}

// Read the prompt template content
$template = file_get_contents($path);

// Ensure the template is not empty
if (empty($template)) {
throw new \Exception("Prompt file {$filename} is empty.");
}

// Replace placeholders in the template with the corresponding values
foreach ($vars as $key => $value) {
$template = str_replace('{{' . $key . '}}', $value, $template);
}

return $template;
}

/**
* Parse OpenAI JSON response into an associative array.
*
* @param string $content The raw response content from OpenAI
* @return array Parsed response data
*/
protected function parseAIResponse($content)
{
try {
// Decode the JSON content from OpenAI's response
return json_decode($content, true);
} catch (\Exception $e) {
// Log the error and return default values
Log::error("Error parsing OpenAI response: " . $e->getMessage());
return [
'match_score' => 'N/A',
'missing_keywords' => [],
'improved_bullets' => ['Error parsing AI response.'],
'summary_comment' => 'Could not analyze resume properly.',
];
}
}
}

Step 3 : Blade Code

resources/views/resume/index.blade.php

@extends('layouts.app')

@section('content')
<div class="max-w-3xl mx-auto mt-10 p-6 bg-white rounded-2xl shadow">
    <h1 class="text-2xl font-bold mb-4"> Resume Match Analyzer</h1>
    <p class="mb-4 text-gray-600">Upload your resume and paste a job description to get AI-powered feedback and match score.</p>

    <form action="{{ route('resume.analyze') }}" method="POST" enctype="multipart/form-data" class="space-y-6">
        @csrf

        <div>
            <label for="resume" class="block font-medium mb-1">Upload Resume</label>
            <input 
                type="file" 
                name="resume" 
                id="resume" 
                required 
                accept=".txt,.pdf,.doc,.docx" 
                class="w-full border rounded px-3 py-2" 
                aria-describedby="fileHelp"
            >
            <small id="fileHelp" class="text-gray-500">Accepted file types: .txt, .pdf, .doc, .docx</small>
            @error('resume') 
                <p class="text-red-500 text-sm">{{ $message }}</p> 
            @enderror
        </div>

        <div>
            <label for="job_description" class="block font-medium mb-1">Paste Job Description</label>
            <textarea 
                name="job_description" 
                id="job_description" 
                rows="8" 
                required 
                class="w-full border rounded px-3 py-2" 
                aria-describedby="jobDescriptionHelp"
            >{{ old('job_description') }}</textarea>
            <small id="jobDescriptionHelp" class="text-gray-500">Paste the job description exactly as it appears in the job listing.</small>
            @error('job_description') 
                <p class="text-red-500 text-sm">{{ $message }}</p> 
            @enderror
        </div>

        <button 
            type="submit" 
            class="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700" 
            aria-label="Analyze the resume"
        >
            Analyze Resume
        </button>
    </form>
</div>
@endsection


resources/views/resume/result.blade.php

@extends('layouts.app')

@section('content')
<div class="max-w-3xl mx-auto mt-10 p-6 bg-white rounded-2xl shadow">
    <h1 class="text-2xl font-bold mb-4">📊 Resume Analysis Result</h1>

    <div class="mb-6">
        <h2 class="text-lg font-semibold">Match Score</h2>
        <p class="text-3xl font-bold text-blue-600">{{ $response['match_score'] ?? 'N/A' }}%</p>
    </div>

    <div class="mb-6">
        <h2 class="text-lg font-semibold">Keyword Suggestions</h2>
        @forelse($response['missing_keywords'] ?? [] as $keyword)
            <ul class="list-disc ml-5 text-gray-700">
                <li>{{ $keyword }}</li>
            </ul>
        @empty
            <p class="text-gray-600">No missing keywords found.</p>
        @endforelse
    </div>

    <div class="mb-6">
        <h2 class="text-lg font-semibold">Bullet Point Improvements</h2>
        @forelse($response['improved_bullets'] ?? [] as $bullet)
            <ul class="space-y-3 text-gray-800">
                <li class="bg-gray-100 p-3 rounded">{{ $bullet }}</li>
            </ul>
        @empty
            <p class="text-gray-600">No improvements found.</p>
        @endforelse
    </div>

    <a href="{{ route('resume.index') }}" class="inline-block mt-4 text-blue-600 hover:underline">🔁 Try another resume</a>
</div>
@endsection

Step 4 : Route in routes/web.php

use App\Http\Controllers\ResumeController;

Route::get('/resume', [ResumeController::class, 'index'])->name('resume.index');
Route::post('/resume/analyze', [ResumeController::class, 'analyze'])->name('resume.analyze');

.env Configuration for OpenAI API Key

OPENAI_API_KEY=your_openai_api_key_here

config/services.php Configuration for OpenAI

'openai' => [
    'key' => env('OPENAI_API_KEY'),
],

OpenAI prompt templates

1. openai_prompts/match_score.txt

This prompt helps analyze how well a resume matches a job description. It returns the match score as a percentage and provides additional insights like missing keywords and a summary comment.

Contents of match_score.txt:

are a professional tech recruiter.

Given the resume content and the job description below:

RESUME:
---
{{resume_text}}
---

JOB DESCRIPTION:
---
{{job_description}}
---

Your task:
1. Analyze how well this resume matches the job.
2. Return a JSON with:
- match_score (0-100)
- missing_keywords (keywords in JD but not in resume)
- summary_comment (1-2 lines)

Only return a valid JSON object like this:
{
"match_score": 82,
"missing_keywords": ["Kubernetes", "GraphQL", "CI/CD"],
"summary_comment": "The resume aligns well with backend requirements but lacks deployment-related skills."
}

Purpose:

  • Match Score: A percentage value that shows how closely the resume matches the job description.
  • Missing Keywords: Lists keywords from the job description that are absent in the resume, highlighting areas to improve.
  • Summary Comment: Provides a brief analysis to help users understand the strengths and weaknesses of the resume for the given job.

2. openai_prompts/bullet_rewrite.txt

This prompt takes weak or average resume bullet points and rewrites them to be more impactful, action-oriented, and quantified. It’s designed to help users improve their resume content in a way that resonates with recruiters.

Contents of bullet_rewrite.txt:

are a resume coach specialized in technical roles.

Given the following weak or average resume bullet points, rewrite them to be:
- Impactful
- Action-oriented
- Quantified (use numbers where possible)
- Clear and confident

Original Bullets:
---
{{bullet_list}}
---

Return a JSON array of improved bullet points. No explanation, just JSON.
Example:
[
"Led migration to microservices, reducing server load by 40%",
"Improved test coverage from 45% to 90% across core modules",
"Decreased page load time by 2.3s through caching and DB optimizations"
]

Purpose:

  • Impactful Bullet Points: Turns vague bullet points into clear, results-driven statements.
  • Action-Oriented: Encourages the use of strong action verbs that highlight the candidate’s role in achieving results.
  • Quantified: Incorporates specific metrics (percentages, time saved, etc.) to demonstrate measurable success.

3. openai_prompts/feedback_analysis.txt

This prompt evaluates both the resume and job description to provide constructive feedback. It highlights the strengths, areas that need improvement, and suggests missing points for better alignment with the job.

Contents of feedback_analysis.txt:

are an AI assistant reviewing a resume and job description for clarity and effectiveness.

RESUME:
---
{{resume_text}}
---

JOB DESCRIPTION:
---
{{job_description}}
---

Give actionable feedback to the candidate:
- What is strong in the resume
- What needs improvement
- What 1-2 major points are missing for this role

Output must be 3 short paragraphs titled:
1. Strengths
2. Areas to Improve
3. Suggestions

Keep it simple, constructive, and specific.

Purpose:

  • Strengths: Identifies the areas where the resume excels and matches the job description well.
  • Areas to Improve: Highlights aspects of the resume that need attention or additional information.
  • Suggestions: Provides targeted advice on what key skills or experiences the candidate might want to add or emphasize.

Expected AI Output Example (JSON)

When the AI processes a resume and a job description, the expected output should be structured as a JSON object containing the following fields:

  1. match_score: A numerical value (0-100) representing how well the resume matches the job description.
  2. missing_keywords: An array of keywords that are present in the job description but missing from the resume.
  3. improved_bullets: An array of rewritten resume bullet points, making them more impactful, action-oriented, and quantified.

Here’s an example of the expected output:

{
"match_score": 78,
"missing_keywords": ["REST API", "GraphQL", "CI/CD"],
"improved_bullets": [
"Developed and maintained 10+ RESTful APIs, reducing page load time by 35%",
"Led a team of 3 developers to deliver a CRM system in 6 weeks",
"Integrated CI/CD pipeline, improving deployment speed by 50%"
]
}

Conclusion

In today’s highly competitive job market, your resume is often the first impression you make on recruiters. Ensuring that your resume aligns with job requirements and highlights your most impactful achievements is crucial. With ResumeFixer AI, powered by OpenAI’s GPT-4o, developers can easily optimize their resumes, making them more likely to catch the attention of hiring managers and secure those all-important callbacks.

By simply uploading your resume and pasting the job description, you get instant, actionable feedback, a matching score, and powerful suggestions for improvement. With features like keyword gap analysis and rewritten bullet points, ResumeFixer AI helps you present yourself in the best possible light.

Whether you’re a mid-level developer looking for better job opportunities, or you’re just beginning to apply for more competitive positions, ResumeFixer AI can be a game-changer in how you approach your job applications.


Need Help or Want to Collaborate?

If you’d like help deploying this for your team, customizing it further, or integrating it into a larger system — reach out to us!

1- Email: info@muneebdev.com
2- Website: muneebdev.com/hire

We’d love to help you take your resume optimization process to the next level. Let’s work together to ensure your success in landing the perfect job!