Building an ATS Resume Scanner with FastAPI and Angular

Follow on LinkedIn

Introduction

In today’s competitive job market, Applicant Tracking Systems (ATS) play a crucial role in filtering resumes before they reach hiring managers. Many job seekers fail to optimize their resumes, resulting in low ATS scores and missed opportunities.

This project solves that problem by analyzing resumes against job descriptions and calculating an ATS score. The system extracts text from PDF resumes and job descriptions, identifies key skills and keywords, and determines how well a resume matches a given job posting. Additionally, it provides AI-generated feedback to improve the resume.

angular-fastapi-ats-resume-scanner
angular-fastapi-ats-resume-scanner

The project consists of two main components:

  1. Backend (FastAPI) – Handles file uploads, text extraction, NLP analysis, and ATS scoring.
  2. Frontend (Angular) – Allows users to upload resumes and job descriptions and displays ATS scores and recommendations.

FastAPI Backend: ATS Score Calculator

1. Setting Up FastAPI

The backend is built using FastAPI, a high-performance web framework for building APIs in Python.

from fastapi import FastAPI, UploadFile, File, Form
from fastapi.middleware.cors import CORSMiddleware
import os
import time
import fitz  # PyMuPDF for PDF extraction
import spacy
import requests
from typing import Dict, Optional

2. Configuring CORS and File Storage

We enable CORS (Cross-Origin Resource Sharing) to allow communication between our Angular frontend and FastAPI backend. Additionally, we create an uploads directory to store uploaded resumes and job descriptions.

app = FastAPI()

# Enable CORS for frontend (Angular)
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:4200"],  
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Folder to store uploaded files
UPLOAD_DIR = "uploads"
os.makedirs(UPLOAD_DIR, exist_ok=True)

3. File Upload Handling

We define an asynchronous function to save uploaded files with a timestamp-based filename.

async def save_uploaded_file(upload_file: UploadFile) -> str:
    timestamp = int(time.time())  
    filename = f"{timestamp}_{upload_file.filename}"
    file_path = os.path.join(UPLOAD_DIR, filename)

    with open(file_path, "wb") as f:
        f.write(await upload_file.read())  

    return file_path

4. Extracting Text from PDFs

We use PyMuPDF (fitz) to extract text from PDF resumes and job descriptions.

def extract_text_from_pdf(file_path: str) -> str:
    doc = fitz.open(file_path)  
    text = "\n".join([page.get_text("text") for page in doc])
    return text.strip()

5. NLP Processing and ATS Score Calculation

We load spaCy’s NLP model to extract relevant nouns and proper nouns from the resume and job description. The ATS score is calculated based on the keyword match percentage.

# Load spaCy NLP model
nlp = spacy.load("en_core_web_sm")

@app.post("/analyze_resume/")
async def analyze_resume(
    resume: UploadFile = File(...),
    job_description: Optional[UploadFile] = File(None),
    job_description_text: Optional[str] = Form(None)
) -> Dict:
    # Save and extract resume text
    resume_path = await save_uploaded_file(resume)
    resume_text = extract_text_from_pdf(resume_path)

    # Extract job description text
    if job_description:
        job_path = await save_uploaded_file(job_description)
        job_text = extract_text_from_pdf(job_path)
    elif job_description_text:
        job_text = job_description_text.strip()
    else:
        return {"error": "Please provide either a job description file or text."}

    # Extract keywords
    resume_keywords = [token.text for token in nlp(resume_text) if token.pos_ in ["NOUN", "PROPN"]]
    job_keywords = [token.text for token in nlp(job_text) if token.pos_ in ["NOUN", "PROPN"]]

    # Calculate ATS Score
    matched_keywords = set(resume_keywords) & set(job_keywords)
    ats_score = round((len(matched_keywords) / len(job_keywords) * 100) if job_keywords else 0, 2)

    return {
        "ATS Score": ats_score,
        "Matched Keywords": list(matched_keywords),
        "Saved Files": {"resume": resume_path, "job_description": job_text[:1000] + "..."}
    }

6. AI-Generated Feedback Using Hugging Face API

To help users improve their resumes, we use Falcon-7B, a large AI model from Hugging Face, to provide recommendations.

# Hugging Face API Key
HUGGING_FACE_API_KEY = "XXXXXXXXXXX"  

model_name = "tiiuae/falcon-7b-instruct"
prompt = f"Your resume has an ATS score of {ats_score}%. The matched keywords are {list(matched_keywords)}. How to improve the score?" 
headers = {"Authorization": f"Bearer {HUGGING_FACE_API_KEY}"}
response = requests.post(
    f"https://api-inference.huggingface.co/models/{model_name}",
    headers=headers,
    json={"inputs": prompt, "parameters": {"max_new_tokens": 1000, "temperature": 0.7}},
)

feedback = response.json()[0].get('generated_text', "No feedback available.") if response.status_code == 200 else "Error generating feedback."

Angular Frontend: User Interface for ATS Analysis

1. File Upload Component in Angular

The Angular component handles resume and job description uploads, text input, and API calls to FastAPI.

import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import axios from 'axios';

@Component({
  selector: 'app-upload',
  standalone: true,
  imports: [CommonModule, FormsModule],
  templateUrl: './upload.component.html',
  styleUrls: ['./upload.component.scss']
})
export class UploadComponent {
  resumeFile: File | null = null;
  jobFile: File | null = null;
  jobText: string = ''; // Job description text
  atsScore: number | null = null;
  matchedKeywords: string[] = [];
  feedback: string = '';
  loading: boolean = false;

  onResumeUpload(event: any) {
    if (event.target.files.length > 0) {
      this.resumeFile = event.target.files[0];
    }
  }

  onJobUpload(event: any) {
    if (event.target.files.length > 0) {
      this.jobFile = event.target.files[0];
      this.jobText = '';
    }
  }

  onJobTextChange() {
    if (this.jobText.trim().length > 0) {
      this.jobFile = null;
    }
  }

  async submitFiles() {
    if (!this.resumeFile) {
      alert("Please upload a Resume file.");
      return;
    }

    if ((this.jobFile && this.jobText.trim().length > 0) || (!this.jobFile && !this.jobText.trim().length)) {
      alert("Please upload a Job Description file OR enter the job description text, but not both.");
      return;
    }

    this.loading = true;
    const formData = new FormData();
    formData.append('resume', this.resumeFile);

    if (this.jobFile) {
      formData.append('job_description', this.jobFile);
    } else {
      formData.append('job_description_text', this.jobText);
    }

    try {
      const response = await axios.post('http://127.0.0.1:8000/analyze_resume/', formData, {
        headers: { 'Content-Type': 'multipart/form-data' },
        withCredentials: true
      });

      this.atsScore = response.data["ATS Score"];
      this.matchedKeywords = response.data["Matched Keywords"];
      this.feedback = response.data["AI Feedback"];
    } catch (error) {
      console.error('Error uploading:', error);
      alert("Failed to analyze the resume.");
    } finally {
      this.loading = false;
    }
  }
}

2. HTML Template for File Upload & Analysis Display

<div class="main-container">
    <div class="upload-section">
        <h1>ATS Score Calculator</h1><hr/>
        <h2>Upload Resume & Job Description</h2>

        <label for="resume">Upload Resume (PDF):</label>
        <input type="file" id="resume" (change)="onResumeUpload($event)" accept="application/pdf">
        <br/>
        <label for="job">Upload Job Description (PDF)</label>
        <input type="file" id="job" (change)="onJobUpload($event)" accept="application/pdf" [disabled]="jobText.trim().length > 0">
        <br/>
        <label for="job">Enter Job Description</label>
        <textarea id="jobText" [(ngModel)]="jobText" placeholder="Enter job description if no file" rows="40" (input)="onJobTextChange()" [disabled]="jobFile !== null"></textarea>

        <p *ngIf="jobFile && jobText.trim().length > 0" class="error-message">Please enter only one: a file OR text.</p>

        <button (click)="submitFiles()" [disabled]="loading">Analyze Resume</button>

        <div *ngIf="loading" class="loading">Processing...</div>
    </div>
    <div class="response-section" *ngIf="atsScore == null"><div class="hide-box">Whats your ATS Score?</div></div>
    <div class="response-section" *ngIf="atsScore !== null">
        <h3>ATS Score</h3>
        <p class="ats-score">{{ atsScore }}%</p>

        <h3>Matched Keywords</h3>
        <div class="keywords-container">
            <span *ngFor="let keyword of matchedKeywords" class="keyword">{{ keyword }}</span>
        </div>

        <h3>Feedback</h3>
        <div class="feedback-container" [innerHTML]="feedback"></div>
    </div>
</div>

Conclusion

This project demonstrates how FastAPI and Angular can be combined to create an ATS Resume Analyzer. It allows job seekers to analyze their resumes and get AI-driven feedback to improve their chances of passing ATS filters.

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

×