Build a Resume Scanner with OpenAI, Node JS & Next JS: A Step-by-Step Tutorial

thumbnail

Hello everyone! In this tutorial, we’ll guide you through building a resume scanner web app using NodeJS and Next.js, powered by OpenAI. Follow along step-by-step as we create a custom solution that highlights key skills, experience, qualifications, and much more from resumes. Let’s dive in!

Prerequisites

  • You need an OpenAI API key, for this please refer to the official docs

https://platform.openai.com/

  • For the Frontend I am using the same code, that I used in my previous post where we built a drag-and-drop file uploader.

Building a Drag-and-Drop File Uploader with Next.js

Backend

Let's start by creating a Node js project npm init in your project directory and install the dependencies express, cors, multer, pdf-parse, openai

Importing Required Modules:

import OpenAI from "openai";
import cors from "cors";
import express from "express";
import multer from "multer";
import PdfParse from "pdf-parse";
  • These lines import the necessary modules for the application.
  • OpenAI, cors, express, multer, and PdfParse are modules required for AI functionality, enabling CORS, creating an Express server, handling file uploads, and parsing PDF files to extract text from it, respectively.

Initializing Variables:

const prompt = "Following is the resume text,Give me a brief description about them in a 300 words paragraph";
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const app = express();
const upload = multer();
  • prompt: Stores a prompt string for requesting a brief description of the resume.
  • openai: Creates a new instance of the OpenAI class, initializing it with an API key.
  • app: Initializes an Express application.
  • upload: Initializes multer for handling file uploads.

Setting Up Middleware:

app.use(cors());
  • Configures Express to use CORS middleware, allowing cross-origin requests.

Defining API Endpoint:

app.post("/resume-ai-scanner", upload.single("file"), async (req, res) => {
  • Defines a POST endpoint at “/resume-ai-scanner” for handling file uploads.
  • Uses multer middleware to handle single-file uploads with the field name “file”.
  • The endpoint is an asynchronous function that takes req (request) and res (response) as parameters.

File Upload Handling:

console.log(req.file);
try {
  if (req.file) {
    const data = await extractTextFromPDF(req.file);
    ...
  • Logs the uploaded file information to the console.
  • Check if a file was uploaded.
  • If a file was uploaded, extract text from the PDF file using the extractTextFromPDF function.

OpenAI API:

const completion = await openai.chat.completions.create({
  messages: [
    {
      role: "user",
      content: `${prompt} ${data}`,
    },
  ],
  model: "gpt-3.5-turbo",
});
console.log(completion.choices[0].message);
let message = completion.choices[0].message.content;
return res.send({ summary: message });
  • This code initiates a request to OpenAI’s API to generate a completion using the openai.chat.completions.create method. The messages array contains an object representing the user's input, with the role set to "user" and the content set to the concatenation of the prompt (defined elsewhere) and the data extracted from the resume. The model parameter specifies the version of the GPT model to be used, in this case, gpt-3.5-turbo.
  • Sends the AI-generated message as a response to the client.

Error Handling:

} catch (err) {
  console.log(err);
  return res.send({ error: "Something went wrong. Please try again later!" });
}
  • Catches any errors that occur during file processing or AI processing.
  • Sends an error message as a response to the client.

PDF Text Extraction Function:

const extractTextFromPDF = async (file: Express.Multer.File) => {
  try {
    const data = await PdfParse(file.buffer);
    return data.text;
  } catch (error) {
    throw new Error("Error extracting text from PDF:" + error);
  }
};
  • Defines an asynchronous function extractTextFromPDF that takes a file object as input.
  • Uses PdfParse to parse the PDF file and extract text from it.
  • Returns the extracted text or throws an error if text extraction fails.

Starting the Server:

app.listen(3001, () => {
  console.log("Server running on 3001");
});
  • Starts the Express server on port 3001.

Final Backend Code

import OpenAI from "openai";
import cors from "cors";
import express from "express";
import multer from "multer";
import PdfParse from "pdf-parse";

const prompt =
  "Following is the resume text,Give me a brief description about them in a 300 words paragraph";
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const app = express();
const upload = multer();
app.use(cors());
app.post("/resume-ai-scanner", upload.single("file"), async (req, res) => {
  console.log(req.file);
  try {
    if (req.file) {
      const data = await extractTextFromPDF(req.file);
      console.log(data);
      const completion = await openai.chat.completions.create({
        messages: [
          {
            role: "user",
            content: `${prompt} ${data}`,
          },
        ],
        model: "gpt-3.5-turbo",
      });
      console.log(completion.choices[0].message);
      let message = completion.choices[0].message.content;
      return res.send({ summary: message });
    }
  } catch (err) {
    console.log(err);
    return res.send({ error: "Something went wrong Please try again later!" });
  }
});

const extractTextFromPDF = async (file: Express.Multer.File) => {
  try {
    const data = await PdfParse(file.buffer);
    return data.text;
  } catch (error) {
    throw new Error("Error extracting text from PDF:" + error);
  }
};

app.listen(3001, () => {
  console.log("Server running on 3001");
});

So this code sets up an Express server with an API endpoint for uploading PDF files, extracting text from them, processing the text using the OpenAI API, and returning the AI-generated summary as a response

Frontend

As mentioned earlier, we are extending the drag-and-drop file uploader.

Importing Required Modules and Components:

import { useEffect, useState } from "react";
import SummaryData from "./summary_data";
  • Imports the necessary modules and components.
  • useEffect and useState are React hooks for managing state and side effects.
  • SummaryData is a component for displaying the resume summary.

Initializing State:

const [file, setFile] = useState<File | null>();
const [fileUrl, setFileUrl] = useState<string>();
const [fileEnter, setFileEnter] = useState(false);
const [summary, setSummary] = useState<string>();
const [loading, setLoading] = useState(false);
  • Initializes state variables using useState hook.
  • file stores the uploaded file.
  • fileUrl stores the URL of the uploaded file.
  • fileEnter tracks whether the file is being dragged over.
  • summary stores the summary generated by OpenAI.
  • loading tracks the loading state of the application.

Call our backend API and get a summary:

const generateSummary = async (f: File) => {
  setLoading(true);
  var data = new FormData();
  data.append("file", f);
  const res = await fetch("http://localhost:3001/resume-ai-scanner", {
    method: "POST",
    body: data,
  });
let respData = await res.json();
  console.log(respData)
  if (respData.error) {
    console.log(respData.error);
  } else {
    setSummary(respData.summary);
  }
  setLoading(false);
};
  • Defines an asynchronous function generateSummary to generate a summary using OpenAI.
  • Creates a FormData object to send the file to the server.
  • Sends a POST request to the server endpoint (“/resume-ai-scanner”) with the file data.
  • Sets the summary state with the response data from the server.
  • Sets the loading state to false after completing the request.

UseEffect Hook:

useEffect(() => {
  if (file) {
    generateSummary(file);
  }
}, [file]);
  • Executes the generateSummary function whenever the file state changes.
  • This hook ensures that the summary is generated whenever a new file is uploaded.

Rendering JSX:

return (
    <div className="container px-4 max-w-5xl mx-auto">
      {!fileUrl ? (
        <div
          onDragOver={(e) => {
            e.preventDefault();
            setFileEnter(true);
          }}
          onDragLeave={(e) => {
            setFileEnter(false);
          }}
          onDragEnd={(e) => {
            e.preventDefault();
            setFileEnter(false);
          }}
          onDrop={(e) => {
            e.preventDefault();
            setFileEnter(false);
            if (e.dataTransfer.items) {
              [...e.dataTransfer.items].forEach((item, i) => {
                if (item.kind === "file") {
                  const file = item.getAsFile();
                  if (file) {
                    let blobUrl = URL.createObjectURL(file);
                    setFileUrl(blobUrl);
                    setFile(file);
                  }
                  console.log(`items file[${i}].name = ${file?.name}`);
                }
              });
            } else {
              [...e.dataTransfer.files].forEach((file, i) => {
                console.log(`… file[${i}].name = ${file.name}`);
              });
            }
          }}
          className={`${
            fileEnter ? "border-4" : "border-2"
          } mx-auto  bg-white flex flex-col w-full max-w-xs h-72 border-dashed items-center justify-center`}
        >
          <label
            htmlFor="file"
            className="h-full flex flex-col justify-center text-center"
          >
            Click to upload or drag and drop
          </label>
          <input
            id="file"
            type="file"
            className="hidden"
            onChange={(e) => {
              console.log(e.target.files);
              let files = e.target.files;
              if (files && files[0]) {
                let blobUrl = URL.createObjectURL(files[0]);
                setFile(files[0]);
                setFileUrl(blobUrl);
              }
            }}
          />
        </div>
      ) : (
        <div className="grid grid-cols-1 lg:grid-cols-2 gap-10">
          <div className="flex flex-col items-center">
            <object
              className="w-full h-96"
              type="application/pdf"
              data={fileUrl}
            ></object>
            {loading ? (
              <div className="text-center mt-10">Loading...</div>
            ) : (
              <button
                onClick={() => {
                  setFileUrl("");
                  setFile(null);
                }}
                className="px-4 mt-10 uppercase py-2 tracking-widest outline-none bg-red-600 text-white rounded"
              >
                Reset
              </button>
            )}
          </div>
          {summary && <SummaryData summary={summary} />}
        </div>
      )}
    </div>
)
  • Returns JSX code to render the file uploader component.
  • The JSX code includes conditional rendering based on the presence of fileUrl.
  • If fileUrl is present, it renders the uploaded file and the summary data. Otherwise, it renders the file uploader interface.
  • object element embeds a PDF document into an HTML document. It's used to display a PDF file within a web page.

Final File Upload component

"use client";
import openai from "@/libs/openai.lib";
import { useEffect, useState } from "react";
import SummaryData from "./summary_data";

export const FileUpload = () => {
  const [file, setFile] = useState<File | null>();
  const [fileUrl, setFileUrl] = useState<string>();
  const [fileEnter, setFileEnter] = useState(false);
  const [summary, setSummary] = useState<string>();
  const [loading, setLoading] = useState(false);
  const generateSummary = async (f: File) => {
    setLoading(true);
    var data = new FormData();
    data.append("file", f);
    const res = await fetch("http://localhost:3001/resume-ai-scanner", {
      method: "POST",
      body: data,
    });

    let respData = await res.json();
    console.log(respData)
    if (respData.error) {
      console.log(respData.error);
    } else {
      setSummary(respData.summary);
    }

    setLoading(false);
  };

  useEffect(() => {
    if (file) {
      generateSummary(file);
    }
  }, [file]);
  return (
    <div className="container px-4 max-w-5xl mx-auto">
      {!fileUrl ? (
        <div
          onDragOver={(e) => {
            e.preventDefault();
            setFileEnter(true);
          }}
          onDragLeave={(e) => {
            setFileEnter(false);
          }}
          onDragEnd={(e) => {
            e.preventDefault();
            setFileEnter(false);
          }}
          onDrop={(e) => {
            e.preventDefault();
            setFileEnter(false);
            if (e.dataTransfer.items) {
              [...e.dataTransfer.items].forEach((item, i) => {
                if (item.kind === "file") {
                  const file = item.getAsFile();
                  if (file) {
                    let blobUrl = URL.createObjectURL(file);
                    setFileUrl(blobUrl);
                    setFile(file);
                  }
                  console.log(`items file[${i}].name = ${file?.name}`);
                }
              });
            } else {
              [...e.dataTransfer.files].forEach((file, i) => {
                console.log(`… file[${i}].name = ${file.name}`);
              });
            }
          }}
          className={`${
            fileEnter ? "border-4" : "border-2"
          } mx-auto  bg-white flex flex-col w-full max-w-xs h-72 border-dashed items-center justify-center`}
        >
          <label
            htmlFor="file"
            className="h-full flex flex-col justify-center text-center"
          >
            Click to upload or drag and drop
          </label>
          <input
            id="file"
            type="file"
            className="hidden"
            onChange={(e) => {
              console.log(e.target.files);
              let files = e.target.files;
              if (files && files[0]) {
                let blobUrl = URL.createObjectURL(files[0]);
                setFile(files[0]);
                setFileUrl(blobUrl);
              }
            }}
          />
        </div>
      ) : (
        <div className="grid grid-cols-1 md:grid-cols-1 gap-10">
          <div className="flex flex-col items-center">
            <object
              className="w-full h-96"
              type="application/pdf"
              data={fileUrl}
            ></object>
            {loading ? (
              <div className="text-center mt-10">Loading...</div>
            ) : (
              <button
                onClick={() => {
                  setFileUrl("");
                  setFile(null);
                }}
                className="px-4 mt-10 uppercase py-2 tracking-widest outline-none bg-red-600 text-white rounded"
              >
                Reset
              </button>
            )}
          </div>
          {summary && <SummaryData summary={summary} />}
        </div>
      )}
    </div>
  );
};

Summary Component

import React from "react";

const SummaryData = ({ summary }: { summary: string }) => {
  return (
    <div>
      <h2 className="text-2xl mb-5">Scanned Results</h2>
      <div className="bg-white rounded py-2 px-4">
        <p className="text-slate-700 tracking-wider mt-5 font-light">{summary || ""}</p>
      </div>
    </div>
  );
};

export default SummaryData;

This code sets up a file uploader component in a React application using Next.js. It allows users to upload a resume file, generates a summary using OpenAI, and displays the uploaded file along with the generated summary.

Now you can start your node js server and next js application, upload your resume, and see it in action.

Congratulations now you have successfully built your resume scanner, also we’ve explored the fascinating intersection of web development and artificial intelligence by building a resume scanner app with OpenAI, Node JS, and Next.js. Now we know how to integrate OpenAI into your web applications, handle file uploads, extract text from PDFs, and generate AI-powered summaries. With the skills and knowledge gained from this project, you’re well-equipped to enhance your web apps with the capabilities of artificial intelligence. Happy Coding