Skip to content

How to Generate PDF in Next.js (2026)

Working App Router Route Handler code (sync and direct), no Puppeteer on your server—get your first PDF in minutes with copy-paste examples.

The real tiny Doppio factory that creates one by one PDF documents from HTML files

Generating PDF in Next.js is a common need: invoices, reports, exports, or any document your app must deliver as a file. Doing it well means choosing the right approach and having production-ready code.

This guide gives you exactly that: why PDF in Next.js often leads to an API instead of Puppeteer, then working App Router Route Handler examples for both sync and direct PDF generation, with correct response handling and env-based API keys. You’ll also see when to use sync vs direct vs async and where to go next.

Why generate PDF in Next.js?

Next.js is a natural fit for PDF generation: you can run server-side logic in API routes or Route Handlers, render HTML (or pass a URL), and return a file or a link. Use cases include on-demand reports, invoice downloads, or exporting dashboard data—all from the same stack your app already uses.

Puppeteer in Next.js: the hidden cost

One option is to run Puppeteer (or Playwright) inside a Next.js API route or Route Handler. You get full Chrome rendering, but you pay for it: cold starts, memory limits in serverless (e.g. Vercel), and the burden of installing and maintaining Chrome/Chromium. At scale, many concurrent PDF jobs can exhaust memory and slow down your app.

If you’re considering Puppeteer, read our Puppeteer vs Doppio comparison to see how a managed API avoids these issues while keeping the same rendering quality.

Generate PDF in Next.js with an API (Doppio)

Using an HTML-to-PDF API like Doppio moves Chrome off your infrastructure: you send a URL or HTML, and get back a PDF (or a link to it). No Chrome to install, no scaling or memory worries. Our Next.js quick start shows the same flow in a compact form; for a broader picture of options in 2026, see HTML to PDF in 2026: what are your options?.

Step 1: Create a Route Handler

Create a Route Handler in the App Router, e.g. app/api/pdf/route.ts. Store your Doppio API key in .env.local as DOPPIO_API_KEY and read it with process.env.DOPPIO_API_KEY.

Sync: get a document URL

The sync endpoint returns JSON with a documentUrl (and related fields). Use it when you need to store the URL, send it in an email, or process it further.

ts
// app/api/pdf/route.ts
import { NextResponse } from 'next/server';

export async function POST() {
  const apiKey = process.env.DOPPIO_API_KEY;
  if (!apiKey) {
    return NextResponse.json({ error: 'Missing DOPPIO_API_KEY' }, { status: 500 });
  }

  const response = await fetch('https://api.doppio.sh/v1/render/pdf/sync', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${apiKey}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      page: {
        pdf: { printBackground: true },
        goto: {
          url: 'https://example.com/your-page',
          options: { waitUntil: ['networkidle0'] },
        },
      },
    }),
  });

  const data = await response.json();
  return NextResponse.json(data);
}

Direct: stream the PDF to the client

The direct endpoint returns the PDF binary. You must stream the response body back with the right headers—do not call response.json().

ts
// app/api/pdf/route.ts
import { NextResponse } from 'next/server';

export async function POST() {
  const apiKey = process.env.DOPPIO_API_KEY;
  if (!apiKey) {
    return NextResponse.json({ error: 'Missing DOPPIO_API_KEY' }, { status: 500 });
  }

  const response = await fetch('https://api.doppio.sh/v1/render/pdf/direct', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${apiKey}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      page: {
        pdf: { printBackground: true },
        goto: {
          url: 'https://example.com/your-page',
          options: { waitUntil: ['networkidle0'] },
        },
      },
    }),
  });

  if (!response.ok) {
    const err = await response.text();
    return NextResponse.json({ error: err }, { status: response.status });
  }

  return new Response(response.body, {
    headers: {
      'Content-Type': 'application/pdf',
      'Content-Disposition': 'attachment; filename="document.pdf"',
    },
  });
}

Sync vs direct: which to use?

  • Direct: the API returns the PDF bytes; you stream them to the client. Best when the user should download the file immediately (e.g. “Download PDF” button).
  • Sync: the API returns JSON with a document URL. Use it when you need to store the link, send it by email, or hand it to another service.

For full request/response details, see render PDF direct and render PDF sync.

Optional: async PDF generation

For batch jobs or when you don’t want to block the request until the PDF is ready, use the async endpoint with a webhook: you get an immediate response, and Doppio calls your webhook when the file is ready. See render PDF async and full async workflow.

What’s next

Use the Next.js quick start for more examples and tabs. To avoid layout and rendering pitfalls, read HTML to PDF common pitfalls. For a high-level comparison of all options (wkhtmltopdf, Puppeteer, APIs), see HTML to PDF in 2026: what are your options?.

Dashboard

Grab you API key and try Doppio out : it’s free !

With our basic plan, generate up to 400 documents per month for free. No credit card required.

All rights reserved