Next.js5 min read · June 11, 2026

HTML to PDF in Next.js: API Routes, Route Handlers, and Server Actions

Generate PDFs in Next.js without Puppeteer. A simple fetch call works in Pages Router, App Router, and Server Actions, including on Vercel and edge runtimes.

Puppeteer is the go-to for PDF generation in Node.js, but it breaks when you deploy to Vercel. Chromium's binary is too large for standard serverless bundles, Edge Functions block native modules entirely, and cold starts under concurrent load are painful. A REST API sidesteps every constraint: it's just a fetch call.

App Router: Route Handler

app/api/pdf/route.tsts
export async function POST(request: Request) {
  const { html } = await request.json() as { html: string }

  const apiRes = await fetch('https://platform.htmltopdfapi.co/api/v1/pdf/generate', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.HTMLTOPDF_API_KEY!}`,
      'Content-Type': 'application/json',
      Accept: 'application/pdf',
    },
    body: JSON.stringify({ html, paper_size: 'a4' }),
    cache: 'no-store',
  })

  if (!apiRes.ok) {
    return Response.json({ error: 'PDF generation failed' }, { status: 500 })
  }

  return new Response(await apiRes.arrayBuffer(), {
    headers: {
      'Content-Type': 'application/pdf',
      'Content-Disposition': 'inline; filename="document.pdf"',
    },
  })
}

Pages Router: API Route

pages/api/pdf.tsts
import type { NextApiRequest, NextApiResponse } from 'next'

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method !== 'POST') return res.status(405).end()

  const { html } = req.body as { html: string }

  const apiRes = await fetch('https://platform.htmltopdfapi.co/api/v1/pdf/generate', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.HTMLTOPDF_API_KEY}`,
      'Content-Type': 'application/json',
      Accept: 'application/pdf',
    },
    body: JSON.stringify({ html, paper_size: 'a4' }),
  })

  if (!apiRes.ok) return res.status(500).json({ error: 'PDF generation failed' })

  res.setHeader('Content-Type', 'application/pdf')
  res.setHeader('Content-Disposition', 'inline; filename="document.pdf"')
  res.send(Buffer.from(await apiRes.arrayBuffer()))
}

Server Action

For form-driven PDF generation, a Server Action keeps everything server-side:

app/actions/generatePdf.tsts
'use server'

export async function generatePdf(html: string): Promise<ArrayBuffer> {
  const res = await fetch('https://platform.htmltopdfapi.co/api/v1/pdf/generate', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.HTMLTOPDF_API_KEY!}`,
      'Content-Type': 'application/json',
      Accept: 'application/pdf',
    },
    body: JSON.stringify({ html, paper_size: 'a4' }),
    cache: 'no-store',
  })
  if (!res.ok) throw new Error('PDF generation failed')
  return res.arrayBuffer()
}

Works on Every Runtime

Because this is fetch(), a standard Web API, it runs identically on Vercel Node.js functions, Edge Functions, Cloudflare Workers, and local development. No binary, no build configuration, no Lambda layer. See the dashboard PDF export guide for a complete Next.js API route implementation.