DEV Community

Cover image for Deploying an HTML‑to‑PDF API on Vercel with Puppeteer
Ivan Muñoz
Ivan Muñoz

Posted on

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

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Step 4: Try it

Run the development server:

npm run dev
Enter fullscreen mode Exit fullscreen mode

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

Step 5: Deploy it to Vercel

Deploy with Vercel

Top comments (1)

Collapse
 
onlineproxy profile image
OnlineProxy

When you're rolling out Puppeteer with Next.js on Vercel, stick to using puppeteer-core with @sparticuz/chromium, and always run it in API routes. Also, avoid sending huge HTML strings in query parameters-it's way cleaner to POST that HTML in the body or grab it from somewhere like S3. Need PDFs from remote URLs - just use page.goto() with a decent timeout and make sure that page is open to the public. You can tweak your PDFs with stuff like page size and margins, plus shave off some launch time by minimizing Chromium flags. If you're scaling big, look into containerized headless browsers or services like browserless.io, especially if you're dealing with a ton of requests.