JavaScriptでPDFを生成:Fetch API・Blobダウンロード・ブラウザ対応ガイド
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: カスタムフックまたはコンポーザブルに包む。
loadingとerrorの状態を管理する
関連リンク
- Node.js PDF生成ガイド — サーバーサイド生成向けのExpress・Lambda・サーバーレスパターン
- TypeScript PDF APIガイド — 型安全ラッパー・Zodバリデーション・Next.js連携
- HTML to PDF完全ガイド — HTML→PDF変換手法の全体像と使い分け
- PDFレポート自動生成ガイド — グラフ・スケジュール配信・自動化パイプライン
- HTML→PDF CSS設計ガイド — PDF向けのページ区切り・フォント・レイアウトのCSS
- Playground — ブラウザでHTMLテンプレートをリアルタイムにPDF変換してテスト
- APIリファレンス — エンドポイントの詳細仕様とオプション