Four npm packages, one fetch() call.

pdf-parse, mammoth, xlsx, tesseract.js: each one solid at its single job. None of them knows the other three exist.

the-parser-zoo

Node's document-parsing story is a shelf of single-purpose packages, not one library. Each package has a real limitation worth knowing before you build around it. pdf-parse is a thin wrapper around Mozilla's pdf.js, and it has not seen much maintenance. It gets plain text out of a normal PDF. But it has no OCR path, and it has no graceful handling for encrypted or malformed files. A corrupt PDF throws an error instead of degrading gracefully. mammoth converts .docx to plain text or HTML, but only .docx. It has nothing for .doc, .pptx, or .xlsx, and it deliberately drops images rather than describing them. xlsx (the ubiquitous SheetJS-derived package) reads spreadsheet cells across a wide format range. But formula evaluation depends on the workbook already carrying a cached result, the same trap as Python's openpyxl. Its API surface has also shifted across versions in ways that break upgrades. tesseract.js runs real OCR through a WASM build of Tesseract, which is genuinely impressive for a browser. But on a server it means downloading language-model data at cold start, a real problem in serverless. It also means running CPU-bound recognition on Node's single main thread, unless you wire up worker threads yourself.

librarycoversstops at
pdf-parseDigital-native PDF text via pdf.jsNo OCR; throws rather than degrading on encrypted or malformed PDFs
mammoth.docx → plain text or HTML, style-awaredocx only — no .doc, .pptx, or .xlsx; images are dropped, not described
xlsxSpreadsheet cells across .xlsx/.xls/.csv and moreFormula values depend on a pre-existing cached result, same as Excel's own file format quirk
tesseract.jsIn-process OCR via a WASM Tesseract buildDownloads language data at cold start; CPU-bound recognition needs manual worker-thread wiring to avoid blocking

one-request

txtfetch collapses the four-package stack into one endpoint. POST a file, or pass ?url=, and get back { "status": "success", "extracted_text": "..." }. That is true regardless of whether the source needed pdf-parse's job, mammoth's, xlsx's, or tesseract.js's. The official @txtfetch/sdk wraps the same request in a zero-dependency TypeScript client, with typed errors. There is no per-format if/else needed to route a file to the right package before extraction can even start.

JavaScript
import { readFile } from "node:fs/promises";

const file = new Blob([await readFile("report.pdf")]);
const form = new FormData();
form.append("file", file, "report.pdf");

const res = await fetch("https://api.txtfetch.com/v1/extract", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.TXTFETCH_KEY}` },
  body: form,
});

const { extracted_text } = await res.json();
console.log(extracted_text);
{
  "status": "success",
  "extracted_text": "..."
}

@txtfetch/sdk (JS/TS)

npm install @txtfetch/sdk. Zero runtime dependencies, dual ESM/CJS, and it ships its own .d.ts.

Install
npm install @txtfetch/sdk
Quickstart
import { Txtfetch } from "@txtfetch/sdk";

// apiKey defaults to process.env.TXTFETCH_KEY
const txtfetch = new Txtfetch();

const { extracted_text, metadata } = await txtfetch.extract({ file: "./whitepaper.pdf" });
console.log(extracted_text, metadata.chars);

const byUrl = await txtfetch.extract({ url: "https://example.com/report.docx" });
console.log(byUrl.extracted_text);

from-a-url

Skip the download entirely. Pass a url parameter and txtfetch fetches the document server-side:

JavaScript
const endpoint = new URL("https://api.txtfetch.com/v1/extract");
endpoint.searchParams.set("url", "https://example.com/report.pdf");

const res = await fetch(endpoint, {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.TXTFETCH_KEY}` },
});

const { extracted_text } = await res.json();
console.log(extracted_text);

errors

Every non-success response carries a stable error.code. Match on that, not on error.message. See the full error reference for every code and HTTP status txtfetch can return.

JavaScript
import { readFile } from "node:fs/promises";

const file = new Blob([await readFile("report.pdf")]);
const form = new FormData();
form.append("file", file, "report.pdf");

const res = await fetch("https://api.txtfetch.com/v1/extract", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.TXTFETCH_KEY}` },
  body: form,
});
const body = await res.json();

if (res.ok) {
  console.log(body.extracted_text);
} else if (res.status === 429) {
  console.log(`back off: ${body.error.code}, retry after ${res.headers.get("Retry-After")}s`);
} else {
  console.log(`extraction failed: ${body.error.code} — ${body.error.message}`);
}

big-files-and-batches

Large uploads or slow documents are routed to an async job automatically. That returns a 202 plus a job_id to poll, and ?async=true forces that path for any request. Direct upload size ceilings by plan: Hobby 10 MB, Developer 50 MB, Scale 200 MB. Use ?url= for anything larger. Server-side fetches aren't held to the upload ceiling. See async jobs & webhooks for the full lifecycle, including webhook delivery instead of polling.

JavaScript
const headers = { Authorization: `Bearer ${process.env.TXTFETCH_KEY}` };

const submitUrl = new URL("https://api.txtfetch.com/v1/extract");
submitUrl.searchParams.set("url", "https://example.com/report.pdf");
submitUrl.searchParams.set("async", "true");

const submit = await fetch(submitUrl, { method: "POST", headers });
const { job_id } = await submit.json();

let result;
do {
  await new Promise((resolve) => setTimeout(resolve, 2000));
  const poll = await fetch(`https://api.txtfetch.com/v1/extract/${job_id}`, { headers });
  result = await poll.json();
} while (result.status === "processing");

console.log(result.extracted_text);

gotchas

  • tesseract.js's default setup fetches its language traineddata files over the network, on first use. A serverless function gets a fresh container on every cold start. It re-downloads the language files every time, unless you bundle them and point at a local path.
  • pdf-parse hasn't seen substantial maintenance in years. Encrypted PDFs, and some malformed ones, throw an unhandled exception rather than a typed error you can catch and branch on.
  • mammoth's plain-text mode discards inline images entirely. That is correct behavior for a text-extraction step. But it means image-embedded diagrams inside a .docx contribute nothing to the output, silently.
  • xlsx's own maintainers have shipped breaking API changes across major versions. Pin the version deliberately, rather than accepting ^ ranges, in a document-ingestion pipeline you don't want to babysit.

formats

faq

How do I parse a DOCX file in Node.js without mammoth?
POST the .docx to https://api.txtfetch.com/v1/extract, or call txtfetch.extract({ file: "..." }) with the @txtfetch/sdk. You get back { "status": "success", "extracted_text": "..." }. The same call also handles .pptx, .xlsx, and legacy .doc, which mammoth doesn't.
Does tesseract.js need any setup to avoid slow cold starts?
With txtfetch, there's no tesseract.js in your own deployment at all. OCR runs server-side on request. There is no WASM bundle to ship, and no language-model download to warm up on a fresh serverless container.
Why does my PDF parsing code throw on some files but not others?
pdf-parse throws on encrypted or malformed PDFs rather than returning a typed error. txtfetch returns a structured { "error": { "code": "encrypted", ... } } or unsupported_format response instead. You can catch and branch on error.code, without a bare try/catch around a library's internals.
Is there an official JavaScript or TypeScript SDK?
Yes. npm install @txtfetch/sdk. It has zero runtime dependencies. It ships both ESM and CJS builds, with .d.ts types. It requires Node ≥ 20.
Can I use txtfetch as a LangChain.js document loader?
Yes. @txtfetch/langchain wraps the SDK as a TxtfetchLoader. Each file or URL becomes one Document, ready for a text splitter. See /docs/quickstarts for the install and a ten-line example.

go-further

Paste it into your project.

The JavaScript snippet above runs as written. Add your key and it works.

Get an API key →

More SDK quickstarts →