Invalid Date

When building web apps with Next.js or Nuxt.js, the requirement to "export this page as PDF" or "let users download invoices and certificates" comes up constantly. Trying to generate PDFs purely on the client side runs into well-known problems: inconsistent CSS rendering, character encoding issues, and large file sizes.

The better approach is to use a server-side API that converts HTML to PDF. This guide walks you through integrating FUNBREW PDF into Next.js (App Router) and Nuxt 3 with practical code examples.

For the fundamentals of HTML-to-PDF conversion, see the HTML to PDF complete guide. For basic API usage across languages, check the language quickstart guide. For type-safe API usage with TypeScript, see the TypeScript PDF API Guide.

Why Use a PDF API in Frontend Frameworks

The browser's native print functionality (window.print()) and client-side libraries like jsPDF have real limitations:

Problem Details
CSS inconsistency Print styles differ across browsers
Dynamic content Can't include data requiring server-side auth
Performance Heavy client-side rendering for complex documents
Security Cannot expose API keys to the client

Generating PDFs on the server solves all of these. Next.js API Routes and Nuxt 3 Server Routes are purpose-built for this pattern. For differences between wkhtmltopdf and Chromium rendering engines, see the engine comparison.

Common Prerequisites

Get Your API Key

Grab a FUNBREW PDF API key from the dashboard. Store it in an environment variable and never include it in client-side code.

For secure API key management best practices, see the security guide.

# .env.local
FUNBREW_PDF_API_KEY=sk-your-api-key

Install the Node.js SDK

npm install @funbrew/pdf

If you prefer raw HTTP calls, Node.js 18+ has native fetch built in — no extra dependencies needed.

Next.js (App Router)

Project Structure

app/
├── api/
│   └── generate-pdf/
│       └── route.ts        # PDF generation endpoint
├── invoice/
│   └── page.tsx            # Invoice page
└── components/
    └── DownloadButton.tsx  # PDF download button

API Route Implementation

Create the server-side PDF generation endpoint at app/api/generate-pdf/route.ts.

// app/api/generate-pdf/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { FunbrewPdf } from '@funbrew/pdf'

const client = new FunbrewPdf({
  apiKey: process.env.FUNBREW_PDF_API_KEY!,
})

export async function POST(request: NextRequest) {
  try {
    const body = await request.json()
    const { html, options } = body

    // Input validation
    if (!html || typeof html !== 'string') {
      return NextResponse.json(
        { error: 'html field is required' },
        { status: 400 }
      )
    }

    const pdfBuffer = await client.generate({
      html,
      options: {
        format: 'A4',
        margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
        ...options,
      },
    })

    return new NextResponse(pdfBuffer, {
      status: 200,
      headers: {
        'Content-Type': 'application/pdf',
        'Content-Disposition': 'attachment; filename="document.pdf"',
      },
    })
  } catch (error) {
    console.error('PDF generation error:', error)
    return NextResponse.json(
      { error: 'Failed to generate PDF' },
      { status: 500 }
    )
  }
}

If you prefer making the HTTP call directly instead of using the SDK:

// Using native fetch
const response = await fetch('https://api.funbrew.dev/api/v1/generate', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.FUNBREW_PDF_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ html, options }),
})

if (!response.ok) {
  throw new Error(`API error: ${response.status}`)
}

const pdfBuffer = await response.arrayBuffer()

Injecting Template Variables

The most common use case is building an HTML string from dynamic data before sending it to the API.

// app/api/generate-pdf/invoice/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { FunbrewPdf } from '@funbrew/pdf'

const client = new FunbrewPdf({
  apiKey: process.env.FUNBREW_PDF_API_KEY!,
})

function buildInvoiceHtml(data: {
  invoiceNumber: string
  customerName: string
  items: Array<{ name: string; quantity: number; price: number }>
  total: number
}): string {
  const rows = data.items
    .map(
      (item) => `
      <tr>
        <td>${item.name}</td>
        <td>${item.quantity}</td>
        <td>$${item.price.toFixed(2)}</td>
        <td>$${(item.quantity * item.price).toFixed(2)}</td>
      </tr>`
    )
    .join('')

  return `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <style>
    body { font-family: sans-serif; padding: 40px; }
    h1 { color: #1a56db; }
    table { width: 100%; border-collapse: collapse; margin-top: 20px; }
    th, td { border: 1px solid #e5e7eb; padding: 10px; text-align: left; }
    th { background: #f9fafb; }
    .total { font-size: 1.2em; font-weight: bold; text-align: right; margin-top: 20px; }
  </style>
</head>
<body>
  <h1>Invoice #${data.invoiceNumber}</h1>
  <p>Bill to: ${data.customerName}</p>
  <table>
    <thead>
      <tr><th>Item</th><th>Qty</th><th>Unit Price</th><th>Subtotal</th></tr>
    </thead>
    <tbody>${rows}</tbody>
  </table>
  <p class="total">Total: $${data.total.toFixed(2)}</p>
</body>
</html>`
}

export async function POST(request: NextRequest) {
  const invoiceData = await request.json()
  const html = buildInvoiceHtml(invoiceData)

  const pdfBuffer = await client.generate({
    html,
    options: { format: 'A4' },
  })

  return new NextResponse(pdfBuffer, {
    headers: {
      'Content-Type': 'application/pdf',
      'Content-Disposition': `attachment; filename="invoice-${invoiceData.invoiceNumber}.pdf"`,
    },
  })
}

For more advanced template patterns, see the template engine guide.

Client-Side Download

From a client component, call the API Route and trigger a file download:

// app/components/DownloadButton.tsx
'use client'

import { useState } from 'react'

interface InvoiceData {
  invoiceNumber: string
  customerName: string
  items: Array<{ name: string; quantity: number; price: number }>
  total: number
}

export function DownloadPdfButton({ invoiceData }: { invoiceData: InvoiceData }) {
  const [loading, setLoading] = useState(false)

  const handleDownload = async () => {
    setLoading(true)
    try {
      const response = await fetch('/api/generate-pdf/invoice', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(invoiceData),
      })

      if (!response.ok) {
        throw new Error('Failed to generate PDF')
      }

      // Convert to Blob and trigger download
      const blob = await response.blob()
      const url = URL.createObjectURL(blob)
      const a = document.createElement('a')
      a.href = url
      a.download = `invoice-${invoiceData.invoiceNumber}.pdf`
      document.body.appendChild(a)
      a.click()
      document.body.removeChild(a)
      URL.revokeObjectURL(url)
    } catch (error) {
      console.error(error)
      alert('Failed to download PDF')
    } finally {
      setLoading(false)
    }
  }

  return (
    <button
      onClick={handleDownload}
      disabled={loading}
      className="btn btn-primary"
    >
      {loading ? 'Generating...' : 'Download PDF'}
    </button>
  )
}

Dynamic Route for Direct PDF Access

You can also expose a GET endpoint for direct PDF access (useful for email links):

// app/invoice/[id]/download/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { FunbrewPdf } from '@funbrew/pdf'
import { getInvoiceById } from '@/lib/db'

const client = new FunbrewPdf({
  apiKey: process.env.FUNBREW_PDF_API_KEY!,
})

export async function GET(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  // Auth check (session/JWT)
  // const session = await getServerSession()
  // if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })

  const invoice = await getInvoiceById(params.id)
  if (!invoice) {
    return NextResponse.json({ error: 'Invoice not found' }, { status: 404 })
  }

  const html = buildInvoiceHtml(invoice)
  const pdfBuffer = await client.generate({ html, options: { format: 'A4' } })

  return new NextResponse(pdfBuffer, {
    headers: {
      'Content-Type': 'application/pdf',
      'Content-Disposition': `inline; filename="invoice-${params.id}.pdf"`,
    },
  })
}

Nuxt 3

Project Structure

server/
├── api/
│   └── generate-pdf.post.ts   # PDF generation endpoint
└── utils/
    └── pdf.ts                 # Shared utilities
pages/
└── invoice.vue               # Invoice page
composables/
└── usePdf.ts                 # PDF composable

Server Route Implementation

Nuxt 3 uses filename conventions to match HTTP methods — the .post.ts suffix restricts the handler to POST requests.

// server/api/generate-pdf.post.ts
import { FunbrewPdf } from '@funbrew/pdf'

export default defineEventHandler(async (event) => {
  const body = await readBody(event)
  const { html, options, filename = 'document.pdf' } = body

  if (!html || typeof html !== 'string') {
    throw createError({
      statusCode: 400,
      statusMessage: 'html field is required',
    })
  }

  const apiKey = useRuntimeConfig().funbrewPdfApiKey
  const client = new FunbrewPdf({ apiKey })

  try {
    const pdfBuffer = await client.generate({
      html,
      options: {
        format: 'A4',
        margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
        ...options,
      },
    })

    setResponseHeaders(event, {
      'Content-Type': 'application/pdf',
      'Content-Disposition': `attachment; filename="${filename}"`,
    })

    return pdfBuffer
  } catch (error) {
    throw createError({
      statusCode: 500,
      statusMessage: 'Failed to generate PDF',
    })
  }
})

nuxt.config.ts — runtimeConfig Setup

Store the API key in server-only runtimeConfig. Anything under public is exposed to the client — never put API keys there.

// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    // Server-only (overridden by NUXT_FUNBREW_PDF_API_KEY env var)
    funbrewPdfApiKey: process.env.FUNBREW_PDF_API_KEY ?? '',
    public: {
      // Client-accessible config only
    },
  },
})
# .env
NUXT_FUNBREW_PDF_API_KEY=sk-your-api-key

Composable for PDF Downloads

Encapsulate the download logic in a reusable composable:

// composables/usePdf.ts
export function usePdf() {
  const loading = ref(false)
  const error = ref<string | null>(null)

  async function downloadPdf(
    html: string,
    options?: Record<string, unknown>,
    filename = 'document.pdf'
  ) {
    loading.value = true
    error.value = null

    try {
      const response = await $fetch<Blob>('/api/generate-pdf', {
        method: 'POST',
        body: { html, options, filename },
        responseType: 'blob',
      })

      const url = URL.createObjectURL(response)
      const a = document.createElement('a')
      a.href = url
      a.download = filename
      a.click()
      URL.revokeObjectURL(url)
    } catch (err) {
      error.value = 'Failed to download PDF'
      console.error(err)
    } finally {
      loading.value = false
    }
  }

  return { loading, error, downloadPdf }
}

Nuxt 3 Page Component

<!-- pages/invoice.vue -->
<script setup lang="ts">
const { loading, error, downloadPdf } = usePdf()

const invoiceData = ref({
  number: 'INV-2026-001',
  customer: 'Acme Corp',
  total: 1500.00,
})

async function handleDownload() {
  const html = `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <style>body { font-family: sans-serif; padding: 40px; }</style>
</head>
<body>
  <h1>Invoice #${invoiceData.value.number}</h1>
  <p>Bill to: ${invoiceData.value.customer}</p>
  <p>Total: $${invoiceData.value.total.toFixed(2)}</p>
</body>
</html>`

  await downloadPdf(html, { format: 'A4' }, `invoice-${invoiceData.value.number}.pdf`)
}
</script>

<template>
  <div>
    <h1>Invoice Management</h1>
    <p v-if="error" class="text-red-500">{{ error }}</p>
    <button
      :disabled="loading"
      @click="handleDownload"
      class="btn btn-primary"
    >
      {{ loading ? 'Generating...' : 'Download PDF' }}
    </button>
  </div>
</template>

Error Handling

Robust error handling is essential for production PDF generation. See the error handling guide for comprehensive patterns.

Retry Logic (Next.js)

// lib/pdf.ts
import { FunbrewPdf } from '@funbrew/pdf'

const client = new FunbrewPdf({
  apiKey: process.env.FUNBREW_PDF_API_KEY!,
})

export async function generatePdfWithRetry(
  html: string,
  options = {},
  maxRetries = 3
): Promise<ArrayBuffer> {
  let lastError: Error | undefined

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await client.generate({ html, options })
    } catch (error) {
      lastError = error as Error
      if (attempt < maxRetries) {
        // Exponential backoff
        await new Promise((resolve) => setTimeout(resolve, 1000 * attempt))
      }
    }
  }

  throw lastError
}

HTTP Status Code Reference

Status Code Meaning Action
400 Malformed HTML or bad request Fix the request body
401 Invalid API key Check environment variable
429 Rate limit exceeded Back off and retry
500 Server error Retry with backoff

Production Deployment

Deploying to Vercel (Next.js)

Add environment variables in the Vercel project settings. Never use the NEXT_PUBLIC_ prefix for the API key — that would expose it to the browser.

# Add via Vercel CLI
vercel env add FUNBREW_PDF_API_KEY production

Timeout configuration: PDF generation can take a few seconds for complex documents. Vercel's free tier limits Serverless Functions to 10 seconds. For large documents, the Pro plan supports up to 60 seconds:

// app/api/generate-pdf/route.ts
export const maxDuration = 60 // seconds (Pro plan only)

Deploying to Netlify (Nuxt 3)

Nuxt 3 runs on Netlify's Serverless Functions. Configure the bundler to include the SDK:

# netlify.toml
[functions]
  node_bundler = "esbuild"
  external_node_modules = ["@funbrew/pdf"]
# Set environment variable
netlify env:set NUXT_FUNBREW_PDF_API_KEY "sk-your-api-key"

Self-Hosted Deployment

Whether you're running on a VPS or bare metal, the same security principles apply:

  • Manage API keys via environment variables (never commit .env to Git)
  • Apply rate limiting to the PDF endpoint
  • Set maximum document size limits to protect memory

For serverless-specific deployment patterns, see the serverless PDF generation guide. For a full production readiness checklist, see the production guide. Docker/Kubernetes deployment is covered in the Docker & Kubernetes Guide. For print-specific CSS optimization, see PDF CSS Layout Tips.

Batch PDF Generation

When you need to generate multiple PDFs at once, use parallel processing with concurrency limits:

// app/api/generate-pdfs-batch/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { FunbrewPdf } from '@funbrew/pdf'

const client = new FunbrewPdf({
  apiKey: process.env.FUNBREW_PDF_API_KEY!,
})

export async function POST(request: NextRequest) {
  const { documents } = await request.json()

  // Process in chunks of 5 (concurrency limit)
  const chunks = []
  for (let i = 0; i < documents.length; i += 5) {
    chunks.push(documents.slice(i, i + 5))
  }

  const results = []
  for (const chunk of chunks) {
    const pdfs = await Promise.allSettled(
      chunk.map((doc: { html: string; filename: string }) =>
        client.generate({ html: doc.html, options: { format: 'A4' } })
      )
    )
    results.push(...pdfs)
  }

  return NextResponse.json({
    total: results.length,
    success: results.filter((r) => r.status === 'fulfilled').length,
    failed: results.filter((r) => r.status === 'rejected').length,
  })
}

For high-volume PDF generation patterns, see the batch processing guide.

Summary

Key takeaways for integrating FUNBREW PDF into Next.js and Nuxt 3:

  • API keys stay server-side only — never use NEXT_PUBLIC_ or runtimeConfig.public
  • Encapsulate PDF logic in API Routes or Server Routes
  • Client components use the Blob download pattern
  • Watch timeout limits on Vercel and Netlify for large documents
  • Implement retry logic for production reliability

Try your HTML templates in the playground before starting implementation. For use case inspiration, browse the use cases page.

For invoice automation see the invoice PDF automation guide, and for certificate generation see the certificate automation guide. For Markdown-based PDF generation, check the Markdown to PDF API Guide. Automated report generation is covered in the Report PDF Generation Guide. Compare plans and pricing at the Pricing Comparison.

Powered by FUNBREW PDF