2026/05/13

JavaScriptでPDFを生成:Fetch API・Blobダウンロード・ブラウザ対応ガイド

JavaScriptPDF生成Fetch APIブラウザPDF API

Node.jsもPuppeteerも重いライブラリも不要です。fetch() を1回呼び出すだけでPDFを生成できます。しかもブラウザと任意のJavaScriptランタイムの両方で動作します。

このガイドではVanilla JavaScript + Fetch APIでのPDF生成に特化して解説します。HTMLをPDF APIへ送信し、バイナリレスポンスを受け取り、ブラウザでBlobダウンロードをトリガーし、エラーを処理し、ReactやVue.jsに組み込む方法を実装コードとともに紹介します。Node.js向けのパターン(Express・Lambda・サーバーレス)はNode.js PDF生成ガイド、TypeScript型安全ラッパーはTypeScript PDF APIガイドを参照してください。

なぜVanilla JavaScript + Fetch APIなのか

ブラウザ組み込みのfetch()は以下のことが単体でできます。

  • HTMLをPDF APIへ送信する
  • バイナリデータ(arraybuffer または blob)を受信する
  • サーバーへの追加リクエストなしにファイルダウンロードをトリガーする
  • React・Vue・Svelte・素のHTMLなどあらゆるフレームワークで動作する

npmパッケージ不要。ビルドステップ不要。JavaScriptだけで完結します。

クイックスタート:10行でPDFを生成する

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生成に失敗しました: ${response.status}`);

  const blob = await response.blob();
  return blob;
}

これが完全な関数です。HTMLの文字列を渡すとPDFのBlobが返ってきます。

ブラウザでPDFをダウンロードする

Blobが手に入ったらオブジェクトURLを作成してアンカーのクリックをトリガーするのが標準的なブラウザダウンロードのパターンです。

async function downloadPdf(html, filename = 'document.pdf') {
  const blob = await generatePdf(html);

  // Blobの一時URLを作成
  const url = URL.createObjectURL(blob);

  // 非表示のアンカーを作ってクリック
  const a = document.createElement('a');
  a.href = url;
  a.download = filename;
  document.body.appendChild(a);
  a.click();

  // クリーンアップ
  document.body.removeChild(a);
  URL.revokeObjectURL(url);
}

// 使用例
const invoiceHtml = document.getElementById('invoice-template').innerHTML;
downloadPdf(invoiceHtml, 'invoice-2026-001.pdf');

このパターンはすべてのモダンブラウザ(Chrome・Firefox・Safari・Edge)でライブラリ依存なしに動作します。

新しいタブでPDFをプレビューする

ダウンロードではなくPDFをプレビューしたい場合は、アンカーのdownload属性の代わりにtarget="_blank"を使います。

async function openPdfInNewTab(html) {
  const blob = await generatePdf(html);
  const url = URL.createObjectURL(blob);

  // 新しいタブで開く
  const newTab = window.open(url, '_blank');

  // タブが読み込まれた後にURLを解放
  if (newTab) {
    setTimeout(() => URL.revokeObjectURL(url), 30000);
  }
}

エラーハンドリング

PDF生成はHTML不正・レート制限・ネットワーク障害などで失敗することがあります。エラーは必ず明示的に処理してください。

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が例外を投げた場合 — オフライン・DNS障害・CORSなど
    throw new Error(`ネットワークエラー: ${networkError.message}`);
  }

  if (!response.ok) {
    // エラーボディから詳細なメッセージを取得
    let errorMessage = `HTTP ${response.status}`;
    try {
      const errorBody = await response.json();
      errorMessage = errorBody.message || errorMessage;
    } catch {
      // エラーボディがJSONでない場合は無視
    }
    throw new Error(`PDF生成失敗: ${errorMessage}`);
  }

  return response.blob();
}

よくあるエラーコード

ステータス 意味 対処法
400 HTMLが不正 または 必須フィールドが不足 htmlフィールドが存在して空でないことを確認
401 APIキーが無効 Authorization: Bearer <key> ヘッダーを確認
413 HTMLペイロードが大きすぎる インラインアセットを減らすか外部CSSリンクを使用
429 レート制限超過 指数バックオフでリトライを追加
500 サーバーエラー しばらく待ってからリトライ

タイムアウトとAbortController

HTMLが大きい場合、レンダリングに数秒かかることがあります。AbortControllerでタイムアウトを設定してリクエストがハングしないようにしてください。

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生成が${timeoutMs}msでタイムアウトしました`);
    }
    throw error;
  }
}

arraybufferの使い方

S3互換ストレージへのアップロードなど、生のバイト列を操作したい場合は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();

// Uint8Arrayに変換して後続処理
const bytes = new Uint8Array(buffer);
console.log(`PDFサイズ: ${bytes.length} バイト`);

// 署名付きS3 URLへアップロード
await fetch(presignedUrl, {
  method: 'PUT',
  body: buffer,
  headers: { 'Content-Type': 'application/pdf' },
});

Reactへの組み込み

Fetch APIの呼び出しをカスタムフックに包むとReactへの組み込みがシンプルになります。

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 };
}

// コンポーネントでの使用例
function InvoiceButton({ invoiceHtml }) {
  const { generateAndDownload, loading, error } = usePdfGeneration();

  return (
    <div>
      <button
        onClick={() => generateAndDownload(invoiceHtml, 'invoice.pdf')}
        disabled={loading}
      >
        {loading ? 'PDF生成中...' : '請求書PDFをダウンロード'}
      </button>
      {error && <p style={{ color: 'red' }}>エラー: {error}</p>}
    </div>
  );
}

Vue.jsへの組み込み

Vue 3 Composition APIでの実装例です。

<template>
  <div>
    <button @click="download" :disabled="loading">
      {{ loading ? 'PDF生成中...' : '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>

セキュリティ:APIキーをブラウザに露出させてはいけない

重要な注意点として、クライアントサイドのJavaScriptにPDF APIキーを埋め込んではいけません。ネットワークタブを確認したユーザーに即座に漏洩します。

正しいパターンはバックエンドを経由させることです。

ブラウザ → 自分のバックエンドエンドポイント → FUNBREW PDF API

APIキーはバックエンドが保持します。ブラウザは自分のエンドポイントを呼び出します。

// ブラウザ側のコード — PDF APIではなく自分のバックエンドを呼ぶ
async function downloadPdfViaBff(html, filename) {
  const response = await fetch('/api/generate-pdf', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    // セッションCookie・JWTなど自前の認証トークンをここに付加
    body: JSON.stringify({ html }),
  });

  if (!response.ok) throw new Error('PDF生成に失敗しました');

  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バックエンド(Express)— APIキーをサーバーサイドで保持
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}`, // サーバーサイドのみ
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ html, options: { format: 'A4', printBackground: true } }),
  });

  if (!pdfResponse.ok) return res.status(500).json({ error: 'PDF生成に失敗しました' });

  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(); },
  }));
});

サーバーからサーバーへの通信ではAPIキーがすでにサーバーサイドにあるため、PDF APIのレスポンスをバッファリングせずにブラウザへそのままストリームできます。

動作確認:Playgroundでテスト

実装コードを書く前にFUNBREW PDF PlaygroundでHTMLを貼り付けてPDF出力を確認してください。レイアウトが正しいことを確認してからFetch APIを組み込むと開発サイクルが速くなります。

APIオプション(ページ形式・余白・エンジン選択・背景色印刷)の一覧はAPIリファレンスを参照してください。請求書・証明書・レポートなどのユースケース例はユースケースページで確認できます。

まとめ

Fetch APIを使ったJavaScript PDF生成の重要ポイント。

  • response.blob(): ブラウザダウンロード向け — URL.createObjectURLと非表示アンカーを組み合わせる
  • response.arrayBuffer(): 生バイト列を処理またはクラウドストレージにアップロードする場合に使用
  • AbortController: タイムアウトを設定してレンダリングが遅い場合にリクエストがハングしないようにする
  • エラーハンドリング: response.okを確認し、エラーボディをパースし、ネットワークエラーとAPIエラーを区別する
  • APIキーのセキュリティ: クライアントサイドのコードにキーを露出させない — バックエンドを経由させる
  • React / Vue: カスタムフックまたはコンポーザブルに包む。loadingerrorの状態を管理する

関連リンク

Powered by FUNBREW PDF