Your backend just generated a PDF — an invoice, a shipping label, a ticket — and it needs to come out of a physical printer. Automatically. No human clicking through a print dialog.
window.print() is not an option: it only runs in a browser, it needs a user present, and the output depends on whichever browser, driver, and OS the user happens to have. For server-side printing you have three realistic options. Let's go through them with working code.
Option 1: Shell out to lp (same machine only)
If the printer is installed on the same machine your Node.js process runs on, the operating system's print spooler already does the hard part. On macOS and Linux that's CUPS, driven by lp:
import { execFile } from 'node:child_process';
execFile('lp', ['-d', 'Office_Printer', 'invoice.pdf'], (err, stdout) => {
if (err) throw err;
console.log(stdout); // request id is Office_Printer-42 (1 file(s))
});This is genuinely fine for a kiosk or a single-location setup, and you should use it when it fits. The limits show up fast, though:
- The printer must be installed and configured on the server itself. A cloud VM or container has no printers.
- "The spooler accepted it" is all you learn. Whether paper actually came out — jammed, offline, out of toner — you don't know without parsing
lpstatyourself. - Windows has no
lp; you're into PowerShell or third-party binaries, and the code stops being portable.
Option 2: Talk IPP to the printer directly
Most network printers speak IPP (Internet Printing Protocol). With the ipp npm package you can send a PDF straight to the printer, no OS spooler involved:
import ipp from 'ipp';
import { readFile } from 'node:fs/promises';
const printer = ipp.Printer('http://192.168.1.50:631/ipp/print');
const data = await readFile('./invoice.pdf');
printer.execute(
'Print-Job',
{ 'operation-attributes-tag': { 'document-format': 'application/pdf' }, data },
(err, res) => console.log(err ?? res)
);The catch is in that IP address: your server must be able to reach the printer. If your app runs in the cloud and the printer sits behind an office NAT, that means a VPN, a tunnel, or port-forwarding the printer to the public internet (please don't — exposed printers are a classic security hole). And IPP support in the real world is uneven: many cheap thermal and label printers implement just enough of it to be frustrating.
Option 3: A cloud printing API
The pattern that removes the network problem entirely: a small agent runs on any computer at the printer's location and holds an outbound connection to a cloud service. Your backend calls a normal HTTPS API; the cloud pushes the job down the already-open connection; the agent hands it to the local driver. No VPN, no exposed devices, printers anywhere in the world.
Here's the full flow with PrintBase. Setup is one-time:
- Sign up (the free plan is 50 jobs/month, no card) and create an API key.
- Install the agent on the computer next to the printer — Windows, macOS, or Linux. Every installed printer registers automatically and gets a numeric
printer_code.
Then printing a PDF from Node.js is one HTTPS call — no SDK required:
import { readFile } from 'node:fs/promises';
const pdf = await readFile('./invoice.pdf');
const res = await fetch('https://api.printbase.cloud/v1/print-jobs', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.PRINTBASE_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
printer_code: 1000000, // from GET /v1/printers or the dashboard
content_type: 'pdf',
content: pdf.toString('base64'),
copies: 1,
options: { duplex: false },
}),
});
const job = await res.json();
// { id: "job_abc", status: "queued" }If the PDF already lives at a URL (an S3 presigned link, your invoice endpoint), skip the base64 and let the agent download it — better for large files:
body: JSON.stringify({
printer_code: 1000000,
content_type: 'pdf',
content_url: 'https://example.com/invoices/inv-2041.pdf',
})Knowing it actually printed
This is the part lp never gave you. A job moves through queued → dispatched → printing → completed, and you can read it at any time:
const status = await fetch(
`https://api.printbase.cloud/v1/print-jobs/${job.id}`,
{ headers: { Authorization: `Bearer ${process.env.PRINTBASE_API_KEY}` } }
).then((r) => r.json());
console.log(status.status); // "completed"Failures come back as a status with a reason code instead of silence — AGENT_OFFLINE, PRINTER_OFFLINE, DOWNLOAD_FAILED, PRINT_ERROR, or INVALID_CONTENT — so your code can retry, alert, or route to a backup printer. For production you'll want webhooks instead of polling: subscribe in the dashboard and PrintBase calls your endpoint when a job completes or fails, with automatic retries if your endpoint is down.
Which one should you use?
| Your situation | Use |
|---|---|
| Node.js runs on the same machine as the printer, and "fire and forget" is acceptable | lp / the OS spooler |
| Printer is on the same network as the server, speaks IPP well, and you control that network | Direct IPP |
| App in the cloud, printers in shops/warehouses/offices; you need status back | Cloud printing API |
For most SaaS and e-commerce backends the honest answer is the third one — the first two quietly assume a network topology that production doesn't have.
Ready to try it? Start free — 50 jobs a month, no card — or read the API docs.
