Tools: How to generate a PDF from HTML in Node.js (without Puppeteer)

Tools: How to generate a PDF from HTML in Node.js (without Puppeteer)

Source: Dev.to

How to Generate a PDF from HTML in Node.js (Without Puppeteer) ## Basic usage ## Capture a live URL instead ## Use in an Express route ## Use in AWS Lambda / Vercel Functions ## CSS in generated PDFs ## Beyond PDFs: add a narrated walkthrough 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: No browser. No Chromium. One fetch, one file. If the content is already on a URL (a hosted invoice, a report page, a dashboard), pass url instead of html: 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. All CSS in your <style> block renders in the PDF. A few practical notes: 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: Same pattern, one API, no extra tools. Try it free — 100 requests/month, no credit card. → pagebolt.dev Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK: 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())); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: 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())); CODE_BLOCK: 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())); CODE_BLOCK: 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 }) }); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: 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 }) }); CODE_BLOCK: 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 }) }); COMMAND_BLOCK: 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); }); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: 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); }); COMMAND_BLOCK: 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); }); COMMAND_BLOCK: // 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 }; }; Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: // 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 }; }; COMMAND_BLOCK: // 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 }; }; CODE_BLOCK: 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())); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: 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())); CODE_BLOCK: 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())); - 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