Guide
Generate a PDF from HTML in Next.js
The usual advice is to install Puppeteer and run headless Chrome inside your app. That works on your laptop and falls over in production: Chromium does not fit in most serverless functions, cold starts add seconds, and it leaks memory under load. Here is the version that does not page you at 2am.
The problem with bundling Chrome
On Vercel and most serverless hosts, a full Chromium binary is too large for the function bundle, so you reach for @sparticuz/chromium and a thin wrapper. Now you own cold starts, memory limits, font loading, and the page-break quirks of print CSS. None of that is your product.
Do it with one fetch instead
Move rendering off your server. A route handler posts your HTML to a rendering API and streams the PDF straight back to the browser. No Chromium in your bundle.
// app/api/invoice/route.ts
export async function POST(req: Request) {
const { orderId } = await req.json();
const res = await fetch("https://api.pdfpipe.xyz/v1/pdf", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PDFPIPE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
html: `<h1>Invoice ${orderId}</h1>`,
options: { format: "A4" },
}),
});
if (!res.ok) {
const { detail } = await res.json();
return new Response(detail, { status: 502 });
}
return new Response(res.body, {
headers: {
"Content-Type": "application/pdf",
"Content-Disposition": `attachment; filename="invoice-${orderId}.pdf"`,
},
});
}Set PDFPIPE_API_KEY in your environment and that is the whole integration. The render waits for web fonts and images before drawing, honors break-inside: avoid so long invoice tables do not split mid-row, and returns a specific error instead of an opaque 500 when something is wrong.
Calling it from a button
async function downloadInvoice(orderId: string) {
const res = await fetch("/api/invoice", {
method: "POST",
body: JSON.stringify({ orderId }),
});
const blob = await res.blob();
const url = URL.createObjectURL(blob);
window.open(url);
}Why this scales
Your function stays tiny and fast because it never loads a browser. Rendering runs on infrastructure built for it, with isolation against malicious HTML (a real risk if any of your template data comes from users). You pay per document, not per server you have to keep warm.
PDFPipe is the API used above. Flat pricing, 500 free documents a month, and a live playground you can try without signing up.
See pricing →