Next.jsやNuxt.jsでWebアプリを開発しているとき、「ページをPDFで出力したい」「請求書や証明書をダウンロードさせたい」という要件はよく発生します。クライアントサイドだけでPDFを生成しようとすると、CSSの再現性や文字化け、ファイルサイズの問題がつきまといます。
そこで有効なのが、サーバーサイドでHTMLからPDFを生成するAPIを使う方法です。この記事ではFUNBREW PDFをNext.js(App Router)とNuxt 3に統合する手順を、実践的なコード例とともに解説します。
フレームワーク非依存の基本的なHTMLからPDF生成の概念はHTML→PDF変換 完全ガイドを、APIの基本的な使い方は言語別クイックスタートを参照してください。TypeScriptでの型安全なAPI利用についてはTypeScript×PDF APIガイドで詳しく解説しています。
なぜフロントエンドフレームワークでPDF APIが必要か
ブラウザのネイティブ印刷機能(window.print())やjsPDFなどのクライアントサイドライブラリには限界があります。
| 課題 | 詳細 |
|---|---|
| CSS再現性の低さ | ブラウザごとに印刷スタイルが異なる |
| 動的コンテンツ | 認証が必要なデータや外部APIデータを含められない |
| ファイルサイズ | クライアントでの変換は処理が重くなりやすい |
| セキュリティ | APIキーをクライアントに渡せない |
サーバーサイドでPDFを生成することで、これらの問題をすべて回避できます。Next.jsのAPI RouteやNuxt 3のServer Routeはこの用途に最適です。wkhtmltopdfとChromiumの違いについてはエンジン比較記事も参考にしてください。
共通の準備
APIキーの取得
ダッシュボードからFUNBREW PDFのAPIキーを取得します。環境変数に保存し、クライアントコードには絶対に含めないでください。
APIキーの安全な管理方法はセキュリティガイドで詳しく解説しています。
# .env.local
FUNBREW_PDF_API_KEY=sk-your-api-key
Node.js SDKのインストール
npm install @funbrew/pdf
SDKを使わず直接HTTPリクエストを送る場合はネイティブのfetchが使えます(Node.js 18以降)。
Next.js(App Router)編
プロジェクト構成
app/
├── api/
│ └── generate-pdf/
│ └── route.ts # PDF生成エンドポイント
├── invoice/
│ └── page.tsx # 請求書ページ
└── components/
└── DownloadButton.tsx # PDFダウンロードボタン
API Route の実装
app/api/generate-pdf/route.ts にサーバーサイドのPDF生成エンドポイントを作成します。
// app/api/generate-pdf/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { FunbrewPdf } from '@funbrew/pdf'
const client = new FunbrewPdf({
apiKey: process.env.FUNBREW_PDF_API_KEY!,
})
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { html, options } = body
// 入力バリデーション
if (!html || typeof html !== 'string') {
return NextResponse.json(
{ error: 'html フィールドは必須です' },
{ status: 400 }
)
}
const pdfBuffer = await client.generate({
html,
options: {
format: 'A4',
margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
...options,
},
})
return new NextResponse(pdfBuffer, {
status: 200,
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': 'attachment; filename="document.pdf"',
},
})
} catch (error) {
console.error('PDF生成エラー:', error)
return NextResponse.json(
{ error: 'PDF生成に失敗しました' },
{ status: 500 }
)
}
}
SDKを使わずに直接APIを呼び出す場合は次のように書けます。
// fetch を使った実装例
const response = await fetch('https://api.funbrew.dev/api/v1/generate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.FUNBREW_PDF_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ html, options }),
})
if (!response.ok) {
throw new Error(`API エラー: ${response.status}`)
}
const pdfBuffer = await response.arrayBuffer()
テンプレート変数の差し込み
動的データをHTMLに埋め込んでPDFを生成するケースが多いでしょう。サーバーサイドでデータを取得し、HTMLテンプレートに変数を差し込む方法を示します。
// app/api/generate-pdf/invoice/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { FunbrewPdf } from '@funbrew/pdf'
const client = new FunbrewPdf({
apiKey: process.env.FUNBREW_PDF_API_KEY!,
})
function buildInvoiceHtml(data: {
invoiceNumber: string
customerName: string
items: Array<{ name: string; quantity: number; price: number }>
total: number
}): string {
const rows = data.items
.map(
(item) => `
<tr>
<td>${item.name}</td>
<td>${item.quantity}</td>
<td>¥${item.price.toLocaleString()}</td>
<td>¥${(item.quantity * item.price).toLocaleString()}</td>
</tr>`
)
.join('')
return `<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<style>
body { font-family: 'Noto Sans JP', sans-serif; padding: 40px; }
h1 { color: #1a56db; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
th, td { border: 1px solid #e5e7eb; padding: 10px; text-align: left; }
th { background: #f9fafb; }
.total { font-size: 1.2em; font-weight: bold; text-align: right; margin-top: 20px; }
</style>
</head>
<body>
<h1>請求書 #${data.invoiceNumber}</h1>
<p>請求先: ${data.customerName} 様</p>
<table>
<thead>
<tr><th>品目</th><th>数量</th><th>単価</th><th>小計</th></tr>
</thead>
<tbody>${rows}</tbody>
</table>
<p class="total">合計: ¥${data.total.toLocaleString()}</p>
</body>
</html>`
}
export async function POST(request: NextRequest) {
const invoiceData = await request.json()
const html = buildInvoiceHtml(invoiceData)
const pdfBuffer = await client.generate({
html,
options: { format: 'A4' },
})
return new NextResponse(pdfBuffer, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="invoice-${invoiceData.invoiceNumber}.pdf"`,
},
})
}
テンプレートエンジンを使った高度な差し込み方法はテンプレートエンジン活用ガイドで解説しています。
クライアントサイドからのダウンロード
クライアントコンポーネントからAPI Routeを叩き、PDFをダウンロードさせます。
// app/components/DownloadButton.tsx
'use client'
import { useState } from 'react'
interface InvoiceData {
invoiceNumber: string
customerName: string
items: Array<{ name: string; quantity: number; price: number }>
total: number
}
export function DownloadPdfButton({ invoiceData }: { invoiceData: InvoiceData }) {
const [loading, setLoading] = useState(false)
const handleDownload = async () => {
setLoading(true)
try {
const response = await fetch('/api/generate-pdf/invoice', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(invoiceData),
})
if (!response.ok) {
throw new Error('PDF生成に失敗しました')
}
// Blob に変換してダウンロードリンクを作成
const blob = await response.blob()
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `invoice-${invoiceData.invoiceNumber}.pdf`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
} catch (error) {
console.error(error)
alert('PDFのダウンロードに失敗しました')
} finally {
setLoading(false)
}
}
return (
<button
onClick={handleDownload}
disabled={loading}
className="btn btn-primary"
>
{loading ? '生成中...' : 'PDFをダウンロード'}
</button>
)
}
Server Componentからの直接生成
App RouterのServer Componentでは、API Routeを経由せずに直接PDF生成ロジックを呼び出すこともできます。
// app/invoice/[id]/download/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { FunbrewPdf } from '@funbrew/pdf'
import { getInvoiceById } from '@/lib/db'
const client = new FunbrewPdf({
apiKey: process.env.FUNBREW_PDF_API_KEY!,
})
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
// 認証チェック(セッション/JWT等)
// const session = await getServerSession()
// if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
const invoice = await getInvoiceById(params.id)
if (!invoice) {
return NextResponse.json({ error: '請求書が見つかりません' }, { status: 404 })
}
const html = buildInvoiceHtml(invoice)
const pdfBuffer = await client.generate({ html, options: { format: 'A4' } })
return new NextResponse(pdfBuffer, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': `inline; filename="invoice-${params.id}.pdf"`,
},
})
}
Nuxt 3編
プロジェクト構成
server/
├── api/
│ └── generate-pdf.post.ts # PDF生成エンドポイント
└── utils/
└── pdf.ts # 共通ユーティリティ
pages/
└── invoice.vue # 請求書ページ
composables/
└── usePdf.ts # PDF操作コンポーザブル
Server Route の実装
Nuxt 3のServer Routeはファイル名の末尾に.post.tsをつけることでPOSTメソッドに限定できます。
// server/api/generate-pdf.post.ts
import { FunbrewPdf } from '@funbrew/pdf'
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const { html, options, filename = 'document.pdf' } = body
if (!html || typeof html !== 'string') {
throw createError({
statusCode: 400,
statusMessage: 'html フィールドは必須です',
})
}
const apiKey = useRuntimeConfig().funbrewPdfApiKey
const client = new FunbrewPdf({ apiKey })
try {
const pdfBuffer = await client.generate({
html,
options: {
format: 'A4',
margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
...options,
},
})
setResponseHeaders(event, {
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="${filename}"`,
})
return pdfBuffer
} catch (error) {
throw createError({
statusCode: 500,
statusMessage: 'PDF生成に失敗しました',
})
}
})
nuxt.config.ts — runtimeConfig 設定
APIキーはサーバー専用のruntimeConfigに設定します。publicに入れるとクライアントに露出するので注意してください。
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
// サーバーサイドのみ(環境変数 NUXT_FUNBREW_PDF_API_KEY で上書き可能)
funbrewPdfApiKey: process.env.FUNBREW_PDF_API_KEY ?? '',
public: {
// ここはクライアントに公開される
},
},
})
# .env
NUXT_FUNBREW_PDF_API_KEY=sk-your-api-key
コンポーザブルの実装
再利用可能なPDFダウンロード処理をコンポーザブルにまとめます。
// composables/usePdf.ts
export function usePdf() {
const loading = ref(false)
const error = ref<string | null>(null)
async function downloadPdf(
html: string,
options?: Record<string, unknown>,
filename = 'document.pdf'
) {
loading.value = true
error.value = null
try {
const response = await $fetch<Blob>('/api/generate-pdf', {
method: 'POST',
body: { html, options, filename },
responseType: 'blob',
})
const url = URL.createObjectURL(response)
const a = document.createElement('a')
a.href = url
a.download = filename
a.click()
URL.revokeObjectURL(url)
} catch (err) {
error.value = 'PDFのダウンロードに失敗しました'
console.error(err)
} finally {
loading.value = false
}
}
return { loading, error, downloadPdf }
}
Nuxt 3 ページコンポーネント
<!-- pages/invoice.vue -->
<script setup lang="ts">
const { loading, error, downloadPdf } = usePdf()
const invoiceData = ref({
number: 'INV-2026-001',
customer: '株式会社サンプル',
total: 150000,
})
async function handleDownload() {
const html = `<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<style>body { font-family: sans-serif; padding: 40px; }</style>
</head>
<body>
<h1>請求書 #${invoiceData.value.number}</h1>
<p>請求先: ${invoiceData.value.customer} 様</p>
<p>合計: ¥${invoiceData.value.total.toLocaleString()}</p>
</body>
</html>`
await downloadPdf(html, { format: 'A4' }, `invoice-${invoiceData.value.number}.pdf`)
}
</script>
<template>
<div>
<h1>請求書管理</h1>
<p v-if="error" class="text-red-500">{{ error }}</p>
<button
:disabled="loading"
@click="handleDownload"
class="btn btn-primary"
>
{{ loading ? '生成中...' : 'PDFをダウンロード' }}
</button>
</div>
</template>
エラーハンドリング
PDF生成APIを呼び出す際は、適切なエラーハンドリングが不可欠です。詳細はエラーハンドリングガイドを参照してください。
リトライロジック(Next.js)
// lib/pdf.ts
import { FunbrewPdf } from '@funbrew/pdf'
const client = new FunbrewPdf({
apiKey: process.env.FUNBREW_PDF_API_KEY!,
})
export async function generatePdfWithRetry(
html: string,
options = {},
maxRetries = 3
): Promise<ArrayBuffer> {
let lastError: Error | undefined
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await client.generate({ html, options })
} catch (error) {
lastError = error as Error
if (attempt < maxRetries) {
// 指数バックオフ
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt))
}
}
}
throw lastError
}
HTTPステータスコード別の対応
| ステータスコード | 意味 | 対応 |
|---|---|---|
| 400 | HTMLの形式が不正 | リクエストを修正する |
| 401 | APIキー不正 | 環境変数を確認する |
| 429 | レート制限超過 | リトライ間隔を空ける |
| 500 | サーバーエラー | リトライする |
本番デプロイ時の注意点
Vercelへのデプロイ(Next.js)
Vercelでは環境変数をプロジェクト設定から追加します。APIキーをクライアントに露出させないために、NEXT_PUBLIC_プレフィックスは絶対に使わないでください。
# Vercel CLI で環境変数を設定
vercel env add FUNBREW_PDF_API_KEY production
タイムアウト設定: PDF生成は場合によって数秒かかります。Vercelの無料プランではServerless Functionのデフォルトタイムアウトは10秒です。大きなドキュメントを扱う場合はProプラン(最大60秒)が必要です。
// app/api/generate-pdf/route.ts
export const maxDuration = 60 // 秒(Pro プランのみ)
Netlify へのデプロイ(Nuxt 3)
Nuxt 3はNetlifyのEdge FunctionsやServerless Functionsで動作します。
# netlify.toml
[functions]
node_bundler = "esbuild"
external_node_modules = ["@funbrew/pdf"]
# Netlify CLI で環境変数を設定
netlify env:set NUXT_FUNBREW_PDF_API_KEY "sk-your-api-key"
セルフホストの場合
VPS等でNext.js/Nuxtをセルフホストする場合も基本的なセキュリティ対策は同じです。
- APIキーは環境変数で管理する(
.envファイルはGitに含めない) - レート制限をかける(ddos防止)
- PDFのサイズ制限を設ける(メモリ保護)
サーバーレス環境でのPDF生成についてはサーバーレスPDF生成ガイドも参考にしてください。また本番運用の包括的なチェックリストは本番運用チェックリストをご覧ください。Docker/Kubernetes環境での運用はDocker & Kubernetesガイドで解説しています。CSS印刷スタイルの最適化はPDF出力向けCSSレイアウトのコツが参考になります。
一括PDF生成
複数のドキュメントを一度に生成する場合は、バッチ処理を活用します。
// app/api/generate-pdfs-batch/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { FunbrewPdf } from '@funbrew/pdf'
const client = new FunbrewPdf({
apiKey: process.env.FUNBREW_PDF_API_KEY!,
})
export async function POST(request: NextRequest) {
const { documents } = await request.json()
// 並列処理(最大5件)
const chunks = []
for (let i = 0; i < documents.length; i += 5) {
chunks.push(documents.slice(i, i + 5))
}
const results = []
for (const chunk of chunks) {
const pdfs = await Promise.allSettled(
chunk.map((doc: { html: string; filename: string }) =>
client.generate({ html: doc.html, options: { format: 'A4' } })
)
)
results.push(...pdfs)
}
// 成功/失敗のサマリーを返す
const summary = {
total: results.length,
success: results.filter((r) => r.status === 'fulfilled').length,
failed: results.filter((r) => r.status === 'rejected').length,
}
return NextResponse.json(summary)
}
大量ドキュメントの一括生成については一括生成ガイドで詳しく解説しています。
まとめ
Next.js(App Router)とNuxt 3でのFUNBREW PDF API統合のポイントをまとめます。
- APIキーは必ずサーバーサイドのみで扱う(
NEXT_PUBLIC_やruntimeConfig.publicに入れない) - API Route / Server RouteでPDF生成ロジックをカプセル化する
- クライアントコンポーネントからはBlobダウンロードパターンを使う
- Vercel / Netlifyへのデプロイ時はタイムアウト設定に注意する
- エラーハンドリングとリトライを実装して安定性を確保する
プレイグラウンドでHTMLテンプレートを試してから実装を始めると、開発が効率的に進みます。ユースケース別の活用例はユースケース一覧もご参照ください。
請求書PDFの自動化については請求書PDF自動化ガイド、証明書PDFの生成については証明書PDF自動化ガイドもあわせてご覧ください。MarkdownからのPDF生成はMarkdown→PDF APIガイド、月次レポートの自動生成はレポートPDF生成ガイドも参考になります。利用量に応じたプラン選択は料金プラン比較をご確認ください。