DEV Community

Hichem Bed
Hichem Bed

Posted on

I Built a PDF Generation API — Here's the Tech Stack and What I Learned

I recently shipped RenderPDFs — a simple API that converts HTML or URLs to PDFs.
 Here's the full breakdown: stack, architecture decisions, and the painful lessons along the way.

The Stack

  • Fastify — blazing fast Node.js framework, perfect for API services
  • Puppeteer — headless Chrome for pixel-perfect PDF rendering
  • PostgreSQL — API keys, users, usage tracking
  • Cloudflare R2 — PDF file storage with presigned URLs (S3-compatible, cheaper than AWS)
  • Next.js 15 — frontend dashboard and docs
  • Railway — deployment with zero DevOps headaches

How It Works

The core flow is dead simple:

const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setContent(html, { waitUntil: "networkidle0" });
const pdf = await page.pdf({ format: "A4", printBackground: true });
await browser.close();
Enter fullscreen mode Exit fullscreen mode

Send HTML, get a PDF. That's it. For URL-to-PDF I added SSRF protection to block requests to internal IPs — important if you're exposing this publicly.

The Auth Flow

Instead of exposing API keys in URLs, I built a login token system:

  1. User signs in with Google OAuth
  2. Server issues a short-lived UUID token (5 min expiry) stored in DB
  3. Token is passed in the redirect URL to the frontend
  4. Frontend exchanges it for the actual API key via a single-use endpoint
  5. Token is deleted immediately after use

This prevents duplicate API keys on every sign-in and keeps credentials out of browser history.

What I Added for Production

Things that seem optional until they're not:

  • Rate limiting per API key + per IP (@fastify/rate-limit)- Sentry for error monitoring — caught silent failures immediately after deploy
  • PDF timeout + retry — Puppeteer hangs sometimes, you need a 30s hard timeout
  • R2 storage — stream PDFs to Cloudflare instead of holding them in memory
  • Request logs — users need to see their usage history in the dashboard

The Unexpected Differentiator: MCP Server

None of my competitors have an MCP server. MCP (Model Context Protocol) lets AI assistants like Claude call your API directly. I added a remote MCP endpoint so developers can generate PDFs from inside their AI tools without writing a single line of code:

"Generate a PDF invoice from this HTML template"
→ Claude calls RenderPDFs MCP server
→ Returns a download URL
Enter fullscreen mode Exit fullscreen mode

This took a few hours to implement and immediately made the product stand out.

Lessons Learned

Google OAuth in production is painful. Cookie domain mismatches, wrong redirect URIs, Cloudflare proxy blocking Railway domain verification — it took a full day to fix what worked perfectly locally.

Railway + Cloudflare needs DNS-only mode. If you use Cloudflare proxy (orange cloud) on your API subdomain, Railway can't verify your custom domain. Switch to grey (DNS only).

Puppeteer needs babysitting. Set a timeout, add retry logic, and never trust it won't hang on complex pages.

Ship the MCP server early. AI-native integrations are a real differentiator right now — the developer community is actively looking for tools that plug into their AI workflows.

Try It

Free tier available, first PDF in under 60 seconds.

curl -X POST https://api.renderpdfs.com/v1/pdf/html \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"html": "<h1>Hello World</h1>", "options": {"format": "A4"}}'
Enter fullscreen mode Exit fullscreen mode

Check it out at renderpdfs.com — happy to answer any questions about the stack or architecture in the comments!

Top comments (0)