Next.js HTML to PDF guide

HTML to PDF API with Next.js App Router

Generate and download PDFs from a Next.js Route Handler while keeping the PixelToPdf API key securely on the server.

Updated

Key outcomes

Put PDF generation in a Route Handler

A Client Component should never call PixelToPdf directly because any key shipped to the browser can be copied and abused. Create an App Router endpoint that authenticates the current user, loads the requested data, builds HTML, and performs the conversion server-side.

Your React interface calls only your own endpoint. That preserves your existing authorization rules and gives you one place to validate document IDs, log failures, and apply rate limits.

Configure the server-only API key

Set PIXELTOPDF_API_KEY in the deployment environment without the NEXT_PUBLIC prefix. Next.js exposes NEXT_PUBLIC variables to browser bundles, so that prefix must never be used for a secret.

  • Use separate keys for local, preview, and production environments.
  • Rotate a key if it ever appears in client code or logs.
  • Fail clearly on startup or request handling when the variable is missing.

Return the generated PDF

The Route Handler below assumes that getAuthorizedInvoiceHtml validates the current user and returns trusted invoice markup. It forwards the PDF bytes without converting them to text or JSON.

app/api/invoices/[id]/pdf/route.tsTypeScript
import { getAuthorizedInvoiceHtml } from '@/lib/invoices';

export async function GET(
  request: Request,
  { params }: { params: Promise<{ id: string }> },
) {
  const { id } = await params;
  const html = await getAuthorizedInvoiceHtml(id, request);
  const apiKey = process.env.PIXELTOPDF_API_KEY;
  if (!apiKey) return new Response('PDF service is not configured', { status: 500 });

  const result = await fetch('https://api.pixeltopdf.com/convert/pdf', {
    method: 'POST',
    headers: { 'X-API-Key': apiKey, 'Content-Type': 'application/json' },
    body: JSON.stringify({
      source: html,
      format: 'A4',
      use_print: true,
      filename: `invoice-${id}.pdf`,
    }),
    cache: 'no-store',
  });

  if (!result.ok) return new Response('PDF generation failed', { status: 502 });
  return new Response(await result.arrayBuffer(), {
    headers: {
      'Content-Type': 'application/pdf',
      'Content-Disposition': `attachment; filename="invoice-${id}.pdf"`,
    },
  });
}

Trigger the download from React

For a simple GET endpoint, a normal link is more resilient than a JavaScript fetch. The browser can stream the response and honor the attachment filename without holding the entire file in component state.

Use a button and fetch only when you need inline progress or must send a POST body. In that case, check the response status, create an object URL from the Blob, trigger the download, and revoke the URL afterward.

Always authorize the invoice inside the Route Handler. A hard-to-guess ID in the URL is not an access control mechanism.

Frequently asked questions

Can I call PixelToPdf from a Next.js Client Component?

Do not put the API key in client code. Call PixelToPdf from a Route Handler, Server Action, or another trusted backend and expose only your own authorized endpoint.

Should the Route Handler run on Edge or Node.js?

The call itself uses standard fetch, but choose the runtime required by your HTML-building and authentication code. The example is compatible with the default Node.js runtime.

How do I return a download filename?

Set Content-Disposition on your Next.js response. You can also send filename to PixelToPdf, but your public endpoint should control the final browser-facing header.

Continue building

Build your first conversion

Start with 75 free credits each month. No credit card required.