Generate PDFs in Next.js Without Bundling Puppeteer
Puppeteer adds 400 MB to your deployment, breaks on Vercel, and turns a 5-minute feature into a week of infrastructure work. Here's the 10-line alternative using a $5/year API.
If you've ever tried to generate PDFs in a Next.js app deployed to Vercel, you know the pain:
Puppeteer needs Chromium, Chromium is 170–400 MB, Vercel's serverless functions have size limits,
so you switch to puppeteer-core and @sparticuz/chromium, and now you're managing
binary compatibility, output file tracing, and 60-second function timeouts.
All of this to call page.pdf(). There's a better way.
The Puppeteer approach (what you're probably doing)
// next.config.ts — already getting complicated
const nextConfig = {
serverExternalPackages: ['puppeteer-core', '@sparticuz/chromium'],
outputFileTracingIncludes: {
'/api/generate-pdf': ['./node_modules/@sparticuz/chromium/**/*'],
},
};
// app/api/generate-pdf/route.ts
import puppeteer from 'puppeteer-core';
import chromium from '@sparticuz/chromium';
export async function POST(req: Request) {
const html = await req.text();
const browser = await puppeteer.launch({
args: chromium.args,
defaultViewport: chromium.defaultViewport,
executablePath: await chromium.executablePath(),
headless: chromium.headless,
});
const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'networkidle0' });
const pdf = await page.pdf({ format: 'A4', printBackground: true });
await browser.close();
return new Response(pdf, {
headers: { 'Content-Type': 'application/pdf' },
});
}
That's 25+ lines of infrastructure code, a special Next.js config, a 143 MB Chromium binary bundled into your deployment, and a cold start that can take 5–8 seconds. And you haven't handled errors, memory leaks, or font rendering yet.
The API approach (what you should do instead)
// app/api/generate-pdf/route.ts
export async function POST(req: Request) {
const html = await req.text();
const response = await fetch(
'https://htmltopdfconverter.com.au/api/programmatic/pdf/generate',
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.PDF_API_KEY}`,
'Content-Type': 'text/html',
},
body: html,
}
);
const { pdfs } = await response.json();
return Response.json({ url: pdfs[0].url });
}
That's it. No Chromium binary. No special Next.js config. No cold start penalty. The PDF is rendered on our infrastructure and you get a URL back.
Full example: invoice download button
Here's a complete example — a React component that renders an invoice template server-side and returns a PDF download link.
// app/api/invoice/[id]/pdf/route.ts
import { renderToStaticMarkup } from 'react-dom/server';
import { InvoiceTemplate } from '@/components/InvoiceTemplate';
import { getInvoice } from '@/lib/invoices';
export async function GET(
req: Request,
{ params }: { params: { id: string } }
) {
const invoice = await getInvoice(params.id);
const html = renderToStaticMarkup(<InvoiceTemplate data={invoice} />);
const res = await fetch(
'https://htmltopdfconverter.com.au/api/programmatic/pdf/generate',
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.PDF_API_KEY}`,
'Content-Type': 'text/html',
},
body: `<!DOCTYPE html><html><head>
<style>${invoiceStyles}</style>
</head><body>${html}</body></html>`,
}
);
const { pdfs } = await res.json();
return Response.redirect(pdfs[0].url);
}
Your users click "Download Invoice" and get a PDF. You wrote zero infrastructure code.
What this costs
$5 AUD per year. That's the entire cost. No per-page fees. No monthly subscription that scales with usage. For context, a single month of most competing PDF APIs costs more than 3 years of ours.
Compare that to the engineering time you'd spend maintaining Puppeteer in production on Vercel. Even one hour of debugging Chromium binary issues costs more than a decade of our API.
Getting started
- Create an account (30 seconds)
- Subscribe from your dashboard ($5 AUD/year)
- Generate an API key
- Add
PDF_API_KEYto your.env.local - Copy the code above and ship it
Full API docs cover request formats, response shapes, error codes, rate limits, and examples in Node, Python, and PHP.
FAQ
Does this work with the Next.js App Router?
Yes. The examples above use App Router route handlers. It works identically with Pages Router API routes.
Can I use this on Vercel?
Yes — that's the point. No Chromium binary means no deployment size issues, no cold start penalty,
and no special next.config.ts configuration. It's just a fetch call.
What about React Server Components?
You can use renderToStaticMarkup in a route handler or server action to render your React template
to HTML, then POST it to the API. RSC rendering happens server-side, which is exactly what you need.
What's the latency?
Typical conversions complete in 1–3 seconds depending on HTML complexity. For most invoice/report use cases, this is imperceptible — the user clicks a button and gets a PDF.