May 13, 2026

JavaScript PDF Generation with Fetch API & Blob Download

JavaScriptPDF generationFetch APIBrowserPDF API

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 (arraybuffer or blob)
  • 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 with URL.createObjectURL and a hidden anchor
  • response.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 loading and error state

Related

Powered by FUNBREW PDF