Node.js Guide
HTML to PDF in Node.js
Generate PDFs from HTML in any Node.js application — no Puppeteer, no Chromium install, no memory spikes. One POST request, one PDF back.
Why not just use Puppeteer?
Puppeteer works, but it brings 300MB+ of Chromium into your Docker image, consumes ~200MB RAM per concurrent request, and serializes under load unless you build a browser pool yourself. On serverless platforms (Vercel, Lambda) it often doesn't work at all. Papyr handles the Chromium infrastructure — your Node.js process makes an HTTP request and gets a PDF back.
Basic usage
No SDK needed. Use native fetch (Node 18+) or any HTTP client.
// Works in Node.js 18+ with native fetch
async function htmlToPdf(html: string): Promise<Buffer> {
const response = await fetch("https://api.getpapyr.dev/v1/render/pdf", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.PAPYR_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
html,
options: {
format: "A4",
margin: { top: "20mm", right: "20mm", bottom: "20mm", left: "20mm" },
},
}),
});
if (!response.ok) {
const err = await response.json();
throw new Error(err.error ?? `HTTP ${response.status}`);
}
return Buffer.from(await response.arrayBuffer());
}
// Usage
const pdf = await htmlToPdf("<h1>Hello, PDF!</h1>");
fs.writeFileSync("output.pdf", pdf);Express route
Return a PDF directly from an Express endpoint — browser triggers a download.
import express from "express";
const app = express();
app.use(express.json());
app.post("/invoices/:id/pdf", async (req, res) => {
const invoice = await db.invoices.findById(req.params.id);
const html = renderInvoiceHtml(invoice); // your template function
const response = await fetch("https://api.getpapyr.dev/v1/render/pdf", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.PAPYR_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ html }),
});
if (!response.ok) {
res.status(500).json({ error: "PDF generation failed" });
return;
}
const pdf = Buffer.from(await response.arrayBuffer());
res.set({
"Content-Type": "application/pdf",
"Content-Disposition": `attachment; filename="invoice-${invoice.number}.pdf"`,
"Content-Length": pdf.length,
});
res.send(pdf);
});Next.js App Router
Works in Next.js API routes. The API key stays server-side — never exposed to the browser.
// app/api/invoices/[id]/pdf/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const invoice = await db.invoices.findById(params.id);
const html = renderInvoiceHtml(invoice);
const response = await fetch("https://api.getpapyr.dev/v1/render/pdf", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.PAPYR_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ html }),
});
if (!response.ok) {
return NextResponse.json({ error: "PDF generation failed" }, { status: 500 });
}
const pdf = await response.arrayBuffer();
return new NextResponse(pdf, {
headers: {
"Content-Type": "application/pdf",
"Content-Disposition": `attachment; filename="invoice-${params.id}.pdf"`,
},
});
}Store on S3
Generate the PDF and pipe it directly to S3 for storage — no temp files on disk.
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
const s3 = new S3Client({ region: "us-east-1" });
async function generateAndStore(html: string, key: string) {
const response = await fetch("https://api.getpapyr.dev/v1/render/pdf", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.PAPYR_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ html }),
});
const pdf = Buffer.from(await response.arrayBuffer());
await s3.send(new PutObjectCommand({
Bucket: "my-documents",
Key: key,
Body: pdf,
ContentType: "application/pdf",
}));
return `https://my-documents.s3.amazonaws.com/${key}`;
}Environment setup
Store your API key in an environment variable — never hardcode it.
PAPYR_API_KEY=pk_live_...Get your API key
Free tier: 100 PDFs/month. No credit card required.