PythonでWebアプリケーションを構築するなら、Django・FastAPI・Flaskのいずれかを選ぶケースがほとんどです。どのフレームワークでも、PDF生成APIを統合すれば請求書・証明書・レポートなどを動的に生成できます。
この記事では、FUNBREW PDFのAPIをDjango・FastAPI・Flaskそれぞれに組み込む方法を、実践的なコード例とともに解説します。同期・非同期処理、テンプレートからのPDF生成、セキュリティ対策、そしてpytestによるテストまでカバーします。
Pythonでの基本的なAPI呼び出しは言語別クイックスタートを、エラーハンドリングの詳細はエラーハンドリングガイドを参照してください。
準備
APIキーの取得
無料アカウントを作成し、ダッシュボードからAPIキーを発行します。
# .envファイルに設定
FUNBREW_PDF_API_KEY="sk-your-api-key"
APIキーの安全な管理方法はセキュリティガイドで詳しく解説しています。
共通ライブラリのインストール
pip install httpx python-dotenv
httpxはasync/syncの両方に対応したHTTPクライアントで、DjangoでもFastAPIでも使えます。
Django編
プロジェクト構成
myproject/
├── myproject/
│ ├── settings.py
│ └── urls.py
├── pdf_app/
│ ├── views.py
│ ├── urls.py
│ ├── services.py
│ └── templates/
│ └── pdf_app/
│ └── invoice.html
└── manage.py
settings.py — API設定
環境変数からAPIキーを読み込みます。ハードコーディングは絶対に避けてください。
# myproject/settings.py
import os
from dotenv import load_dotenv
load_dotenv()
FUNBREW_PDF_API_KEY = os.getenv("FUNBREW_PDF_API_KEY")
FUNBREW_PDF_API_URL = "https://api.pdf.funbrew.cloud/v1/pdf/from-html"
services.py — PDF生成サービス
ビジネスロジックをviewから分離し、再利用性を高めます。
# pdf_app/services.py
import httpx
from django.conf import settings
class PDFGenerationError(Exception):
"""PDF生成に失敗した場合の例外"""
pass
def generate_pdf(html: str, options: dict | None = None) -> bytes:
"""HTMLからPDFを生成して、バイナリを返す"""
payload = {
"html": html,
"format": "A4",
"engine": "quality",
}
if options:
payload.update(options)
response = httpx.post(
settings.FUNBREW_PDF_API_URL,
json=payload,
headers={
"Authorization": f"Bearer {settings.FUNBREW_PDF_API_KEY}",
"Content-Type": "application/json",
},
timeout=60.0,
)
if response.status_code != 200:
raise PDFGenerationError(
f"PDF generation failed: {response.status_code} - {response.text}"
)
return response.content
views.py — PDFエンドポイント
# pdf_app/views.py
from django.http import HttpResponse, JsonResponse
from django.template.loader import render_to_string
from django.views.decorators.http import require_POST
from .services import generate_pdf, PDFGenerationError
@require_POST
def generate_invoice_pdf(request, invoice_id):
"""請求書PDFを生成してHTTPレスポンスとして返す"""
# 実際にはDBから請求書データを取得
invoice_data = {
"invoice_id": invoice_id,
"company": "サンプル株式会社",
"items": [
{"name": "PDF API Proプラン", "quantity": 1, "price": 4980},
{"name": "追加API呼び出し 500件", "quantity": 1, "price": 2000},
],
"total": 6980,
}
# Djangoテンプレートを使ってHTMLを生成
html = render_to_string("pdf_app/invoice.html", invoice_data)
try:
pdf_bytes = generate_pdf(html)
except PDFGenerationError as e:
return JsonResponse({"error": str(e)}, status=502)
response = HttpResponse(pdf_bytes, content_type="application/pdf")
response["Content-Disposition"] = f'attachment; filename="invoice-{invoice_id}.pdf"'
return response
urls.py — ルーティング
# pdf_app/urls.py
from django.urls import path
from . import views
urlpatterns = [
path(
"invoices/<int:invoice_id>/pdf/",
views.generate_invoice_pdf,
name="invoice-pdf",
),
]
# myproject/urls.py
from django.urls import path, include
urlpatterns = [
path("api/", include("pdf_app.urls")),
]
テンプレートからのPDF生成
Djangoテンプレートエンジンを使えば、HTMLテンプレートにデータを差し込んでPDFを生成できます。テンプレート設計のコツはテンプレートエンジンガイドで解説しています。
<!-- pdf_app/templates/pdf_app/invoice.html -->
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<style>
body {
font-family: 'Noto Sans JP', sans-serif;
padding: 40px;
color: #1a1a1a;
}
.header {
display: flex;
justify-content: space-between;
border-bottom: 3px solid #1a56db;
padding-bottom: 16px;
margin-bottom: 32px;
}
h1 { font-size: 24px; margin: 0; }
table {
width: 100%;
border-collapse: collapse;
margin-top: 24px;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #e5e7eb;
}
th { background: #f9fafb; font-weight: 600; }
.total {
text-align: right;
font-size: 20px;
font-weight: 700;
margin-top: 24px;
}
</style>
</head>
<body>
<div class="header">
<h1>請求書</h1>
<div>
<p>請求書番号: #{{ invoice_id }}</p>
<p>{{ company }}</p>
</div>
</div>
<table>
<thead>
<tr>
<th>項目</th>
<th>数量</th>
<th>金額</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr>
<td>{{ item.name }}</td>
<td>{{ item.quantity }}</td>
<td>¥{{ item.price|stringformat:",.0f" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="total">合計: ¥{{ total|stringformat:",.0f" }}</div>
</body>
</html>
Celeryで非同期PDF生成(Django)
大量のPDFを生成する場合や、PDF生成に時間がかかる場合は、Celeryでバックグラウンド処理にするのが定番です。バッチ処理の詳細はPDF一括生成ガイドを参照してください。
# pdf_app/tasks.py
from celery import shared_task
from django.core.files.storage import default_storage
from .services import generate_pdf, PDFGenerationError
@shared_task(
bind=True,
max_retries=3,
default_retry_delay=10,
)
def generate_pdf_async(self, html: str, filename: str, options: dict | None = None):
"""非同期でPDFを生成し、ストレージに保存する"""
try:
pdf_bytes = generate_pdf(html, options)
path = f"generated_pdfs/{filename}"
default_storage.save(path, pdf_bytes)
return {"status": "success", "path": path}
except PDFGenerationError as exc:
# 指数バックオフでリトライ
raise self.retry(exc=exc, countdown=2 ** self.request.retries * 10)
# pdf_app/views.py(非同期版エンドポイント追加)
from django.http import JsonResponse
from django.template.loader import render_to_string
from django.views.decorators.http import require_POST
from .tasks import generate_pdf_async
@require_POST
def generate_invoice_pdf_async(request, invoice_id):
"""PDFの非同期生成をキューに投入する"""
invoice_data = get_invoice_data(invoice_id) # DB取得関数
html = render_to_string("pdf_app/invoice.html", invoice_data)
task = generate_pdf_async.delay(
html=html,
filename=f"invoice-{invoice_id}.pdf",
)
return JsonResponse({
"task_id": task.id,
"status": "processing",
"poll_url": f"/api/tasks/{task.id}/status/",
})
FastAPI編
プロジェクト構成
myproject/
├── app/
│ ├── main.py
│ ├── config.py
│ ├── services/
│ │ └── pdf_service.py
│ ├── routers/
│ │ └── pdf.py
│ └── templates/
│ └── report.html
├── tests/
│ └── test_pdf.py
└── requirements.txt
config.py — 設定管理
Pydantic Settingsで型安全に環境変数を管理します。
# app/config.py
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
funbrew_pdf_api_key: str
funbrew_pdf_api_url: str = "https://api.pdf.funbrew.cloud/v1/pdf/from-html"
class Config:
env_file = ".env"
settings = Settings()
pdf_service.py — 非同期PDF生成サービス
FastAPIのasync/awaitを最大限活用して、ノンブロッキングでPDFを生成します。
# app/services/pdf_service.py
import httpx
from app.config import settings
class PDFGenerationError(Exception):
def __init__(self, status_code: int, detail: str):
self.status_code = status_code
self.detail = detail
async def generate_pdf(html: str, options: dict | None = None) -> bytes:
"""非同期でHTMLからPDFを生成する"""
payload = {
"html": html,
"format": "A4",
"engine": "quality",
}
if options:
payload.update(options)
async with httpx.AsyncClient(timeout=60.0) as client:
response = await client.post(
settings.funbrew_pdf_api_url,
json=payload,
headers={
"Authorization": f"Bearer {settings.funbrew_pdf_api_key}",
"Content-Type": "application/json",
},
)
if response.status_code != 200:
raise PDFGenerationError(
status_code=response.status_code,
detail=response.text,
)
return response.content
routers/pdf.py — エンドポイント定義
# app/routers/pdf.py
from fastapi import APIRouter, HTTPException
from fastapi.responses import Response
from pydantic import BaseModel
from app.services.pdf_service import generate_pdf, PDFGenerationError
router = APIRouter(prefix="/pdf", tags=["PDF"])
class PDFRequest(BaseModel):
html: str
filename: str = "document.pdf"
format: str = "A4"
engine: str = "quality"
@router.post("/generate")
async def generate_pdf_endpoint(req: PDFRequest):
"""HTMLからPDFを生成してバイナリで返す"""
try:
pdf_bytes = await generate_pdf(
html=req.html,
options={"format": req.format, "engine": req.engine},
)
except PDFGenerationError as e:
raise HTTPException(status_code=e.status_code, detail=e.detail)
return Response(
content=pdf_bytes,
media_type="application/pdf",
headers={
"Content-Disposition": f'attachment; filename="{req.filename}"',
},
)
テンプレートとJinja2によるPDF生成
FastAPIはJinja2をネイティブサポートしています。テンプレートにデータを差し込んでPDFを生成するパターンです。
# app/routers/pdf.py(テンプレートベースのエンドポイント追加)
from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import Response
from fastapi.templating import Jinja2Templates
from app.services.pdf_service import generate_pdf, PDFGenerationError
router = APIRouter(prefix="/pdf", tags=["PDF"])
templates = Jinja2Templates(directory="app/templates")
@router.post("/reports/{report_id}")
async def generate_report_pdf(report_id: int, request: Request):
"""レポートPDFをテンプレートから生成する"""
# 実際にはDBからデータを取得
report_data = {
"request": request,
"report_id": report_id,
"title": "月次レポート",
"metrics": [
{"name": "PDF生成件数", "value": "1,234"},
{"name": "平均レスポンス時間", "value": "0.8秒"},
{"name": "成功率", "value": "99.7%"},
],
}
# Jinja2テンプレートでHTMLを生成
html = templates.get_template("report.html").render(report_data)
try:
pdf_bytes = await generate_pdf(html)
except PDFGenerationError as e:
raise HTTPException(status_code=502, detail="PDF generation failed")
return Response(
content=pdf_bytes,
media_type="application/pdf",
headers={
"Content-Disposition": f'attachment; filename="report-{report_id}.pdf"',
},
)
main.py — アプリケーション初期化
# app/main.py
from fastapi import FastAPI
from app.routers import pdf
app = FastAPI(
title="PDF Generation Service",
version="1.0.0",
)
app.include_router(pdf.router, prefix="/api/v1")
BackgroundTasksで非同期PDF生成(FastAPI)
FastAPIのBackgroundTasksを使えば、Celeryなしで軽量な非同期処理を実現できます。
# app/routers/pdf.py(BackgroundTasks版)
import uuid
from pathlib import Path
from fastapi import APIRouter, BackgroundTasks, HTTPException
from pydantic import BaseModel
from app.services.pdf_service import generate_pdf
router = APIRouter(prefix="/pdf", tags=["PDF"])
# メモリ内のタスクストア(本番ではRedis等を使用)
task_store: dict[str, dict] = {}
class AsyncPDFRequest(BaseModel):
html: str
filename: str = "document.pdf"
async def _generate_and_save(task_id: str, html: str, filename: str):
"""バックグラウンドでPDFを生成し、ファイルに保存する"""
try:
pdf_bytes = await generate_pdf(html)
output_dir = Path("storage/generated_pdfs")
output_dir.mkdir(parents=True, exist_ok=True)
output_path = output_dir / filename
output_path.write_bytes(pdf_bytes)
task_store[task_id] = {"status": "completed", "path": str(output_path)}
except Exception as e:
task_store[task_id] = {"status": "failed", "error": str(e)}
@router.post("/generate/async")
async def generate_pdf_async(req: AsyncPDFRequest, background_tasks: BackgroundTasks):
"""PDFの非同期生成を開始する"""
task_id = str(uuid.uuid4())
task_store[task_id] = {"status": "processing"}
background_tasks.add_task(_generate_and_save, task_id, req.html, req.filename)
return {"task_id": task_id, "status": "processing"}
@router.get("/tasks/{task_id}")
async def get_task_status(task_id: str):
"""非同期タスクの状態を確認する"""
task = task_store.get(task_id)
if not task:
raise HTTPException(status_code=404, detail="Task not found")
return task
Webhookで生成完了を通知する方法はWebhook連携ガイドを参照してください。
Flask編
プロジェクト構成
myproject/
├── app/
│ ├── __init__.py
│ ├── config.py
│ ├── services/
│ │ └── pdf_service.py
│ └── pdf/
│ ├── __init__.py
│ ├── routes.py
│ └── templates/
│ └── invoice.html
├── tests/
│ └── test_pdf.py
└── requirements.txt
config.py — 設定管理
# app/config.py
import os
from dotenv import load_dotenv
load_dotenv()
class Config:
FUNBREW_PDF_API_KEY = os.getenv("FUNBREW_PDF_API_KEY")
FUNBREW_PDF_API_URL = "https://api.pdf.funbrew.cloud/v1/pdf/from-html"
services/pdf_service.py — PDF生成サービス
Flaskではhttpxの同期クライアントをそのまま使えます。
# app/services/pdf_service.py
import httpx
from flask import current_app
class PDFGenerationError(Exception):
"""PDF生成に失敗した場合の例外"""
pass
def generate_pdf(html: str, options: dict | None = None) -> bytes:
"""HTMLからPDFを生成して、バイナリを返す"""
payload = {
"html": html,
"format": "A4",
"engine": "quality",
}
if options:
payload.update(options)
response = httpx.post(
current_app.config["FUNBREW_PDF_API_URL"],
json=payload,
headers={
"Authorization": f"Bearer {current_app.config['FUNBREW_PDF_API_KEY']}",
"Content-Type": "application/json",
},
timeout=60.0,
)
if response.status_code != 200:
raise PDFGenerationError(
f"PDF generation failed: {response.status_code} - {response.text}"
)
return response.content
基本的なFlaskルートでのPDF生成
# app/pdf/routes.py
from flask import Blueprint, render_template, make_response, jsonify
from app.services.pdf_service import generate_pdf, PDFGenerationError
pdf_bp = Blueprint("pdf", __name__, template_folder="templates")
@pdf_bp.route("/invoices/<int:invoice_id>/pdf", methods=["POST"])
def generate_invoice_pdf(invoice_id):
"""請求書PDFを生成してHTTPレスポンスとして返す"""
# 実際にはDBから請求書データを取得
invoice_data = {
"invoice_id": invoice_id,
"company": "サンプル株式会社",
"items": [
{"name": "PDF API Proプラン", "quantity": 1, "price": 4980},
{"name": "追加API呼び出し 500件", "quantity": 1, "price": 2000},
],
"total": 6980,
}
# Jinja2テンプレートでHTMLを生成
html = render_template("invoice.html", **invoice_data)
try:
pdf_bytes = generate_pdf(html)
except PDFGenerationError as e:
return jsonify({"error": str(e)}), 502
response = make_response(pdf_bytes)
response.headers["Content-Type"] = "application/pdf"
response.headers["Content-Disposition"] = (
f'attachment; filename="invoice-{invoice_id}.pdf"'
)
return response
Flask-RESTfulでのAPI実装
Flask-RESTfulを使うと、よりREST APIらしい構成にできます。
# app/pdf/resources.py
from flask import request
from flask_restful import Resource
from app.services.pdf_service import generate_pdf, PDFGenerationError
class PDFResource(Resource):
def post(self):
"""HTMLからPDFを生成してバイナリを返す"""
data = request.get_json()
if not data or "html" not in data:
return {"error": "html field is required"}, 400
html = data["html"]
filename = data.get("filename", "document.pdf")
options = {
"format": data.get("format", "A4"),
"engine": data.get("engine", "quality"),
}
try:
pdf_bytes = generate_pdf(html, options)
except PDFGenerationError as e:
return {"error": str(e)}, 502
from flask import make_response
response = make_response(pdf_bytes)
response.headers["Content-Type"] = "application/pdf"
response.headers["Content-Disposition"] = (
f'attachment; filename="{filename}"'
)
return response
# app/__init__.py(Flask-RESTful版)
from flask import Flask
from flask_restful import Api
from app.config import Config
from app.pdf.resources import PDFResource
def create_app():
app = Flask(__name__)
app.config.from_object(Config)
api = Api(app)
api.add_resource(PDFResource, "/api/v1/pdf/generate")
return app
Blueprintを使った構成例
大規模アプリでは、Blueprintでモジュールを分割するのが定番パターンです。
# app/pdf/__init__.py
from flask import Blueprint
pdf_bp = Blueprint(
"pdf",
__name__,
url_prefix="/api/v1/pdf",
template_folder="templates",
)
from app.pdf import routes # noqa: E402, F401
# app/__init__.py(Blueprint版)
from flask import Flask
from app.config import Config
from app.pdf import pdf_bp
def create_app():
app = Flask(__name__)
app.config.from_object(Config)
app.register_blueprint(pdf_bp)
return app
これでPDFルートは /api/v1/pdf/invoices/<id>/pdf としてアクセスできます。Blueprintを活用すれば、請求書・証明書・レポートなど用途ごとにモジュールを分けて管理できます。テンプレート設計のコツはテンプレートエンジンガイドを参照してください。
セキュリティ考慮事項
APIキーの管理はPDF API統合における最も重要なセキュリティポイントです。詳細はセキュリティガイドに譲りますが、最低限以下を守ってください。
APIキーをソースコードに書かない
# NG: ハードコーディング
API_KEY = "sk-abc123..."
# OK: 環境変数から取得
API_KEY = os.getenv("FUNBREW_PDF_API_KEY")
.envをバージョン管理に含めない
# .gitignore
.env
.env.local
.env.production
サーバーサイドのみでAPI呼び出し
PDF APIの呼び出しは必ずサーバーサイドで行います。フロントエンドのJavaScriptからAPIキーを送信してはいけません。DjangoもFastAPIもサーバーサイドフレームワークなので、この点は自然に守られます。
pytestによるテスト
テストではAPIの実呼び出しを避け、respx(httpxのモックライブラリ)を使います。
Djangoのテスト
# tests/test_pdf_service.py
import pytest
import respx
from httpx import Response
from pdf_app.services import generate_pdf, PDFGenerationError
@respx.mock
def test_generate_pdf_success():
"""PDF生成が成功した場合、PDFバイナリが返る"""
fake_pdf = b"%PDF-1.4 fake content"
respx.post("https://api.pdf.funbrew.cloud/v1/pdf/from-html").mock(
return_value=Response(200, content=fake_pdf)
)
result = generate_pdf("<h1>Test</h1>")
assert result == fake_pdf
@respx.mock
def test_generate_pdf_api_error():
"""APIがエラーを返した場合、PDFGenerationErrorが発生する"""
respx.post("https://api.pdf.funbrew.cloud/v1/pdf/from-html").mock(
return_value=Response(500, text="Internal Server Error")
)
with pytest.raises(PDFGenerationError):
generate_pdf("<h1>Test</h1>")
@respx.mock
def test_generate_invoice_pdf_view(client):
"""請求書PDFエンドポイントがPDFを返す"""
fake_pdf = b"%PDF-1.4 fake content"
respx.post("https://api.pdf.funbrew.cloud/v1/pdf/from-html").mock(
return_value=Response(200, content=fake_pdf)
)
response = client.post("/api/invoices/1/pdf/")
assert response.status_code == 200
assert response["Content-Type"] == "application/pdf"
FastAPIのテスト
# tests/test_pdf.py
import pytest
import respx
from httpx import AsyncClient, Response
from app.main import app
@pytest.mark.anyio
@respx.mock
async def test_generate_pdf_endpoint():
"""PDF生成エンドポイントが正常に動作する"""
fake_pdf = b"%PDF-1.4 fake content"
respx.post("https://api.pdf.funbrew.cloud/v1/pdf/from-html").mock(
return_value=Response(200, content=fake_pdf)
)
async with AsyncClient(app=app, base_url="http://test") as client:
response = await client.post(
"/api/v1/pdf/generate",
json={"html": "<h1>Test</h1>", "filename": "test.pdf"},
)
assert response.status_code == 200
assert response.headers["content-type"] == "application/pdf"
assert response.content == fake_pdf
@pytest.mark.anyio
@respx.mock
async def test_generate_pdf_api_failure():
"""外部APIが失敗した場合、502を返す"""
respx.post("https://api.pdf.funbrew.cloud/v1/pdf/from-html").mock(
return_value=Response(500, text="Server Error")
)
async with AsyncClient(app=app, base_url="http://test") as client:
response = await client.post(
"/api/v1/pdf/generate",
json={"html": "<h1>Test</h1>"},
)
assert response.status_code == 500
@pytest.mark.anyio
@respx.mock
async def test_async_pdf_generation():
"""非同期PDF生成がタスクIDを返す"""
async with AsyncClient(app=app, base_url="http://test") as client:
response = await client.post(
"/api/v1/pdf/generate/async",
json={"html": "<h1>Test</h1>", "filename": "async-test.pdf"},
)
assert response.status_code == 200
data = response.json()
assert "task_id" in data
assert data["status"] == "processing"
Django・FastAPI・Flaskの使い分け
| 観点 | Django | FastAPI | Flask |
|---|---|---|---|
| 向いているケース | 管理画面付きの業務アプリ、既存Djangoプロジェクトへの統合 | マイクロサービス、高スループットAPI、リアルタイム処理 | 軽量API、プロトタイプ、シンプルなWebアプリ |
| PDF生成パターン | テンプレートエンジン + Celery非同期 | async/await + BackgroundTasks | Blueprint + 同期処理(Celeryも利用可) |
| パフォーマンス | 同期処理がデフォルト(非同期はDjango 4.1+で対応) | async/awaitがネイティブ、I/Oバウンド処理に強い | 同期処理がデフォルト、シンプルで予測しやすい |
| エコシステム | Django REST Framework、管理画面、ORM | Pydantic自動バリデーション、OpenAPI自動生成 | Flask-RESTful、Blueprint、豊富な拡張ライブラリ |
| 非同期処理 | Celery + Redis/RabbitMQ | BackgroundTasks(軽量)、またはCelery(大規模) | Celery + Redis/RabbitMQ |
| 学習コスト | フルスタックの学習が必要 | APIに特化、軽量 | 最小限の概念で始められる、柔軟性が高い |
選び方の目安:
- 既存のDjangoアプリがある → Djangoのviewに統合するのが自然
- 新規のAPIサービスを作る → FastAPIでasync/awaitを活かす
- 軽量なサービスやプロトタイプ → FlaskのBlueprintで素早く構築
- 大量のPDFを一括生成する → どのフレームワークでもCeleryを導入すべき
- 管理画面が必要 → Djangoの管理画面が強力
請求書や証明書などのPDF生成ユースケースは請求書自動化ガイドやユースケース一覧も参考にしてください。本番環境での運用ノウハウはプロダクション運用ガイドにまとめています。
まとめ
Django・FastAPI・Flaskのどれを使っても、FUNBREW PDF APIの統合はシンプルです。HTTPリクエストでHTMLを送り、PDFバイナリを受け取る――この基本パターンを押さえれば、あとはフレームワークの作法に沿って組み込むだけです。
- サービス層を分離する — viewやrouterからPDF生成ロジックを切り離す
- 非同期処理を活用する — 重いPDF生成はバックグラウンドに回す
- テストを書く — respxでAPIモックし、エッジケースもカバーする
- セキュリティを守る — APIキーは環境変数で管理し、サーバーサイドのみで使う
まずはPlaygroundで自分のHTMLがどんなPDFになるか試してみてください。準備ができたらドキュメントでAPIの全機能を確認できます。