Back to Blog
Guide February 25, 2026 · 4 min read

How to Generate a PDF from HTML in Node.js (Without Puppeteer)

The canonical Node.js answer for HTML-to-PDF is Puppeteer — but it pulls 200–400MB of Chromium into your dependency tree and breaks in serverless. Here's the one-fetch alternative.

The canonical Node.js answer for HTML-to-PDF is Puppeteer: spin up a headless Chromium, navigate to a page or set content, call page.pdf(). It works, but it pulls Chromium into your dependency tree, adds 200–400MB to your deployment, and breaks in serverless environments unless you configure a Chromium layer.

Here's the one-fetch alternative:

Basic usage

import fs from 'fs';

const html = `<!DOCTYPE html>
<html>
<head>
  <style>
    body { font-family: system-ui, sans-serif; padding: 40px; }
    h1 { font-size: 24px; margin-bottom: 8px; }
    .amount { font-size: 32px; font-weight: bold; color: #111; }
  </style>
</head>
<body>
  <h1>Invoice #1042</h1>
  <p>Due: March 1, 2026</p>
  <div class="amount">$429.00</div>
</body>
</html>`;

const res = await fetch('https://pagebolt.dev/api/v1/pdf', {
  method: 'POST',
  headers: {
    'x-api-key': process.env.PAGEBOLT_API_KEY,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ html })
});

fs.writeFileSync('invoice.pdf', Buffer.from(await res.arrayBuffer()));

No browser. No Chromium. One fetch, one file.

Capture a live URL instead

If the content is already on a URL (a hosted invoice, a report page, a dashboard), pass url instead of html:

const res = await fetch('https://pagebolt.dev/api/v1/pdf', {
  method: 'POST',
  headers: { 'x-api-key': process.env.PAGEBOLT_API_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    url: 'https://yourapp.com/invoices/1042',
    blockBanners: true
  })
});

Use in an Express route

import express from 'express';

const app = express();

app.get('/invoices/:id/pdf', async (req, res) => {
  const invoice = await getInvoice(req.params.id); // your data fetch
  const html = renderInvoiceHtml(invoice);          // your template function

  const capture = await fetch('https://pagebolt.dev/api/v1/pdf', {
    method: 'POST',
    headers: { 'x-api-key': process.env.PAGEBOLT_API_KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({ html })
  });

  const pdf = Buffer.from(await capture.arrayBuffer());

  res.setHeader('Content-Type', 'application/pdf');
  res.setHeader('Content-Disposition', `attachment; filename="invoice-${req.params.id}.pdf"`);
  res.send(pdf);
});

Use in AWS Lambda / Vercel Functions

No extra config needed. The capture is an outbound HTTPS call — it runs in any serverless environment without Chromium layers, memory tuning, or cold-start mitigation.

// Lambda handler
export const handler = async (event) => {
  const { html } = JSON.parse(event.body);

  const res = await fetch('https://pagebolt.dev/api/v1/pdf', {
    method: 'POST',
    headers: { 'x-api-key': process.env.PAGEBOLT_API_KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({ html })
  });

  const pdf = Buffer.from(await res.arrayBuffer());

  return {
    statusCode: 200,
    headers: { 'Content-Type': 'application/pdf' },
    body: pdf.toString('base64'),
    isBase64Encoded: true
  };
};

CSS in generated PDFs

All CSS in your <style> block renders in the PDF. A few practical notes:

  • Use pt or px units — em/rem relative to viewport can behave unexpectedly in print context
  • For page breaks, use page-break-before: always or break-before: page
  • Web fonts work if you include a <link> tag pointing to a publicly accessible font URL
  • Print-specific styles can be added in a @media print {} block

Beyond PDFs: add a narrated walkthrough

If you want to show users how a document was generated — useful for invoice portals or report builders — record a narrated video of the flow in the same API call pattern:

const res = await fetch('https://pagebolt.dev/api/v1/video', {
  method: 'POST',
  headers: { 'x-api-key': process.env.PAGEBOLT_API_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    steps: [
      { action: 'navigate', url: 'https://yourapp.com/invoices/1042', note: 'Open the invoice' },
      { action: 'click', selector: '#download-pdf', note: 'Download as PDF' }
    ],
    audioGuide: {
      enabled: true,
      voice: 'nova',
      script: "Here's your invoice. {{1}} {{2}} One click to download."
    },
    pace: 'slow'
  })
});
fs.writeFileSync('invoice-demo.mp4', Buffer.from(await res.arrayBuffer()));

Same pattern, one API, no extra tools.

Get Started Free

100 requests/month, no credit card

Generate PDFs from HTML or live URLs — no Puppeteer, no Chromium, no browser to manage.

Get Your Free API Key →