PDFPipe

Guide

HTML to PDF in Express.js

The two common approaches to PDF generation in an Express app both come with significant baggage: html-pdf wraps a deprecated PhantomJS binary, and Puppeteer ships 150 MB of Chromium into your Docker image. PDFPipe is an HTTP call that returns a fully rendered PDF, with nothing to install, nothing to maintain, and nothing to run alongside your server.

Route that streams a PDF

Build the HTML server-side, POST it to PDFPipe, and pipe the response body directly to the Express response. No temp files, no disk writes:

javascript
// npm install express node-fetch  (or use native fetch in Node 18+)
import express from "express";

const app = express();
const PDFPIPE_KEY = process.env.PDFPIPE_API_KEY;

app.get("/invoice/:id.pdf", async (req, res) => {
  const html = `
    <html>
    <body style="font-family: system-ui; padding: 40px">
      <h1>Invoice #${req.params.id}</h1>
      <p>Thank you for your order.</p>
    </body>
    </html>
  `;

  const upstream = await fetch("https://api.pdfpipe.xyz/v1/pdf", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${PDFPIPE_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ html, options: { format: "A4" } }),
  });

  if (!upstream.ok) {
    const err = await upstream.json().catch(() => ({}));
    return res.status(502).json({ error: err.detail ?? "render failed" });
  }

  res.setHeader("Content-Type", "application/pdf");
  res.setHeader("Content-Disposition", `attachment; filename="invoice-${req.params.id}.pdf"`);
  // Stream the response body directly to the client.
  const { Readable } = await import("stream");
  Readable.fromWeb(upstream.body).pipe(res);
});

app.listen(3000);

With a template engine (EJS)

Render your EJS (or Pug, Handlebars, etc.) template to an HTML string first, then hand it to PDFPipe. The template runs on your server so all your data is already available when the render starts:

javascript
// Render an EJS template, then pass the HTML to PDFPipe.
// npm install ejs
import ejs from "ejs";

app.get("/report/:id.pdf", async (req, res) => {
  const data = await getReportData(req.params.id); // your data source

  const html = await ejs.renderFile("views/report.ejs", { data });

  const upstream = await fetch("https://api.pdfpipe.xyz/v1/pdf", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.PDFPIPE_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ html, options: { format: "A4", landscape: false } }),
  });

  if (!upstream.ok) return res.sendStatus(502);
  res.setHeader("Content-Type", "application/pdf");
  res.setHeader("Content-Disposition", `attachment; filename="report-${req.params.id}.pdf"`);
  const { Readable } = await import("stream");
  Readable.fromWeb(upstream.body).pipe(res);
});

Store the PDF and return a URL

Pass store: true to keep the document on the CDN and get a retrieval URL back. Useful when you want to email a link, log the URL in your database, or generate the PDF in a background job and serve it later:

javascript
// Store the PDF and return a download URL instead of streaming.
app.post("/invoices", express.json(), async (req, res) => {
  const html = buildInvoiceHtml(req.body); // your template function

  const upstream = await fetch("https://api.pdfpipe.xyz/v1/pdf", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.PDFPIPE_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      html,
      options: { format: "A4" },
      store: true,
      filename: `invoice-${req.body.orderId}.pdf`,
    }),
  });

  if (!upstream.ok) return res.sendStatus(502);

  const docId  = upstream.headers.get("X-PDFPipe-Document-Id");
  const docUrl = upstream.headers.get("X-PDFPipe-Document-Url");
  const expiresAt = upstream.headers.get("X-PDFPipe-Document-Expires");

  res.json({ docId, docUrl, expiresAt });
});

Getting an API key

The Hobby plan gives 500 free documents a month with no credit card required. Paid plans start at $19 and include a longer document archive, higher limits, and email support.

Full API reference on the docs page. Try it now in the live playground with no signup.

Get an API key →