Four npm packages, one fetch() call.
pdf-parse, mammoth, xlsx, tesseract.js — each one solid at its single job, none of them aware the other three exist.
the-parser-zoo
Node's document-parsing story is a shelf of single-purpose packages rather than one library, and each has a real limitation worth knowing before you build around it. pdf-parse is a thin, long-unmaintained wrapper around Mozilla's pdf.js — it gets plain text out of a normal PDF, but has no OCR path and no graceful handling for encrypted or malformed files; a corrupt PDF throws instead of degrading. mammoth converts .docx to plain text or HTML — but only .docx: no .doc, no .pptx, no .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 — same trap as Python's openpyxl — and its API surface has shifted across versions in ways that break upgrades. tesseract.js runs real OCR via 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) and running CPU-bound recognition on Node's single main thread unless you wire up worker threads yourself.
| library | covers | stops at |
|---|---|---|
pdf-parse | Digital-native PDF text via pdf.js | No OCR; throws rather than degrading on encrypted or malformed PDFs |
mammoth | .docx → plain text or HTML, style-aware | docx only — no .doc, .pptx, or .xlsx; images are dropped, not described |
xlsx | Spreadsheet cells across .xlsx/.xls/.csv and more | Formula values depend on a pre-existing cached result, same as Excel's own file format quirk |
tesseract.js | In-process OCR via a WASM Tesseract build | Downloads 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 { "status": "success", "extracted_text": "..." } back, 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, so there's no per-format if/else to route a file to the right package before extraction can even start.
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, ships its own .d.ts.
npm install @txtfetch/sdkimport { 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:
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.
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 — 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.
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 with a fresh container on every cold start re-downloads them every time unless you bundle 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's 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 majors; 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": "..." } — and 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, so there's 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, so you can catch and branch on error.code without a bare try/catch around a library internals.
- Is there an official JavaScript or TypeScript SDK?
- Yes — npm install @txtfetch/sdk. It has zero runtime dependencies, ships both ESM and CJS builds with .d.ts types, and 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
other-languages
Stop parsing. Start shipping.
Create an account and get an API key in minutes — the free Hobby plan needs no card.
Get started →