Guide

Fill a PDF in Node.js

To fill a PDF form from Node you can run it in-process with pdf-lib, or POST the template to an HTTP API and get the filled PDF back. On Node 18+ the HTTP path needs zero dependencies — fetch and FormData are global — and the same code runs unchanged on Workers, Lambda, and edge functions. Here is both, with an honest read on which fits.

The fastest path: one fetch

PDFops fills AcroForm fields server-side. From modern Node it is a few lines, no packages installed:

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

const form = new FormData();
form.append("pdf", new Blob([await readFile("template.pdf")]), "template.pdf");
form.append("fields", JSON.stringify({
  customer_name: "Acme Co",
  invoice_total: "$1,250.00",
  paid: "Yes",
}));

const resp = await fetch("https://pdfops.dev/api/fill-form", {
  method: "POST",
  body: form,
});
if (!resp.ok) throw new Error(`fill failed: ${resp.status}`);
await writeFile("filled.pdf", Buffer.from(await resp.arrayBuffer()));

The keys in the fields object must match the AcroForm field names in the template. Run the PDF through the Form-Field Inspector to see the exact names and types. No API key or signup during beta.

Doing it in-process: pdf-lib

The local route is pure JavaScript and keeps everything in-process — no network call:

import { readFile, writeFile } from "node:fs/promises";
import { PDFDocument } from "pdf-lib";

const doc = await PDFDocument.load(await readFile("template.pdf"));
const form = doc.getForm();
form.getTextField("customer_name").setText("Acme Co");
form.getTextField("invoice_total").setText("$1,250.00");
// form.flatten();  // optional: bake values in, drop interactivity
await writeFile("filled.pdf", await doc.save());

This is a genuinely good library and for many backends it is the right answer. The work you take on is the form internals: getting the right typed accessor per field (getTextField vs getCheckBox vs getRadioGroup), deciding when to flatten(), and handling appearance/font cases on unusual templates. PDFops is built on a modernized fork of pdf-lib, so the API path is essentially that same engine offered as a managed, deterministic service — you trade a network call for not owning the edge cases.

Why this matters on serverless and the edge

The reason the fetch version is interesting isn’t brevity — it’s portability. Because the call is plain fetch + FormData, both Web-standard, the identical code runs on Cloudflare Workers, Vercel Edge, Deno Deploy, and Bun, not only Node. There is no headless Chromium to bundle and no native addon to compile, which is exactly what tends to block PDF tooling on those platforms.

PDFops (HTTP)pdf-lib (in-process)
Dependencies on Node 18+None — global fetch/FormDatapdf-lib (pure JS, no native addons)
Runs on Workers / Edge / Deno / BunYes — same code, unchangedYes — pure JS, but you bundle it
Network call requiredYes — one POST per fillNo — fully in-process
Form internals you manageNone — handled server-sideTyped accessors, flatten, appearances
Merge in the same toolYes — /api/mergeYes — copyPages + save
DeterminismDeterministic, audit-safeDeterministic (no AI either)
Best forNo-dependency serverless, fill+merge as a serviceIn-process control, no-network constraints

The honest summary: if you want the fill fully in-process and don’t mind owning form internals, pdf-lib is an excellent, free choice. If you want one deterministic API for fill and merge, identical behavior from local Node to the edge, and nothing to bundle or compile, the HTTP call is worth the round-trip.

Merging PDFs from Node too

The same primitive merges several PDFs into one in a single call:

const form = new FormData();
for (const path of ["a.pdf", "b.pdf", "c.pdf"]) {
  form.append("pdfs", new Blob([await readFile(path)]), path);
}
const resp = await fetch("https://pdfops.dev/api/merge", { method: "POST", body: form });
await writeFile("merged.pdf", Buffer.from(await resp.arrayBuffer()));

A common backend shape is fill-then-merge: fill several templates, then concatenate the results into one document to deliver. Both halves are the same deterministic primitive, so the combined output is reproducible. Endpoint details: fill-form docs, merge docs.

Frequently asked

How do I fill a PDF form in Node.js?

Either run pdf-lib in-process to set AcroForm values, or build a FormData with the template plus a JSON field map and POST it. On Node 18+, fetch/FormData/Blob are global, so the HTTP fill needs no dependencies and runs unchanged on Workers, Lambda, and edge functions.

Should I use pdf-lib or PDFops?

pdf-lib when you want the fill fully in-process with no network call and are comfortable managing form internals. PDFops when you want one deterministic API for fill and merge, identical behavior across Node and edge, and nothing to bundle. PDFops is built on a modernized pdf-lib fork, so the API gives you that engine as a managed service.

Can I fill a PDF on Cloudflare Workers or Vercel Edge?

Yes — the call is plain fetch + FormData, both Web-standard, so the same code runs on Workers, Vercel Edge, Deno Deploy, and Bun. No headless browser, no native addon — which is the usual blocker for PDF tooling on those platforms.

Do I need a library to call PDFops from Node?

No. On Node 18+, fetch/FormData/Blob are global, so a fill is a few lines with zero dependencies. On older Node, add undici or node-fetch plus form-data, or just upgrade the runtime. There is no SDK to install during beta — the API is plain HTTP.

Is the fill deterministic and audit-safe?

Yes — same template plus same field values yields the same field-level output, no AI inference in the path. Outputs are diffable and reproducible, which matters when a filled PDF is a record you may need to defend. The argument is in this essay.

Try it in 30 seconds

No API key, no signup during beta. Paste the snippet above into a Node script, or explore fields in the playground. On another stack? See fill a PDF in Python and fill a PDF in JavaScript.

If the deterministic fill + merge primitive fits your usage, join the waitlist and tell me the forms you fill most — that signal is what the pricing tiers and the in-function library get built around.