ResumeJSON

Resume parser Node.js: the npm packages that work, and the code

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

PackageLatest versionPublishedWhat it returnsMaintained?
pdfjs-dist6.3.28929 August 2026Text items from a PDF, with x/y position and font metadataYes
pdf-parse2.4.520 October 2025Text, images and tables from a PDFYes
unpdf1.8.113 August 2026PDF text across Node, browser and edge runtimesYes
mammoth1.12.312 September 2026HTML or Markdown from a .docxYes
officeparser8.0.016 September 2026Text from .docx, .pptx, .xlsx, .odt, PDFYes
textract2.5.03 June 2019Text from many formats, via system binariesNo — no release in seven years
resume-parser1.1.023 January 2018Name, email, phone and a few fieldsNo — 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 mammoth
import { 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:

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 heuristicsCall an API
Time to first working fieldAn afternoon for name and emailOne fetch
Two-column CVsYour problem, and the hard oneHandled
Non-English CVsYour problemHandled
Scanned or photographed CVsNeeds an OCR stack you also runHandled on the same endpoint
Ongoing maintenanceEvery template change is a bug reportNone
Cost at 40 CVs a monthYour time$2.00
Cost at 1,000 a monthYour time$29
Data leaves your processNoYes

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:

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

  1. Keep your reader. pdf-parse and mammoth stay useful for previews, text search and anything you do not need fields for.
  2. 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.
  3. Branch on meta.read. It comes back text when the document had a text layer and vision when the pages were read as images — useful for telling a user why an upload was slower than usual.
  4. Keep the x-request-id header 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.

All articles