Build an AI Resume Builder

Build a full-stack AI resume builder with Node.js, React, and Gemini.

Introduction

30 Second Summary

Updating your resume for every job application is tedious. Tailoring bullet points, writing cover letters, and guessing which keywords an applicant tracking system wants to see takes hours you could spend actually preparing for interviews.

In this project, you will build a full-stack AI Resume Builder that generates professional resume content, cover letters, and ATS compatibility scores using Google Gemini. You will deploy the entire application for free.

What You'll Build

A web application with a Node.js backend and a React frontend that uses AI to generate tailored resumes, cover letters, and job-matching scores.

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

  • A REST API backend connected to MongoDB Atlas that stores user profiles and generates AI content with Google Gemini.
  • A React frontend with multiple professional resume templates, real-time preview, and PDF download functionality.
  • A live, deployed application on Render that costs zero dollars to run.
  • Secret Mission: A Job Description Analyzer that extracts key skills from a job posting and highlights which ones are missing from your resume.

Are there any prerequisites?

Basic familiarity with JavaScript, HTML, and CSS is helpful. No prior React or Node.js experience is required. This guide covers full setup for every tool.

Set Up Free Cloud Services

Your resume builder needs three cloud services to work: a database to store user profiles, an AI model to generate resume content, and a hosting platform to make it live on the internet.

The good news is that all three services offer generous free tiers. You will not need a credit card for any of them. In this step, you will create accounts on MongoDB Atlas, Google AI Studio, and Render, then grab the credentials your app needs to connect to each one.

In this step, get ready to:
  • Create a MongoDB Atlas account and provision a free M0 cluster.
  • Generate a free Gemini API key in Google AI Studio.
  • Create a Render account connected to GitHub.
Create a MongoDB Atlas account and free cluster

MongoDB Atlas is a cloud-hosted database service. Your app will store user profiles, resumes, and cover letters here.

  • Open your browser and go to the MongoDB Atlas registration page:
  • https://www.mongodb.com/cloud/atlas/register
  • Click Sign Up with Google (recommended, since you already have a Google account for AI Studio later) or register with your email address.
  • Accept the Terms of Service and Privacy Policy, then complete the signup.
  • When prompted to deploy your first cluster, select the M0 Free tier.
  • Choose any cloud provider (AWS, Google Cloud, or Azure) and a region close to you.
  • Click Create Deployment.
  • Atlas will ask you to create a database user. Enter a username and password you will remember. Write these down. You will need them for your connection string later.
  • For the network access step, select Allow Access from Anywhere (0.0.0.0/0). This lets your backend connect from any IP, which is necessary for Render's free tier.
  • Click Finish and Close.

What is a free M0 cluster?

The M0 cluster is MongoDB Atlas's free-forever tier. It gives you 512 MB of storage with shared RAM and vCPU. No credit card is required.

For a personal resume builder, 512 MB is more than enough to store hundreds of profiles and resumes.

Once your cluster finishes provisioning (usually under a minute), you will see it listed as active in your Atlas dashboard.

Save your connection string now

Click Connect on your cluster, then select Drivers. Copy the connection string that starts with mongodb+srv://. You will paste this into your backend's .env file in a later step.

Cluster not appearing?

If you do not see a cluster after signing up, check that you selected the M0 Free tier during setup. If you skipped the initial deployment flow, click Build a Database from the Atlas dashboard and select the free tier.

help me set up my MongoDB Atlas free cluster

Generate a free Gemini API key

Google Gemini is the AI model that will generate resume content, cover letters, and ATS scores. You access it through an API key from Google AI Studio.

  • Open Google AI Studio in your browser:
  • https://aistudio.google.com
  • Sign in with your Google account.
  • After accepting the Terms of Service, Google AI Studio creates a default project and API key for new users automatically.
  • Click Get API Key in the left sidebar to view your keys.
  • If you do not see a key listed, click Create API Key and select the default project.
  • Copy your API key and save it somewhere safe (a text file or password manager). You will need it in a later step.

What does the Gemini free tier include?

The Gemini API free tier provides free input and output tokens for the gemini-2.5-flash model. There are rate limits, but for a personal resume builder project they are more than sufficient.

One thing to note: content submitted on the free tier may be used by Google to improve their products.

Keep your API key private

Never share your API key publicly or commit it to GitHub. Treat it like a password. You will store it in a .env file that stays on your machine only.

Create API Key button greyed out?

This happens when your Google account does not have the right permissions on the project. The simplest fix is to create a new project that is not associated with an organization. Click Create API Key and select Create new project from the dropdown.

help me get my Gemini API key from Google AI Studio

Create a Render account

Render is the hosting platform where you will deploy both your backend API and your frontend. It connects directly to GitHub so your app deploys automatically when you push code.

  • Open Render's signup page in your browser:
  • https://dashboard.render.com/register
  • Click GitHub to sign up using your GitHub account.
  • Authorize Render to access your GitHub account when prompted.
  • Complete any remaining signup steps (email verification if requested).

Why connect Render to GitHub?

Connecting Render to GitHub enables automatic deployments. Every time you push code to your repository, Render detects the change and rebuilds your app. This means you deploy by simply running git push.

Render's free tier gives you 750 instance hours per month for web services. Your backend will spin down after 15 minutes of inactivity and wake up on the next request (taking about 30 seconds).

GitHub authorization not working?

Make sure you are signed into GitHub in the same browser before clicking the GitHub signup button on Render. If you belong to a GitHub organization with strict OAuth app policies, you may need to grant access manually from your GitHub settings under Authorized OAuth Apps.

help me connect my Render account to GitHub

You now have all three cloud services ready. Your MongoDB Atlas cluster stores data, your Gemini API key powers the AI features, and your Render account handles deployment. Next up, you will start building the backend API that ties them all together.

Build the Backend API Foundation

Your cloud services are ready. Now it's time to build the backend that powers the AI Resume Builder.

The backend is a Node.js server that handles three jobs: storing user profiles in MongoDB, calling Google Gemini for AI generation, and serving data to the React frontend through a REST API. In this step, you will set up the project, define your database schemas, and create the first set of API routes.

In this step, get ready to:
  • Initialize a Node.js project with Express and its dependencies.
  • Define MongoDB schemas for user profiles and resumes.
  • Create CRUD API routes for managing profiles.
Initialize the backend project

Every Node.js project starts with a folder and a package.json file that tracks your dependencies and scripts.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your backend project folder and move into it by running these commands:
mkdir ai-resume-builder-api
cd ai-resume-builder-api
  • Confirm the folder exists by running:
ls

You should see the ai-resume-builder-api folder listed in the output.

  • Open the project folder in VS Code by running:
code .

Code command not recognized?

If code . does not work, open VS Code manually. Press Cmd+Space (macOS) or the Windows key (Windows), type VS Code, and press Enter. Then click File > Open Folder and select your ai-resume-builder-api folder on the Desktop.

help me open my project folder in VS Code

  • Open the integrated terminal in VS Code by pressing Ctrl+` (backtick key, same on macOS and Windows).
  • Initialize a new Node.js project by running:
npm init -y

What does npm init -y do?

This command creates a package.json file with default values. The -y flag skips the interactive questionnaire and accepts all defaults.

The default package.json needs a few changes before you install packages. You need to enable ES modules and add custom scripts.

  • In VS Code, click on package.json in the Explorer sidebar to open it.
  • Replace the entire contents of package.json with the following:
{
  "name": "ai-resume-builder-api",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "start": "node server.js",
    "dev": "node --watch server.js"
  }
}

What does this configure?

The "type": "module" field enables ES module syntax (import/export instead of require).

The dev script uses Node's built-in --watch flag. This automatically restarts the server when you save changes to your files.

  • Save package.json.
  • Install the project dependencies by running:
npm install express mongoose cors dotenv @google/genai

You should see output confirming packages were added. Your package.json now includes a dependencies section with the five packages listed.

What are these packages?

  • express is the web framework that handles HTTP requests.
  • mongoose connects to MongoDB and provides schema validation.
  • cors allows your frontend (on a different port) to talk to the backend.
  • dotenv loads environment variables from a .env file.
  • @google/genai is the official Google Gemini SDK for AI generation.

npm install failed?

Make sure you are inside the ai-resume-builder-api folder. Run pwd to check your current directory. If your terminal shows a network error, check your internet connection and try again.

help me fix npm install failing in my project

Next, you need environment variables to store your MongoDB connection string and Gemini API key without hardcoding them.

  • Create a new file called .env.example in the root of your project by clicking the new file icon in the Explorer sidebar.
  • Paste the following into .env.example:
PORT=5000
MONGODB_URI=mongodb+srv://your-username:your-password@cluster0.xxxxx.mongodb.net/resume-builder?retryWrites=true&w=majority
GEMINI_API_KEY=your-gemini-api-key-here

Why an .env.example file?

This file shows other developers (and future you) which environment variables the project needs, without exposing real credentials. You will create a real .env file next with your actual values.

  • Save .env.example.
  • Create another new file called .env in the root of your project.
  • Paste the following into .env, replacing the placeholder values with your real MongoDB connection string and Gemini API key from the previous step:
PORT=5000
MONGODB_URI=your-mongodb-connection-string-here
GEMINI_API_KEY=your-gemini-api-key-here

Finding your MongoDB connection string

In MongoDB Atlas, click Connect on your cluster, then choose Drivers. Copy the connection string and replace <password> with your actual database user password. Make sure the database name at the end is resume-builder.

  • Save .env.
Define the MongoDB schemas

Schemas define the shape of your data in MongoDB. You need two schemas: one for user profiles (personal info, experience, skills) and one for generated resumes.

  • Create a new folder called models inside your project. Right-click in the Explorer sidebar and select New Folder.
  • Create a new file inside models called Profile.js and paste the following:
import mongoose from 'mongoose';

const profileSchema = new mongoose.Schema({
  fullName: { type: String, required: true },
  email: { type: String, required: true },
  phone: { type: String },
  location: { type: String },
  summary: { type: String },
  experience: [{
    title: String,
    company: String,
    startDate: String,
    endDate: String,
    description: String,
    bullets: [String]
  }],
  education: [{
    degree: String,
    institution: String,
    year: String,
    gpa: String
  }],
  skills: [String],
  links: {
    linkedin: String,
    github: String,
    portfolio: String
  }
}, { timestamps: true });

export default mongoose.model('Profile', profileSchema);

What does this schema define?

The Profile schema stores everything a user needs for their resume: name, contact info, work experience (as an array of jobs), education history, skills list, and social links.

The { timestamps: true } option automatically adds createdAt and updatedAt fields to every document.

  • Save models/Profile.js.
  • Create another file inside models called Resume.js and paste the following:
import mongoose from 'mongoose';

const resumeSchema = new mongoose.Schema({
  profileId: { type: mongoose.Schema.Types.ObjectId, ref: 'Profile', required: true },
  template: { type: String, default: 'professional' },
  generatedSummary: { type: String },
  generatedBullets: { type: Map, of: [String] },
  targetRole: { type: String },
  atsScore: { type: Number },
  atsFeedback: { type: String }
}, { timestamps: true });

export default mongoose.model('Resume', resumeSchema);

What does the Resume schema store?

The Resume schema links to a Profile through profileId and stores AI-generated content: the professional summary, improved bullet points for each job, the selected template, and the ATS compatibility score.

  • Save models/Resume.js.
Create the profile API routes and wire up the server

Routes define the URLs your frontend will call to create, read, and update profiles. You also need the main server file that ties everything together.

  • Create a new folder called routes in the project root.
  • Create a file inside routes called profiles.js and paste the following:
import express from 'express';
import Profile from '../models/Profile.js';

const router = express.Router();

router.post('/', async (req, res) => {
  try {
    const profile = new Profile(req.body);
    const saved = await profile.save();
    res.status(201).json(saved);
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

router.get('/:id', async (req, res) => {
  try {
    const profile = await Profile.findById(req.params.id);
    if (!profile) return res.status(404).json({ error: 'Profile not found' });
    res.json(profile);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

router.put('/:id', async (req, res) => {
  try {
    const profile = await Profile.findByIdAndUpdate(req.params.id, req.body, { new: true });
    if (!profile) return res.status(404).json({ error: 'Profile not found' });
    res.json(profile);
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

router.get('/', async (req, res) => {
  try {
    const profiles = await Profile.find().sort({ updatedAt: -1 });
    res.json(profiles);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

export default router;

What do these routes do?

  • POST / creates a new profile and saves it to MongoDB.
  • GET /:id retrieves a single profile by its database ID.
  • PUT /:id updates an existing profile with new data.
  • GET / lists all profiles, sorted by most recently updated.
  • Save routes/profiles.js.

The server also imports route files for AI generation and ATS scoring. You will build those in the next steps, but the server needs placeholder files so it can start without errors.

  • Create a file called generate.js inside routes and paste:
import express from 'express';

const router = express.Router();

export default router;
  • Create a file called ats.js inside routes and paste:
import express from 'express';

const router = express.Router();

export default router;
  • Save both files.

Now create the main server file that connects everything: Express, MongoDB, and your routes.

  • Create a file called server.js in the project root and paste:
import express from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import mongoose from 'mongoose';
import profileRoutes from './routes/profiles.js';
import generateRoutes from './routes/generate.js';
import atsRoutes from './routes/ats.js';

dotenv.config();

const app = express();
const PORT = process.env.PORT || 5000;

app.use(cors());
app.use(express.json());

mongoose.connect(process.env.MONGODB_URI)
  .then(() => console.log('Connected to MongoDB'))
  .catch((err) => console.error('MongoDB connection error:', err));

app.use('/api/profiles', profileRoutes);
app.use('/api/generate', generateRoutes);
app.use('/api/ats', atsRoutes);

app.get('/', (req, res) => {
  res.json({ message: 'AI Resume Builder API is running' });
});

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

What does server.js do?

  • Loads environment variables from .env so your credentials stay out of source code.
  • Enables CORS so the React frontend (running on a different port) can make requests.
  • Connects to your MongoDB Atlas cluster using the connection string.
  • Mounts route handlers at /api/profiles, /api/generate, and /api/ats.
  • Starts the server on port 5000.
  • Save server.js.
  • Start your server by running:
npm run dev

You should see two lines in the terminal:

Server running on port 5000
Connected to MongoDB
  • Open your browser and navigate to http://localhost:5000.

You should see a JSON response: {"message":"AI Resume Builder API is running"}.

Not seeing Connected to MongoDB?

Double-check your .env file. Make sure MONGODB_URI contains your full connection string with the correct password and that the database name is resume-builder.

In MongoDB Atlas, make sure you added 0.0.0.0/0 to your IP Access List under Network Access so your local machine can connect.

help me fix my MongoDB connection error

✔️ Awesome, I've got everything!

Great. Make sure you have saved all files before moving on.

ⓧ I'd like to double check the full code

Here is the complete code for each file at this point:

{
  "name": "ai-resume-builder-api",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "start": "node server.js",
    "dev": "node --watch server.js"
  },
  "dependencies": {
    "@google/genai": "^2.0.1",
    "cors": "^2.8.5",
    "dotenv": "^16.4.7",
    "express": "^4.21.2",
    "mongoose": "^8.9.5"
  }
}
PORT=5000
MONGODB_URI=mongodb+srv://your-username:your-password@cluster0.xxxxx.mongodb.net/resume-builder?retryWrites=true&w=majority
GEMINI_API_KEY=your-gemini-api-key-here
import express from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import mongoose from 'mongoose';
import profileRoutes from './routes/profiles.js';
import generateRoutes from './routes/generate.js';
import atsRoutes from './routes/ats.js';

dotenv.config();

const app = express();
const PORT = process.env.PORT || 5000;

app.use(cors());
app.use(express.json());

mongoose.connect(process.env.MONGODB_URI)
  .then(() => console.log('Connected to MongoDB'))
  .catch((err) => console.error('MongoDB connection error:', err));

app.use('/api/profiles', profileRoutes);
app.use('/api/generate', generateRoutes);
app.use('/api/ats', atsRoutes);

app.get('/', (req, res) => {
  res.json({ message: 'AI Resume Builder API is running' });
});

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});
import mongoose from 'mongoose';

const profileSchema = new mongoose.Schema({
  fullName: { type: String, required: true },
  email: { type: String, required: true },
  phone: { type: String },
  location: { type: String },
  summary: { type: String },
  experience: [{
    title: String,
    company: String,
    startDate: String,
    endDate: String,
    description: String,
    bullets: [String]
  }],
  education: [{
    degree: String,
    institution: String,
    year: String,
    gpa: String
  }],
  skills: [String],
  links: {
    linkedin: String,
    github: String,
    portfolio: String
  }
}, { timestamps: true });

export default mongoose.model('Profile', profileSchema);
import mongoose from 'mongoose';

const resumeSchema = new mongoose.Schema({
  profileId: { type: mongoose.Schema.Types.ObjectId, ref: 'Profile', required: true },
  template: { type: String, default: 'professional' },
  generatedSummary: { type: String },
  generatedBullets: { type: Map, of: [String] },
  targetRole: { type: String },
  atsScore: { type: Number },
  atsFeedback: { type: String }
}, { timestamps: true });

export default mongoose.model('Resume', resumeSchema);
import express from 'express';
import Profile from '../models/Profile.js';

const router = express.Router();

router.post('/', async (req, res) => {
  try {
    const profile = new Profile(req.body);
    const saved = await profile.save();
    res.status(201).json(saved);
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

router.get('/:id', async (req, res) => {
  try {
    const profile = await Profile.findById(req.params.id);
    if (!profile) return res.status(404).json({ error: 'Profile not found' });
    res.json(profile);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

router.put('/:id', async (req, res) => {
  try {
    const profile = await Profile.findByIdAndUpdate(req.params.id, req.body, { new: true });
    if (!profile) return res.status(404).json({ error: 'Profile not found' });
    res.json(profile);
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

router.get('/', async (req, res) => {
  try {
    const profiles = await Profile.find().sort({ updatedAt: -1 });
    res.json(profiles);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

export default router;

Your backend foundation is solid. You have a running Express server connected to MongoDB with working CRUD routes for profiles. Next up, you will add the AI-powered features that make this resume builder intelligent.

Integrate Google Gemini for AI Features

Your backend can store and retrieve profile data. Now it needs the ability to transform that data into polished, professional content.

In this step, you will connect your API to Google Gemini so it can generate tailored resume summaries, experience bullet points, and cover letters on demand.

In this step, get ready to:
  • Configure the Google Gemini AI client using the @google/genai SDK.
  • Build a resume content generation endpoint.
  • Build a cover letter generation endpoint.
Create the generate routes file and configure the Gemini client

The @google/genai package is already listed in your package.json dependencies from the earlier setup. Your server.js also already imports and mounts a generate routes file at /api/generate. You just need to create that file.

  • Create a new file called routes/generate.js inside your ai-resume-builder-api folder and add the following initial setup code:
import express from 'express';
import { GoogleGenAI } from '@google/genai';

// Set up the router and Gemini AI client
const router = express.Router();
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });

export default router;

What does this code do?

This file imports the GoogleGenAI class from the official Google Gen AI SDK. It then creates an ai client instance using your GEMINI_API_KEY environment variable.

All AI generation calls go through this ai object. Specifically, you will use ai.models.generateContent() to send prompts to the gemini-2.5-flash model and receive text responses.

  • Save routes/generate.js.
  • Confirm the file exists by running:
ls routes/

You should see generate.js and profiles.js listed in the output.

Don't see generate.js?

Make sure you created the file inside the routes/ folder, not in the project root. The path should be routes/generate.js.

help me find where I saved my generate.js file

Build the resume generation endpoint

This endpoint receives a user's profile data and a target job title, then asks Gemini to write a professional summary and improved experience bullet points tailored for that role.

  • In routes/generate.js, find the line export default router; and add the following code above it:
router.post('/resume', async (req, res) => {
  try {
    const { profile, targetRole } = req.body;

    const prompt = `You are a professional resume writer. Based on the following profile, generate a compelling professional summary (2-3 sentences) and improved bullet points for each work experience entry, tailored for the role of "${targetRole}".

Profile:
Name: ${profile.fullName}
Skills: ${profile.skills?.join(', ')}
Experience: ${JSON.stringify(profile.experience)}

Respond in JSON format:
{
  "summary": "professional summary here",
  "experienceBullets": {
    "0": ["bullet 1", "bullet 2", "bullet 3"],
    "1": ["bullet 1", "bullet 2", "bullet 3"]
  }
}

Only return valid JSON, no markdown fences.`;

    const response = await ai.models.generateContent({
      model: 'gemini-2.5-flash',
      contents: prompt,
      config: { temperature: 0.7 }
    });

    const text = response.text;
    const parsed = JSON.parse(text);
    res.json(parsed);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

What does this code do?

  • The endpoint extracts profile and targetRole from the request body.
  • It builds a detailed prompt that includes the user's name, skills, and experience. The prompt instructs Gemini to respond in strict JSON format.
  • The temperature: 0.7 setting gives Gemini some creative freedom while keeping output professional and coherent.
  • The response text is parsed from JSON into an object and sent back to the client.

What is prompt engineering?

The technique of crafting specific instructions for an AI model is called prompt engineering. By telling Gemini exactly what format to return (JSON with specific keys), you get structured, predictable responses your frontend can parse and display.

  • Save routes/generate.js.
  • You won't be able to test this endpoint just yet because the server also needs the ATS route file (created in the next step). You will test everything together shortly.

Seeing a syntax error when you save?

Template literals (the backtick strings) are sensitive to unclosed quotes. Make sure the prompt string starts and ends with a backtick (`) and that all ${...} expressions inside it have matching braces.

help me fix a syntax error in my generate.js prompt template literal

Build the cover letter generation endpoint

This endpoint takes the user's profile, a job description, and a company name, then generates a tailored cover letter using Gemini.

  • In routes/generate.js, find the line export default router; and add the following code above it (after the resume route you just added):
router.post('/cover-letter', async (req, res) => {
  try {
    const { profile, jobDescription, companyName } = req.body;

    const prompt = `You are a professional cover letter writer. Write a compelling cover letter for the following candidate applying to ${companyName}.

Candidate Profile:
Name: ${profile.fullName}
Skills: ${profile.skills?.join(', ')}
Experience: ${JSON.stringify(profile.experience?.slice(0, 2))}

Job Description:
${jobDescription}

Write a professional cover letter (3-4 paragraphs). Be specific about how the candidate's experience matches the role. Do not use generic filler.`;

    const response = await ai.models.generateContent({
      model: 'gemini-2.5-flash',
      contents: prompt,
      config: { temperature: 0.7 }
    });

    res.json({ coverLetter: response.text });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

What does this code do?

  • The endpoint receives profile, jobDescription, and companyName from the request body.
  • It only includes the first two experience entries (.slice(0, 2)) to keep the prompt focused and within token limits.
  • Unlike the resume endpoint, this response is plain text (not JSON), so it sends the raw response.text back wrapped in a coverLetter key.
  • Save routes/generate.js.

Worried about Gemini API costs?

The gemini-2.5-flash model on the free tier provides free input and output tokens with no credit card required. For a personal project like this, you will stay well within the free tier limits.

help me understand Gemini API free tier limits

✔️ Awesome, I've got everything!

Great. Double check you have saved routes/generate.js with both route handlers.

ⓧ I'd like to double check the full code

Here is the complete routes/generate.js file:

import express from 'express';
import { GoogleGenAI } from '@google/genai';

const router = express.Router();
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });

router.post('/resume', async (req, res) => {
  try {
    const { profile, targetRole } = req.body;

    const prompt = `You are a professional resume writer. Based on the following profile, generate a compelling professional summary (2-3 sentences) and improved bullet points for each work experience entry, tailored for the role of "${targetRole}".

Profile:
Name: ${profile.fullName}
Skills: ${profile.skills?.join(', ')}
Experience: ${JSON.stringify(profile.experience)}

Respond in JSON format:
{
  "summary": "professional summary here",
  "experienceBullets": {
    "0": ["bullet 1", "bullet 2", "bullet 3"],
    "1": ["bullet 1", "bullet 2", "bullet 3"]
  }
}

Only return valid JSON, no markdown fences.`;

    const response = await ai.models.generateContent({
      model: 'gemini-2.5-flash',
      contents: prompt,
      config: { temperature: 0.7 }
    });

    const text = response.text;
    const parsed = JSON.parse(text);
    res.json(parsed);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

router.post('/cover-letter', async (req, res) => {
  try {
    const { profile, jobDescription, companyName } = req.body;

    const prompt = `You are a professional cover letter writer. Write a compelling cover letter for the following candidate applying to ${companyName}.

Candidate Profile:
Name: ${profile.fullName}
Skills: ${profile.skills?.join(', ')}
Experience: ${JSON.stringify(profile.experience?.slice(0, 2))}

Job Description:
${jobDescription}

Write a professional cover letter (3-4 paragraphs). Be specific about how the candidate's experience matches the role. Do not use generic filler.`;

    const response = await ai.models.generateContent({
      model: 'gemini-2.5-flash',
      contents: prompt,
      config: { temperature: 0.7 }
    });

    res.json({ coverLetter: response.text });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

export default router;

Your AI generation routes are ready. Next up, you will build the ATS score checker that analyzes resume content against job descriptions and returns a compatibility score.

Build the ATS Score Checker

Your backend can now generate polished resume content and cover letters on demand. The next piece of the puzzle is helping users understand how well their resume matches a specific job posting.

Most companies use Applicant Tracking Systems to filter resumes before a human ever reads them. In this step, you will build an endpoint that uses Gemini to score a resume against a job description, returning matched keywords, missing keywords, and actionable suggestions.

In this step, get ready to:
  • Create the ATS scoring endpoint that analyzes resume content against a job description.
  • Return a structured score (0-100) with matched keywords, missing keywords, and improvement suggestions.
Create the ATS scoring route

What is an ATS?

An ATS (Applicant Tracking System) is software that scans resumes for relevant keywords before a recruiter ever sees them. If your resume doesn't match enough keywords from the job description, it gets filtered out automatically.

Your endpoint will use Gemini to play the role of an ATS expert, comparing a resume's content against a job posting and identifying gaps.

Your server.js already imports and mounts atsRoutes at /api/ats, but the file does not exist yet. Creating it will complete the connection.

  • Create a new file called routes/ats.js inside your ai-resume-builder-api folder, then paste in the following code:
import express from 'express';
import { GoogleGenAI } from '@google/genai';

const router = express.Router();
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });

router.post('/score', async (req, res) => {
  try {
    const { resumeContent, jobDescription } = req.body;

    const prompt = `You are an ATS (Applicant Tracking System) expert. Analyze the following resume content against the job description and provide a compatibility score.

Resume Content:
${resumeContent}

Job Description:
${jobDescription}

Respond in JSON format:
{
  "score": 75,
  "matchedKeywords": ["keyword1", "keyword2"],
  "missingKeywords": ["keyword3", "keyword4"],
  "suggestions": ["suggestion 1", "suggestion 2", "suggestion 3"]
}

Score should be 0-100. Only return valid JSON, no markdown fences.`;

    const response = await ai.models.generateContent({
      model: 'gemini-2.5-flash',
      contents: prompt,
      config: { temperature: 0.3 }
    });

    const text = response.text;
    const parsed = JSON.parse(text);
    res.json(parsed);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

export default router;

What does this code do?

  • The endpoint accepts two pieces of text: resumeContent (the user's resume as a string) and jobDescription (the target job posting).
  • The prompt instructs Gemini to act as an ATS expert and return a structured JSON response with a score, matched keywords, missing keywords, and suggestions.
  • The temperature: 0.3 keeps responses more consistent and analytical compared to the 0.7 used for creative writing tasks like resume generation.
  • The response is parsed from JSON and sent directly back to the client, giving the frontend structured data it can display visually.
  • Save routes/ats.js.
  • Switch back to your terminal running the dev server. You should see it restart automatically and print Server running on port 5000 without any errors.

If your server was showing an error about the missing routes/ats.js file, that error should now be resolved.

Server still showing errors?

  • Make sure the file is named exactly ats.js and lives inside the routes folder. The import in server.js expects ./routes/ats.js.
  • Check that you have export default router; at the bottom of the file. A missing export causes the import in server.js to fail.
  • Confirm your backtick template literal is closed properly. An unclosed backtick after the prompt string causes a syntax error.

Why is my ATS route file not loading?

  • Test the endpoint by running this command in a new terminal window:
curl -X POST http://localhost:5000/api/ats/score \
  -H "Content-Type: application/json" \
  -d '{"resumeContent": "JavaScript, React, Node.js developer with 3 years experience building web applications", "jobDescription": "Looking for a senior React developer with TypeScript, GraphQL, and 5 years experience"}'

You should see a JSON response with a score, matchedKeywords, missingKeywords, and suggestions arrays. The score will likely be below 100 because the sample resume is missing TypeScript, GraphQL, and the required years of experience.

Why use a lower temperature for scoring?

Temperature controls how creative or deterministic the AI's output is. A lower value like 0.3 keeps responses consistent and factual, which is what you want when analyzing keywords and assigning scores.

The resume and cover letter endpoints use 0.7 because creative writing benefits from more variation. Scoring and analysis need precision.

✔️ Awesome, I've got everything!

Great. Double check you have saved routes/ats.js and your server is running without errors.

ⓧ I'd like to double check the full code

Here is the complete routes/ats.js file:

import express from 'express';
import { GoogleGenAI } from '@google/genai';

const router = express.Router();
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });

router.post('/score', async (req, res) => {
  try {
    const { resumeContent, jobDescription } = req.body;

    const prompt = `You are an ATS (Applicant Tracking System) expert. Analyze the following resume content against the job description and provide a compatibility score.

Resume Content:
${resumeContent}

Job Description:
${jobDescription}

Respond in JSON format:
{
  "score": 75,
  "matchedKeywords": ["keyword1", "keyword2"],
  "missingKeywords": ["keyword3", "keyword4"],
  "suggestions": ["suggestion 1", "suggestion 2", "suggestion 3"]
}

Score should be 0-100. Only return valid JSON, no markdown fences.`;

    const response = await ai.models.generateContent({
      model: 'gemini-2.5-flash',
      contents: prompt,
      config: { temperature: 0.3 }
    });

    const text = response.text;
    const parsed = JSON.parse(text);
    res.json(parsed);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

export default router;

Your backend is now complete. It can store profiles, generate resume content, write cover letters, and score resumes against job descriptions. Next up, you will build the React frontend that ties all of these features together into a polished user interface.

Build the React Frontend

Your backend API is up and running with all three route groups ready to generate resumes, cover letters, and ATS scores. Now you need a frontend that users can actually interact with.

In this step, you will scaffold a React application using Vite, set up page routing, and build the first interactive component. By the end, you will have a working multi-page frontend that connects to your backend API.

In this step, get ready to:
  • Scaffold a React app with Vite and install dependencies.
  • Create the main layout with React Router navigation.
  • Build the Profile form component for user data entry.
Scaffold the React app using Vite

Vite is a modern build tool that gives you instant hot-reloading during development and optimized production builds. You will use it to scaffold a brand new React project alongside your backend.

  • Open a new terminal window (keep your backend terminal running separately).
  • Navigate to your Desktop by running this command:
cd ~/Desktop
  • Scaffold the React frontend project by running this command:
npm create vite@latest ai-resume-builder-frontend -- --template react

What does this command do?

The npm create vite@latest command downloads and runs the Vite project scaffolder. The -- --template react flag tells it to use the React JavaScript template, skipping interactive prompts.

  • Move into the new project folder and install the base dependencies by running these commands:
cd ai-resume-builder-frontend
npm install
  • Install the additional packages your project needs by running this command:
npm install react-router-dom @react-pdf/renderer

What are these packages?

The react-router-dom package handles client-side page navigation without full page reloads. The @react-pdf/renderer package lets you create downloadable PDF files using React components.

  • Confirm the packages installed by running:
ls node_modules | grep react-router

You should see react-router and react-router-dom listed in the output.

Install failed or command not found?

Make sure you are inside the ai-resume-builder-frontend folder before running npm install. If you see permission errors, try closing and reopening your terminal.

Help me troubleshoot my npm install error in the ai-resume-builder-frontend project.

  • Clean up the default Vite files you will not need by running:
rm src/App.css src/assets/react.svg
Create the main layout with React Router

Your app needs multiple pages: a Dashboard, Profile editor, Resume editor, Cover Letter generator, and ATS checker. React Router lets you define these pages as components and switch between them without reloading the browser.

  • Open the ai-resume-builder-frontend folder in VS Code.
  • Replace the contents of src/main.jsx with the following code:
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import './index.css';

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>
);

What does this code do?

This wraps your entire app in a BrowserRouter, which enables client-side navigation. Every Link and Route component inside the app can now switch pages without a full browser reload.

  • Replace the contents of src/App.jsx with the following code:
import { Routes, Route, Link } from 'react-router-dom';
import Dashboard from './pages/Dashboard';
import ProfilePage from './pages/ProfilePage';
import ResumeEditor from './pages/ResumeEditor';
import CoverLetterPage from './pages/CoverLetterPage';
import ATSChecker from './pages/ATSChecker';

function App() {
  return (
    <div className="app">
      <nav className="navbar">
        <h1 className="logo">AI Resume Builder</h1>
        <div className="nav-links">
          <Link to="/">Dashboard</Link>
          <Link to="/profile">Profile</Link>
          <Link to="/editor">Resume</Link>
          <Link to="/cover-letter">Cover Letter</Link>
          <Link to="/ats">ATS Score</Link>
        </div>
      </nav>
      <main className="main-content">
        <Routes>
          <Route path="/" element={<Dashboard />} />
          <Route path="/profile" element={<ProfilePage />} />
          <Route path="/editor" element={<ResumeEditor />} />
          <Route path="/cover-letter" element={<CoverLetterPage />} />
          <Route path="/ats" element={<ATSChecker />} />
        </Routes>
      </main>
    </div>
  );
}

export default App;

What does this code do?

The App component renders a navigation bar at the top and a content area below. Each Route maps a URL path to a page component, so clicking a nav link swaps the page content instantly.

  • Create the pages directory and placeholder page files by running these commands:
mkdir src/pages
  • Create src/pages/Dashboard.jsx with the following code:
import { Link } from 'react-router-dom';

function Dashboard() {
  return (
    <div>
      <h1 className="page-title">Welcome to AI Resume Builder</h1>
      <div className="template-grid">
        <div className="card">
          <h3>1. Create Your Profile</h3>
          <p>Add your experience, education, and skills.</p>
          <Link to="/profile"><button className="btn btn-primary" style={{ marginTop: '1rem' }}>Go to Profile</button></Link>
        </div>
        <div className="card">
          <h3>2. Generate Resume</h3>
          <p>Let AI craft your resume with professional templates.</p>
          <Link to="/editor"><button className="btn btn-primary" style={{ marginTop: '1rem' }}>Resume Editor</button></Link>
        </div>
        <div className="card">
          <h3>3. Cover Letter</h3>
          <p>Generate a tailored cover letter for any job.</p>
          <Link to="/cover-letter"><button className="btn btn-primary" style={{ marginTop: '1rem' }}>Cover Letter</button></Link>
        </div>
        <div className="card">
          <h3>4. ATS Score Check</h3>
          <p>See how well your resume matches a job description.</p>
          <Link to="/ats"><button className="btn btn-primary" style={{ marginTop: '1rem' }}>Check Score</button></Link>
        </div>
      </div>
    </div>
  );
}

export default Dashboard;

What does this code do?

The Dashboard is the landing page users see first. It shows four cards representing the main features, each linking to its respective page.

  • Create placeholder files for the pages you will build in later steps. Create src/pages/ResumeEditor.jsx with:
// Placeholder - built in the next step
function ResumeEditor() {
  return <div><h1 className="page-title">Resume Editor</h1><p>Coming soon...</p></div>;
}

export default ResumeEditor;
  • Create src/pages/CoverLetterPage.jsx with:
// Placeholder - built in a later step
function CoverLetterPage() {
  return <div><h1 className="page-title">Cover Letter Generator</h1><p>Coming soon...</p></div>;
}

export default CoverLetterPage;
  • Create src/pages/ATSChecker.jsx with:
// Placeholder - built in a later step
function ATSChecker() {
  return <div><h1 className="page-title">ATS Score Checker</h1><p>Coming soon...</p></div>;
}

export default ATSChecker;
  • Now replace the contents of src/index.css with the project's full stylesheet:
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background-color: #f5f5f5;
  color: #333;
}

.app {
  min-height: 100vh;
}

.navbar {
  background: #1a1a2e;
  padding: 1rem 2rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.logo {
  color: #4ecca3;
  font-size: 1.5rem;
}

.nav-links {
  display: flex;
  gap: 1.5rem;
}

.nav-links a {
  color: #e0e0e0;
  text-decoration: none;
  font-weight: 500;
}

.nav-links a:hover {
  color: #4ecca3;
}

.main-content {
  max-width: 1200px;
  margin: 2rem auto;
  padding: 0 2rem;
}

.page-title {
  font-size: 2rem;
  margin-bottom: 1.5rem;
  color: #1a1a2e;
}

.card {
  background: white;
  border-radius: 12px;
  padding: 2rem;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  margin-bottom: 1.5rem;
}

.btn {
  padding: 0.75rem 1.5rem;
  border: none;
  border-radius: 8px;
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.2s;
}

.btn-primary {
  background: #4ecca3;
  color: #1a1a2e;
}

.btn-primary:hover {
  background: #3db890;
}

.btn-secondary {
  background: #e0e0e0;
  color: #333;
}

.form-group {
  margin-bottom: 1rem;
}

.form-group label {
  display: block;
  margin-bottom: 0.5rem;
  font-weight: 600;
  color: #555;
}

.form-group input,
.form-group textarea {
  width: 100%;
  padding: 0.75rem;
  border: 2px solid #e0e0e0;
  border-radius: 8px;
  font-size: 1rem;
  transition: border-color 0.2s;
}

.form-group input:focus,
.form-group textarea:focus {
  outline: none;
  border-color: #4ecca3;
}

.template-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 1.5rem;
}

.template-card {
  border: 3px solid #e0e0e0;
  border-radius: 12px;
  padding: 1.5rem;
  cursor: pointer;
  transition: border-color 0.2s;
}

.template-card.selected {
  border-color: #4ecca3;
}

.score-display {
  font-size: 3rem;
  font-weight: 700;
  text-align: center;
  padding: 2rem;
}

.score-high { color: #4ecca3; }
.score-medium { color: #f0a500; }
.score-low { color: #e74c3c; }

.keywords-list {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 1rem;
}

.keyword-tag {
  padding: 0.25rem 0.75rem;
  border-radius: 20px;
  font-size: 0.85rem;
  font-weight: 500;
}

.keyword-matched { background: #d4edda; color: #155724; }
.keyword-missing { background: #f8d7da; color: #721c24; }

.loading {
  text-align: center;
  padding: 2rem;
  color: #888;
}

What does this stylesheet cover?

This CSS defines the dark navbar, card layouts, form styling, buttons, and utility classes for the entire app. Every page component you build later will use these shared classes.

  • Verify that the index.html file in your project root has the title set to AI Resume Builder. Open index.html and find this line:
<title>Vite + React</title>
  • Replace it so the line now looks like this:
<title>AI Resume Builder</title>
  • Save all files.
  • Start the development server by running:
npm run dev
  • Open the URL shown in the terminal (typically http://localhost:5173) in your browser.

You should see a dark navigation bar at the top with links, and four feature cards on the Dashboard page.

Seeing a blank page or import errors?

Check that all five page files exist inside src/pages/ (Dashboard.jsx, ProfilePage.jsx, ResumeEditor.jsx, CoverLetterPage.jsx, ATSChecker.jsx). Missing any one of them causes an import error that blanks the page.

Also confirm you deleted src/App.css and that src/App.jsx does not import it.

Help me debug why my Vite React app shows a blank page or import errors.

Build the Profile form component

The Profile page is where users enter their personal information, work experience, education, and skills. This data feeds into the AI generation endpoints you built on the backend.

  • Create src/pages/ProfilePage.jsx with the following code:
import { useState } from 'react';

const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:5000';

function ProfilePage() {
  const [profile, setProfile] = useState({
    fullName: '',
    email: '',
    phone: '',
    location: '',
    summary: '',
    skills: [],
    experience: [{ title: '', company: '', startDate: '', endDate: '', description: '' }],
    education: [{ degree: '', institution: '', year: '' }]
  });
  const [skillInput, setSkillInput] = useState('');
  const [saved, setSaved] = useState(false);
  const [profileId, setProfileId] = useState(localStorage.getItem('profileId') || '');

  const handleChange = (e) => {
    setProfile({ ...profile, [e.target.name]: e.target.value });
  };

  const addSkill = () => {
    if (skillInput.trim()) {
      setProfile({ ...profile, skills: [...profile.skills, skillInput.trim()] });
      setSkillInput('');
    }
  };

  const removeSkill = (index) => {
    setProfile({ ...profile, skills: profile.skills.filter((_, i) => i !== index) });
  };

  const updateExperience = (index, field, value) => {
    const updated = [...profile.experience];
    updated[index] = { ...updated[index], [field]: value };
    setProfile({ ...profile, experience: updated });
  };

  const addExperience = () => {
    setProfile({ ...profile, experience: [...profile.experience, { title: '', company: '', startDate: '', endDate: '', description: '' }] });
  };

  const updateEducation = (index, field, value) => {
    const updated = [...profile.education];
    updated[index] = { ...updated[index], [field]: value };
    setProfile({ ...profile, education: updated });
  };

  // Save profile to the backend API
  const saveProfile = async () => {
    try {
      const method = profileId ? 'PUT' : 'POST';
      const url = profileId ? `${API_URL}/api/profiles/${profileId}` : `${API_URL}/api/profiles`;
      const res = await fetch(url, {
        method,
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(profile)
      });
      const data = await res.json();
      setProfileId(data._id);
      localStorage.setItem('profileId', data._id);
      setSaved(true);
      setTimeout(() => setSaved(false), 3000);
    } catch (error) {
      console.error('Error saving profile:', error);
    }
  };

  return (
    <div>
      <h1 className="page-title">Your Profile</h1>
      <div className="card">
        <h3>Personal Information</h3>
        <div className="form-group">
          <label>Full Name</label>
          <input name="fullName" value={profile.fullName} onChange={handleChange} placeholder="John Doe" />
        </div>
        <div className="form-group">
          <label>Email</label>
          <input name="email" value={profile.email} onChange={handleChange} placeholder="john@example.com" />
        </div>
        <div className="form-group">
          <label>Phone</label>
          <input name="phone" value={profile.phone} onChange={handleChange} placeholder="+1 234 567 8900" />
        </div>
        <div className="form-group">
          <label>Location</label>
          <input name="location" value={profile.location} onChange={handleChange} placeholder="New York, NY" />
        </div>
      </div>

      <div className="card">
        <h3>Skills</h3>
        <div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1rem' }}>
          <input value={skillInput} onChange={(e) => setSkillInput(e.target.value)} placeholder="Add a skill" onKeyDown={(e) => e.key === 'Enter' && addSkill()} />
          <button className="btn btn-primary" onClick={addSkill}>Add</button>
        </div>
        <div className="keywords-list">
          {profile.skills.map((skill, i) => (
            <span key={i} className="keyword-tag keyword-matched" onClick={() => removeSkill(i)} style={{ cursor: 'pointer' }}>{skill} ✕</span>
          ))}
        </div>
      </div>

      <div className="card">
        <h3>Experience</h3>
        {profile.experience.map((exp, i) => (
          <div key={i} style={{ marginBottom: '1.5rem', paddingBottom: '1.5rem', borderBottom: '1px solid #eee' }}>
            <div className="form-group">
              <label>Job Title</label>
              <input value={exp.title} onChange={(e) => updateExperience(i, 'title', e.target.value)} placeholder="Software Engineer" />
            </div>
            <div className="form-group">
              <label>Company</label>
              <input value={exp.company} onChange={(e) => updateExperience(i, 'company', e.target.value)} placeholder="Google" />
            </div>
            <div style={{ display: 'flex', gap: '1rem' }}>
              <div className="form-group" style={{ flex: 1 }}>
                <label>Start Date</label>
                <input value={exp.startDate} onChange={(e) => updateExperience(i, 'startDate', e.target.value)} placeholder="Jan 2022" />
              </div>
              <div className="form-group" style={{ flex: 1 }}>
                <label>End Date</label>
                <input value={exp.endDate} onChange={(e) => updateExperience(i, 'endDate', e.target.value)} placeholder="Present" />
              </div>
            </div>
            <div className="form-group">
              <label>Description</label>
              <textarea value={exp.description} onChange={(e) => updateExperience(i, 'description', e.target.value)} placeholder="Describe your responsibilities..." rows={3} />
            </div>
          </div>
        ))}
        <button className="btn btn-secondary" onClick={addExperience}>+ Add Experience</button>
      </div>

      <div className="card">
        <h3>Education</h3>
        {profile.education.map((edu, i) => (
          <div key={i} style={{ marginBottom: '1rem' }}>
            <div className="form-group">
              <label>Degree</label>
              <input value={edu.degree} onChange={(e) => updateEducation(i, 'degree', e.target.value)} placeholder="B.S. Computer Science" />
            </div>
            <div className="form-group">
              <label>Institution</label>
              <input value={edu.institution} onChange={(e) => updateEducation(i, 'institution', e.target.value)} placeholder="MIT" />
            </div>
            <div className="form-group">
              <label>Year</label>
              <input value={edu.year} onChange={(e) => updateEducation(i, 'year', e.target.value)} placeholder="2022" />
            </div>
          </div>
        ))}
      </div>

      <button className="btn btn-primary" onClick={saveProfile} style={{ fontSize: '1.1rem', padding: '1rem 2rem' }}>
        {profileId ? 'Update Profile' : 'Save Profile'}
      </button>
      {saved && <span style={{ marginLeft: '1rem', color: '#4ecca3', fontWeight: 600 }}>Profile saved!</span>}
    </div>
  );
}

export default ProfilePage;

What does this component do?

The Profile page manages a form with four sections: personal info, skills, experience, and education. Each section uses React state to track form values as the user types.

The saveProfile function sends the data to your backend API. It uses POST for new profiles and PUT for updates, storing the profile ID in localStorage so it persists between browser sessions.

  • Save src/pages/ProfilePage.jsx.
  • Return to your browser and click the Profile link in the navigation bar.

You should see a form with fields for Full Name, Email, Phone, and Location, followed by Skills, Experience, and Education sections.

Profile page not showing or showing errors?

Check your browser console (press F12 then click Console) for specific error messages. Common issues are missing imports or typos in the component name.

Make sure the filename is exactly ProfilePage.jsx (capital P, capital P) to match the import in App.jsx.

Help me fix React component errors on my Profile page.

✔️ Awesome, I've got everything!

Great. Make sure all files are saved before moving on.

ⓧ I'd like to double check the full code

Here are the key files for this step. Compare yours against these:

import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import './index.css';

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>
);
import { Routes, Route, Link } from 'react-router-dom';
import Dashboard from './pages/Dashboard';
import ProfilePage from './pages/ProfilePage';
import ResumeEditor from './pages/ResumeEditor';
import CoverLetterPage from './pages/CoverLetterPage';
import ATSChecker from './pages/ATSChecker';

function App() {
  return (
    <div className="app">
      <nav className="navbar">
        <h1 className="logo">AI Resume Builder</h1>
        <div className="nav-links">
          <Link to="/">Dashboard</Link>
          <Link to="/profile">Profile</Link>
          <Link to="/editor">Resume</Link>
          <Link to="/cover-letter">Cover Letter</Link>
          <Link to="/ats">ATS Score</Link>
        </div>
      </nav>
      <main className="main-content">
        <Routes>
          <Route path="/" element={<Dashboard />} />
          <Route path="/profile" element={<ProfilePage />} />
          <Route path="/editor" element={<ResumeEditor />} />
          <Route path="/cover-letter" element={<CoverLetterPage />} />
          <Route path="/ats" element={<ATSChecker />} />
        </Routes>
      </main>
    </div>
  );
}

export default App;
import { Link } from 'react-router-dom';

function Dashboard() {
  return (
    <div>
      <h1 className="page-title">Welcome to AI Resume Builder</h1>
      <div className="template-grid">
        <div className="card">
          <h3>1. Create Your Profile</h3>
          <p>Add your experience, education, and skills.</p>
          <Link to="/profile"><button className="btn btn-primary" style={{ marginTop: '1rem' }}>Go to Profile</button></Link>
        </div>
        <div className="card">
          <h3>2. Generate Resume</h3>
          <p>Let AI craft your resume with professional templates.</p>
          <Link to="/editor"><button className="btn btn-primary" style={{ marginTop: '1rem' }}>Resume Editor</button></Link>
        </div>
        <div className="card">
          <h3>3. Cover Letter</h3>
          <p>Generate a tailored cover letter for any job.</p>
          <Link to="/cover-letter"><button className="btn btn-primary" style={{ marginTop: '1rem' }}>Cover Letter</button></Link>
        </div>
        <div className="card">
          <h3>4. ATS Score Check</h3>
          <p>See how well your resume matches a job description.</p>
          <Link to="/ats"><button className="btn btn-primary" style={{ marginTop: '1rem' }}>Check Score</button></Link>
        </div>
      </div>
    </div>
  );
}

export default Dashboard;
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>AI Resume Builder</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()]
});

Your frontend is fully scaffolded with working navigation and a Profile form that talks to your backend. Next up, you will build professional resume templates and a live preview editor.

Build Resume Templates and the Profile Page

Your backend is complete and your frontend skeleton is running. Now it's time to build the pages where users actually interact with the app.

In this step, you'll create the Profile form page that collects user data and saves it to your backend API. You'll also build three professional resume template components that will render that data into polished, visually distinct layouts.

In this step, get ready to:
  • Build the Profile page with forms for personal info, skills, experience, and education.
  • Create three resume template components (Professional, Modern, Minimal).
  • Connect the Profile page to your backend API for saving and loading data.
Build the Profile form page

The Profile page is the entry point for users. It collects all the information needed to generate a resume: name, contact details, skills, work experience, and education.

  • In VS Code, create a new file at src/pages/ProfilePage.jsx inside your frontend folder.
  • Paste the following code into ProfilePage.jsx:
import { useState } from 'react';

const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:5000';

function ProfilePage() {
  const [profile, setProfile] = useState({
    fullName: '',
    email: '',
    phone: '',
    location: '',
    summary: '',
    skills: [],
    experience: [{ title: '', company: '', startDate: '', endDate: '', description: '' }],
    education: [{ degree: '', institution: '', year: '' }]
  });
  const [skillInput, setSkillInput] = useState('');
  const [saved, setSaved] = useState(false);
  const [profileId, setProfileId] = useState(localStorage.getItem('profileId') || '');

  const handleChange = (e) => {
    setProfile({ ...profile, [e.target.name]: e.target.value });
  };

  const addSkill = () => {
    if (skillInput.trim()) {
      setProfile({ ...profile, skills: [...profile.skills, skillInput.trim()] });
      setSkillInput('');
    }
  };

  const removeSkill = (index) => {
    setProfile({ ...profile, skills: profile.skills.filter((_, i) => i !== index) });
  };

  const updateExperience = (index, field, value) => {
    const updated = [...profile.experience];
    updated[index] = { ...updated[index], [field]: value };
    setProfile({ ...profile, experience: updated });
  };

  const addExperience = () => {
    setProfile({ ...profile, experience: [...profile.experience, { title: '', company: '', startDate: '', endDate: '', description: '' }] });
  };

  const updateEducation = (index, field, value) => {
    const updated = [...profile.education];
    updated[index] = { ...updated[index], [field]: value };
    setProfile({ ...profile, education: updated });
  };

  const saveProfile = async () => {
    try {
      const method = profileId ? 'PUT' : 'POST';
      const url = profileId ? `${API_URL}/api/profiles/${profileId}` : `${API_URL}/api/profiles`;
      const res = await fetch(url, {
        method,
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(profile)
      });
      const data = await res.json();
      setProfileId(data._id);
      localStorage.setItem('profileId', data._id);
      setSaved(true);
      setTimeout(() => setSaved(false), 3000);
    } catch (error) {
      console.error('Error saving profile:', error);
    }
  };

  return (
    <div>
      <h1 className="page-title">Your Profile</h1>
      <div className="card">
        <h3>Personal Information</h3>
        <div className="form-group">
          <label>Full Name</label>
          <input name="fullName" value={profile.fullName} onChange={handleChange} placeholder="John Doe" />
        </div>
        <div className="form-group">
          <label>Email</label>
          <input name="email" value={profile.email} onChange={handleChange} placeholder="john@example.com" />
        </div>
        <div className="form-group">
          <label>Phone</label>
          <input name="phone" value={profile.phone} onChange={handleChange} placeholder="+1 234 567 8900" />
        </div>
        <div className="form-group">
          <label>Location</label>
          <input name="location" value={profile.location} onChange={handleChange} placeholder="New York, NY" />
        </div>
      </div>

      <div className="card">
        <h3>Skills</h3>
        <div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1rem' }}>
          <input value={skillInput} onChange={(e) => setSkillInput(e.target.value)} placeholder="Add a skill" onKeyDown={(e) => e.key === 'Enter' && addSkill()} />
          <button className="btn btn-primary" onClick={addSkill}>Add</button>
        </div>
        <div className="keywords-list">
          {profile.skills.map((skill, i) => (
            <span key={i} className="keyword-tag keyword-matched" onClick={() => removeSkill(i)} style={{ cursor: 'pointer' }}>{skill} ✕</span>
          ))}
        </div>
      </div>

      <div className="card">
        <h3>Experience</h3>
        {profile.experience.map((exp, i) => (
          <div key={i} style={{ marginBottom: '1.5rem', paddingBottom: '1.5rem', borderBottom: '1px solid #eee' }}>
            <div className="form-group">
              <label>Job Title</label>
              <input value={exp.title} onChange={(e) => updateExperience(i, 'title', e.target.value)} placeholder="Software Engineer" />
            </div>
            <div className="form-group">
              <label>Company</label>
              <input value={exp.company} onChange={(e) => updateExperience(i, 'company', e.target.value)} placeholder="Google" />
            </div>
            <div style={{ display: 'flex', gap: '1rem' }}>
              <div className="form-group" style={{ flex: 1 }}>
                <label>Start Date</label>
                <input value={exp.startDate} onChange={(e) => updateExperience(i, 'startDate', e.target.value)} placeholder="Jan 2022" />
              </div>
              <div className="form-group" style={{ flex: 1 }}>
                <label>End Date</label>
                <input value={exp.endDate} onChange={(e) => updateExperience(i, 'endDate', e.target.value)} placeholder="Present" />
              </div>
            </div>
            <div className="form-group">
              <label>Description</label>
              <textarea value={exp.description} onChange={(e) => updateExperience(i, 'description', e.target.value)} placeholder="Describe your responsibilities..." rows={3} />
            </div>
          </div>
        ))}
        <button className="btn btn-secondary" onClick={addExperience}>+ Add Experience</button>
      </div>

      <div className="card">
        <h3>Education</h3>
        {profile.education.map((edu, i) => (
          <div key={i} style={{ marginBottom: '1rem' }}>
            <div className="form-group">
              <label>Degree</label>
              <input value={edu.degree} onChange={(e) => updateEducation(i, 'degree', e.target.value)} placeholder="B.S. Computer Science" />
            </div>
            <div className="form-group">
              <label>Institution</label>
              <input value={edu.institution} onChange={(e) => updateEducation(i, 'institution', e.target.value)} placeholder="MIT" />
            </div>
            <div className="form-group">
              <label>Year</label>
              <input value={edu.year} onChange={(e) => updateEducation(i, 'year', e.target.value)} placeholder="2022" />
            </div>
          </div>
        ))}
      </div>

      <button className="btn btn-primary" onClick={saveProfile} style={{ fontSize: '1.1rem', padding: '1rem 2rem' }}>
        {profileId ? 'Update Profile' : 'Save Profile'}
      </button>
      {saved && <span style={{ marginLeft: '1rem', color: '#4ecca3', fontWeight: 600 }}>Profile saved!</span>}
    </div>
  );
}

export default ProfilePage;

What does this component do?

The profile state object holds all the form data: personal info, skills array, experience entries, and education entries.

The saveProfile function uses POST for new profiles and PUT for updates. It stores the returned profile ID in localStorage so the app remembers which profile belongs to this user across sessions.

Each section (Skills, Experience, Education) uses helper functions like addSkill, updateExperience, and updateEducation that immutably update nested arrays in state.

  • Save ProfilePage.jsx.
  • In your browser, navigate to http://localhost:5173/profile. You should see a form with sections for Personal Information, Skills, Experience, and Education.

Don't see the profile form?

  • Make sure your Vite dev server is still running in the terminal. If it stopped, run npm run dev again from the frontend folder.
  • Check that the file is saved at exactly src/pages/ProfilePage.jsx (not src/ProfilePage.jsx).
  • Look at the terminal for red error messages. A missing comma or bracket in the JSX is the most common cause.

help me debug why my Profile page isn't showing up

✔️ Awesome, I've got everything!

Great. Make sure you've saved ProfilePage.jsx before moving on.

ⓧ I'd like to double check the full code

Here is the complete ProfilePage.jsx file:

import { useState } from 'react';

const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:5000';

function ProfilePage() {
  const [profile, setProfile] = useState({
    fullName: '',
    email: '',
    phone: '',
    location: '',
    summary: '',
    skills: [],
    experience: [{ title: '', company: '', startDate: '', endDate: '', description: '' }],
    education: [{ degree: '', institution: '', year: '' }]
  });
  const [skillInput, setSkillInput] = useState('');
  const [saved, setSaved] = useState(false);
  const [profileId, setProfileId] = useState(localStorage.getItem('profileId') || '');

  const handleChange = (e) => {
    setProfile({ ...profile, [e.target.name]: e.target.value });
  };

  const addSkill = () => {
    if (skillInput.trim()) {
      setProfile({ ...profile, skills: [...profile.skills, skillInput.trim()] });
      setSkillInput('');
    }
  };

  const removeSkill = (index) => {
    setProfile({ ...profile, skills: profile.skills.filter((_, i) => i !== index) });
  };

  const updateExperience = (index, field, value) => {
    const updated = [...profile.experience];
    updated[index] = { ...updated[index], [field]: value };
    setProfile({ ...profile, experience: updated });
  };

  const addExperience = () => {
    setProfile({ ...profile, experience: [...profile.experience, { title: '', company: '', startDate: '', endDate: '', description: '' }] });
  };

  const updateEducation = (index, field, value) => {
    const updated = [...profile.education];
    updated[index] = { ...updated[index], [field]: value };
    setProfile({ ...profile, education: updated });
  };

  const saveProfile = async () => {
    try {
      const method = profileId ? 'PUT' : 'POST';
      const url = profileId ? `${API_URL}/api/profiles/${profileId}` : `${API_URL}/api/profiles`;
      const res = await fetch(url, {
        method,
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(profile)
      });
      const data = await res.json();
      setProfileId(data._id);
      localStorage.setItem('profileId', data._id);
      setSaved(true);
      setTimeout(() => setSaved(false), 3000);
    } catch (error) {
      console.error('Error saving profile:', error);
    }
  };

  return (
    <div>
      <h1 className="page-title">Your Profile</h1>
      <div className="card">
        <h3>Personal Information</h3>
        <div className="form-group">
          <label>Full Name</label>
          <input name="fullName" value={profile.fullName} onChange={handleChange} placeholder="John Doe" />
        </div>
        <div className="form-group">
          <label>Email</label>
          <input name="email" value={profile.email} onChange={handleChange} placeholder="john@example.com" />
        </div>
        <div className="form-group">
          <label>Phone</label>
          <input name="phone" value={profile.phone} onChange={handleChange} placeholder="+1 234 567 8900" />
        </div>
        <div className="form-group">
          <label>Location</label>
          <input name="location" value={profile.location} onChange={handleChange} placeholder="New York, NY" />
        </div>
      </div>

      <div className="card">
        <h3>Skills</h3>
        <div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1rem' }}>
          <input value={skillInput} onChange={(e) => setSkillInput(e.target.value)} placeholder="Add a skill" onKeyDown={(e) => e.key === 'Enter' && addSkill()} />
          <button className="btn btn-primary" onClick={addSkill}>Add</button>
        </div>
        <div className="keywords-list">
          {profile.skills.map((skill, i) => (
            <span key={i} className="keyword-tag keyword-matched" onClick={() => removeSkill(i)} style={{ cursor: 'pointer' }}>{skill} ✕</span>
          ))}
        </div>
      </div>

      <div className="card">
        <h3>Experience</h3>
        {profile.experience.map((exp, i) => (
          <div key={i} style={{ marginBottom: '1.5rem', paddingBottom: '1.5rem', borderBottom: '1px solid #eee' }}>
            <div className="form-group">
              <label>Job Title</label>
              <input value={exp.title} onChange={(e) => updateExperience(i, 'title', e.target.value)} placeholder="Software Engineer" />
            </div>
            <div className="form-group">
              <label>Company</label>
              <input value={exp.company} onChange={(e) => updateExperience(i, 'company', e.target.value)} placeholder="Google" />
            </div>
            <div style={{ display: 'flex', gap: '1rem' }}>
              <div className="form-group" style={{ flex: 1 }}>
                <label>Start Date</label>
                <input value={exp.startDate} onChange={(e) => updateExperience(i, 'startDate', e.target.value)} placeholder="Jan 2022" />
              </div>
              <div className="form-group" style={{ flex: 1 }}>
                <label>End Date</label>
                <input value={exp.endDate} onChange={(e) => updateExperience(i, 'endDate', e.target.value)} placeholder="Present" />
              </div>
            </div>
            <div className="form-group">
              <label>Description</label>
              <textarea value={exp.description} onChange={(e) => updateExperience(i, 'description', e.target.value)} placeholder="Describe your responsibilities..." rows={3} />
            </div>
          </div>
        ))}
        <button className="btn btn-secondary" onClick={addExperience}>+ Add Experience</button>
      </div>

      <div className="card">
        <h3>Education</h3>
        {profile.education.map((edu, i) => (
          <div key={i} style={{ marginBottom: '1rem' }}>
            <div className="form-group">
              <label>Degree</label>
              <input value={edu.degree} onChange={(e) => updateEducation(i, 'degree', e.target.value)} placeholder="B.S. Computer Science" />
            </div>
            <div className="form-group">
              <label>Institution</label>
              <input value={edu.institution} onChange={(e) => updateEducation(i, 'institution', e.target.value)} placeholder="MIT" />
            </div>
            <div className="form-group">
              <label>Year</label>
              <input value={edu.year} onChange={(e) => updateEducation(i, 'year', e.target.value)} placeholder="2022" />
            </div>
          </div>
        ))}
      </div>

      <button className="btn btn-primary" onClick={saveProfile} style={{ fontSize: '1.1rem', padding: '1rem 2rem' }}>
        {profileId ? 'Update Profile' : 'Save Profile'}
      </button>
      {saved && <span style={{ marginLeft: '1rem', color: '#4ecca3', fontWeight: 600 }}>Profile saved!</span>}
    </div>
  );
}

export default ProfilePage;
Create the Professional resume template

Resume templates are React components that receive profile data as props and render it in a specific visual style. The Resume Editor page (built in the next step) will import these templates and let users switch between them.

  • Create a new folder called templates inside frontend/src.
  • Inside src/templates, create a file called ProfessionalTemplate.jsx and paste the following code:
function ProfessionalTemplate({ profile, generatedContent }) {
  if (!profile) return null;

  return (
    <div style={{ fontFamily: 'Georgia, serif', padding: '2rem', background: 'white', border: '1px solid #ddd' }}>
      <div style={{ textAlign: 'center', borderBottom: '2px solid #333', paddingBottom: '1rem', marginBottom: '1.5rem' }}>
        <h1 style={{ fontSize: '1.8rem', margin: 0 }}>{profile.fullName}</h1>
        <p style={{ color: '#555', marginTop: '0.5rem' }}>
          {profile.email} | {profile.phone} | {profile.location}
        </p>
      </div>

      {(generatedContent?.summary || profile.summary) && (
        <div style={{ marginBottom: '1.5rem' }}>
          <h2 style={{ fontSize: '1.1rem', textTransform: 'uppercase', borderBottom: '1px solid #ccc', paddingBottom: '0.3rem' }}>Professional Summary</h2>
          <p style={{ marginTop: '0.5rem', lineHeight: 1.6 }}>{generatedContent?.summary || profile.summary}</p>
        </div>
      )}

      {profile.experience?.length > 0 && (
        <div style={{ marginBottom: '1.5rem' }}>
          <h2 style={{ fontSize: '1.1rem', textTransform: 'uppercase', borderBottom: '1px solid #ccc', paddingBottom: '0.3rem' }}>Experience</h2>
          {profile.experience.map((exp, i) => (
            <div key={i} style={{ marginTop: '1rem' }}>
              <div style={{ display: 'flex', justifyContent: 'space-between' }}>
                <strong>{exp.title}</strong>
                <span style={{ color: '#666' }}>{exp.startDate} - {exp.endDate}</span>
              </div>
              <p style={{ color: '#555', fontStyle: 'italic' }}>{exp.company}</p>
              <ul style={{ paddingLeft: '1.5rem', marginTop: '0.5rem' }}>
                {(generatedContent?.experienceBullets?.[String(i)] || [exp.description]).map((bullet, j) => (
                  <li key={j} style={{ marginBottom: '0.3rem', lineHeight: 1.5 }}>{bullet}</li>
                ))}
              </ul>
            </div>
          ))}
        </div>
      )}

      {profile.education?.length > 0 && (
        <div style={{ marginBottom: '1.5rem' }}>
          <h2 style={{ fontSize: '1.1rem', textTransform: 'uppercase', borderBottom: '1px solid #ccc', paddingBottom: '0.3rem' }}>Education</h2>
          {profile.education.map((edu, i) => (
            <div key={i} style={{ marginTop: '0.75rem' }}>
              <strong>{edu.degree}</strong> - {edu.institution} ({edu.year})
            </div>
          ))}
        </div>
      )}

      {profile.skills?.length > 0 && (
        <div>
          <h2 style={{ fontSize: '1.1rem', textTransform: 'uppercase', borderBottom: '1px solid #ccc', paddingBottom: '0.3rem' }}>Skills</h2>
          <p style={{ marginTop: '0.5rem' }}>{profile.skills.join(' • ')}</p>
        </div>
      )}
    </div>
  );
}

export default ProfessionalTemplate;

What does this template do?

Each template receives two props: profile (the user's saved data) and generatedContent (AI-generated text from Gemini). If AI content exists, it takes priority over the raw profile data.

The Professional template uses Georgia serif font, a centered header with a border, and uppercase section headings. It renders experience bullets from either the AI-generated content or the original description.

  • Save ProfessionalTemplate.jsx.
  • You won't see a change in the browser yet. The next step builds the Resume Editor page that imports and displays these templates.
Create the Modern and Minimal templates

The remaining two templates give users visual variety. The Modern template uses a bold dark header with green accents, while the Minimal template takes a lightweight, text-focused approach.

  • Create a new file at src/templates/ModernTemplate.jsx and paste the following code:
function ModernTemplate({ profile, generatedContent }) {
  if (!profile) return null;

  return (
    <div style={{ fontFamily: '-apple-system, sans-serif', padding: '2rem', background: 'white', border: '1px solid #ddd' }}>
      <div style={{ background: '#1a1a2e', color: 'white', padding: '1.5rem', borderRadius: '8px', marginBottom: '1.5rem' }}>
        <h1 style={{ fontSize: '1.8rem', margin: 0 }}>{profile.fullName}</h1>
        <p style={{ color: '#4ecca3', marginTop: '0.5rem', fontSize: '0.95rem' }}>
          {profile.email} | {profile.phone} | {profile.location}
        </p>
      </div>

      {(generatedContent?.summary || profile.summary) && (
        <div style={{ marginBottom: '1.5rem' }}>
          <h2 style={{ color: '#1a1a2e', fontSize: '1.1rem', marginBottom: '0.5rem' }}>About</h2>
          <p style={{ lineHeight: 1.7, color: '#444' }}>{generatedContent?.summary || profile.summary}</p>
        </div>
      )}

      {profile.experience?.length > 0 && (
        <div style={{ marginBottom: '1.5rem' }}>
          <h2 style={{ color: '#1a1a2e', fontSize: '1.1rem', marginBottom: '0.75rem' }}>Experience</h2>
          {profile.experience.map((exp, i) => (
            <div key={i} style={{ marginBottom: '1.25rem', paddingLeft: '1rem', borderLeft: '3px solid #4ecca3' }}>
              <strong style={{ fontSize: '1rem' }}>{exp.title}</strong>
              <p style={{ color: '#666', fontSize: '0.9rem' }}>{exp.company} | {exp.startDate} - {exp.endDate}</p>
              <ul style={{ paddingLeft: '1.25rem', marginTop: '0.5rem' }}>
                {(generatedContent?.experienceBullets?.[String(i)] || [exp.description]).map((bullet, j) => (
                  <li key={j} style={{ marginBottom: '0.3rem', lineHeight: 1.5, color: '#444' }}>{bullet}</li>
                ))}
              </ul>
            </div>
          ))}
        </div>
      )}

      {profile.skills?.length > 0 && (
        <div style={{ marginBottom: '1.5rem' }}>
          <h2 style={{ color: '#1a1a2e', fontSize: '1.1rem', marginBottom: '0.5rem' }}>Skills</h2>
          <div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem' }}>
            {profile.skills.map((skill, i) => (
              <span key={i} style={{ background: '#e8f5e9', color: '#1a1a2e', padding: '0.3rem 0.8rem', borderRadius: '20px', fontSize: '0.85rem' }}>{skill}</span>
            ))}
          </div>
        </div>
      )}

      {profile.education?.length > 0 && (
        <div>
          <h2 style={{ color: '#1a1a2e', fontSize: '1.1rem', marginBottom: '0.5rem' }}>Education</h2>
          {profile.education.map((edu, i) => (
            <div key={i} style={{ marginBottom: '0.5rem' }}>
              <strong>{edu.degree}</strong> - {edu.institution} ({edu.year})
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

export default ModernTemplate;

How does the Modern template differ?

The Modern template uses a dark header block (#1a1a2e) with green accent text (#4ecca3). Experience items have a green left border instead of a traditional heading. Skills render as rounded pill badges instead of a plain list.

  • Save ModernTemplate.jsx.
  • Create one more file at src/templates/MinimalTemplate.jsx and paste the following code:
function MinimalTemplate({ profile, generatedContent }) {
  if (!profile) return null;

  return (
    <div style={{ fontFamily: 'Helvetica, Arial, sans-serif', padding: '2rem', background: 'white', border: '1px solid #ddd', maxWidth: '700px' }}>
      <h1 style={{ fontSize: '2rem', fontWeight: 300, margin: 0 }}>{profile.fullName}</h1>
      <p style={{ color: '#888', marginTop: '0.25rem', fontSize: '0.9rem' }}>
        {profile.email} • {profile.phone} • {profile.location}
      </p>
      <hr style={{ border: 'none', borderTop: '1px solid #eee', margin: '1.5rem 0' }} />

      {(generatedContent?.summary || profile.summary) && (
        <>
          <p style={{ lineHeight: 1.8, color: '#555' }}>{generatedContent?.summary || profile.summary}</p>
          <hr style={{ border: 'none', borderTop: '1px solid #eee', margin: '1.5rem 0' }} />
        </>
      )}

      {profile.experience?.length > 0 && (
        <>
          {profile.experience.map((exp, i) => (
            <div key={i} style={{ marginBottom: '1.5rem' }}>
              <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
                <strong>{exp.title}</strong>
                <span style={{ color: '#999', fontSize: '0.85rem' }}>{exp.startDate} - {exp.endDate}</span>
              </div>
              <p style={{ color: '#666', fontSize: '0.9rem' }}>{exp.company}</p>
              <ul style={{ paddingLeft: '1.25rem', marginTop: '0.5rem', color: '#555' }}>
                {(generatedContent?.experienceBullets?.[String(i)] || [exp.description]).map((bullet, j) => (
                  <li key={j} style={{ marginBottom: '0.25rem', lineHeight: 1.6 }}>{bullet}</li>
                ))}
              </ul>
            </div>
          ))}
          <hr style={{ border: 'none', borderTop: '1px solid #eee', margin: '1.5rem 0' }} />
        </>
      )}

      {profile.education?.length > 0 && (
        <>
          {profile.education.map((edu, i) => (
            <p key={i} style={{ marginBottom: '0.5rem', color: '#555' }}>
              <strong>{edu.degree}</strong>, {edu.institution} ({edu.year})
            </p>
          ))}
          <hr style={{ border: 'none', borderTop: '1px solid #eee', margin: '1.5rem 0' }} />
        </>
      )}

      {profile.skills?.length > 0 && (
        <p style={{ color: '#555' }}>{profile.skills.join(' / ')}</p>
      )}
    </div>
  );
}

export default MinimalTemplate;

How does the Minimal template differ?

The Minimal template uses Helvetica with thin font weights, horizontal rule dividers between sections, and joins skills with / separators. It strips away visual decoration to let the content speak for itself.

  • Save MinimalTemplate.jsx.
  • You won't see these templates in the browser yet. The Resume Editor page (built in the next step) will import all three and let users preview and switch between them.

Seeing import errors in the terminal?

If Vite shows errors about missing modules, check that all three template files are in src/templates/ (not src/pages/ or the project root). The folder name must be exactly templates with a lowercase t.

help me fix template import errors in my Vite project

✔️ Awesome, I've got everything!

Great. Double check you've saved all three template files before moving on.

ⓧ I'd like to double check the full code

Here is ProfessionalTemplate.jsx:

function ProfessionalTemplate({ profile, generatedContent }) {
  if (!profile) return null;

  return (
    <div style={{ fontFamily: 'Georgia, serif', padding: '2rem', background: 'white', border: '1px solid #ddd' }}>
      <div style={{ textAlign: 'center', borderBottom: '2px solid #333', paddingBottom: '1rem', marginBottom: '1.5rem' }}>
        <h1 style={{ fontSize: '1.8rem', margin: 0 }}>{profile.fullName}</h1>
        <p style={{ color: '#555', marginTop: '0.5rem' }}>
          {profile.email} | {profile.phone} | {profile.location}
        </p>
      </div>

      {(generatedContent?.summary || profile.summary) && (
        <div style={{ marginBottom: '1.5rem' }}>
          <h2 style={{ fontSize: '1.1rem', textTransform: 'uppercase', borderBottom: '1px solid #ccc', paddingBottom: '0.3rem' }}>Professional Summary</h2>
          <p style={{ marginTop: '0.5rem', lineHeight: 1.6 }}>{generatedContent?.summary || profile.summary}</p>
        </div>
      )}

      {profile.experience?.length > 0 && (
        <div style={{ marginBottom: '1.5rem' }}>
          <h2 style={{ fontSize: '1.1rem', textTransform: 'uppercase', borderBottom: '1px solid #ccc', paddingBottom: '0.3rem' }}>Experience</h2>
          {profile.experience.map((exp, i) => (
            <div key={i} style={{ marginTop: '1rem' }}>
              <div style={{ display: 'flex', justifyContent: 'space-between' }}>
                <strong>{exp.title}</strong>
                <span style={{ color: '#666' }}>{exp.startDate} - {exp.endDate}</span>
              </div>
              <p style={{ color: '#555', fontStyle: 'italic' }}>{exp.company}</p>
              <ul style={{ paddingLeft: '1.5rem', marginTop: '0.5rem' }}>
                {(generatedContent?.experienceBullets?.[String(i)] || [exp.description]).map((bullet, j) => (
                  <li key={j} style={{ marginBottom: '0.3rem', lineHeight: 1.5 }}>{bullet}</li>
                ))}
              </ul>
            </div>
          ))}
        </div>
      )}

      {profile.education?.length > 0 && (
        <div style={{ marginBottom: '1.5rem' }}>
          <h2 style={{ fontSize: '1.1rem', textTransform: 'uppercase', borderBottom: '1px solid #ccc', paddingBottom: '0.3rem' }}>Education</h2>
          {profile.education.map((edu, i) => (
            <div key={i} style={{ marginTop: '0.75rem' }}>
              <strong>{edu.degree}</strong> - {edu.institution} ({edu.year})
            </div>
          ))}
        </div>
      )}

      {profile.skills?.length > 0 && (
        <div>
          <h2 style={{ fontSize: '1.1rem', textTransform: 'uppercase', borderBottom: '1px solid #ccc', paddingBottom: '0.3rem' }}>Skills</h2>
          <p style={{ marginTop: '0.5rem' }}>{profile.skills.join(' • ')}</p>
        </div>
      )}
    </div>
  );
}

export default ProfessionalTemplate;

Here is ModernTemplate.jsx:

function ModernTemplate({ profile, generatedContent }) {
  if (!profile) return null;

  return (
    <div style={{ fontFamily: '-apple-system, sans-serif', padding: '2rem', background: 'white', border: '1px solid #ddd' }}>
      <div style={{ background: '#1a1a2e', color: 'white', padding: '1.5rem', borderRadius: '8px', marginBottom: '1.5rem' }}>
        <h1 style={{ fontSize: '1.8rem', margin: 0 }}>{profile.fullName}</h1>
        <p style={{ color: '#4ecca3', marginTop: '0.5rem', fontSize: '0.95rem' }}>
          {profile.email} | {profile.phone} | {profile.location}
        </p>
      </div>

      {(generatedContent?.summary || profile.summary) && (
        <div style={{ marginBottom: '1.5rem' }}>
          <h2 style={{ color: '#1a1a2e', fontSize: '1.1rem', marginBottom: '0.5rem' }}>About</h2>
          <p style={{ lineHeight: 1.7, color: '#444' }}>{generatedContent?.summary || profile.summary}</p>
        </div>
      )}

      {profile.experience?.length > 0 && (
        <div style={{ marginBottom: '1.5rem' }}>
          <h2 style={{ color: '#1a1a2e', fontSize: '1.1rem', marginBottom: '0.75rem' }}>Experience</h2>
          {profile.experience.map((exp, i) => (
            <div key={i} style={{ marginBottom: '1.25rem', paddingLeft: '1rem', borderLeft: '3px solid #4ecca3' }}>
              <strong style={{ fontSize: '1rem' }}>{exp.title}</strong>
              <p style={{ color: '#666', fontSize: '0.9rem' }}>{exp.company} | {exp.startDate} - {exp.endDate}</p>
              <ul style={{ paddingLeft: '1.25rem', marginTop: '0.5rem' }}>
                {(generatedContent?.experienceBullets?.[String(i)] || [exp.description]).map((bullet, j) => (
                  <li key={j} style={{ marginBottom: '0.3rem', lineHeight: 1.5, color: '#444' }}>{bullet}</li>
                ))}
              </ul>
            </div>
          ))}
        </div>
      )}

      {profile.skills?.length > 0 && (
        <div style={{ marginBottom: '1.5rem' }}>
          <h2 style={{ color: '#1a1a2e', fontSize: '1.1rem', marginBottom: '0.5rem' }}>Skills</h2>
          <div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem' }}>
            {profile.skills.map((skill, i) => (
              <span key={i} style={{ background: '#e8f5e9', color: '#1a1a2e', padding: '0.3rem 0.8rem', borderRadius: '20px', fontSize: '0.85rem' }}>{skill}</span>
            ))}
          </div>
        </div>
      )}

      {profile.education?.length > 0 && (
        <div>
          <h2 style={{ color: '#1a1a2e', fontSize: '1.1rem', marginBottom: '0.5rem' }}>Education</h2>
          {profile.education.map((edu, i) => (
            <div key={i} style={{ marginBottom: '0.5rem' }}>
              <strong>{edu.degree}</strong> - {edu.institution} ({edu.year})
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

export default ModernTemplate;

Here is MinimalTemplate.jsx:

function MinimalTemplate({ profile, generatedContent }) {
  if (!profile) return null;

  return (
    <div style={{ fontFamily: 'Helvetica, Arial, sans-serif', padding: '2rem', background: 'white', border: '1px solid #ddd', maxWidth: '700px' }}>
      <h1 style={{ fontSize: '2rem', fontWeight: 300, margin: 0 }}>{profile.fullName}</h1>
      <p style={{ color: '#888', marginTop: '0.25rem', fontSize: '0.9rem' }}>
        {profile.email} • {profile.phone} • {profile.location}
      </p>
      <hr style={{ border: 'none', borderTop: '1px solid #eee', margin: '1.5rem 0' }} />

      {(generatedContent?.summary || profile.summary) && (
        <>
          <p style={{ lineHeight: 1.8, color: '#555' }}>{generatedContent?.summary || profile.summary}</p>
          <hr style={{ border: 'none', borderTop: '1px solid #eee', margin: '1.5rem 0' }} />
        </>
      )}

      {profile.experience?.length > 0 && (
        <>
          {profile.experience.map((exp, i) => (
            <div key={i} style={{ marginBottom: '1.5rem' }}>
              <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
                <strong>{exp.title}</strong>
                <span style={{ color: '#999', fontSize: '0.85rem' }}>{exp.startDate} - {exp.endDate}</span>
              </div>
              <p style={{ color: '#666', fontSize: '0.9rem' }}>{exp.company}</p>
              <ul style={{ paddingLeft: '1.25rem', marginTop: '0.5rem', color: '#555' }}>
                {(generatedContent?.experienceBullets?.[String(i)] || [exp.description]).map((bullet, j) => (
                  <li key={j} style={{ marginBottom: '0.25rem', lineHeight: 1.6 }}>{bullet}</li>
                ))}
              </ul>
            </div>
          ))}
          <hr style={{ border: 'none', borderTop: '1px solid #eee', margin: '1.5rem 0' }} />
        </>
      )}

      {profile.education?.length > 0 && (
        <>
          {profile.education.map((edu, i) => (
            <p key={i} style={{ marginBottom: '0.5rem', color: '#555' }}>
              <strong>{edu.degree}</strong>, {edu.institution} ({edu.year})
            </p>
          ))}
          <hr style={{ border: 'none', borderTop: '1px solid #eee', margin: '1.5rem 0' }} />
        </>
      )}

      {profile.skills?.length > 0 && (
        <p style={{ color: '#555' }}>{profile.skills.join(' / ')}</p>
      )}
    </div>
  );
}

export default MinimalTemplate;

Your Profile page is live and your three resume templates are ready. Next up, you'll build the Resume Editor page that ties them all together with AI-powered content generation and a live template preview.

Build the Resume Editor, Cover Letter, and ATS Pages

Your profile form saves data to MongoDB, and your three resume templates are ready to display it. Now you need the pages that tie everything together.

In this step, you will build the Resume Editor (where users pick a template, generate AI content, and download a PDF), the Cover Letter generator, and the ATS Score Checker. Each page loads the saved profile, calls your backend AI endpoints, and displays the results.

In this step, get ready to:
  • Create a PDF document component that mirrors your resume layout.
  • Build the Resume Editor with template selection, AI generation, and PDF download.
  • Build the Cover Letter and ATS Checker pages that call your backend AI endpoints.
Create the PDF document template

To let users download their resume as a PDF, you need a special component built with @react-pdf/renderer. This library uses React components to define PDF layouts, but it only supports flexbox positioning.

  • Create a new file src/templates/ResumePDF.jsx in your frontend project and add the following code:
import { Document, Page, Text, View, StyleSheet } from '@react-pdf/renderer';

// Define PDF-specific styles (only flexbox is supported)
const styles = StyleSheet.create({
  page: { padding: 40, fontFamily: 'Helvetica', fontSize: 11 },
  header: { textAlign: 'center', marginBottom: 20 },
  name: { fontSize: 22, fontWeight: 'bold', marginBottom: 4 },
  contact: { fontSize: 10, color: '#555' },
  section: { marginBottom: 15 },
  sectionTitle: { fontSize: 13, fontWeight: 'bold', borderBottomWidth: 1, borderBottomColor: '#333', paddingBottom: 3, marginBottom: 8, textTransform: 'uppercase' },
  expTitle: { fontWeight: 'bold', fontSize: 11 },
  expCompany: { color: '#555', fontSize: 10, fontStyle: 'italic', marginBottom: 4 },
  bullet: { marginLeft: 15, marginBottom: 3, lineHeight: 1.4 },
  skillsText: { lineHeight: 1.6 }
});

function ResumePDF({ profile, generatedContent }) {
  if (!profile) return null;

  return (
    <Document>
      <Page size="A4" style={styles.page}>
        {/* Header with name and contact info */}
        <View style={styles.header}>
          <Text style={styles.name}>{profile.fullName}</Text>
          <Text style={styles.contact}>{profile.email} | {profile.phone} | {profile.location}</Text>
        </View>

        {/* Professional summary section */}
        {(generatedContent?.summary || profile.summary) && (
          <View style={styles.section}>
            <Text style={styles.sectionTitle}>Professional Summary</Text>
            <Text style={{ lineHeight: 1.5 }}>{generatedContent?.summary || profile.summary}</Text>
          </View>
        )}

        {/* Experience section with AI-generated bullets */}
        {profile.experience?.length > 0 && (
          <View style={styles.section}>
            <Text style={styles.sectionTitle}>Experience</Text>
            {profile.experience.map((exp, i) => (
              <View key={i} style={{ marginBottom: 10 }}>
                <Text style={styles.expTitle}>{exp.title} - {exp.company}</Text>
                <Text style={styles.expCompany}>{exp.startDate} - {exp.endDate}</Text>
                {(generatedContent?.experienceBullets?.[String(i)] || [exp.description]).map((bullet, j) => (
                  <Text key={j} style={styles.bullet}>• {bullet}</Text>
                ))}
              </View>
            ))}
          </View>
        )}

        {/* Education section */}
        {profile.education?.length > 0 && (
          <View style={styles.section}>
            <Text style={styles.sectionTitle}>Education</Text>
            {profile.education.map((edu, i) => (
              <Text key={i} style={{ marginBottom: 4 }}>{edu.degree} - {edu.institution} ({edu.year})</Text>
            ))}
          </View>
        )}

        {/* Skills section */}
        {profile.skills?.length > 0 && (
          <View style={styles.section}>
            <Text style={styles.sectionTitle}>Skills</Text>
            <Text style={styles.skillsText}>{profile.skills.join(' • ')}</Text>
          </View>
        )}
      </Page>
    </Document>
  );
}

export default ResumePDF;

What does this code do?

This component defines a PDF document layout using @react-pdf/renderer primitives instead of HTML elements. You use Document, Page, View, and Text instead of div, p, and span.

StyleSheet.create works like CSS but only supports flexbox layout. The component renders the same profile data and AI-generated content that your browser templates display, but formatted for an A4 PDF page.

  • Save src/templates/ResumePDF.jsx.

Why a separate PDF component?

@react-pdf/renderer cannot render standard HTML or CSS Grid. It uses its own layout engine under the hood. That means you need a dedicated component that uses View and Text primitives, even though the data is the same as your browser templates.

Build the Resume Editor page

The Resume Editor is the central page of your app. It loads the user's profile, lets them choose a template, triggers AI content generation, and provides a PDF download button.

  • Create a new file src/pages/ResumeEditor.jsx and add the following code:
import { useState, useEffect } from 'react';
import { pdf } from '@react-pdf/renderer';
import ProfessionalTemplate from '../templates/ProfessionalTemplate';
import ModernTemplate from '../templates/ModernTemplate';
import MinimalTemplate from '../templates/MinimalTemplate';
import ResumePDF from '../templates/ResumePDF';

const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:5000';

function ResumeEditor() {
  const [profile, setProfile] = useState(null);
  const [selectedTemplate, setSelectedTemplate] = useState('professional');
  const [targetRole, setTargetRole] = useState('');
  const [generatedContent, setGeneratedContent] = useState(null);
  const [loading, setLoading] = useState(false);

  // Load the saved profile from the backend on mount
  useEffect(() => {
    const profileId = localStorage.getItem('profileId');
    if (profileId) {
      fetch(`${API_URL}/api/profiles/${profileId}`)
        .then(res => res.json())
        .then(data => setProfile(data))
        .catch(err => console.error(err));
    }
  }, []);

  // Call the AI resume generation endpoint
  const generateResume = async () => {
    if (!profile || !targetRole) return;
    setLoading(true);
    try {
      const res = await fetch(`${API_URL}/api/generate/resume`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ profile, targetRole })
      });
      const data = await res.json();
      setGeneratedContent(data);
    } catch (error) {
      console.error('Error generating resume:', error);
    }
    setLoading(false);
  };

  // Generate a PDF blob and trigger a download
  const downloadPDF = async () => {
    const blob = await pdf(<ResumePDF profile={profile} generatedContent={generatedContent} />).toBlob();
    const url = URL.createObjectURL(blob);
    const link = document.createElement('a');
    link.href = url;
    link.download = `${profile.fullName.replace(/\s+/g, '_')}_Resume.pdf`;
    link.click();
    URL.revokeObjectURL(url);
  };

  // Map template names to components
  const templates = { professional: ProfessionalTemplate, modern: ModernTemplate, minimal: MinimalTemplate };
  const TemplateComponent = templates[selectedTemplate];

  return (
    <div>
      <h1 className="page-title">Resume Editor</h1>

      {!profile ? (
        <div className="card"><p>Please create a profile first.</p></div>
      ) : (
        <>
          <div className="card">
            <h3>Target Role</h3>
            <div className="form-group">
              <input value={targetRole} onChange={(e) => setTargetRole(e.target.value)} placeholder="e.g., Senior Frontend Developer" />
            </div>
            <button className="btn btn-primary" onClick={generateResume} disabled={loading}>
              {loading ? 'Generating...' : 'Generate with AI'}
            </button>
          </div>

          <div className="card">
            <h3>Choose Template</h3>
            <div className="template-grid">
              {['professional', 'modern', 'minimal'].map(t => (
                <div key={t} className={`template-card ${selectedTemplate === t ? 'selected' : ''}`} onClick={() => setSelectedTemplate(t)}>
                  <h4 style={{ textTransform: 'capitalize' }}>{t}</h4>
                  <p style={{ color: '#888', fontSize: '0.9rem' }}>
                    {t === 'professional' && 'Traditional layout, ideal for corporate roles'}
                    {t === 'modern' && 'Clean with color accents, great for tech roles'}
                    {t === 'minimal' && 'Simple and elegant, works for any industry'}
                  </p>
                </div>
              ))}
            </div>
          </div>

          <div className="card">
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
              <h3>Preview</h3>
              {generatedContent && <button className="btn btn-primary" onClick={downloadPDF}>Download PDF</button>}
            </div>
            <TemplateComponent profile={profile} generatedContent={generatedContent} />
          </div>
        </>
      )}
    </div>
  );
}

export default ResumeEditor;

What does this code do?

The useEffect hook loads the user's profile from the backend using the ID stored in localStorage. The generateResume function sends the profile and target role to your POST /api/generate/resume endpoint, which returns AI-generated summary and bullet points.

The downloadPDF function uses the pdf() function from @react-pdf/renderer to render the ResumePDF component into a blob, then creates a temporary download link. The template selector dynamically swaps which preview component renders based on the user's choice.

  • Save src/pages/ResumeEditor.jsx.
  • In your browser, navigate to the Resume page using the nav bar. You should see the "Target Role" input field and template selector cards. If you have not saved a profile yet, you will see a message saying "Please create a profile first."

Seeing a blank page or import errors?

Make sure you saved ResumePDF.jsx in the src/templates/ folder before creating ResumeEditor.jsx. The editor imports ResumePDF and all three template components. Check that the import paths match your file locations exactly.

help me debug import errors in my ResumeEditor component

Build the Cover Letter and ATS Checker pages

The final two pages follow the same pattern: load the profile, collect user input (a job description), call the backend AI endpoint, and display results. The Cover Letter page generates custom letters. The ATS Checker scores your resume against a job posting.

  • Create src/pages/CoverLetterPage.jsx and add the following code:
import { useState, useEffect } from 'react';

const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:5000';

function CoverLetterPage() {
  const [profile, setProfile] = useState(null);
  const [jobDescription, setJobDescription] = useState('');
  const [companyName, setCompanyName] = useState('');
  const [coverLetter, setCoverLetter] = useState('');
  const [loading, setLoading] = useState(false);

  // Load saved profile on mount
  useEffect(() => {
    const profileId = localStorage.getItem('profileId');
    if (profileId) {
      fetch(`${API_URL}/api/profiles/${profileId}`)
        .then(res => res.json())
        .then(data => setProfile(data))
        .catch(err => console.error(err));
    }
  }, []);

  // Send profile and job info to the cover letter endpoint
  const generateCoverLetter = async () => {
    if (!profile || !jobDescription || !companyName) return;
    setLoading(true);
    try {
      const res = await fetch(`${API_URL}/api/generate/cover-letter`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ profile, jobDescription, companyName })
      });
      const data = await res.json();
      setCoverLetter(data.coverLetter);
    } catch (error) {
      console.error('Error:', error);
    }
    setLoading(false);
  };

  return (
    <div>
      <h1 className="page-title">Cover Letter Generator</h1>
      {!profile ? (
        <div className="card"><p>Please create a profile first.</p></div>
      ) : (
        <>
          <div className="card">
            <div className="form-group">
              <label>Company Name</label>
              <input value={companyName} onChange={(e) => setCompanyName(e.target.value)} placeholder="e.g., Google" />
            </div>
            <div className="form-group">
              <label>Job Description</label>
              <textarea value={jobDescription} onChange={(e) => setJobDescription(e.target.value)} placeholder="Paste the job description here..." rows={6} />
            </div>
            <button className="btn btn-primary" onClick={generateCoverLetter} disabled={loading}>
              {loading ? 'Generating...' : 'Generate Cover Letter'}
            </button>
          </div>
          {coverLetter && (
            <div className="card">
              <h3>Your Cover Letter</h3>
              <div style={{ whiteSpace: 'pre-wrap', lineHeight: 1.8, marginTop: '1rem' }}>{coverLetter}</div>
            </div>
          )}
        </>
      )}
    </div>
  );
}

export default CoverLetterPage;

What does this code do?

The generateCoverLetter function sends the user's profile, a company name, and the job description to POST /api/generate/cover-letter. Gemini uses all three to write a targeted cover letter. The response displays with whiteSpace: 'pre-wrap' to preserve the paragraph formatting that Gemini returns.

  • Save src/pages/CoverLetterPage.jsx.
  • Now create src/pages/ATSChecker.jsx and add the following code:
import { useState, useEffect } from 'react';

const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:5000';

function ATSChecker() {
  const [profile, setProfile] = useState(null);
  const [jobDescription, setJobDescription] = useState('');
  const [result, setResult] = useState(null);
  const [loading, setLoading] = useState(false);

  // Load saved profile on mount
  useEffect(() => {
    const profileId = localStorage.getItem('profileId');
    if (profileId) {
      fetch(`${API_URL}/api/profiles/${profileId}`)
        .then(res => res.json())
        .then(data => setProfile(data))
        .catch(err => console.error(err));
    }
  }, []);

  // Build a text version of the resume and send to ATS endpoint
  const checkATS = async () => {
    if (!profile || !jobDescription) return;
    setLoading(true);
    const resumeContent = `${profile.fullName}\n${profile.summary || ''}\nSkills: ${profile.skills?.join(', ')}\n${profile.experience?.map(e => `${e.title} at ${e.company}: ${e.description}`).join('\n')}`;
    try {
      const res = await fetch(`${API_URL}/api/ats/score`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ resumeContent, jobDescription })
      });
      const data = await res.json();
      setResult(data);
    } catch (error) {
      console.error('Error:', error);
    }
    setLoading(false);
  };

  // Assign a CSS class based on the score range
  const getScoreClass = (score) => {
    if (score >= 70) return 'score-high';
    if (score >= 40) return 'score-medium';
    return 'score-low';
  };

  return (
    <div>
      <h1 className="page-title">ATS Score Checker</h1>
      {!profile ? (
        <div className="card"><p>Please create a profile first.</p></div>
      ) : (
        <>
          <div className="card">
            <div className="form-group">
              <label>Paste Job Description</label>
              <textarea value={jobDescription} onChange={(e) => setJobDescription(e.target.value)} placeholder="Paste the full job description here..." rows={6} />
            </div>
            <button className="btn btn-primary" onClick={checkATS} disabled={loading}>
              {loading ? 'Analyzing...' : 'Check ATS Score'}
            </button>
          </div>
          {result && (
            <div className="card">
              <div className={`score-display ${getScoreClass(result.score)}`}>{result.score}/100</div>
              <h3>Matched Keywords</h3>
              <div className="keywords-list">
                {result.matchedKeywords?.map((kw, i) => (
                  <span key={i} className="keyword-tag keyword-matched">{kw}</span>
                ))}
              </div>
              <h3 style={{ marginTop: '1.5rem' }}>Missing Keywords</h3>
              <div className="keywords-list">
                {result.missingKeywords?.map((kw, i) => (
                  <span key={i} className="keyword-tag keyword-missing">{kw}</span>
                ))}
              </div>
              <h3 style={{ marginTop: '1.5rem' }}>Suggestions</h3>
              <ul style={{ paddingLeft: '1.5rem', marginTop: '0.5rem' }}>
                {result.suggestions?.map((s, i) => <li key={i} style={{ marginBottom: '0.5rem' }}>{s}</li>)}
              </ul>
            </div>
          )}
        </>
      )}
    </div>
  );
}

export default ATSChecker;

What does this code do?

The checkATS function builds a plain-text version of the resume by combining the profile's name, summary, skills, and experience descriptions. It sends this text alongside the job description to POST /api/ats/score.

The getScoreClass helper returns score-high (green, 70+), score-medium (yellow, 40-69), or score-low (red, below 40) to color the score display. The results also show matched keywords in green tags and missing keywords in red tags.

  • Save src/pages/ATSChecker.jsx.
  • In your browser, click ATS Score in the navigation bar. You should see a textarea for the job description and a Check ATS Score button.

Pages not rendering when you click the nav links?

Check that your file names match the imports in App.jsx exactly. The imports expect CoverLetterPage and ATSChecker from ./pages/CoverLetterPage and ./pages/ATSChecker. Also confirm your Vite dev server is still running in the terminal.

help me fix routing issues in my React app

✔️ Awesome, I've got everything!

Great. Make sure all four files are saved before moving on.

ⓧ I'd like to double check the full code

Here is the complete code for each file you created in this step.

import { Document, Page, Text, View, StyleSheet } from '@react-pdf/renderer';

const styles = StyleSheet.create({
  page: { padding: 40, fontFamily: 'Helvetica', fontSize: 11 },
  header: { textAlign: 'center', marginBottom: 20 },
  name: { fontSize: 22, fontWeight: 'bold', marginBottom: 4 },
  contact: { fontSize: 10, color: '#555' },
  section: { marginBottom: 15 },
  sectionTitle: { fontSize: 13, fontWeight: 'bold', borderBottomWidth: 1, borderBottomColor: '#333', paddingBottom: 3, marginBottom: 8, textTransform: 'uppercase' },
  expTitle: { fontWeight: 'bold', fontSize: 11 },
  expCompany: { color: '#555', fontSize: 10, fontStyle: 'italic', marginBottom: 4 },
  bullet: { marginLeft: 15, marginBottom: 3, lineHeight: 1.4 },
  skillsText: { lineHeight: 1.6 }
});

function ResumePDF({ profile, generatedContent }) {
  if (!profile) return null;

  return (
    <Document>
      <Page size="A4" style={styles.page}>
        <View style={styles.header}>
          <Text style={styles.name}>{profile.fullName}</Text>
          <Text style={styles.contact}>{profile.email} | {profile.phone} | {profile.location}</Text>
        </View>

        {(generatedContent?.summary || profile.summary) && (
          <View style={styles.section}>
            <Text style={styles.sectionTitle}>Professional Summary</Text>
            <Text style={{ lineHeight: 1.5 }}>{generatedContent?.summary || profile.summary}</Text>
          </View>
        )}

        {profile.experience?.length > 0 && (
          <View style={styles.section}>
            <Text style={styles.sectionTitle}>Experience</Text>
            {profile.experience.map((exp, i) => (
              <View key={i} style={{ marginBottom: 10 }}>
                <Text style={styles.expTitle}>{exp.title} - {exp.company}</Text>
                <Text style={styles.expCompany}>{exp.startDate} - {exp.endDate}</Text>
                {(generatedContent?.experienceBullets?.[String(i)] || [exp.description]).map((bullet, j) => (
                  <Text key={j} style={styles.bullet}>• {bullet}</Text>
                ))}
              </View>
            ))}
          </View>
        )}

        {profile.education?.length > 0 && (
          <View style={styles.section}>
            <Text style={styles.sectionTitle}>Education</Text>
            {profile.education.map((edu, i) => (
              <Text key={i} style={{ marginBottom: 4 }}>{edu.degree} - {edu.institution} ({edu.year})</Text>
            ))}
          </View>
        )}

        {profile.skills?.length > 0 && (
          <View style={styles.section}>
            <Text style={styles.sectionTitle}>Skills</Text>
            <Text style={styles.skillsText}>{profile.skills.join(' • ')}</Text>
          </View>
        )}
      </Page>
    </Document>
  );
}

export default ResumePDF;
import { useState, useEffect } from 'react';
import { pdf } from '@react-pdf/renderer';
import ProfessionalTemplate from '../templates/ProfessionalTemplate';
import ModernTemplate from '../templates/ModernTemplate';
import MinimalTemplate from '../templates/MinimalTemplate';
import ResumePDF from '../templates/ResumePDF';

const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:5000';

function ResumeEditor() {
  const [profile, setProfile] = useState(null);
  const [selectedTemplate, setSelectedTemplate] = useState('professional');
  const [targetRole, setTargetRole] = useState('');
  const [generatedContent, setGeneratedContent] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    const profileId = localStorage.getItem('profileId');
    if (profileId) {
      fetch(`${API_URL}/api/profiles/${profileId}`)
        .then(res => res.json())
        .then(data => setProfile(data))
        .catch(err => console.error(err));
    }
  }, []);

  const generateResume = async () => {
    if (!profile || !targetRole) return;
    setLoading(true);
    try {
      const res = await fetch(`${API_URL}/api/generate/resume`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ profile, targetRole })
      });
      const data = await res.json();
      setGeneratedContent(data);
    } catch (error) {
      console.error('Error generating resume:', error);
    }
    setLoading(false);
  };

  const downloadPDF = async () => {
    const blob = await pdf(<ResumePDF profile={profile} generatedContent={generatedContent} />).toBlob();
    const url = URL.createObjectURL(blob);
    const link = document.createElement('a');
    link.href = url;
    link.download = `${profile.fullName.replace(/\s+/g, '_')}_Resume.pdf`;
    link.click();
    URL.revokeObjectURL(url);
  };

  const templates = { professional: ProfessionalTemplate, modern: ModernTemplate, minimal: MinimalTemplate };
  const TemplateComponent = templates[selectedTemplate];

  return (
    <div>
      <h1 className="page-title">Resume Editor</h1>

      {!profile ? (
        <div className="card"><p>Please create a profile first.</p></div>
      ) : (
        <>
          <div className="card">
            <h3>Target Role</h3>
            <div className="form-group">
              <input value={targetRole} onChange={(e) => setTargetRole(e.target.value)} placeholder="e.g., Senior Frontend Developer" />
            </div>
            <button className="btn btn-primary" onClick={generateResume} disabled={loading}>
              {loading ? 'Generating...' : 'Generate with AI'}
            </button>
          </div>

          <div className="card">
            <h3>Choose Template</h3>
            <div className="template-grid">
              {['professional', 'modern', 'minimal'].map(t => (
                <div key={t} className={`template-card ${selectedTemplate === t ? 'selected' : ''}`} onClick={() => setSelectedTemplate(t)}>
                  <h4 style={{ textTransform: 'capitalize' }}>{t}</h4>
                  <p style={{ color: '#888', fontSize: '0.9rem' }}>
                    {t === 'professional' && 'Traditional layout, ideal for corporate roles'}
                    {t === 'modern' && 'Clean with color accents, great for tech roles'}
                    {t === 'minimal' && 'Simple and elegant, works for any industry'}
                  </p>
                </div>
              ))}
            </div>
          </div>

          <div className="card">
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
              <h3>Preview</h3>
              {generatedContent && <button className="btn btn-primary" onClick={downloadPDF}>Download PDF</button>}
            </div>
            <TemplateComponent profile={profile} generatedContent={generatedContent} />
          </div>
        </>
      )}
    </div>
  );
}

export default ResumeEditor;
import { useState, useEffect } from 'react';

const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:5000';

function CoverLetterPage() {
  const [profile, setProfile] = useState(null);
  const [jobDescription, setJobDescription] = useState('');
  const [companyName, setCompanyName] = useState('');
  const [coverLetter, setCoverLetter] = useState('');
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    const profileId = localStorage.getItem('profileId');
    if (profileId) {
      fetch(`${API_URL}/api/profiles/${profileId}`)
        .then(res => res.json())
        .then(data => setProfile(data))
        .catch(err => console.error(err));
    }
  }, []);

  const generateCoverLetter = async () => {
    if (!profile || !jobDescription || !companyName) return;
    setLoading(true);
    try {
      const res = await fetch(`${API_URL}/api/generate/cover-letter`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ profile, jobDescription, companyName })
      });
      const data = await res.json();
      setCoverLetter(data.coverLetter);
    } catch (error) {
      console.error('Error:', error);
    }
    setLoading(false);
  };

  return (
    <div>
      <h1 className="page-title">Cover Letter Generator</h1>
      {!profile ? (
        <div className="card"><p>Please create a profile first.</p></div>
      ) : (
        <>
          <div className="card">
            <div className="form-group">
              <label>Company Name</label>
              <input value={companyName} onChange={(e) => setCompanyName(e.target.value)} placeholder="e.g., Google" />
            </div>
            <div className="form-group">
              <label>Job Description</label>
              <textarea value={jobDescription} onChange={(e) => setJobDescription(e.target.value)} placeholder="Paste the job description here..." rows={6} />
            </div>
            <button className="btn btn-primary" onClick={generateCoverLetter} disabled={loading}>
              {loading ? 'Generating...' : 'Generate Cover Letter'}
            </button>
          </div>
          {coverLetter && (
            <div className="card">
              <h3>Your Cover Letter</h3>
              <div style={{ whiteSpace: 'pre-wrap', lineHeight: 1.8, marginTop: '1rem' }}>{coverLetter}</div>
            </div>
          )}
        </>
      )}
    </div>
  );
}

export default CoverLetterPage;
import { useState, useEffect } from 'react';

const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:5000';

function ATSChecker() {
  const [profile, setProfile] = useState(null);
  const [jobDescription, setJobDescription] = useState('');
  const [result, setResult] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    const profileId = localStorage.getItem('profileId');
    if (profileId) {
      fetch(`${API_URL}/api/profiles/${profileId}`)
        .then(res => res.json())
        .then(data => setProfile(data))
        .catch(err => console.error(err));
    }
  }, []);

  const checkATS = async () => {
    if (!profile || !jobDescription) return;
    setLoading(true);
    const resumeContent = `${profile.fullName}\n${profile.summary || ''}\nSkills: ${profile.skills?.join(', ')}\n${profile.experience?.map(e => `${e.title} at ${e.company}: ${e.description}`).join('\n')}`;
    try {
      const res = await fetch(`${API_URL}/api/ats/score`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ resumeContent, jobDescription })
      });
      const data = await res.json();
      setResult(data);
    } catch (error) {
      console.error('Error:', error);
    }
    setLoading(false);
  };

  const getScoreClass = (score) => {
    if (score >= 70) return 'score-high';
    if (score >= 40) return 'score-medium';
    return 'score-low';
  };

  return (
    <div>
      <h1 className="page-title">ATS Score Checker</h1>
      {!profile ? (
        <div className="card"><p>Please create a profile first.</p></div>
      ) : (
        <>
          <div className="card">
            <div className="form-group">
              <label>Paste Job Description</label>
              <textarea value={jobDescription} onChange={(e) => setJobDescription(e.target.value)} placeholder="Paste the full job description here..." rows={6} />
            </div>
            <button className="btn btn-primary" onClick={checkATS} disabled={loading}>
              {loading ? 'Analyzing...' : 'Check ATS Score'}
            </button>
          </div>
          {result && (
            <div className="card">
              <div className={`score-display ${getScoreClass(result.score)}`}>{result.score}/100</div>
              <h3>Matched Keywords</h3>
              <div className="keywords-list">
                {result.matchedKeywords?.map((kw, i) => (
                  <span key={i} className="keyword-tag keyword-matched">{kw}</span>
                ))}
              </div>
              <h3 style={{ marginTop: '1.5rem' }}>Missing Keywords</h3>
              <div className="keywords-list">
                {result.missingKeywords?.map((kw, i) => (
                  <span key={i} className="keyword-tag keyword-missing">{kw}</span>
                ))}
              </div>
              <h3 style={{ marginTop: '1.5rem' }}>Suggestions</h3>
              <ul style={{ paddingLeft: '1.5rem', marginTop: '0.5rem' }}>
                {result.suggestions?.map((s, i) => <li key={i} style={{ marginBottom: '0.5rem' }}>{s}</li>)}
              </ul>
            </div>
          )}
        </>
      )}
    </div>
  );
}

export default ATSChecker;

All your frontend pages are connected to the backend AI endpoints. Next up, you will deploy the entire application for free so anyone can access your AI Resume Builder from the internet.

Deploy for Free

Your AI Resume Builder works locally. Now it is time to put it on the internet so anyone can access it.

In this step, you will push your code to GitHub and deploy both your backend and frontend to Render for free. When you are done, your full-stack app will be live at a public URL with zero cost.

In this step, get ready to:
  • Push your backend and frontend code to GitHub repositories.
  • Deploy the backend API to Render as a free web service.
  • Deploy the frontend to Render as a free static site.
Push your code to GitHub

Before Render can deploy your app, it needs access to your code on GitHub. You will create two repositories: one for the backend and one for the frontend.

First, make sure your backend does not accidentally include your secret .env file or the node_modules folder.

  • Switch back to VS Code with your ai-resume-builder-api folder open.
  • Create a new file called .gitignore in the root of your backend project by running this command in the terminal:
echo "node_modules\n.env" > .gitignore

What does .gitignore do?

Any file or folder listed in .gitignore will be excluded from Git tracking. This prevents your API keys and large dependency folders from being uploaded to GitHub.

  • Initialize a Git repository and commit your backend code by running these commands:
git init
git add .
git commit -m "Initial backend commit"
git branch -M main
  • Go to github.com/new in your browser.
  • Enter ai-resume-builder-api as the repository name.
  • Leave it set to Public.
  • Click Create repository.
  • GitHub will show you a set of commands. Copy the two lines under "push an existing repository from the command line" and run them in your terminal. They will look like this:
git remote add origin https://github.com/[[GITHUB_USERNAME="your-github-username"]]/ai-resume-builder-api.git
git push -u origin main
  • Confirm the repository appears on your GitHub profile by refreshing the page. You should see your code files listed.

Now repeat the same process for the frontend.

  • Open a new terminal in your ai-resume-builder-frontend folder.
  • Create a .gitignore file by running:
echo "node_modules\n.env" > .gitignore
  • Initialize and commit the frontend code by running:
git init
git add .
git commit -m "Initial frontend commit"
git branch -M main
  • Go to github.com/new again.
  • Enter ai-resume-builder-frontend as the repository name.
  • Click Create repository.
  • Copy and run the push commands from GitHub:
git remote add origin https://github.com/[[GITHUB_USERNAME="your-github-username"]]/ai-resume-builder-frontend.git
git push -u origin main
  • Confirm both repos are visible on your GitHub profile. You should see ai-resume-builder-api and ai-resume-builder-frontend listed.

Push rejected or authentication error?

If Git asks for a password and rejects it, GitHub requires a Personal Access Token instead of your password. Go to GitHub Settings, then Developer settings, then Personal access tokens, then Tokens (classic). Generate a new token with repo scope, and use that token as your password.

Help me troubleshoot my GitHub push error.

Deploy the backend to Render

Render will pull your code from GitHub, install dependencies, and start your server. You need to tell it how to build and run your app, and provide the environment variables your server needs.

  • Return to the Render Dashboard at dashboard.render.com.
  • Click New in the top navigation, then select Web Service.
  • Under the Git Provider section, find and select your ai-resume-builder-api repository.
  • Click Connect.

The service creation form appears. Fill in the following fields:

  • Set Name to ai-resume-builder-api.
  • Set Branch to main.
  • Set Language to Node.
  • Set Build Command to npm install.
  • Set Start Command to node server.js.
  • Under Instance Type, select Free.

Now add your environment variables so the server can connect to MongoDB and Gemini.

  • Scroll down to the Advanced section and expand it.
  • Under Environment Variables, add three key-value pairs:
  • Key: PORT with value 5000.
  • Key: MONGODB_URI with value set to your MongoDB Atlas connection string (the same one from your local .env file).
  • Key: GEMINI_API_KEY with value set to your Gemini API key.
  • Click Create Web Service.

Render kicks off your first build and deploy. You can watch the progress in the log explorer. Wait until the status updates to Live.

  • Once the deploy finishes, copy your service's public URL from the top of the page. It will look something like https://ai-resume-builder-api-xxxx.onrender.com.
  • Open that URL in your browser. You should see a JSON response: {"message":"AI Resume Builder API is running"}.

Why does it take a while to respond?

Render's free tier spins down your service after 15 minutes of inactivity. The first request after a spin-down takes about 30 seconds while the service wakes up. Subsequent requests are fast.

Deploy failed?

Check the build logs in the Render Dashboard. Common causes include a missing package.json in the repo root, a typo in the start command, or a missing environment variable. Fix the issue, push a new commit to GitHub, and Render will automatically redeploy.

Help me debug my Render backend deploy failure.

Deploy the frontend to Render

The frontend is a static React app. Render will install your dependencies, run the Vite build, and serve the resulting files over a CDN.

  • Return to the Render Dashboard.
  • Click New, then select Static Site.
  • Find and select your ai-resume-builder-frontend repository.
  • Click Connect.

Fill in the creation form:

  • Set Name to ai-resume-builder-frontend.
  • Set Branch to main.
  • Set Build Command to npm install && npm run build.
  • Set Publish Directory to dist.

Now add an environment variable so the frontend knows where to send API requests.

  • Under Environment Variables, add one key-value pair:
  • Key: VITE_API_URL with the value set to your Render backend URL (e.g. https://ai-resume-builder-api-xxxx.onrender.com). Do not include a trailing slash.
  • Click Create Static Site.

Render builds your frontend and deploys it to a CDN. Wait until the log shows your site is live.

Why set VITE_API_URL as an environment variable?

Vite bakes environment variables into the built JavaScript at build time. By setting VITE_API_URL in Render's environment settings, the production build will use your Render backend URL instead of http://localhost:5000.

  • Once the deploy finishes, click the public URL at the top of your static site's page.
  • You should see the AI Resume Builder Dashboard page load in your browser.
  • Navigate to the Profile page, fill in some test data, and save it to confirm the frontend can talk to the backend.

Set up a rewrite rule for React Router

If you refresh the page on a route like /profile and see a "Not Found" error, you need a rewrite rule. Go to your static site's Redirects/Rewrites tab in Render and add a rule with Source /*, Destination /index.html, and Action Rewrite.

Frontend loads but API calls fail?

Double-check that VITE_API_URL is set correctly in your static site's environment variables. It must match your backend's Render URL exactly, without a trailing slash. If you changed the variable, click Manual Deploy then Clear build cache & deploy because Vite bakes env vars at build time.

Help me debug my frontend-to-backend connection on Render.

Your full-stack AI Resume Builder is now live on the internet at zero cost. Anyone with the URL can access it. Every time you push new code to the main branch, Render automatically rebuilds and redeploys your app.

Secret mission

Job Description Analyzer

Your resume builder generates great content, but how does a candidate know which skills they're missing? Build a JD Analyzer that extracts requirements from a job posting, compares them against the user's resume, and shows a visual gap analysis with color-coded skill tags.

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 uses only free-tier services, so there are no ongoing costs regardless of what you choose.

Resources you used:

  • MongoDB Atlas free cluster (512 MB storage, free forever).
  • Render free web service (backend API, spins down after 15 min of inactivity).
  • Render free static site (frontend).
  • Google Gemini API key (free tier, rate limited).
  • GitHub repositories (backend and frontend).
  • Local project folders on your Desktop.

Keep everything running

No action needed. Choose this if you are still actively building or want to keep your resume builder live.

  • All services are on free tiers with no charges.
  • The Render backend will spin down after 15 minutes of inactivity and wake on the next request (takes about 30 seconds).
  • MongoDB Atlas M0 clusters remain free forever with no expiry.
  • Your Gemini API key remains active and rate-limited on the free tier.

Pause - I'll come back to this later

Since all services are free tier, there is nothing to pause. Your resources will not incur charges while idle.

  • The Render backend automatically spins down after 15 minutes of inactivity. No manual action required.
  • MongoDB Atlas keeps your data stored at no cost.
  • When you are ready to resume, just visit your Render URL or open your local project.

Delete - I don't want to use this again

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

  • Delete your Render services: open the Render Dashboard, click your backend web service, go to its Settings page, scroll to the bottom, and click Delete Web Service. Repeat for your frontend static site.
  • Delete your MongoDB Atlas cluster: log in to MongoDB Atlas, go to the Clusters page for your project, click the ellipsis (...) next to your cluster, select Terminate, type the cluster name to confirm, and click Terminate.
  • Revoke your Gemini API key: open the Google Cloud API Credentials page, find your API key, click the Actions icon, select Delete API key, and confirm in the modal.
  • Delete your GitHub repositories: go to your repository page on GitHub, click Settings, scroll to the bottom under Danger Zone, and click Delete this repository. Repeat for both backend and frontend repos.
  • Delete your local project folders by running these commands:
cd ~/Desktop
rm -rf ai-resume-builder-api ai-resume-builder-frontend

What about Node.js, VS Code, and Git?

These tools are useful for future projects and do not take up significant disk space. You can leave them installed.

That's a Wrap!

That's a Wrap!

Nice work! You just built and deployed a full-stack AI Resume Builder from scratch, at zero cost.

You've learned how to:

  • Built a Node.js/Express REST API with MongoDB for data persistence, handling profiles, resumes, and AI-generated content.
  • Integrated Google Gemini AI to generate professional resume content, tailored cover letters, and ATS compatibility scores.
  • Deployed the entire application live on the internet for free using Render and MongoDB Atlas.
  • Secret Mission: Added a Job Description Analyzer with visual skill gap analysis that highlights matched, partially matched, and missing skills.

Ready to quiz yourself?