Deploying an HTML‑to‑PDF API on Vercel with Puppeteer



This content originally appeared on DEV Community and was authored by Ivan Muñoz

Original article https://buglesstack.com/blog/deploying-an-html-to-pdf-api-on-vercel-with-puppeteer/

In this article, we will explore how to create an HTML-to-PDF API on Vercel using Puppeteer.

You can find the complete code in the GitHub repository.

Deploy with Vercel

Demo

https://html-to-pdf-on-vercel.vercel.app/

Step 1: Project Setup

npx create-next-app@latest html-to-pdf-on-vercel --typescript --tailwind --app

Now, install the packages puppeteer-core @sparticuz/chromium for running Puppeteer in Vercel and puppeteer for local development:

npm install puppeteer-core @sparticuz/chromium
npm install -D puppeteer

Step 2: Setup the HTML to PDF api route

Create a new file at app/api/pdf/route.ts:

import { NextRequest, NextResponse } from "next/server";
import puppeteer from 'puppeteer-core';
import chromium from '@sparticuz/chromium';

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const htmlParam = searchParams.get("html");
  if (!htmlParam) {
    return new NextResponse("Please provide the HTML.", { status: 400 });
  }

  let browser;

  try {
    const isVercel = !!process.env.VERCEL_ENV;

    const pptr = isVercel ? puppeteer : (await import("puppeteer")) as unknown as typeof puppeteer;

    browser = await pptr.launch(isVercel ? {
      args: chromium.args,
      executablePath: await chromium.executablePath(),
      headless: true
    } : { 
      headless: true, 
      args: puppeteer.defaultArgs()
    });

    const page = await browser.newPage();
    await page.setContent(htmlParam, { waitUntil: 'load' });

    const pdf = await page.pdf({ 
        path: undefined,
        printBackground: true
    });
    return new NextResponse(Buffer.from(pdf), {
      headers: {
        "Content-Type": "application/pdf",
        "Content-Disposition": 'inline; filename="page-output.pdf"',
      },
    });
  } catch (error) {
    console.error(error);
    return new NextResponse(
      "An error occurred while generating the PDF.",
      { status: 500 }
    );
  } finally {
    if (browser) {
      await browser.close();
    }
  }
}

This route handles and HTML as a url query param and add it to the page with page.setContent() to then generate the PDF.

Step 3: Add a frontend to call the API

To interact with our API, let’s create a simple frontend. Replace the content of app/page.tsx:

"use client";

import { useState } from "react";

const defaultHtml = `<p style="text-align:center">
  Hello World! 
  <br />
  <b>
    This PDF was created using 
    <br />
    <a href="https://github.com/ivanalemunioz/html-to-pdf-on-vercel">
      https://github.com/ivanalemunioz/html-to-pdf-on-vercel
    </a>
  </b>
</p>`;

export default function HomePage() {
  const [html, setHtml] = useState(defaultHtml);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const createPDF = async () => {
    if (!html) {
      setError("Please enter a valid HTML.");
      return;
    }

    setLoading(true);
    setError(null);

    try {
      const response = await fetch(
        `/api/pdf?html=${encodeURIComponent(html)}`
      );
      if (!response.ok) {
        throw new Error("Failed to create PDF.");
      }
      const blob = await response.blob();
      const objectUrl = URL.createObjectURL(blob);
      const link = document.createElement('a');
      link.href = objectUrl;
      link.download = 'output.pdf'; // Desired filename
      document.body.appendChild(link); // Temporarily add to the DOM
      link.click(); // Programmatically click the link to trigger download
      document.body.removeChild(link); // Remove the link
      URL.revokeObjectURL(objectUrl); // Release the object URL
    } catch (err) {
      setError(
        err instanceof Error ? err.message : "An unknown error occurred."
      );
    } finally {
      setLoading(false);
    }
  };

  return (
    <main className="flex min-h-screen flex-col items-center justify-center p-24 bg-gray-50">
      <div className="w-full max-w-2xl text-center">
        <h1 className="text-4xl font-bold mb-4 text-gray-800">
          HTML to PDF on Vercel using Puppeteer
        </h1>
        <p className="text-lg text-gray-600 mb-8">
          Enter the HTML below to generate a PDF using Puppeteer running in
          a Vercel Function.
        </p>
        <div className="flex gap-2 flex-col">
          <textarea
            value={html}
            rows={13}
            onChange={(e) => setHtml(e.target.value)}
            placeholder="https://vercel.com"
            className="flex-grow p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 text-black focus:outline-none font-mono"
          />
          <button
            onClick={createPDF}
            disabled={loading}
            className="px-6 py-3 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 disabled:bg-gray-400 transition-colors"
          >
            {loading ? "Creating PDF..." : "Create PDF"}
          </button>
        </div>
        {error && <p className="text-red-500 mt-4">{error}</p>}
      </div>
    </main>
  );
}

Step 4: Vercel Configuration

To ensure Puppeteer runs correctly when deployed, you need to configure Next.js.

Update your next.config.ts file.

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  /* config options here */
  // The `serverExternalPackages` option allows you to opt-out of bundling dependencies in your Server Components.
  serverExternalPackages: ["@sparticuz/chromium", "puppeteer-core"],
};

export default nextConfig;

Step 4: Try it

Run the development server:

npm run dev

Open http://localhost:3000 with your browser to try it.

Step 5: Deploy it to Vercel

Deploy with Vercel


This content originally appeared on DEV Community and was authored by Ivan Muñoz