Every Node.js project that needs PDF generation eventually ends up with the same 80 lines of code.
You know the ones. puppeteer.launch(), browser pool management, --no-sandbox flags that only work in some environments, memory limits, crash recovery, graceful shutdown. It's not hard — but it's 4–6 hours of work that has absolutely nothing to do with what you're actually building.
I copy-pasted this boilerplate three times across different projects before I decided to wrap it into an API. That API is Renderly.
What it does
One POST request. Your HTML in, a PDF out.
const res = await fetch('https://api.renderlyapi.com/v1/pdf/from-html', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
html: invoiceHTML,
format: 'A4',
}),
});
const pdf = await res.arrayBuffer();
// stream it, save it, attach it to an email — done
No SDK to install. No Puppeteer config. No --no-sandbox archaeology. You get back a binary PDF in the response body.
Why Chromium
The rendering engine matters. Renderly uses Chromium — the same engine as Chrome. That means CSS Grid, Flexbox, custom @font-face fonts, background images, CSS variables — all work exactly as they do in your browser.
A lot of alternatives use older engines like WeasyPrint or Prince XML. They're fine for simple documents, but the moment you try a modern CSS layout, you hit quirks. I wanted something that renders exactly what I see when I open the HTML in Chrome.
What I learned building it
Puppeteer on Railway crashes silently without --no-sandbox.
I spent a full day debugging this. No error. No log. The process just returned nothing. The fix is one flag, but discovering it requires either knowing where to look or suffering through it yourself.
const browser = await puppeteer.launch({
args: ['--no-sandbox', '--disable-setuid-sandbox'],
headless: 'new',
});
Never use the native bcrypt package on Railway.
Native Node modules get recompiled on every deploy. On Railway's clean installs, bcrypt would silently fail to install, breaking my entire auth middleware. The fix: switch to bcryptjs — pure JavaScript, same API, always installs.
# Don't do this if you're deploying to Railway/Heroku
npm install bcrypt
# Do this instead
npm install bcryptjs
Fastify's schema validation is underrated.
I started with Express. Switched to Fastify mid-build. The schema validation on routes caught three auth bugs before I shipped them — malformed request bodies that Express would have happily passed through.
Always set a fallback for success_url in Stripe Checkout.
// This will throw if DASHBOARD_URL is undefined
const session = await stripe.checkout.sessions.create({
success_url: `${process.env.DASHBOARD_URL}/dashboard?upgraded=1`,
});
// This won't
const baseUrl = (process.env.DASHBOARD_URL ?? 'https://yourapp.com').replace(/\/$/, '');
const session = await stripe.checkout.sessions.create({
success_url: `${baseUrl}/dashboard?upgraded=1`,
});
The stack
- API: Node.js + Fastify + Puppeteer → Railway
- Dashboard + landing page: Next.js → Vercel
- Database: PostgreSQL on Railway (usage metering)
- Payments: Stripe (live mode)
- Domain: renderlyapi.com on Cloudflare
Total infrastructure cost at zero users: ~€5/month (Railway hobby plan).
Pricing
| Plan | PDFs/month | Price |
|---|---|---|
| Free | 50 | €0 |
| Starter | 500 | €19/mo |
| Pro | 2,000 | €49/mo |
| Scale | 10,000 | €99/mo |
No credit card required for the free tier.
Try it
If you've copy-pasted the Puppeteer boilerplate before, give Renderly a try. Free account, API key in 60 seconds.
If you have feedback — especially on the pricing or missing features — I'd love to hear it in the comments.
Top comments (0)