JavaScript PDF Generation with Fetch API & Blob Download
You do not need Node.js, Puppeteer, or a heavy library to generate PDFs from JavaScript. A simple fetch() call against a PDF API is all it takes — and it works in both the browser and any modern JavaScript runtime.
This guide focuses on vanilla JavaScript with the Fetch API: how to request a PDF, handle the binary response, trigger a Blob download in the browser, deal with errors, and drop the pattern into React or Vue.js. For Node.js-specific patterns (Express, Lambda, serverless), see the Node.js PDF Generation Guide. For TypeScript type-safe wrappers, see the TypeScript PDF API Guide.
Why Vanilla JavaScript + Fetch API?
The browser's built-in fetch() is capable of:
- Sending an HTML string to a PDF API
- Receiving binary data (
arraybufferorblob) - Triggering a file download with no server round-trip
- Running inside any framework — React, Vue, Svelte, or plain HTML
No npm packages. No build step. Just JavaScript.
Quick Start: Generate a PDF in 10 Lines
async function generatePdf(html) {
const response = await fetch('https://pdf.funbrew.cloud/api/v1/generate', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
html,
options: { format: 'A4', printBackground: true },
}),
});
if (!response.ok) throw new Error(`PDF generation failed: ${response.status}`);
const blob = await response.blob();
return blob;
}
That is the complete function. Call it with any HTML string, get back a PDF Blob.
Download the PDF in the Browser
Once you have a Blob, create an object URL and trigger an anchor click — the standard browser download pattern:
async function downloadPdf(html, filename = 'document.pdf') {
const blob = await generatePdf(html);
// Create a temporary URL for the blob
const url = URL.createObjectURL(blob);
// Create a hidden anchor and click it
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
// Clean up
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
// Usage
const invoiceHtml = document.getElementById('invoice-template').innerHTML;
downloadPdf(invoiceHtml, 'invoice-2026-001.pdf');
This pattern works in all modern browsers (Chrome, Firefox, Safari, Edge) without any library dependency.
Open the PDF in a New Tab
Sometimes you want to preview the PDF rather than download it. Replace the anchor download attribute with target="_blank":
async function openPdfInNewTab(html) {
const blob = await generatePdf(html);
const url = URL.createObjectURL(blob);
// Open in new tab
const newTab = window.open(url, '_blank');
// Revoke after a short delay to allow the tab to load
if (newTab) {
setTimeout(() => URL.revokeObjectURL(url), 30000);
}
}
Error Handling
PDF generation can fail for several reasons: invalid HTML, rate limits, or network issues. Always handle errors explicitly:
async function generatePdfWithErrorHandling(html) {
let response;
try {
response = await fetch('https://pdf.funbrew.cloud/api/v1/generate', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
html,
options: { format: 'A4', printBackground: true },
}),
});
} catch (networkError) {
// fetch itself threw — offline, DNS failure, CORS, etc.
throw new Error(`Network error: ${networkError.message}`);
}
if (!response.ok) {
// Try to read the error body for a useful message
let errorMessage = `HTTP ${response.status}`;
try {
const errorBody = await response.json();
errorMessage = errorBody.message || errorMessage;
} catch {
// error body is not JSON — ignore
}
throw new Error(`PDF generation failed: ${errorMessage}`);
}
return response.blob();
}
Common Error Codes
| Status | Meaning | Fix |
|---|---|---|
400 |
Invalid HTML or missing fields | Check the html field is present and non-empty |
401 |
Invalid API key | Verify Authorization: Bearer <key> header |
413 |
HTML payload too large | Inline fewer assets or use external CSS links |
429 |
Rate limit exceeded | Add retry with exponential backoff |
500 |
Server error | Retry after a short delay |
Timeout and AbortController
Long HTML documents can take several seconds to render. Add a timeout with AbortController to avoid hanging requests:
async function generatePdfWithTimeout(html, timeoutMs = 30000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch('https://pdf.funbrew.cloud/api/v1/generate', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
html,
options: { format: 'A4', printBackground: true },
}),
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.blob();
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('PDF generation timed out after ' + timeoutMs + 'ms');
}
throw error;
}
}
Using arraybuffer Instead of blob
When you need to manipulate the raw bytes — for example, to upload the PDF to an S3-compatible storage — use arraybuffer:
const response = await fetch('https://pdf.funbrew.cloud/api/v1/generate', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({ html, options: { format: 'A4' } }),
});
const buffer = await response.arrayBuffer();
// Convert to Uint8Array for further processing
const bytes = new Uint8Array(buffer);
console.log(`PDF size: ${bytes.length} bytes`);
// Upload to pre-signed S3 URL
await fetch(presignedUrl, {
method: 'PUT',
body: buffer,
headers: { 'Content-Type': 'application/pdf' },
});
React Integration
Wrap the Fetch API call in a custom hook for clean React integration:
import { useState, useCallback } from 'react';
function usePdfGeneration() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const generateAndDownload = useCallback(async (html, filename = 'document.pdf') => {
setLoading(true);
setError(null);
try {
const response = await fetch('https://pdf.funbrew.cloud/api/v1/generate', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
html,
options: { format: 'A4', printBackground: true },
}),
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}, []);
return { generateAndDownload, loading, error };
}
// Usage in a component
function InvoiceButton({ invoiceHtml }) {
const { generateAndDownload, loading, error } = usePdfGeneration();
return (
<div>
<button
onClick={() => generateAndDownload(invoiceHtml, 'invoice.pdf')}
disabled={loading}
>
{loading ? 'Generating PDF...' : 'Download Invoice PDF'}
</button>
{error && <p style={{ color: 'red' }}>Error: {error}</p>}
</div>
);
}
Vue.js Integration
The same pattern in a Vue 3 Composition API component:
<template>
<div>
<button @click="download" :disabled="loading">
{{ loading ? 'Generating PDF...' : 'Download PDF' }}
</button>
<p v-if="error" class="error">{{ error }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue';
const props = defineProps({ html: String, filename: { type: String, default: 'document.pdf' } });
const loading = ref(false);
const error = ref(null);
async function download() {
loading.value = true;
error.value = null;
try {
const response = await fetch('https://pdf.funbrew.cloud/api/v1/generate', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
html: props.html,
options: { format: 'A4', printBackground: true },
}),
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = props.filename;
a.click();
URL.revokeObjectURL(url);
} catch (err) {
error.value = err.message;
} finally {
loading.value = false;
}
}
</script>
Security: Do Not Expose Your API Key in the Browser
A critical point: never embed your PDF API key in client-side JavaScript. The key would be visible to anyone who inspects the network tab.
The correct pattern is to proxy the request through your backend:
Browser → Your backend endpoint → FUNBREW PDF API
Your backend holds the API key. The browser calls your own endpoint:
// Browser code — calls YOUR backend, not the PDF API directly
async function downloadPdfViaBff(html, filename) {
const response = await fetch('/api/generate-pdf', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
// Add your own auth token here (session cookie, JWT, etc.)
body: JSON.stringify({ html }),
});
if (!response.ok) throw new Error('PDF generation failed');
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
// Node.js backend (Express) — holds the API key server-side
app.post('/api/generate-pdf', authenticateUser, async (req, res) => {
const { html } = req.body;
const pdfResponse = await fetch('https://pdf.funbrew.cloud/api/v1/generate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.FUNBREW_API_KEY}`, // server-side only
'Content-Type': 'application/json',
},
body: JSON.stringify({ html, options: { format: 'A4', printBackground: true } }),
});
if (!pdfResponse.ok) return res.status(500).json({ error: 'PDF generation failed' });
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', 'attachment; filename="document.pdf"');
pdfResponse.body.pipeTo(new WritableStream({
write(chunk) { res.write(chunk); },
close() { res.end(); },
}));
});
For server-to-server usage where the API key is already server-side, you can stream the FUNBREW PDF API response directly to the browser without buffering the entire PDF in memory.
Complete Working Example
A standalone HTML page that generates and downloads a PDF with a button click — no build step, no dependencies:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>PDF Generator</title>
<style>
body { font-family: sans-serif; padding: 20px; }
button { padding: 10px 20px; font-size: 16px; cursor: pointer; }
button:disabled { opacity: 0.6; cursor: not-allowed; }
#status { margin-top: 10px; color: #666; }
.error { color: red; }
</style>
</head>
<body>
<h1>Invoice Generator</h1>
<button id="btn">Download Invoice PDF</button>
<div id="status"></div>
<script>
const btn = document.getElementById('btn');
const status = document.getElementById('status');
const INVOICE_HTML = `
<!DOCTYPE html>
<html><head><style>
@page { size: A4; margin: 20mm; }
body { font-family: Arial, sans-serif; font-size: 11pt; color: #222; }
h1 { color: #1e3a5f; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
th { background: #1e3a5f; color: white; padding: 8px 12px; text-align: left; }
td { border-bottom: 1px solid #e5e7eb; padding: 8px 12px; }
.total { font-weight: bold; background: #f0f4ff; }
</style></head>
<body>
<h1>Invoice #INV-2026-001</h1>
<p>Date: May 13, 2026 | Due: May 31, 2026</p>
<table>
<thead><tr><th>Item</th><th>Qty</th><th>Price</th><th>Total</th></tr></thead>
<tbody>
<tr><td>Web Development</td><td>1</td><td>$8,000</td><td>$8,000</td></tr>
<tr><td>API Integration</td><td>2</td><td>$1,500</td><td>$3,000</td></tr>
<tr class="total"><td colspan="3">Total</td><td>$11,000</td></tr>
</tbody>
</table>
</body></html>
`;
btn.addEventListener('click', async () => {
btn.disabled = true;
status.textContent = 'Generating PDF...';
status.className = '';
try {
const response = await fetch('/api/generate-pdf', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ html: INVOICE_HTML }),
});
if (!response.ok) throw new Error(`Server returned ${response.status}`);
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'invoice-2026-001.pdf';
a.click();
URL.revokeObjectURL(url);
status.textContent = 'PDF downloaded successfully.';
} catch (err) {
status.textContent = `Error: ${err.message}`;
status.className = 'error';
} finally {
btn.disabled = false;
}
});
</script>
</body>
</html>
Testing Your Setup
Before writing integration code, try the FUNBREW PDF Playground — paste your HTML and see the PDF output immediately in your browser. Once the layout looks right, integrate the Fetch API call into your application.
For a full list of API options (page format, margins, engine selection, print background), see the API Reference. For use-case inspiration — invoices, certificates, reports — explore the Use Cases page.
Summary
Key patterns for JavaScript PDF generation with Fetch API:
response.blob(): Best for browser downloads — use withURL.createObjectURLand a hidden anchorresponse.arrayBuffer(): Best when you need raw bytes for further processing or cloud upload- AbortController: Add timeouts to avoid hanging requests on slow renders
- Error handling: Check
response.ok, parse the error body, distinguish network errors from API errors - API key security: Never expose the key in client-side code — proxy through your backend
- React / Vue: Wrap in a custom hook or composable; track
loadinganderrorstate
Related
- Node.js PDF Generation Guide — Express, Lambda, and serverless patterns for server-side generation
- TypeScript PDF API Guide — Type-safe wrappers, Zod validation, and Next.js integration
- HTML to PDF Complete Guide — Full overview of HTML-to-PDF methods and when to use each
- PDF Report Generation Guide — Charts, scheduling, and automated delivery
- CSS Tips for HTML to PDF — Page breaks, fonts, and layout for PDF-first HTML
- Playground — Test your HTML template instantly in the browser
- API Reference — Full endpoint documentation and options