Generate a PDF from Any URL in Node.js (Without Puppeteer)
There's a common pattern in web apps: user clicks "Export to PDF", and you need to turn a URL — a report page, an invoice, a dashboard — into a downloadable PDF file.
The go-to solution for years has been Puppeteer: launch a headless Chrome, navigate to the URL, call page.pdf(). It works. But it's a 150MB+ dependency, it needs a Chrome binary, and it fails silently in serverless environments.
Here's a lighter approach.
The Problem with Puppeteer in Production
// This is what most tutorials show you
const puppeteer = require('puppeteer');
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com/report/123');
const pdf = await page.pdf({ format: 'A4' });
await browser.close();
This works locally. But in production:
- Lambda/Vercel functions time out trying to spin up Chrome
- Docker images balloon in size
- Memory usage spikes on concurrent requests
- You need to manage the browser lifecycle yourself
A Simpler Way: API-Based PDF Generation
Instead of running a browser yourself, you can call an API that handles the headless rendering and returns a PDF buffer.
const fetch = require('node-fetch');
async function urlToPdf(url) {
const response = await fetch('https://api.renderpdfs.com/v1/pdf/url', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_API_KEY',
},
body: JSON.stringify({ url }),
});
if (!response.ok) {
throw new Error(`PDF generation failed: ${response.statusText}`);
}
return response.buffer();
}
That's it. No browser binary. No memory management. No 150MB dependency.
Full Example: Express Endpoint
const express = require('express');
const fetch = require('node-fetch');
const app = express();
app.get('/export-pdf', async (req, res) => {
const { url } = req.query;
if (!url) {
return res.status(400).json({ error: 'url param required' });
}
try {
const response = await fetch('https://api.renderpdfs.com/v1/pdf/url', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.RENDERPDFS_API_KEY}`,
},
body: JSON.stringify({ url }),
});
if (!response.ok) {
throw new Error(`API error: ${response.statusText}`);
}
const pdfBuffer = await response.buffer();
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', 'attachment; filename="export.pdf"');
res.send(pdfBuffer);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
app.listen(3000);
Call it with:
GET /export-pdf?url=https://yourapp.com/invoices/123
Passing Auth to the Target URL
If the URL you want to convert is behind authentication, you have two options:
Option 1: Use a pre-signed or token-based URL
// Generate a short-lived signed URL for the report
const signedUrl = generateSignedUrl('/invoices/123', { expiresIn: '5m' });
const pdf = await urlToPdf(signedUrl);
Option 2: Render HTML server-side and send it directly
// Render your template to HTML string
const html = await renderInvoiceHtml(invoiceId);
// Send the HTML directly instead of a URL
const response = await fetch('https://api.renderpdfs.com/v1/pdf', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.RENDERPDFS_API_KEY}`,
},
body: JSON.stringify({ html }),
});
Comparison
| Puppeteer | API Approach | |
|---|---|---|
| Setup | Chrome binary + config | npm install + API key |
| Cold start | 2–8s | ~200ms |
| Memory | 200–500MB | Minimal |
| Serverless | Painful | Works out of the box |
| Maintenance | Browser updates, crashes | Zero |
When to Use Each
Use Puppeteer if:
- You need full browser control (screenshots, interactions)
- You're already running a persistent server with enough memory
- You need to handle complex auth flows in the browser
Use the API approach if:
- You're on serverless (Lambda, Vercel, Render)
- You need PDF export as a feature, not a core product
- You want to ship fast and not maintain infrastructure
The API is free to start (100 PDFs/month) at renderpdfs.com. No credit card needed.
Top comments (0)