DEV Community

myougaTheAxo
myougaTheAxo

Posted on • Originally published at zenn.dev

Claude CodeでPDF生成を設計する:Puppeteer・HTMLテンプレート・S3保存・非同期処理

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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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,
  });
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 });
});
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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 });
Enter fullscreen mode Exit fullscreen mode

まとめ

  1. ブラウザ再利用 — Puppeteerを使い回しつつ100回でリスタート。メモリリーク防止。
  2. テンプレート分離 — HandlebarsでHTML管理。デザイン変更がコードに波及しない。
  3. 非同期キュー — BullMQ + 202パターンで重い処理をノンブロッキングに。
  4. S3 + presigned URL — 7日間限定URLで安全に配信。直接ダウンロードリンクを渡せる。

Claude Codeにこれらを一度に伝えると、ボイラープレートから型定義・エラーハンドリングまで一気に生成してくれる。設計の意図を自然言語で伝えることがポイントだ。


Code Review Pack ¥980 — Claude Codeで生成したコードをレビューするためのプロンプトセット。セキュリティ・パフォーマンス・可読性の観点でチェックリストを網羅。
https://prompt-works.jp

Top comments (0)