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生成にも対応
- エラーハンドリング: カスタムエラークラスで本番運用に耐える堅牢な実装
本番環境へのデプロイ前には本番運用チェックリストを確認してください。
次のステップとして以下のリソースも活用してください。
- Playground でブラウザからAPIを試す
- テンプレートエンジン入門 でHTMLテンプレートの活用を学ぶ
- Webhook連携ガイド で非同期PDF生成を導入する
- 料金プラン比較 で最適なプランを選ぶ