PDFCraft
Next.js

PDF generation in Next.js

A route handler returns a Web `Response`, so the PDF can be piped straight through without ever touching the filesystem — which matters because on Vercel there is no writable filesystem to touch.

The code

// app/api/invoice/[id]/route.ts
import { NextResponse } from 'next/server';

export async function GET(
  _request: Request,
  { params }: { params: Promise<{ id: string }> },
) {
  const { id } = await params;
  const html = await renderInvoiceHtml(id); // your existing template

  const upstream = await fetch('https://api.pdfcraft.dev/v1/render', {
    method: 'POST',
    headers: {
      authorization: `Bearer ${process.env.PDFCRAFT_API_KEY}`,
      'content-type': 'application/json',
    },
    body: JSON.stringify({
      html,
      options: {
        printBackground: true,
        margin: { top: '20mm', bottom: '20mm' },
        footerHtml:
          '<div style="font-size:9px;width:100%;text-align:center">' +
          'Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>',
      },
    }),
  });

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

  // Stream it through. Nothing is buffered in the function.
  return new Response(upstream.body, {
    headers: {
      'content-type': 'application/pdf',
      'content-disposition': `attachment; filename="invoice-${id}.pdf"`,
    },
  });
}

What to watch for

Why not run the browser yourself

You can. Puppeteer and Playwright both work, and for a script you run by hand they are the right answer. The cost arrives at the point it becomes a service someone depends on: one long-lived browser per process rather than one per request, a semaphore so four renders do not become forty, a hard timeout that force-closes a hung page before it holds a slot forever, a scheduled restart because Chromium leaks, and a Chromium version you are now responsible for patching.

None of that is hard. All of it is work that has nothing to do with your product, and it fails under concurrency rather than in testing — which is the worst time to discover it.

Other frameworks

Or by language