How to Generate PDFs in Go with LightningPDF - Complete Tutorial
Learn how to generate PDFs in Go using LightningPDF API. Covers HTML to PDF, templates, batch generation, error handling, and best practices.
How to Generate PDFs in Go with LightningPDF
Generating PDFs in Go is a common requirement for invoices, reports, receipts, and other documents. While there are several Go PDF libraries available, using an API like LightningPDF offers better performance, easier maintenance, and more features than building your own solution.
This tutorial covers everything you need to generate production-quality PDFs in Go using LightningPDF.
Why Use an API Instead of a Library?
Before we dive in, let's address the elephant in the room: why use an API instead of a Go library like gofpdf or gopdf?
Go PDF Libraries:
- ❌ Limited HTML/CSS support
- ❌ Manual layout calculations
- ❌ No built-in templates
- ❌ Memory-intensive for complex documents
- ❌ Difficult to maintain (page breaks, fonts, etc.)
LightningPDF API:
- ✅ Full HTML/CSS support (like Chrome)
- ✅ Automatic layout calculations
- ✅ 50+ pre-built templates
- ✅ Sub-100ms generation for invoices
- ✅ Zero maintenance (we handle infrastructure)
Let's get started.
Prerequisites
# Install Go (1.20+)
go version
# Create new project
mkdir pdf-demo && cd pdf-demo
go mod init pdf-demo
Quick Start: Your First PDF
Step 1: Get API Key
- Sign up at lightningpdf.dev/signup (free, 50 PDFs/month)
- Copy your API key from the dashboard
Step 2: Basic HTML to PDF
// main.go
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
const (
apiURL = "https://lightningpdf.dev/api/v1/generate"
apiKey = "your_api_key_here" // Replace with your actual key
)
type PDFRequest struct {
HTML string `json:"html"`
Format string `json:"format,omitempty"`
}
func generatePDF(html string) ([]byte, error) {
// Prepare request
reqBody := PDFRequest{
HTML: html,
Format: "A4",
}
jsonData, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("marshal error: %w", err)
}
// Make request
req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonData))
if err != nil {
return nil, fmt.Errorf("request error: %w", err)
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("http error: %w", err)
}
defer resp.Body.Close()
// Check response
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("API error %d: %s", resp.StatusCode, string(body))
}
// Read PDF bytes
pdfData, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read error: %w", err)
}
return pdfData, nil
}
func main() {
html := `
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; padding: 40px; }
h1 { color: #333; }
</style>
</head>
<body>
<h1>Hello from Go!</h1>
<p>This PDF was generated using LightningPDF API.</p>
</body>
</html>
`
pdf, err := generatePDF(html)
if err != nil {
fmt.Printf("Error: %v\n", err)
os.Exit(1)
}
// Save to file
err = os.WriteFile("output.pdf", pdf, 0644)
if err != nil {
fmt.Printf("Save error: %v\n", err)
os.Exit(1)
}
fmt.Println("PDF generated successfully: output.pdf")
}
Run it:
go run main.go
# PDF generated successfully: output.pdf
Congratulations! You just generated your first PDF with Go.
Real-World Example: Invoice Generation
Let's build a proper invoice generator:
// invoice.go
package main
import (
"bytes"
"encoding/json"
"fmt"
"html/template"
"net/http"
"os"
"time"
)
const apiKey = "your_api_key_here"
type Invoice struct {
Number string
Date time.Time
DueDate time.Time
CustomerName string
CustomerEmail string
Items []LineItem
Subtotal float64
Tax float64
Total float64
}
type LineItem struct {
Description string
Quantity int
Price float64
Total float64
}
func (i *Invoice) Calculate() {
i.Subtotal = 0
for _, item := range i.Items {
item.Total = float64(item.Quantity) * item.Price
i.Subtotal += item.Total
}
i.Tax = i.Subtotal * 0.10 // 10% tax
i.Total = i.Subtotal + i.Tax
}
const invoiceTemplate = `
<!DOCTYPE html>
<html>
<head>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Helvetica', 'Arial', sans-serif;
padding: 40px;
color: #333;
}
.header {
display: flex;
justify-content: space-between;
margin-bottom: 40px;
padding-bottom: 20px;
border-bottom: 2px solid #4F46E5;
}
.company { font-size: 24px; font-weight: bold; color: #4F46E5; }
.invoice-info { text-align: right; }
.invoice-number { font-size: 18px; font-weight: bold; }
.customer {
margin-bottom: 30px;
padding: 20px;
background: #F3F4F6;
border-radius: 8px;
}
table {
width: 100%;
border-collapse: collapse;
margin: 30px 0;
}
th {
background: #4F46E5;
color: white;
padding: 12px;
text-align: left;
}
td {
padding: 12px;
border-bottom: 1px solid #E5E7EB;
}
.text-right { text-align: right; }
.totals {
margin-left: auto;
width: 300px;
margin-top: 20px;
}
.totals tr td { border: none; padding: 8px; }
.totals .total {
font-size: 18px;
font-weight: bold;
padding-top: 12px;
border-top: 2px solid #4F46E5;
}
.footer {
margin-top: 60px;
text-align: center;
color: #6B7280;
font-size: 12px;
}
</style>
</head>
<body>
<div class="header">
<div class="company">ACME Corp</div>
<div class="invoice-info">
<div class="invoice-number">Invoice #{{.Number}}</div>
<div>Date: {{.Date.Format "Jan 2, 2006"}}</div>
<div>Due: {{.DueDate.Format "Jan 2, 2006"}}</div>
</div>
</div>
<div class="customer">
<strong>Bill To:</strong><br>
{{.CustomerName}}<br>
{{.CustomerEmail}}
</div>
<table>
<thead>
<tr>
<th>Description</th>
<th class="text-right">Qty</th>
<th class="text-right">Price</th>
<th class="text-right">Total</th>
</tr>
</thead>
<tbody>
{{range .Items}}
<tr>
<td>{{.Description}}</td>
<td class="text-right">{{.Quantity}}</td>
<td class="text-right">${{printf "%.2f" .Price}}</td>
<td class="text-right">${{printf "%.2f" .Total}}</td>
</tr>
{{end}}
</tbody>
</table>
<table class="totals">
<tr>
<td>Subtotal:</td>
<td class="text-right">${{printf "%.2f" .Subtotal}}</td>
</tr>
<tr>
<td>Tax (10%):</td>
<td class="text-right">${{printf "%.2f" .Tax}}</td>
</tr>
<tr class="total">
<td>Total:</td>
<td class="text-right">${{printf "%.2f" .Total}}</td>
</tr>
</table>
<div class="footer">
Thank you for your business!<br>
Questions? Contact us at billing@acmecorp.com
</div>
</body>
</html>
`
func generateInvoicePDF(invoice *Invoice) ([]byte, error) {
// Calculate totals
invoice.Calculate()
// Render HTML template
tmpl, err := template.New("invoice").Parse(invoiceTemplate)
if err != nil {
return nil, fmt.Errorf("template error: %w", err)
}
var htmlBuf bytes.Buffer
if err := tmpl.Execute(&htmlBuf, invoice); err != nil {
return nil, fmt.Errorf("template execute error: %w", err)
}
// Call LightningPDF API
reqBody := map[string]interface{}{
"html": htmlBuf.String(),
"format": "A4",
}
jsonData, _ := json.Marshal(reqBody)
req, err := http.NewRequest(
"POST",
"https://lightningpdf.dev/api/v1/generate",
bytes.NewBuffer(jsonData),
)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API error: %d", resp.StatusCode)
}
return io.ReadAll(resp.Body)
}
func main() {
invoice := &Invoice{
Number: "INV-2026-001",
Date: time.Now(),
DueDate: time.Now().AddDate(0, 0, 30),
CustomerName: "Jane Smith",
CustomerEmail: "jane@example.com",
Items: []LineItem{
{Description: "Website Development", Quantity: 1, Price: 5000.00},
{Description: "Logo Design", Quantity: 2, Price: 500.00},
{Description: "Hosting (Annual)", Quantity: 1, Price: 299.00},
},
}
pdf, err := generateInvoicePDF(invoice)
if err != nil {
fmt.Printf("Error: %v\n", err)
os.Exit(1)
}
filename := fmt.Sprintf("invoice-%s.pdf", invoice.Number)
os.WriteFile(filename, pdf, 0644)
fmt.Printf("Invoice generated: %s\n", filename)
}
This generates a professional invoice in <100ms using LightningPDF's native engine.
Using Templates
Instead of HTML strings, use LightningPDF templates:
type TemplateRequest struct {
TemplateID string `json:"template_id"`
Data map[string]interface{} `json:"data"`
}
func generateFromTemplate(templateID string, data map[string]interface{}) ([]byte, error) {
reqBody := TemplateRequest{
TemplateID: templateID,
Data: data,
}
jsonData, _ := json.Marshal(reqBody)
req, _ := http.NewRequest(
"POST",
"https://lightningpdf.dev/api/v1/generate",
bytes.NewBuffer(jsonData),
)
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
// Usage
pdf, err := generateFromTemplate("invoice-v2", map[string]interface{}{
"invoice_number": "INV-001",
"customer_name": "John Doe",
"amount": 1000.00,
})
Batch Generation
Generate multiple PDFs in one API call:
type BatchRequest struct {
TemplateID string `json:"template_id"`
Data []map[string]interface{} `json:"data"`
}
func generateBatch(templateID string, invoices []Invoice) ([]byte, error) {
// Convert invoices to data array
data := make([]map[string]interface{}, len(invoices))
for i, inv := range invoices {
data[i] = map[string]interface{}{
"invoice_number": inv.Number,
"customer_name": inv.CustomerName,
"total": inv.Total,
// ... more fields
}
}
reqBody := BatchRequest{
TemplateID: templateID,
Data: data,
}
jsonData, _ := json.Marshal(reqBody)
req, _ := http.NewRequest(
"POST",
"https://lightningpdf.dev/api/v1/batch",
bytes.NewBuffer(jsonData),
)
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
// Returns ZIP file with all PDFs
return io.ReadAll(resp.Body)
}
Error Handling
Production-ready error handling:
type APIError struct {
Code int `json:"code"`
Message string `json:"message"`
}
func generatePDFWithRetry(html string, maxRetries int) ([]byte, error) {
var lastErr error
for attempt := 0; attempt < maxRetries; attempt++ {
pdf, err := generatePDF(html)
if err == nil {
return pdf, nil
}
lastErr = err
// Exponential backoff
backoff := time.Duration(attempt+1) * time.Second
time.Sleep(backoff)
}
return nil, fmt.Errorf("failed after %d attempts: %w", maxRetries, lastErr)
}
Best Practices
1. Reuse HTTP Client
var httpClient = &http.Client{
Timeout: 30 * time.Second,
}
// Use httpClient instead of http.DefaultClient
2. Use Context for Timeouts
func generatePDFWithContext(ctx context.Context, html string) ([]byte, error) {
req, _ := http.NewRequestWithContext(
ctx,
"POST",
apiURL,
bytes.NewBuffer(jsonData),
)
// ... rest of request
}
3. Pool Goroutines for Batch Jobs
func generateManyPDFs(invoices []Invoice) error {
sem := make(chan struct{}, 10) // Max 10 concurrent
errCh := make(chan error, len(invoices))
for _, inv := range invoices {
go func(invoice Invoice) {
sem <- struct{}{} // Acquire
defer func() { <-sem }() // Release
pdf, err := generateInvoicePDF(&invoice)
if err != nil {
errCh <- err
return
}
// Save PDF...
}(inv)
}
// Wait and check errors...
return nil
}
Conclusion
LightningPDF makes PDF generation in Go simple and fast:
- Sub-100ms for invoices (native engine)
- No dependencies (just
net/http) - Production-ready templates
- Batch API for high volumes
- Zero maintenance (we handle infrastructure)
Ready to start? Get your free API key — 50 PDFs/month, no credit card required.
Next steps:
- Browse the template marketplace
- Read the API documentation
- View pricing plans
Related Reading
- Generate PDFs in Node.js — Node.js integration guide
- Generate PDFs in Python — Python integration guide
- HTML to PDF: The Complete Guide — All approaches compared
- How to Fix PDF Page Breaks — Solve page break issues
- Best PDF APIs in 2026 — Full API comparison
- LightningPDF vs Puppeteer — Why use an API instead of DIY