Node.js PDF生成完全ガイド|Express・サーバーレス・Honoで実装
「Node.jsでPDFを生成したい」という要件は、請求書・レポート・証明書・帳票など、あらゆるWebアプリで発生します。ライブラリを自前でインストールするか、APIを使うか、サーバーレスで動かすか――選択肢は複数あります。
この記事では、Node.jsプロジェクトでPDFを生成するすべての主要パターンを、実際に動くコード例とともに解説します。FUNBREW PDF APIを使う方法を中心に、Express・Hono・AWS Lambda・Cloudflare Workersでの実装まで幅広くカバーします。
Node.js でのPDF生成:4つのアプローチ比較
まず主要な手法を比較します。
| アプローチ | メリット | デメリット | おすすめ用途 |
|---|---|---|---|
| PDF API(FUNBREW PDF) | セットアップ不要・CSS完全対応・スケーラブル | ネットワーク依存 | 本番環境・SaaS・高品質 |
| Puppeteer(ローカル) | 無料・細かい制御 | 重い(Chromium同梱)・サーバーレス不向き | 開発・少量生成 |
| PDFKit | 軽量・ゼロ依存 | HTML非対応・低水準API | シンプルな帳票 |
| jsPDF | クライアントサイドも可 | CSSレイアウト再現が困難 | 軽量なクライアント生成 |
複雑なHTMLレイアウト(グラフ・テーブル・日本語フォント)を含む業務PDFには PDF API が最も適しています。PDF APIとライブラリの比較で詳しく解説しています。
インストールと基本設定
# プロジェクト初期化
mkdir my-pdf-app && cd my-pdf-app
npm init -y
npm install axios dotenv
# .env ファイル作成
echo "FUNBREW_API_KEY=your-api-key" > .env
echo "FUNBREW_API_URL=https://api.pdf.funbrew.cloud/v1" >> .env
PDF生成の基本モジュール
// src/pdf-client.js
require('dotenv').config();
const axios = require('axios');
const client = axios.create({
baseURL: process.env.FUNBREW_API_URL || 'https://api.pdf.funbrew.cloud/v1',
headers: {
Authorization: `Bearer ${process.env.FUNBREW_API_KEY}`,
'Content-Type': 'application/json',
},
timeout: 30000,
responseType: 'arraybuffer',
});
/**
* HTMLからPDFバイナリを生成する
* @param {string} html - PDF化するHTML文字列
* @param {object} options - 用紙サイズ・余白・エンジンなどのオプション
* @returns {Promise<Buffer>} - PDFのバイナリデータ
*/
async function htmlToPdf(html, options = {}) {
const {
engine = 'quality', // 'quality'(Chromium)または 'fast'(wkhtmltopdf)
format = 'A4', // 'A4', 'Letter', 'Legal' など
landscape = false,
margin = { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
displayHeaderFooter = false,
headerTemplate = '',
footerTemplate = '',
} = options;
const response = await client.post('/pdf/from-html', {
html,
engine,
format,
landscape,
margin,
displayHeaderFooter,
headerTemplate,
footerTemplate,
});
return Buffer.from(response.data);
}
module.exports = { htmlToPdf };
APIドキュメントでオプションの完全仕様を確認できます。
パターン1: Express アプリへの組み込み
最も一般的なパターン。既存のExpressアプリにPDF生成エンドポイントを追加します。
npm install express
// src/app.js
require('dotenv').config();
const express = require('express');
const { htmlToPdf } = require('./pdf-client');
const app = express();
app.use(express.json());
/**
* POST /api/pdf/invoice
* 請求書PDFを生成してレスポンスで返す
*/
app.post('/api/pdf/invoice', async (req, res) => {
const { customerName, amount, items = [] } = req.body;
if (!customerName || !amount) {
return res.status(400).json({ error: 'customerName と amount は必須です' });
}
try {
const html = buildInvoiceHtml({ customerName, amount, items });
const pdf = await htmlToPdf(html, {
engine: 'quality',
format: 'A4',
margin: { top: '25mm', bottom: '20mm', left: '20mm', right: '20mm' },
});
res.set({
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="invoice-${Date.now()}.pdf"`,
'Content-Length': pdf.length,
});
res.send(pdf);
} catch (error) {
console.error('PDF生成エラー:', error.message);
res.status(500).json({ error: 'PDF生成に失敗しました', detail: error.message });
}
});
/**
* GET /api/pdf/report/:type
* レポートPDFをS3などのストレージに保存してURLを返す(非同期パターン)
*/
app.get('/api/pdf/report/:type', async (req, res) => {
const { type } = req.params;
try {
const html = buildReportHtml(type);
const pdf = await htmlToPdf(html, { format: 'A4', landscape: type === 'chart' });
// 実際のアプリではS3やGCSに保存してURLを返す
const filename = `report-${type}-${Date.now()}.pdf`;
// await uploadToS3(pdf, filename);
res.json({
success: true,
size: pdf.length,
filename,
// downloadUrl: `https://your-bucket.s3.amazonaws.com/${filename}`,
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
function buildInvoiceHtml({ customerName, amount, items }) {
const tax = Math.floor(amount * 0.1);
const total = amount + tax;
const invoiceNumber = `INV-${Date.now()}`;
const date = new Date().toLocaleDateString('ja-JP');
return `<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Noto Sans JP', 'Hiragino Sans', sans-serif; color: #1e293b; padding: 40px; }
h1 { font-size: 28px; color: #2563eb; margin-bottom: 8px; }
.meta { color: #64748b; font-size: 13px; margin-bottom: 32px; }
table { width: 100%; border-collapse: collapse; margin: 24px 0; font-size: 13px; }
th { background: #f1f5f9; padding: 10px 12px; text-align: left; border-bottom: 2px solid #e2e8f0; }
td { padding: 10px 12px; border-bottom: 1px solid #e2e8f0; }
.total-section { text-align: right; margin-top: 16px; }
.total-row { font-size: 20px; font-weight: 700; color: #2563eb; }
-webkit-print-color-adjust: exact; print-color-adjust: exact;
</style>
</head>
<body>
<h1>請求書</h1>
<div class="meta">
<p>請求番号: ${invoiceNumber}</p>
<p>発行日: ${date}</p>
<p>請求先: <strong>${customerName} 御中</strong></p>
</div>
<table>
<thead>
<tr><th>品目</th><th style="text-align:right">金額</th></tr>
</thead>
<tbody>
${items.length > 0
? items.map(item => `<tr><td>${item.name}</td><td style="text-align:right">¥${item.price.toLocaleString()}</td></tr>`).join('')
: `<tr><td>サービス利用料</td><td style="text-align:right">¥${amount.toLocaleString()}</td></tr>`
}
</tbody>
</table>
<div class="total-section">
<p>小計: ¥${amount.toLocaleString()}</p>
<p>消費税 (10%): ¥${tax.toLocaleString()}</p>
<p class="total-row">合計: ¥${total.toLocaleString()}</p>
</div>
</body>
</html>`;
}
function buildReportHtml(type) {
return `<!DOCTYPE html>
<html lang="ja">
<head><meta charset="UTF-8">
<style>body{font-family:sans-serif;padding:40px;}h1{color:#2563eb;}</style>
</head><body>
<h1>${type === 'weekly' ? '週次' : '月次'}レポート</h1>
<p>生成日時: ${new Date().toLocaleString('ja-JP')}</p>
</body></html>`;
}
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`サーバー起動: http://localhost:${PORT}`));
module.exports = app;
curlでのテスト
# 請求書PDF生成テスト
curl -X POST http://localhost:3000/api/pdf/invoice \
-H "Content-Type: application/json" \
-d '{"customerName":"株式会社サンプル","amount":100000}' \
-o invoice.pdf
# レポートPDF生成テスト
curl http://localhost:3000/api/pdf/report/monthly
パターン2: AWS Lambda(サーバーレス)
Puppeteerはサーバーレス環境でのコールドスタートが重い問題がありますが、PDF APIを使えば軽量な Lambda 関数でPDFを生成できます。
npm install @aws-sdk/client-s3
// lambda/generate-pdf.mjs (ESM形式)
import { htmlToPdf } from './pdf-client.js';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
const s3 = new S3Client({ region: process.env.AWS_REGION || 'ap-northeast-1' });
const BUCKET = process.env.S3_BUCKET_NAME;
/**
* Lambda ハンドラ
* イベント例: { "type": "invoice", "customerId": "C001", "amount": 50000 }
*/
export const handler = async (event) => {
const { type = 'invoice', customerId, amount } = event;
try {
// PDF 生成
const html = buildHtml(type, { customerId, amount });
const pdfBuffer = await htmlToPdf(html, {
engine: 'quality',
format: 'A4',
});
// S3 に保存
const key = `pdfs/${type}/${customerId}-${Date.now()}.pdf`;
await s3.send(new PutObjectCommand({
Bucket: BUCKET,
Key: key,
Body: pdfBuffer,
ContentType: 'application/pdf',
ServerSideEncryption: 'AES256',
}));
// 署名付きURL(15分有効)を生成して返す
const downloadUrl = await getSignedUrl(
s3,
new PutObjectCommand({ Bucket: BUCKET, Key: key }),
{ expiresIn: 900 }
);
return {
statusCode: 200,
body: JSON.stringify({ success: true, key, downloadUrl }),
};
} catch (error) {
console.error('Lambda PDF生成エラー:', error);
return {
statusCode: 500,
body: JSON.stringify({ error: error.message }),
};
}
};
function buildHtml(type, data) {
return `<!DOCTYPE html>
<html lang="ja"><head><meta charset="UTF-8">
<style>body{font-family:'Noto Sans JP',sans-serif;padding:40px;}</style>
</head><body>
<h1>${type === 'invoice' ? '請求書' : 'レポート'}</h1>
<p>顧客ID: ${data.customerId}</p>
<p>金額: ¥${(data.amount || 0).toLocaleString()}</p>
<p>生成日時: ${new Date().toLocaleString('ja-JP')}</p>
</body></html>`;
}
# serverless.yml(Serverless Frameworkを使う場合)
service: pdf-generator
provider:
name: aws
runtime: nodejs20.x
region: ap-northeast-1
environment:
FUNBREW_API_KEY: ${ssm:/pdf-generator/api-key}
FUNBREW_API_URL: https://api.pdf.funbrew.cloud/v1
S3_BUCKET_NAME: !Ref PdfBucket
iam:
role:
statements:
- Effect: Allow
Action: [s3:PutObject, s3:GetObject]
Resource: !Sub arn:aws:s3:::${PdfBucket}/*
functions:
generatePdf:
handler: lambda/generate-pdf.handler
timeout: 30
events:
- http:
path: /pdf
method: post
cors: true
パターン3: Hono(Edge Runtime / Cloudflare Workers)
HonoはCloudflare Workers・Deno・Bun などのEdge Runtimeで動作する軽量フレームワークです。
npm create hono@latest pdf-api-hono -- --template cloudflare-workers
cd pdf-api-hono && npm install
// src/index.ts
import { Hono } from 'hono';
import { cors } from 'hono/cors';
type Env = {
FUNBREW_API_KEY: string;
FUNBREW_API_URL: string;
};
const app = new Hono<{ Bindings: Env }>();
app.use('*', cors());
app.post('/pdf/invoice', async (c) => {
const { customerName, amount } = await c.req.json<{
customerName: string;
amount: number;
}>();
if (!customerName || !amount) {
return c.json({ error: 'customerName と amount は必須です' }, 400);
}
const html = buildInvoiceHtml(customerName, amount);
const response = await fetch(`${c.env.FUNBREW_API_URL}/pdf/from-html`, {
method: 'POST',
headers: {
Authorization: `Bearer ${c.env.FUNBREW_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
html,
engine: 'quality',
format: 'A4',
margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
}),
});
if (!response.ok) {
return c.json({ error: 'PDF生成失敗' }, 500);
}
const pdfBuffer = await response.arrayBuffer();
return new Response(pdfBuffer, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="invoice-${Date.now()}.pdf"`,
},
});
});
function buildInvoiceHtml(customerName: string, amount: number): string {
const tax = Math.floor(amount * 0.1);
return `<!DOCTYPE html>
<html lang="ja"><head><meta charset="UTF-8">
<style>body{font-family:sans-serif;padding:40px;color:#1e293b;}</style>
</head><body>
<h1 style="color:#2563eb">請求書</h1>
<p><strong>${customerName} 御中</strong></p>
<p>金額: ¥${amount.toLocaleString()}</p>
<p>消費税: ¥${tax.toLocaleString()}</p>
<p>合計: ¥${(amount + tax).toLocaleString()}</p>
</body></html>`;
}
export default app;
Cloudflare Workers での Hono は Puppeteer 非対応ですが、PDF APIを呼び出すだけなので完全に動作します。サーバーレス環境でのPDF生成も参照してください。
パターン4: HTMLテンプレートファイルからの生成
実際の業務では、HTMLテンプレートをファイルとして管理し、変数を差し込んでPDFを生成するパターンが主流です。
npm install mustache # または handlebars, ejs など
// src/template-renderer.js
const fs = require('fs');
const path = require('path');
const Mustache = require('mustache');
/**
* テンプレートファイルにデータを差し込んでHTMLを生成する
*/
function renderTemplate(templateName, data) {
const templatePath = path.join(__dirname, '../templates', `${templateName}.html`);
const template = fs.readFileSync(templatePath, 'utf-8');
return Mustache.render(template, data);
}
module.exports = { renderTemplate };
<!-- templates/invoice.html -->
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<style>
@page { size: A4; margin: 20mm 15mm; }
* { box-sizing: border-box; }
body {
font-family: 'Noto Sans JP', 'Hiragino Sans', sans-serif;
font-size: 12px;
color: #1e293b;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
.header { display: flex; justify-content: space-between; border-bottom: 3px solid #2563eb; padding-bottom: 16px; margin-bottom: 24px; }
.title { font-size: 28px; font-weight: 700; color: #2563eb; }
table { width: 100%; border-collapse: collapse; margin-top: 16px; }
th { background: #dbeafe; padding: 10px; text-align: left; }
td { padding: 10px; border-bottom: 1px solid #e2e8f0; }
.total { text-align: right; margin-top: 16px; font-size: 18px; font-weight: 700; }
</style>
</head>
<body>
<div class="header">
<div>
<div class="title">請求書</div>
<div>請求番号: {{invoiceNumber}}</div>
<div>発行日: {{issueDate}}</div>
</div>
<div style="text-align:right">
<strong>{{companyName}}</strong>
</div>
</div>
<p><strong>{{customerName}} 御中</strong></p>
<table>
<thead>
<tr><th>品目</th><th>数量</th><th>単価</th><th>小計</th></tr>
</thead>
<tbody>
{{#items}}
<tr>
<td>{{name}}</td>
<td style="text-align:right">{{quantity}}</td>
<td style="text-align:right">¥{{price}}</td>
<td style="text-align:right">¥{{subtotal}}</td>
</tr>
{{/items}}
</tbody>
</table>
<div class="total">
<div>小計: ¥{{subtotal}}</div>
<div>消費税 (10%): ¥{{tax}}</div>
<div style="font-size:22px; color:#2563eb">合計: ¥{{total}}</div>
</div>
</body>
</html>
// src/generate-invoice.js
const { renderTemplate } = require('./template-renderer');
const { htmlToPdf } = require('./pdf-client');
async function generateInvoicePdf(invoiceData) {
const {
customerName,
companyName = '株式会社FUNBREW',
items = [],
} = invoiceData;
// 合計計算
const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
const tax = Math.floor(subtotal * 0.1);
const total = subtotal + tax;
const html = renderTemplate('invoice', {
invoiceNumber: `INV-${Date.now()}`,
issueDate: new Date().toLocaleDateString('ja-JP'),
customerName,
companyName,
items: items.map(item => ({
...item,
price: item.price.toLocaleString(),
subtotal: (item.price * item.quantity).toLocaleString(),
})),
subtotal: subtotal.toLocaleString(),
tax: tax.toLocaleString(),
total: total.toLocaleString(),
});
return htmlToPdf(html, {
engine: 'quality',
format: 'A4',
margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
});
}
module.exports = { generateInvoicePdf };
PDFテンプレートエンジンガイドでより高度なテンプレート設計パターンを解説しています。
エラーハンドリングと本番環境対応
リトライとタイムアウト
// src/pdf-client.js — 本番対応版
const axiosRetry = require('axios-retry').default;
axiosRetry(client, {
retries: 3,
retryDelay: axiosRetry.exponentialDelay,
retryCondition: (error) => {
// 5xx エラーとネットワークエラーのみリトライ
return axiosRetry.isNetworkOrIdempotentRequestError(error)
|| (error.response?.status >= 500);
},
onRetry: (retryCount, error) => {
console.warn(`[PDF] リトライ ${retryCount}回目: ${error.message}`);
},
});
npm install axios-retry
ストリーミングレスポンス(大きなPDF向け)
// 大きなPDFはストリームで返す
app.get('/api/pdf/large-report', async (req, res) => {
const html = buildLargeReportHtml();
const response = await client.post('/pdf/from-html', { html, engine: 'quality' }, {
responseType: 'stream', // arraybuffer ではなく stream
});
res.set('Content-Type', 'application/pdf');
res.set('Content-Disposition', 'attachment; filename="report.pdf"');
response.data.pipe(res);
});
PDF生成のパフォーマンス最適化
// src/pdf-queue.js — キューで並行数を制御
const { default: PQueue } = require('p-queue');
// 同時PDF生成を最大5件に制限
const queue = new PQueue({ concurrency: 5 });
async function generatePdfQueued(html, options) {
return queue.add(() => htmlToPdf(html, options));
}
// キューの状態を監視
queue.on('active', () => {
console.log(`[Queue] 実行中: ${queue.size}件待機, ${queue.pending}件処理中`);
});
module.exports = { generatePdfQueued };
npm install p-queue
詳細な本番環境最適化はPDF API本番運用チェックリストを参照してください。
よくある質問
Node.js で最も簡単にPDFを生成するには?
FUNBREW PDF APIを使う方法が最も簡単です。axiosで数行のHTTPリクエストを書くだけで、複雑なHTMLをPDFに変換できます。Puppeteerのようなブラウザのインストールが不要で、サーバーレス環境でも動作します。
PuppeteerでPDFを生成する場合の問題点は?
Puppeteerはローカル開発には便利ですが、本番環境では以下の問題が発生することがあります。(1) Chromiumバイナリ(~300MB)のデプロイ管理、(2) AWS LambdaやCloud Functionsでのコールドスタート増加、(3) メモリ使用量の増加、(4) Dockerイメージサイズの増大。これらを回避するためにPDF APIを使う開発者が増えています。
Node.js v18以降で変わった点は?
Node.js v18以降では組み込みのfetchAPIが使えるため、axiosなどの外部HTTPライブラリなしでPDF APIを呼び出せます。
const response = await fetch(`${process.env.FUNBREW_API_URL}/pdf/from-html`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.FUNBREW_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ html, engine: 'quality', format: 'A4' }),
});
const pdfBuffer = Buffer.from(await response.arrayBuffer());
日本語PDFが文字化けする場合は?
HTMLの<meta charset="UTF-8">の記述と、CSSのfont-familyに'Noto Sans JP'を指定することで解決します。FUNBREW PDFはNoto Sans JPをプリインストール済みのため、追加フォントのアップロードは不要です。詳しくは日本語フォントガイドを参照してください。
まとめ
Node.jsでのPDF生成パターンをまとめます。
| パターン | 実装 | 特徴 |
|---|---|---|
| Express API | POST /pdf エンドポイント |
同期レスポンス・即時ダウンロード |
| Lambda / サーバーレス | Event-driven + S3 | スケーラブル・コスト効率 |
| Edge Runtime(Hono) | Cloudflare Workers対応 | 低レイテンシ・グローバル配信 |
| テンプレートファイル | Mustache/Handlebars | 保守性・デザイナー対応 |
いずれのパターンでも、PDF生成の実体はFUNBREW PDF APIへのHTTPリクエストです。プレイグラウンドでHTMLテンプレートをすぐに試せます。
関連記事
- PDF生成APIクイックスタート — Node.js/Python/PHPの基本的なAPIコール
- TypeScript × PDF API型安全ガイド — TypeScriptでの型安全な実装
- Next.js・Nuxt 3でPDF APIを使う完全ガイド — フレームワーク別の統合方法
- PDFサーバーレス・Lambda ガイド — AWS Lambda・Cloudflare Workersでの詳細実装
- PDF一括生成ガイド — 数百件のPDFを効率的に処理
- HTML→PDF CSS tips — PDF専用CSSのベストプラクティス
- PDF APIセキュリティガイド — APIキー管理とセキュリティ対策
- PDF API本番運用チェックリスト — 本番環境での安定運用
- PDFテンプレートエンジン入門 — 変数・ループ・条件分岐を使ったテンプレート設計
- APIリファレンス — エンドポイントの詳細仕様