NaN/NaN/NaN

RubyやGoでPDFを生成する場合、wkhtmltopdfやChromiumをサーバーにインストールするのが従来のアプローチでした。しかし依存関係の管理やメモリ消費が課題になります。特にGoでは、Chromium依存のライブラリを導入するとDockerイメージが数百MB膨らみ、CI/CDパイプラインの速度にも影響します。

FUNBREW PDF APIを使えば、HTTPリクエストを送るだけでHTMLからPDFを生成できます。サーバーにブラウザエンジンをインストールする必要はなく、RubyのFaradayやGoのnet/httpなど使い慣れたHTTPクライアントだけで完結します。日本語フォント(Noto Sans JP)もプリインストール済みのため、フォント周りの設定も不要です。

この記事では、RubyとGoそれぞれで複数のHTTPクライアントを使った実装例を紹介します。HTML直接指定、テンプレート利用、フレームワーク統合、バッチ処理まで網羅しています。Node.js・Python・PHP版は言語別クイックスタートを参照してください。主要なHTML to PDF変換手段の比較はHTML to PDF API比較でまとめています。

準備

1. アカウント作成

無料アカウントを作成します。月30件まで無料でPDF生成が可能です。

2. APIキーの取得

ダッシュボードの「APIキー」セクションからキーを発行します。キーはsk-で始まる文字列です。

# 環境変数に設定(推奨)
export FUNBREW_PDF_API_KEY="sk-your-api-key"

APIキーの安全な管理方法についてはセキュリティガイドを参照してください。

3. APIエンドポイント

POST https://api.pdf.funbrew.cloud/v1/pdf/from-html

HTMLを送信するとPDFバイナリが返ります。シンプルなリクエスト/レスポンスモデルです。APIの全エンドポイントはドキュメントで確認できます。

Ruby

net/http(標準ライブラリ)

追加gemなしで使えます。まずは最小限のコードでPDFを生成してみましょう。

require 'net/http'
require 'json'
require 'uri'

uri = URI('https://api.pdf.funbrew.cloud/v1/pdf/from-html')

http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.open_timeout = 10
http.read_timeout = 30

request = Net::HTTP::Post.new(uri)
request['Authorization'] = "Bearer #{ENV['FUNBREW_PDF_API_KEY']}"
request['Content-Type'] = 'application/json'
request.body = {
  html: <<~HTML,
    <html>
    <head>
      <style>
        body { font-family: 'Noto Sans JP', sans-serif; padding: 40px; }
        h1 { color: #1a1a1a; border-bottom: 2px solid #3b82f6; padding-bottom: 8px; }
        .date { color: #6b7280; margin-top: 24px; }
      </style>
    </head>
    <body>
      <h1>レポート</h1>
      <p>これはAPIから生成されたPDFです。</p>
      <p class="date">生成日時: #{Time.now.iso8601}</p>
    </body>
    </html>
  HTML
  engine: 'quality',
  format: 'A4'
}.to_json

response = http.request(request)

if response.code == '200'
  File.binwrite('output.pdf', response.body)
  puts "PDF generated: output.pdf (#{response.body.bytesize} bytes)"
else
  puts "Error: #{response.code} - #{response.body}"
end

Faraday

Faradayを使うと、リトライやミドルウェアの設定が簡単になります。

require 'faraday'
require 'json'

conn = Faraday.new(url: 'https://api.pdf.funbrew.cloud') do |f|
  f.request :retry, max: 3, interval: 1, backoff_factor: 2
  f.options.timeout = 30
  f.options.open_timeout = 10
end

response = conn.post('/v1/pdf/from-html') do |req|
  req.headers['Authorization'] = "Bearer #{ENV['FUNBREW_PDF_API_KEY']}"
  req.headers['Content-Type'] = 'application/json'
  req.body = {
    html: '<h1>Hello PDF</h1><p>Generated with Faraday</p>',
    engine: 'fast',
    format: 'A4'
  }.to_json
end

if response.success?
  File.binwrite('output.pdf', response.body)
  puts "PDF generated: output.pdf"
else
  puts "Error: #{response.status} - #{response.body}"
end

テンプレートを使ったPDF生成

登録済みのテンプレートを使えば、データを渡すだけでPDFを生成できます。テンプレートの作成方法はテンプレートエンジンガイドを参照してください。

require 'faraday'
require 'json'

conn = Faraday.new(url: 'https://api.pdf.funbrew.cloud') do |f|
  f.options.timeout = 30
end

response = conn.post('/v1/pdf/from-template') do |req|
  req.headers['Authorization'] = "Bearer #{ENV['FUNBREW_PDF_API_KEY']}"
  req.headers['Content-Type'] = 'application/json'
  req.body = {
    template_id: 'invoice-v1',
    data: {
      company_name: '株式会社サンプル',
      invoice_number: 'INV-2026-0042',
      items: [
        { name: 'Webコンサルティング', quantity: 1, price: 150_000 },
        { name: 'デザイン制作', quantity: 3, price: 50_000 }
      ],
      total: 300_000
    },
    format: 'A4'
  }.to_json
end

File.binwrite('invoice.pdf', response.body) if response.success?

請求書PDFの自動化については請求書PDF自動生成ガイドも参考になります。

URLからPDF変換

既存のWebページをそのままPDF化することもできます。

require 'faraday'
require 'json'

conn = Faraday.new(url: 'https://api.pdf.funbrew.cloud') do |f|
  f.options.timeout = 60  # URL変換はページの読み込みがあるため長めに
end

response = conn.post('/v1/pdf/from-url') do |req|
  req.headers['Authorization'] = "Bearer #{ENV['FUNBREW_PDF_API_KEY']}"
  req.headers['Content-Type'] = 'application/json'
  req.body = {
    url: 'https://example.com/report',
    format: 'A4',
    wait_for: 'networkidle'  # JSレンダリング完了を待つ
  }.to_json
end

File.binwrite('page.pdf', response.body) if response.success?

Go

net/http(標準ライブラリ)

Goの標準ライブラリだけでPDF生成APIを呼び出せます。外部依存ゼロで動作します。

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"time"
)

type PDFRequest struct {
	HTML   string `json:"html"`
	Engine string `json:"engine"`
	Format string `json:"format"`
}

func generatePDF() error {
	payload := PDFRequest{
		HTML: `<html>
<head>
  <style>
    body { font-family: 'Noto Sans JP', sans-serif; padding: 40px; }
    h1 { color: #1a1a1a; border-bottom: 2px solid #3b82f6; padding-bottom: 8px; }
  </style>
</head>
<body>
  <h1>レポート</h1>
  <p>GoからAPIで生成されたPDFです。</p>
</body>
</html>`,
		Engine: "quality",
		Format: "A4",
	}

	body, err := json.Marshal(payload)
	if err != nil {
		return fmt.Errorf("marshal error: %w", err)
	}

	req, err := http.NewRequest("POST", "https://api.pdf.funbrew.cloud/v1/pdf/from-html", bytes.NewReader(body))
	if err != nil {
		return fmt.Errorf("request error: %w", err)
	}

	req.Header.Set("Authorization", "Bearer "+os.Getenv("FUNBREW_PDF_API_KEY"))
	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{Timeout: 30 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return fmt.Errorf("request failed: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		errBody, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("API error %d: %s", resp.StatusCode, string(errBody))
	}

	pdf, err := io.ReadAll(resp.Body)
	if err != nil {
		return fmt.Errorf("read error: %w", err)
	}

	if err := os.WriteFile("output.pdf", pdf, 0644); err != nil {
		return fmt.Errorf("write error: %w", err)
	}

	fmt.Printf("PDF generated: output.pdf (%d bytes)\n", len(pdf))
	return nil
}

func main() {
	if err := generatePDF(); err != nil {
		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
		os.Exit(1)
	}
}

Resty

Restyを使うとリトライやエラーハンドリングがシンプルに書けます。

package main

import (
	"fmt"
	"os"
	"time"

	"github.com/go-resty/resty/v2"
)

type PDFRequest struct {
	HTML   string `json:"html"`
	Engine string `json:"engine"`
	Format string `json:"format"`
}

func main() {
	client := resty.New().
		SetTimeout(30 * time.Second).
		SetRetryCount(3).
		SetRetryWaitTime(1 * time.Second).
		SetRetryMaxWaitTime(10 * time.Second)

	resp, err := client.R().
		SetHeader("Authorization", "Bearer "+os.Getenv("FUNBREW_PDF_API_KEY")).
		SetHeader("Content-Type", "application/json").
		SetBody(PDFRequest{
			HTML:   "<h1>Hello PDF</h1><p>Generated with Resty</p>",
			Engine: "fast",
			Format: "A4",
		}).
		Post("https://api.pdf.funbrew.cloud/v1/pdf/from-html")

	if err != nil {
		fmt.Fprintf(os.Stderr, "Request failed: %v\n", err)
		os.Exit(1)
	}

	if resp.StatusCode() != 200 {
		fmt.Fprintf(os.Stderr, "API error %d: %s\n", resp.StatusCode(), resp.String())
		os.Exit(1)
	}

	os.WriteFile("output.pdf", resp.Body(), 0644)
	fmt.Printf("PDF generated: output.pdf (%d bytes)\n", len(resp.Body()))
}

テンプレートを使ったPDF生成

package main

import (
	"fmt"
	"os"
	"time"

	"github.com/go-resty/resty/v2"
)

type TemplateRequest struct {
	TemplateID string      `json:"template_id"`
	Data       interface{} `json:"data"`
	Format     string      `json:"format"`
}

type InvoiceData struct {
	CompanyName   string        `json:"company_name"`
	InvoiceNumber string        `json:"invoice_number"`
	Items         []InvoiceItem `json:"items"`
	Total         int           `json:"total"`
}

type InvoiceItem struct {
	Name     string `json:"name"`
	Quantity int    `json:"quantity"`
	Price    int    `json:"price"`
}

func main() {
	client := resty.New().SetTimeout(30 * time.Second)

	resp, err := client.R().
		SetHeader("Authorization", "Bearer "+os.Getenv("FUNBREW_PDF_API_KEY")).
		SetHeader("Content-Type", "application/json").
		SetBody(TemplateRequest{
			TemplateID: "invoice-v1",
			Data: InvoiceData{
				CompanyName:   "株式会社サンプル",
				InvoiceNumber: "INV-2026-0042",
				Items: []InvoiceItem{
					{Name: "Webコンサルティング", Quantity: 1, Price: 150000},
					{Name: "デザイン制作", Quantity: 3, Price: 50000},
				},
				Total: 300000,
			},
			Format: "A4",
		}).
		Post("https://api.pdf.funbrew.cloud/v1/pdf/from-template")

	if err != nil {
		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
		os.Exit(1)
	}

	if resp.StatusCode() == 200 {
		os.WriteFile("invoice.pdf", resp.Body(), 0644)
		fmt.Println("Invoice PDF generated")
	}
}

エラーハンドリング

APIのエラーレスポンスはJSON形式で返されます。適切にハンドリングすることで、問題の原因を素早く特定できます。エラーハンドリングの詳細はエラーハンドリングガイドを参照してください。

Ruby版

require 'faraday'
require 'json'

class PdfApiError < StandardError
  attr_reader :status, :code, :detail

  def initialize(status, body)
    parsed = JSON.parse(body) rescue {}
    @status = status
    @code = parsed['error'] || 'unknown'
    @detail = parsed['message'] || body
    super("PDF API Error [#{status}] #{@code}: #{@detail}")
  end
end

def generate_pdf_safe(html, options = {})
  conn = Faraday.new(url: 'https://api.pdf.funbrew.cloud') do |f|
    f.request :retry, max: 3,
      retry_statuses: [429, 500, 502, 503],
      interval: 1,
      backoff_factor: 2
    f.options.timeout = options.fetch(:timeout, 30)
  end

  response = conn.post('/v1/pdf/from-html') do |req|
    req.headers['Authorization'] = "Bearer #{ENV['FUNBREW_PDF_API_KEY']}"
    req.headers['Content-Type'] = 'application/json'
    req.body = { html: html, engine: 'quality', format: 'A4' }.to_json
  end

  raise PdfApiError.new(response.status, response.body) unless response.success?

  response.body
rescue Faraday::TimeoutError
  raise PdfApiError.new(408, '{"error":"timeout","message":"Request timed out"}')
rescue Faraday::ConnectionFailed
  raise PdfApiError.new(0, '{"error":"connection","message":"Could not connect to API"}')
end

Go版

package pdfapi

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
)

type APIError struct {
	StatusCode int
	ErrorCode  string `json:"error"`
	Message    string `json:"message"`
}

func (e *APIError) Error() string {
	return fmt.Sprintf("PDF API Error [%d] %s: %s", e.StatusCode, e.ErrorCode, e.Message)
}

func (e *APIError) IsRetryable() bool {
	switch e.StatusCode {
	case 429, 500, 502, 503:
		return true
	default:
		return false
	}
}

func parseErrorResponse(resp *http.Response) error {
	body, _ := io.ReadAll(resp.Body)
	apiErr := &APIError{StatusCode: resp.StatusCode}
	if err := json.Unmarshal(body, apiErr); err != nil {
		apiErr.ErrorCode = "unknown"
		apiErr.Message = string(body)
	}
	return apiErr
}

フレームワーク統合

PDF APIをWebフレームワークに統合して、エンドポイントからPDFダウンロードを提供するパターンを紹介します。いずれもAPIクライアントをサービス層に切り出し、コントローラーからはsend_dataやバイナリレスポンスで返す構成です。請求書の自動生成証明書の発行など、実務での活用パターンもあわせて参照してください。

Rails(Ruby on Rails)

Railsコントローラーから直接PDFをダウンロードレスポンスとして返す例です。サービスクラスにAPI呼び出しを集約し、コントローラーはデータの取得とレスポンスに専念します。

# app/services/pdf_generator.rb
class PdfGenerator
  def initialize
    @conn = Faraday.new(url: 'https://api.pdf.funbrew.cloud') do |f|
      f.request :retry, max: 3, retry_statuses: [429, 500, 502, 503]
      f.options.timeout = 30
    end
  end

  def from_html(html, options = {})
    response = @conn.post('/v1/pdf/from-html') do |req|
      req.headers['Authorization'] = "Bearer #{ENV['FUNBREW_PDF_API_KEY']}"
      req.headers['Content-Type'] = 'application/json'
      req.body = {
        html: html,
        engine: options.fetch(:engine, 'quality'),
        format: options.fetch(:format, 'A4')
      }.to_json
    end

    raise "PDF generation failed: #{response.status}" unless response.success?
    response.body
  end

  def from_template(template_id, data, options = {})
    response = @conn.post('/v1/pdf/from-template') do |req|
      req.headers['Authorization'] = "Bearer #{ENV['FUNBREW_PDF_API_KEY']}"
      req.headers['Content-Type'] = 'application/json'
      req.body = {
        template_id: template_id,
        data: data,
        format: options.fetch(:format, 'A4')
      }.to_json
    end

    raise "PDF generation failed: #{response.status}" unless response.success?
    response.body
  end
end

# app/controllers/invoices_controller.rb
class InvoicesController < ApplicationController
  def download
    invoice = Invoice.find(params[:id])
    generator = PdfGenerator.new

    pdf = generator.from_template('invoice-v1', {
      company_name: invoice.company_name,
      invoice_number: invoice.number,
      items: invoice.items.map { |i| { name: i.name, quantity: i.quantity, price: i.price } },
      total: invoice.total
    })

    send_data pdf,
      filename: "invoice-#{invoice.number}.pdf",
      type: 'application/pdf',
      disposition: 'attachment'
  end
end

Gin(Go)

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"time"

	"github.com/gin-gonic/gin"
)

var httpClient = &http.Client{Timeout: 30 * time.Second}

func generatePDFFromHTML(html string) ([]byte, error) {
	body, _ := json.Marshal(map[string]string{
		"html":   html,
		"engine": "quality",
		"format": "A4",
	})

	req, _ := http.NewRequest("POST", "https://api.pdf.funbrew.cloud/v1/pdf/from-html", bytes.NewReader(body))
	req.Header.Set("Authorization", "Bearer "+os.Getenv("FUNBREW_PDF_API_KEY"))
	req.Header.Set("Content-Type", "application/json")

	resp, err := httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("API request failed: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		errBody, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("API error %d: %s", resp.StatusCode, string(errBody))
	}

	return io.ReadAll(resp.Body)
}

func main() {
	r := gin.Default()

	r.GET("/invoices/:id/pdf", func(c *gin.Context) {
		invoiceID := c.Param("id")

		html := fmt.Sprintf(`<html>
<body>
  <h1>Invoice %s</h1>
  <p>Generated via FUNBREW PDF API</p>
</body>
</html>`, invoiceID)

		pdf, err := generatePDFFromHTML(html)
		if err != nil {
			c.JSON(500, gin.H{"error": err.Error()})
			return
		}

		c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="invoice-%s.pdf"`, invoiceID))
		c.Data(200, "application/pdf", pdf)
	})

	r.Run(":8080")
}

Echo(Go)

package main

import (
	"fmt"
	"net/http"
	"os"
	"time"

	"github.com/go-resty/resty/v2"
	"github.com/labstack/echo/v4"
)

var restyClient = resty.New().SetTimeout(30 * time.Second)

func generatePDF(html string) ([]byte, error) {
	resp, err := restyClient.R().
		SetHeader("Authorization", "Bearer "+os.Getenv("FUNBREW_PDF_API_KEY")).
		SetHeader("Content-Type", "application/json").
		SetBody(map[string]string{
			"html":   html,
			"engine": "quality",
			"format": "A4",
		}).
		Post("https://api.pdf.funbrew.cloud/v1/pdf/from-html")

	if err != nil {
		return nil, err
	}
	if resp.StatusCode() != 200 {
		return nil, fmt.Errorf("API error %d: %s", resp.StatusCode(), resp.String())
	}
	return resp.Body(), nil
}

func main() {
	e := echo.New()

	e.GET("/reports/:id/pdf", func(c echo.Context) error {
		reportID := c.Param("id")

		html := fmt.Sprintf(`<html>
<body>
  <h1>Report %s</h1>
  <p>Generated via FUNBREW PDF API</p>
</body>
</html>`, reportID)

		pdf, err := generatePDF(html)
		if err != nil {
			return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
		}

		c.Response().Header().Set("Content-Disposition",
			fmt.Sprintf(`attachment; filename="report-%s.pdf"`, reportID))
		return c.Blob(http.StatusOK, "application/pdf", pdf)
	})

	e.Logger.Fatal(e.Start(":8080"))
}

バッチ処理

月末の請求書一括生成や年度末の証明書発行など、大量のPDFを生成する場面では並行処理が不可欠です。Rubyではスレッドベースの並行処理が手軽で、GoではgoroutineとセマフォでCPU効率よく処理できます。APIのレート制限(デフォルト60リクエスト/分)を超えないよう、同時実行数を適切に制限することがポイントです。バッチ処理の詳細なパターンはバッチ処理ガイドを参照してください。

Ruby版(Parallel gem)

require 'faraday'
require 'json'
require 'parallel'

conn = Faraday.new(url: 'https://api.pdf.funbrew.cloud') do |f|
  f.request :retry, max: 3, retry_statuses: [429, 500, 502, 503]
  f.options.timeout = 30
end

invoices = [
  { id: 'INV-001', company: '株式会社A', total: 100_000 },
  { id: 'INV-002', company: '株式会社B', total: 250_000 },
  { id: 'INV-003', company: '株式会社C', total: 180_000 },
]

results = Parallel.map(invoices, in_threads: 5) do |invoice|
  html = "<html><body><h1>請求書 #{invoice[:id]}</h1>
          <p>#{invoice[:company]}</p>
          <p>合計: ¥#{invoice[:total].to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse}</p>
          </body></html>"

  response = conn.post('/v1/pdf/from-html') do |req|
    req.headers['Authorization'] = "Bearer #{ENV['FUNBREW_PDF_API_KEY']}"
    req.headers['Content-Type'] = 'application/json'
    req.body = { html: html, engine: 'fast', format: 'A4' }.to_json
  end

  if response.success?
    File.binwrite("#{invoice[:id]}.pdf", response.body)
    { id: invoice[:id], status: :ok, size: response.body.bytesize }
  else
    { id: invoice[:id], status: :error, code: response.status }
  end
end

results.each { |r| puts "#{r[:id]}: #{r[:status]}" }

Go版(goroutine)

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"sync"
	"time"
)

type Invoice struct {
	ID      string
	Company string
	Total   int
}

type Result struct {
	ID     string
	Status string
	Size   int
	Err    error
}

func main() {
	invoices := []Invoice{
		{ID: "INV-001", Company: "株式会社A", Total: 100000},
		{ID: "INV-002", Company: "株式会社B", Total: 250000},
		{ID: "INV-003", Company: "株式会社C", Total: 180000},
	}

	client := &http.Client{Timeout: 30 * time.Second}
	results := make([]Result, len(invoices))

	var wg sync.WaitGroup
	sem := make(chan struct{}, 5) // 同時実行数を5に制限

	for i, inv := range invoices {
		wg.Add(1)
		go func(idx int, invoice Invoice) {
			defer wg.Done()
			sem <- struct{}{}
			defer func() { <-sem }()

			html := fmt.Sprintf(`<html><body>
				<h1>請求書 %s</h1>
				<p>%s</p>
				<p>合計: ¥%d</p>
			</body></html>`, invoice.ID, invoice.Company, invoice.Total)

			body, _ := json.Marshal(map[string]string{
				"html": html, "engine": "fast", "format": "A4",
			})

			req, _ := http.NewRequest("POST",
				"https://api.pdf.funbrew.cloud/v1/pdf/from-html",
				bytes.NewReader(body))
			req.Header.Set("Authorization", "Bearer "+os.Getenv("FUNBREW_PDF_API_KEY"))
			req.Header.Set("Content-Type", "application/json")

			resp, err := client.Do(req)
			if err != nil {
				results[idx] = Result{ID: invoice.ID, Status: "error", Err: err}
				return
			}
			defer resp.Body.Close()

			pdf, _ := io.ReadAll(resp.Body)
			if resp.StatusCode == 200 {
				os.WriteFile(invoice.ID+".pdf", pdf, 0644)
				results[idx] = Result{ID: invoice.ID, Status: "ok", Size: len(pdf)}
			} else {
				results[idx] = Result{ID: invoice.ID, Status: "error",
					Err: fmt.Errorf("status %d", resp.StatusCode)}
			}
		}(i, inv)
	}

	wg.Wait()

	for _, r := range results {
		if r.Err != nil {
			fmt.Printf("%s: error - %v\n", r.ID, r.Err)
		} else {
			fmt.Printf("%s: %s (%d bytes)\n", r.ID, r.Status, r.Size)
		}
	}
}

まとめ

RubyとGoのどちらでも、FUNBREW PDF APIを使えばHTTPリクエストを送るだけでPDFを生成できます。

  • 標準ライブラリだけで動作: net/http(Ruby/Go)でサードパーティgemなしでも実装可能
  • リッチなクライアント: Faraday(Ruby)やResty(Go)でリトライ・タイムアウトを簡潔に制御
  • フレームワーク統合: Rails/Gin/EchoからPDFダウンロードエンドポイントをすぐに提供
  • バッチ対応: スレッド(Ruby)やgoroutine(Go)で大量PDF生成にも対応
  • エラーハンドリング: カスタムエラークラスで本番運用に耐える堅牢な実装

本番環境へのデプロイ前には本番運用チェックリストを確認してください。

次のステップとして以下のリソースも活用してください。

Powered by FUNBREW PDF