1. URL-to-PDF in API Routes
The simplest approach: send a URL to SnapRender and get a PDF back. Works for any publicly accessible page:
// app/api/pdf/route.js
import { NextResponse } from 'next/server';
export async function POST(request) {
const { url } = await request.json();
// Generate PDF from any URL via SnapRender
const response = await fetch(
'https://api.snaprender.dev/v1/pdf',
{
method: 'POST',
headers: {
'x-api-key': process.env.SNAPRENDER_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
url,
format: 'A4',
margin: {
top: '20mm',
bottom: '20mm',
left: '15mm',
right: '15mm',
},
}),
}
);
const pdfBuffer = await response.arrayBuffer();
return new NextResponse(pdfBuffer, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': 'attachment; filename="document.pdf"',
},
});
}2. HTML-to-PDF: Invoice generation
For dynamic documents like invoices, build HTML from your data and convert it to PDF:
// app/api/invoice/route.js
import { NextResponse } from 'next/server';
export async function POST(request) {
const { invoiceId, items, customer } = await request.json();
const total = items.reduce((sum, i) => sum + i.qty * i.price, 0);
// Build HTML template with inline styles
const html = `
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: 'Helvetica', sans-serif; padding: 40px; }
.header { display: flex; justify-content: space-between; }
.company { font-size: 24px; font-weight: bold; }
.invoice-id { color: #666; }
table { width: 100%; border-collapse: collapse; margin-top: 30px; }
th { background: #f5f5f5; text-align: left; padding: 10px; }
td { padding: 10px; border-bottom: 1px solid #eee; }
.total { font-size: 20px; text-align: right; margin-top: 20px; }
</style>
</head>
<body>
<div class="header">
<div class="company">Your Company</div>
<div class="invoice-id">Invoice #${invoiceId}</div>
</div>
<p>Bill to: ${customer.name}</p>
<table>
<thead>
<tr><th>Item</th><th>Qty</th><th>Price</th><th>Total</th></tr>
</thead>
<tbody>
${items.map(i => `
<tr>
<td>${i.name}</td>
<td>${i.qty}</td>
<td>$${i.price.toFixed(2)}</td>
<td>$${(i.qty * i.price).toFixed(2)}</td>
</tr>
`).join('')}
</tbody>
</table>
<div class="total">Total: $${total.toFixed(2)}</div>
</body>
</html>
`;
const response = await fetch(
'https://api.snaprender.dev/v1/pdf',
{
method: 'POST',
headers: {
'x-api-key': process.env.SNAPRENDER_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
html,
format: 'A4',
margin: { top: '10mm', bottom: '10mm' },
print_background: true,
}),
}
);
const pdfBuffer = await response.arrayBuffer();
return new NextResponse(pdfBuffer, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition':
`attachment; filename="invoice-${invoiceId}.pdf"`,
},
});
}Pro tip
Use inline CSS in your HTML templates. External stylesheets need to be publicly accessible or inlined at build time. SnapRender renders exactly what you send.
3. Server Actions
Next.js Server Actions let you call server-side code directly from React components:
// app/actions/generate-pdf.js
'use server';
export async function generateReport(reportData) {
const html = buildReportHtml(reportData);
const response = await fetch(
'https://api.snaprender.dev/v1/pdf',
{
method: 'POST',
headers: {
'x-api-key': process.env.SNAPRENDER_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
html,
format: 'A4',
landscape: true,
print_background: true,
}),
}
);
const buffer = await response.arrayBuffer();
return Buffer.from(buffer).toString('base64');
}
function buildReportHtml(data) {
return `<!DOCTYPE html>
<html>
<head><style>
body { font-family: system-ui; padding: 40px; }
h1 { color: #1a1a1a; }
.metric { display: inline-block; padding: 20px; margin: 10px;
background: #f8f8f8; border-radius: 8px; }
.metric .value { font-size: 32px; font-weight: bold; }
.metric .label { color: #666; font-size: 14px; }
</style></head>
<body>
<h1>${data.title}</h1>
<p>Generated: ${new Date().toLocaleDateString()}</p>
<div>
${data.metrics.map(m => `
<div class="metric">
<div class="value">${m.value}</div>
<div class="label">${m.label}</div>
</div>
`).join('')}
</div>
</body>
</html>`;
}4. Client-side download button
Wire up the API route to a download button in your React component:
// app/components/DownloadButton.jsx
'use client';
import { useState } from 'react';
export default function DownloadPdfButton({ invoiceId }) {
const [loading, setLoading] = useState(false);
async function handleDownload() {
setLoading(true);
try {
const response = await fetch('/api/invoice', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
invoiceId,
customer: { name: 'Acme Corp' },
items: [
{ name: 'API Credits', qty: 1000, price: 0.01 },
{ name: 'Premium Support', qty: 1, price: 99 },
],
}),
});
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `invoice-${invoiceId}.pdf`;
a.click();
URL.revokeObjectURL(url);
} catch (error) {
console.error('PDF generation failed:', error);
} finally {
setLoading(false);
}
}
return (
<button onClick={handleDownload} disabled={loading}>
{loading ? 'Generating...' : 'Download Invoice PDF'}
</button>
);
}5. Edge Runtime support
Because SnapRender is just a fetch call, it works on Edge Runtime with zero Node.js dependencies:
// app/api/pdf-edge/route.js
export const runtime = 'edge';
export async function POST(request) {
const { url } = await request.json();
// Works on Edge - just a fetch call, no Node.js APIs needed
const response = await fetch(
'https://api.snaprender.dev/v1/pdf',
{
method: 'POST',
headers: {
'x-api-key': process.env.SNAPRENDER_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({ url, format: 'A4' }),
}
);
return new Response(response.body, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': 'attachment; filename="document.pdf"',
},
});
}Comparison: PDF approaches in Next.js
| Approach | Best for | Limitation |
|---|---|---|
| Puppeteer | Full control, self-hosted | 300MB binary, no serverless |
| jsPDF | Simple text PDFs | No CSS/HTML rendering |
| react-pdf | Custom layouts | Own component system, not HTML |
| SnapRender API | HTML/URL to PDF, any runtime | API cost at high volume |
Generate PDFs from any Next.js app
SnapRender handles the browser rendering. Send HTML or a URL, get a pixel-perfect PDF back. Works on Node.js, Edge, and serverless.
Get Your API Key — FreeFrequently asked questions
Next.js itself does not have PDF generation built in. You need either a library (Puppeteer, jsPDF, react-pdf) or an API like SnapRender. API Routes and Server Actions are the ideal place to generate PDFs because they run on the server with no browser restrictions.
Puppeteer works but requires a Chromium binary (300MB+), makes serverless deployments difficult (Lambda has a 250MB limit), and is slow on cold starts. An API like SnapRender handles the browser infrastructure for you, works from any hosting, and returns PDFs in 2-5 seconds.
Render your React component to HTML (using ReactDOMServer.renderToString), include your CSS (inline or as a style tag), then send the HTML to SnapRender's PDF endpoint. This lets you use the same components and styles from your app in your PDFs.
Edge Runtime does not support Puppeteer or native Node.js modules. However, you can call the SnapRender API from Edge functions since it is just a fetch() call. This gives you PDF generation at the edge with sub-100ms response times.
Include header/footer HTML in your template, or use CSS @page rules for margins and running headers. SnapRender supports custom header and footer HTML, margin configuration, and CSS @page rules for print-specific styling.