Claude Codeに「PDFレポートを自動生成する仕組みを作って」と頼むと、Puppeteer・HTMLテンプレート・非同期キュー・S3保存を一気に設計してくれる。本記事ではその実装パターンを整理する。
全体アーキテクチャ
リクエスト受信 → BullMQキューに積む → ワーカーがPuppeteerでPDF生成 → S3にアップロード → presigned URLを返す、という流れが基本形だ。
POST /reports → 202 + jobId
└─ BullMQ Queue
└─ Worker: Puppeteer → S3
GET /reports/:jobId → URL or pending
1. Puppeteerブラウザの再利用(100回でリスタート)
Puppeteerは起動コストが高い。インスタンスを使い回すが、メモリリークを防ぐため100回ごとに再起動する。
let browser: Browser | null = null;
let useCount = 0;
const MAX_USE = 100;
async function getBrowser(): Promise<Browser> {
if (!browser || useCount >= MAX_USE) {
if (browser) await browser.close();
browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
useCount = 0;
}
useCount++;
return browser;
}
Claude Codeへの指示:「ブラウザを再利用するが、100回使ったら閉じて再起動する仕組みにして」と伝えるとこのパターンを生成する。
2. HandlebarsテンプレートでHTML生成
PDFのレイアウトはHandlebarsで管理する。テンプレートを分離することで、デザイン変更がコードに影響しない。
import Handlebars from 'handlebars';
import fs from 'fs';
const templateSrc = fs.readFileSync('templates/report.hbs', 'utf-8');
const template = Handlebars.compile(templateSrc);
function renderHtml(data: ReportData): string {
return template({
title: data.title,
generatedAt: new Date().toLocaleString('ja-JP'),
items: data.items,
});
}
templates/report.hbs の中身:
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<style>
body { font-family: 'Noto Sans JP', sans-serif; margin: 40px; }
h1 { color: #333; }
table { width: 100%; border-collapse: collapse; }
td, th { border: 1px solid #ddd; padding: 8px; }
</style>
</head>
<body>
<h1>{{title}}</h1>
<p>生成日時: {{generatedAt}}</p>
<table>
{{#each items}}
<tr><td>{{this.name}}</td><td>{{this.value}}</td></tr>
{{/each}}
</table>
</body>
</html>
3. BullMQで非同期キュー(202 + jobId)
PDF生成は時間がかかるため、即座に202 Acceptedを返してjobIdで進捗を確認する設計にする。
import { Queue, Worker, Job } from 'bullmq';
import { Redis } from 'ioredis';
const connection = new Redis({ host: 'localhost', port: 6379 });
const pdfQueue = new Queue('pdf-generation', { connection });
// POST /reports
app.post('/reports', async (req, res) => {
const job = await pdfQueue.add('generate', req.body);
res.status(202).json({ jobId: job.id, status: 'pending' });
});
// GET /reports/:jobId
app.get('/reports/:jobId', async (req, res) => {
const job = await Job.fromId(pdfQueue, req.params.jobId);
if (!job) return res.status(404).json({ error: 'not found' });
const state = await job.getState();
if (state === 'completed') {
return res.json({ status: 'completed', url: job.returnvalue });
}
res.json({ status: state });
});
Claude Codeへの指示:「PDF生成は重いのでBullMQでキューに積み、202とjobIdを返す設計にして」と伝えると完全な実装を出力する。
4. S3にアップロードしてpresigned URLを7日間発行
生成したPDFはS3に保存し、7日間有効のpresigned URLをクライアントに返す。
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
const s3 = new S3Client({ region: 'ap-northeast-1' });
async function uploadToS3(pdf: Buffer, key: string): Promise<string> {
await s3.send(new PutObjectCommand({
Bucket: process.env.S3_BUCKET!,
Key: key,
Body: pdf,
ContentType: 'application/pdf',
}));
const url = await getSignedUrl(
s3,
new GetObjectCommand({ Bucket: process.env.S3_BUCKET!, Key: key }),
{ expiresIn: 60 * 60 * 24 * 7 }
);
return url;
}
Workerの全体像
const worker = new Worker('pdf-generation', async (job) => {
const html = renderHtml(job.data);
const browser = await getBrowser();
const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'networkidle0' });
const pdf = await page.pdf({ format: 'A4', printBackground: true });
await page.close();
const key = `reports/${job.id}-${Date.now()}.pdf`;
const url = await uploadToS3(pdf, key);
return url;
}, { connection });
まとめ
- ブラウザ再利用 — Puppeteerを使い回しつつ100回でリスタート。メモリリーク防止。
- テンプレート分離 — HandlebarsでHTML管理。デザイン変更がコードに波及しない。
- 非同期キュー — BullMQ + 202パターンで重い処理をノンブロッキングに。
- S3 + presigned URL — 7日間限定URLで安全に配信。直接ダウンロードリンクを渡せる。
Claude Codeにこれらを一度に伝えると、ボイラープレートから型定義・エラーハンドリングまで一気に生成してくれる。設計の意図を自然言語で伝えることがポイントだ。
Code Review Pack ¥980 — Claude Codeで生成したコードをレビューするためのプロンプトセット。セキュリティ・パフォーマンス・可読性の観点でチェックリストを網羅。
https://prompt-works.jp
Top comments (0)