Resume parser Node.js: the npm packages that work, and the code
Published
If you are looking for a resume parser Node.js developers can npm install today, the honest answer is that npm has excellent document readers and no maintained resume-field parser. pdf-parse and unpdf will hand you the text of a PDF, mammoth will hand you the text of a .docx, and all three are actively published — but none of them knows what a job title is. The package literally called resume-parser last shipped a release in January 2018. So the decision is not which package to install; it is whether you write the field extraction yourself or call something that returns typed JSON.
Every version, date and download figure below was read from the npm registry, GitHub or the vendor's own page as of September 2026. We build ResumeJSON, a per-parse resume parsing API, so read the last section as written by an interested party — everything above it is checkable with one npm view, and you should check it.
The npm packages, compared
| Package | Latest version | Published | What it returns | Maintained? |
|---|---|---|---|---|
| pdfjs-dist | 6.3.289 | 29 August 2026 | Text items from a PDF, with x/y position and font metadata | Yes |
| pdf-parse | 2.4.5 | 20 October 2025 | Text, images and tables from a PDF | Yes |
| unpdf | 1.8.1 | 13 August 2026 | PDF text across Node, browser and edge runtimes | Yes |
| mammoth | 1.12.3 | 12 September 2026 | HTML or Markdown from a .docx | Yes |
| officeparser | 8.0.0 | 16 September 2026 | Text from .docx, .pptx, .xlsx, .odt, PDF | Yes |
| textract | 2.5.0 | 3 June 2019 | Text from many formats, via system binaries | No — no release in seven years |
| resume-parser | 1.1.0 | 23 January 2018 | Name, email, phone and a few fields | No — no release in eight years |
Two columns decide this. "What it returns" is the gap you will be filling yourself: every maintained package on that list is a document reader, so everything between "here are 4,000 characters of text" and "here is this candidate's third-most-recent employer" is your code. "Maintained?" is the risk you take on, and it is sharper here than in most package choices, because resume parsing is a pile of heuristics about how humans format documents, and those formats keep moving.
The weekly download figures say the same thing from another angle. In the week ending 20 September 2026, pdfjs-dist was downloaded about 19.7 million times, pdf-parse about 6.2 million and mammoth about 6.1 million. resume-parser was downloaded 60 times. The ecosystem has settled on reading documents in JavaScript, and has not settled on parsing resumes in it.
Reading the file: the part Node does well
Whatever route you choose, you need the document's text, and in Node that is a solved problem.
npm install pdf-parse mammothimport { readFile } from 'node:fs/promises'
import { PDFParse } from 'pdf-parse'
import mammoth from 'mammoth'
async function textOf (path) {
if (path.endsWith('.pdf')) {
const parser = new PDFParse({ data: await readFile(path) })
const { text } = await parser.getText()
return text
}
const { value } = await mammoth.extractRawText({ path })
return value
}That is fifteen lines and it covers the two formats candidates actually send. unpdf is the one to reach for instead of pdf-parse if the same code has to run on Cloudflare Workers, Deno or in a browser — it describes itself as "PDF extraction and rendering across all JavaScript runtimes", which is a real constraint if your upload handler lives at the edge. officeparser is the single-dependency option when you also have to accept .pptx or .odt.
What none of them gives you is a candidate. You have text. Nothing in it is labelled.
Route one: write the heuristics yourself
This is a legitimate choice, and there is a good open-source reference for how far it gets you. OpenResume is a TypeScript resume builder and parser whose parser playground documents its own algorithm in full: it reads text items with Mozilla's pdf.js, joins broken items into lines, groups lines into sections by looking for a line that is bolded and uppercase, and then scores each text item against hand-written feature sets — a name gets +3 for containing only letters, spaces or periods, +2 for being bolded, and -4 for containing an @, which is the email's signature rather than the name's.
It is the clearest explanation of resume heuristics published anywhere, and it is worth reading before you write your own. It also states its own bounds, on the same page:
Note that the algorithm is designed to parse single column resume in English language
Three things follow from that, and they are the three things that make this route expensive:
- Layout. A two-column CV — the most common template on every resume-builder site — breaks line grouping, because two unrelated columns share a y coordinate.
- Language. The section-title fallback is keyword matching against English words. A German or Indonesian CV has no
EXPERIENCEheading to find. - Scanned pages. A PDF exported from a phone scanner has no text layer at all, so there is nothing for pdf.js to read and nothing for any of the packages above to return.
OpenResume's repository was last pushed on 29 October 2024, and its algorithm write-up is signed June 2023. That is not a criticism of a project that does exactly what it says and does it in the browser, where "File data is used locally and never leaves your browser" is a genuine feature. It is a warning about what you are signing up for if you copy the approach: the heuristics are the product, and they need maintaining for as long as you have users.
Route two: call something that returns fields
The other route is to send the document somewhere that already owns those heuristics and get typed JSON back. In Node that is a fetch, which is why there is no SDK to weigh up:
import { readFile } from 'node:fs/promises'
const form = new FormData()
form.set('file', new Blob([await readFile('cv.pdf')]), 'cv.pdf')
const res = await fetch('https://resumejson-resume-cv-parser-api.p.rapidapi.com/v1/parse', {
method: 'POST',
headers: {
'x-rapidapi-key': process.env.RAPIDAPI_KEY,
'x-rapidapi-host': 'resumejson-resume-cv-parser-api.p.rapidapi.com'
},
body: form
})
const { resume, meta } = await res.json()
console.log(resume.basics.name, meta.read, meta.durationMs)FormData and Blob are global in Node 18 and later, so that snippet has no dependencies at all. The same endpoint takes a JSON body when you already have the text, and raw bytes when you are streaming an upload straight through.
A few vendors publish a typed client instead. Affinda maintains @affinda/affinda on npm (7.7.1, published 10 December 2025), and LlamaParse is reached from JavaScript through the llamaindex package — though LlamaParse returns a document's content, in Markdown or text, rather than resume fields, so it belongs on the first list rather than this one. If your reason for reaching for a JS client is types, a JSON response and your own interface gets you there without a dependency.
What each route actually costs you
| Own the heuristics | Call an API | |
|---|---|---|
| Time to first working field | An afternoon for name and email | One fetch |
| Two-column CVs | Your problem, and the hard one | Handled |
| Non-English CVs | Your problem | Handled |
| Scanned or photographed CVs | Needs an OCR stack you also run | Handled on the same endpoint |
| Ongoing maintenance | Every template change is a bug report | None |
| Cost at 40 CVs a month | Your time | $2.00 |
| Cost at 1,000 a month | Your time | $29 |
| Data leaves your process | No | Yes |
That last row is the one that should decide it for some teams, and it is a real point for route one. If the CV may not leave your infrastructure, an in-process reader plus your own extraction is the answer, and OpenResume's browser-local design is the model to copy.
For everyone else, the arithmetic is unkind to route one. The ResumeJSON plans are pay-per-use at $0.05 a parse with no monthly fee, $29 a month for 1,000 parses, and $99 a month for 5,000 — so a month with 40 CVs costs $2.00 and a month with none costs nothing. A single afternoon of your own time spent on two-column layouts costs more than the first year of the $29 tier, and the layouts keep arriving after that afternoon ends.
Who should stay on the packages
Be honest about which of these you are:
- You need text, not fields. If your pipeline feeds a search index or your own model,
pdf-parse,unpdforofficeparseris the whole answer and an API is a pointless hop. - The document cannot leave your network. A compliance constraint beats a cost argument. Own the heuristics, and budget for them.
- You parse a handful of CVs from one known template. A regex over known text is fine. Do not build a platform for eleven documents.
- You are learning how parsing works. Read OpenResume's algorithm page and implement it. It is a genuinely good exercise.
Reach for an API when CVs arrive in formats you do not control, in languages you did not plan for, and in volumes where a wrong employer field is somebody's support ticket rather than your curiosity.
Moving from packages to an API, in an afternoon
- Keep your reader.
pdf-parseandmammothstay useful for previews, text search and anything you do not need fields for. - Send the original bytes, not your extracted text. A scanned page has no text layer, and a parser that gets the file can fall back to reading the page as an image. Handing it your empty string cannot.
- Branch on
meta.read. It comes backtextwhen the document had a text layer andvisionwhen the pages were read as images — useful for telling a user why an upload was slower than usual. - Keep the
x-request-idheader with whatever you log. It identifies one call, which saves you reproducing anything when a field comes back wrong.
If step one turns up everything you need from plain text, stay on the packages — that is the right result, and it cost you an afternoon rather than a migration.
Related: Python resume parser: the libraries that work, and the code, Resume parser PHP: what exists, the code, and when to call an API, and Open source resume parser: what works, and when to buy instead.