DEV Community

Hichem Bed
Hichem Bed

Posted on

Generate a PDF from Any URL in Node.js (Without Puppeteer)

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();
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Call it with:

GET /export-pdf?url=https://yourapp.com/invoices/123
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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 }),
});
Enter fullscreen mode Exit fullscreen mode

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)