PDF generation in Express
The most common Node setup, and the one where people reach for Puppeteer first — which means a Chromium on every box, a memory ceiling nobody planned for, and a browser that has to be patched.
The code
import express from 'express';
import { Readable } from 'node:stream';
const app = express();
app.use(express.json({ limit: '5mb' }));
app.post('/render', async (req, res) => {
const upstream = await fetch('https://api.pdfcraft.dev/v1/render', {
method: 'POST',
headers: {
authorization: `Bearer ${process.env.PDFCRAFT_API_KEY}`,
'content-type': 'application/json',
},
body: JSON.stringify({ html: req.body.html, options: { printBackground: true } }),
});
if (!upstream.ok) {
const { error } = await upstream.json();
return res.status(upstream.status).json({ error });
}
res.type('application/pdf');
res.setHeader('content-disposition', 'attachment; filename="document.pdf"');
Readable.fromWeb(upstream.body).pipe(res);
});What to watch for
- `Readable.fromWeb(upstream.body).pipe(res)` streams it. `res.send(await upstream.arrayBuffer())` works too but holds the whole PDF in memory for no reason.
- Raise the JSON body limit if you post large HTML — Express defaults to 100 kb and a document with inlined images passes that easily.
- If you are replacing Puppeteer: the reason to is that one browser per process is the hard part, and it is the part that breaks under concurrency rather than in testing.
Why not run the browser yourself
You can. Puppeteer and Playwright both work, and for a script you run by hand they are the right answer. The cost arrives at the point it becomes a service someone depends on: one long-lived browser per process rather than one per request, a semaphore so four renders do not become forty, a hard timeout that force-closes a hung page before it holds a slot forever, a scheduled restart because Chromium leaks, and a Chromium version you are now responsible for patching.
None of that is hard. All of it is work that has nothing to do with your product, and it fails under concurrency rather than in testing — which is the worst time to discover it.
Other frameworks
- PDF generation in Next.js
- PDF generation in Django
- PDF generation in Ruby on Rails
- PDF generation in Laravel
- PDF generation in AWS Lambda
- PDF generation in Cloudflare Workers
- PDF generation in Supabase Edge Functions
- PDF generation in Spring Boot
- PDF generation in FastAPI
- PDF generation in ASP.NET Core
- PDF generation in n8n