Tutorial

Generate PDF from URL in Node.js: Puppeteer, Playwright & API (2026)

Generating a PDF from a URL in Node.js comes down to three approaches in 2026: Puppeteer, Playwright, or a PDF API. This post gives you working code for all three, plus patterns for Express.js and serverless environments.

Method 1: Puppeteer

npm install puppeteer
import puppeteer from 'puppeteer';

async function urlToPDF(url) {
  const browser = await puppeteer.launch({
    headless: 'new',
    args: ['--no-sandbox', '--disable-setuid-sandbox']
  });

  const page = await browser.newPage();

  // Navigate and wait for all network activity to settle
  await page.goto(url, {
    waitUntil: 'networkidle0',  // stricter than networkidle2 — waits for 0 connections
    timeout: 30000
  });

  // Wait for fonts to render (prevents FOUT)
  await page.evaluate(() => document.fonts.ready);

  const pdfBuffer = await page.pdf({
    format: 'A4',
    printBackground: true,   // include background colors/images
    margin: {
      top: '20mm',
      bottom: '20mm',
      left: '15mm',
      right: '15mm'
    },
    // Optional: page ranges
    // pageRanges: '1-3',
    // Optional: header/footer
    // displayHeaderFooter: true,
    // headerTemplate: '',
    // footerTemplate: ' of ',
  });

  await browser.close();
  return pdfBuffer; // Buffer
}

// Save to file
import { writeFileSync } from 'fs';
const pdf = await urlToPDF('https://yourapp.com/invoice/123');
writeFileSync('./invoice.pdf', pdf);

Reuse the browser for multiple PDFs

import puppeteer from 'puppeteer';

class PDFService {
  constructor() {
    this.browser = null;
  }

  async init() {
    this.browser = await puppeteer.launch({
      headless: 'new',
      args: ['--no-sandbox', '--disable-setuid-sandbox']
    });
  }

  async generate(url, options = {}) {
    if (!this.browser) await this.init();

    const page = await this.browser.newPage();
    try {
      await page.goto(url, { waitUntil: 'networkidle0', timeout: 30000 });
      await page.evaluate(() => document.fonts.ready);
      return await page.pdf({
        format: options.format || 'A4',
        printBackground: true,
        margin: options.margin || { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
      });
    } finally {
      await page.close(); // always close the page
    }
  }

  async shutdown() {
    if (this.browser) await this.browser.close();
  }
}

// Singleton — reuse across requests
export const pdfService = new PDFService();

Method 2: Playwright

npm install playwright
npx playwright install chromium
import { chromium } from 'playwright';

async function urlToPDF(url) {
  const browser = await chromium.launch();
  const page = await browser.newPage();

  await page.goto(url, { waitUntil: 'networkidle' });
  await page.evaluate(() => document.fonts.ready);

  const pdfBuffer = await page.pdf({
    format: 'A4',
    printBackground: true,
    margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' }
  });

  await browser.close();
  return pdfBuffer;
}

Playwright and Puppeteer have nearly identical PDF APIs. The difference: Playwright uses waitUntil: 'networkidle' instead of 'networkidle0', and provides browser contexts for better session isolation across concurrent requests.

Method 3: PDF API

Send a URL, get a PDF back. No Chromium binary, no memory management, works anywhere including serverless.

async function urlToPDF(url) {
  const res = await fetch('https://api.snapapi.pics/v1/screenshot', {
    method: 'POST',
    headers: {
      'X-Api-Key': process.env.SNAPAPI_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      url,
      format: 'pdf',
      pdf_format: 'A4',            // A4, Letter, Legal, Tabloid, A3
      pdf_print_background: true,  // include backgrounds
      pdf_margin_top: '20mm',
      pdf_margin_bottom: '20mm',
      pdf_margin_left: '15mm',
      pdf_margin_right: '15mm',
      // Optional: wait for dynamic content
      wait_for: '.invoice-ready',  // CSS selector
      delay: 1000,                 // additional wait in ms
    }),
  });

  if (!res.ok) throw new Error(`PDF API error: ${res.status}`);

  const { url: pdfUrl } = await res.json();
  return pdfUrl; // CDN URL valid for 24h
}

// Usage
const pdfUrl = await urlToPDF('https://yourapp.com/invoice/123?token=abc');
// Redirect user to pdfUrl, or download and proxy the bytes

Download as Buffer

async function urlToPDFBuffer(url) {
  const apiRes = await fetch('https://api.snapapi.pics/v1/screenshot', {
    method: 'POST',
    headers: { 'X-Api-Key': process.env.SNAPAPI_KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({ url, format: 'pdf', pdf_format: 'A4', pdf_print_background: true }),
  });
  const { url: pdfUrl } = await apiRes.json();

  const pdfRes = await fetch(pdfUrl);
  return Buffer.from(await pdfRes.arrayBuffer());
}

Express.js: PDF Download Endpoint

import express from 'express';
import { authenticateRequest } from './middleware/auth.js';

const app = express();

// GET /invoices/:id/pdf — download as PDF
app.get('/invoices/:id/pdf', authenticateRequest, async (req, res) => {
  const { id } = req.params;

  // Generate a signed URL to your invoice render view
  const token = generateSignedToken(id, req.user.id, { expiresIn: '5m' });
  const invoiceUrl = `${process.env.APP_URL}/invoices/${id}/render?token=${token}`;

  try {
    // Option A: redirect to CDN URL (fast, no bandwidth cost)
    const apiRes = await fetch('https://api.snapapi.pics/v1/screenshot', {
      method: 'POST',
      headers: { 'X-Api-Key': process.env.SNAPAPI_KEY, 'Content-Type': 'application/json' },
      body: JSON.stringify({ url: invoiceUrl, format: 'pdf', pdf_format: 'A4', pdf_print_background: true }),
    });
    const { url: pdfUrl } = await apiRes.json();
    return res.redirect(pdfUrl);

    // Option B: proxy bytes (for Content-Disposition download)
    // const pdfBytes = await fetch(pdfUrl).then(r => r.arrayBuffer());
    // res.set({
    //   'Content-Type': 'application/pdf',
    //   'Content-Disposition': `attachment; filename="invoice-${id}.pdf"`,
    //   'Content-Length': pdfBytes.byteLength,
    // });
    // res.send(Buffer.from(pdfBytes));

  } catch (err) {
    console.error('PDF generation failed:', err);
    res.status(500).json({ error: 'PDF generation failed' });
  }
});

Serverless: AWS Lambda

Puppeteer and Playwright don't run reliably on Lambda without complex layer setups (250MB+ Chromium layer, cold start penalty). The PDF API approach is the recommended pattern for serverless.
// Lambda handler (Node.js 20.x)
export const handler = async (event) => {
  const { invoiceId, userId } = JSON.parse(event.body);

  // Validate, generate signed URL, etc.
  const invoiceUrl = `https://yourapp.com/invoices/${invoiceId}/render?token=${signedToken}`;

  const res = await fetch('https://api.snapapi.pics/v1/screenshot', {
    method: 'POST',
    headers: { 'X-Api-Key': process.env.SNAPAPI_KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({ url: invoiceUrl, format: 'pdf', pdf_format: 'A4', pdf_print_background: true }),
  });

  const { url: pdfUrl } = await res.json();

  return {
    statusCode: 302,
    headers: { Location: pdfUrl },
    body: '',
  };
};

PDF Quality Tips

Whether you use Puppeteer, Playwright, or a PDF API, these settings consistently improve PDF quality:

  • Always set printBackground: true — background colors and images won't render without it
  • Use @media print CSS — add print-specific styles to hide navbars, sidebars, and interactive elements
  • Wait for fontsdocument.fonts.ready prevents FOUT (flash of unstyled text) in PDFs
  • Avoid networkidle2 for PDFs — use networkidle0 (Puppeteer) or networkidle (Playwright) for stricter loading
  • Set explicit viewport — PDF layout depends on the viewport width; match it to your design's breakpoint
  • Use page-break-inside: avoid on tables and cards to prevent awkward mid-element page breaks

When to Use Which

Method Best for Avoid when
Puppeteer Scripts, cron jobs, low volume High concurrency, serverless, web APIs
Playwright Self-hosted at low-medium volume Lambda, Vercel Edge, memory-constrained
PDF API Web apps, serverless, high volume Airgapped environments (no outbound HTTP)

Generate PDFs from any URL in Node.js with one fetch call

200 free PDFs/month. No Chromium binary, no memory leaks, works in Lambda and Cloudflare Workers.

Get your free API key →