Your backend fetches the data, renders it to HTML, and POSTs to the API. Users get a PDF download button. No Chromium on your server.
Start freeChromium exceeds the 250 MB unzipped code limit. Requires a Lambda Layer that is painful to keep updated.
Edge Functions block Node.js native modules. Puppeteer cannot run here at all.
No file system, no native binaries. Puppeteer is completely incompatible.
The pattern for private dashboards: your API route fetches the same data the dashboard displays, builds an HTML template from it, then calls the PDF API. The user gets a file download. No browser, no auth tokens passed to a third party.
export async function POST(request: Request) {
const { reportId } = await request.json() as { reportId: string }
// 1. Fetch your data server-side (auth handled here)
const data = await fetchReportData(reportId)
// 2. Build the HTML from your data
const html = `
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: sans-serif; padding: 32px; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 8px 12px; border-bottom: 1px solid #e5e7eb; text-align: left; }
th { background: #f8fafc; font-weight: 600; }
</style>
</head>
<body>
<h1>${data.title}</h1>
<p>Period: ${data.period}</p>
<table>
<thead><tr>${data.columns.map((c: string) => `<th>${c}</th>`).join('')}</tr></thead>
<tbody>${data.rows.map((r: string[]) => `<tr>${r.map(v => `<td>${v}</td>`).join('')}</tr>`).join('')}</tbody>
</table>
</body>
</html>
`
// 3. Generate the PDF
const res = await fetch('https://platform.htmltopdfapi.co/api/v1/pdf/generate', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.HTMLTOPDF_API_KEY!}`,
'Content-Type': 'application/json',
Accept: 'application/pdf',
},
body: JSON.stringify({ html, paper_size: 'a4', orientation: 'landscape' }),
})
if (!res.ok) return Response.json({ error: 'Export failed' }, { status: 500 })
return new Response(await res.arrayBuffer(), {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': 'attachment; filename="dashboard.pdf"',
},
})
}async function exportDashboardPdf() {
const res = await fetch('/api/export-pdf', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ reportId: currentReportId }),
})
if (!res.ok) throw new Error('Export failed')
const blob = await res.blob()
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = 'dashboard.pdf'
link.click()
URL.revokeObjectURL(link.href)
}If your dashboard is publicly accessible (or behind a token-based preview URL), you can pass the URL directly instead of building HTML server-side. Use wait_until: networkidle0 to ensure JS-rendered charts finish loading before the PDF is captured.
{
"url": "https://your-app.com/dashboard/report?token=preview_abc123",
"paper_size": "a4",
"orientation": "landscape",
"wait_until": "networkidle0"
}200 pages/day on the free tier. No credit card required.
Get your free API key