Tony Santana
PortfolioResumeBlog

Command Palette

Search for a command to run...

Blog
Next

Built by tonysantana1492. The source code is available on GitHub.

llms.txt
sitemap.xml
RSS

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

Step 1: Project Setup

Start by creating a new Next.js project:

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

Install the required packages for Puppeteer:

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

Step 2: Setup the API Route

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

ts
1import { NextRequest, NextResponse } from "next/server";
2import puppeteer from "puppeteer-core";
3import chromium from "@sparticuz/chromium";
4
5export async function GET(request: NextRequest) {
6  const { searchParams } = new URL(request.url);
7  const htmlParam = searchParams.get("html");
8  if (!htmlParam) {
9    return new NextResponse("Please provide the HTML.", { status: 400 });
10  }
11  let browser;
12  try {
13    const isVercel = !!process.env.VERCEL_ENV;
14    const pptr = isVercel
15      ? puppeteer
16      : ((await import("puppeteer")) as unknown as typeof puppeteer);
17    browser = await pptr.launch(
18      isVercel
19        ? {
20            args: chromium.args,
21            executablePath: await chromium.executablePath(),
22            headless: true,
23          }
24        : { headless: true, args: puppeteer.defaultArgs() }
25    );
26
27    const page = await browser.newPage();
28    await page.setContent(htmlParam, { waitUntil: "load" });
29    const pdf = await page.pdf({ printBackground: true });
30
31    return new NextResponse(Buffer.from(pdf), {
32      headers: {
33        "Content-Type": "application/pdf",
34        "Content-Disposition": 'inline; filename="page-output.pdf"',
35      },
36    });
37  } catch (error) {
38    console.error(error);
39    return new NextResponse("An error occurred while generating the PDF.", {
40      status: 500,
41    });
42  } finally {
43    if (browser) {
44      await browser.close();
45    }
46  }
47}

This route takes an HTML string as a query param, renders it with Puppeteer, and returns a PDF.


Step 3: Add a Frontend

To interact with the API, replace the content of app/page.tsx:

tsx
1"use client";
2
3import { useState } from "react";
4
5const defaultHtml = `<p style="text-align:center">
6Hello World! <br />
7
8  <b>
9    This PDF was created using <br />
10    <a href="https://github.com/tonysantana1492/html-to-pdf-on-vercel">
11      https://github.com/tonysantana1492/html-to-pdf-on-vercel
12    </a>
13  </b>
14</p>`;
15
16export default function HomePage() {
17  const [html, setHtml] = useState(defaultHtml);
18  const [loading, setLoading] = useState(false);
19  const [error, setError] = useState<string | null>(null);
20
21  const createPDF = async () => {
22    if (!html) {
23      setError("Please enter a valid HTML.");
24      return;
25    }
26    setLoading(true);
27    setError(null);
28    try {
29      const response = await fetch(
30        `/api/pdf?html=\${encodeURIComponent(html)}`
31      );
32      if (!response.ok) throw new Error("Failed to create PDF.");
33
34      const blob = await response.blob();
35      const objectUrl = URL.createObjectURL(blob);
36      const link = document.createElement("a");
37      link.href = objectUrl;
38      link.download = "output.pdf";
39      document.body.appendChild(link);
40      link.click();
41      document.body.removeChild(link);
42      URL.revokeObjectURL(objectUrl);
43    } catch (err) {
44      setError(
45        err instanceof Error ? err.message : "An unknown error occurred."
46      );
47    } finally {
48      setLoading(false);
49    }
50  };
51
52  return (
53    <main className="flex min-h-screen flex-col items-center justify-center p-24 bg-gray-50">
54      <div className="w-full max-w-2xl text-center">
55        <h1 className="text-4xl font-bold mb-4 text-gray-800">
56          HTML to PDF on Vercel using Puppeteer
57        </h1>
58        <p className="text-lg text-gray-600 mb-8">
59          Enter the HTML below to generate a PDF using Puppeteer running in a
60          Vercel Function.
61        </p>
62        <div className="flex gap-2 flex-col">
63          <textarea
64            value={html}
65            rows={13}
66            onChange={(e) => setHtml(e.target.value)}
67            className="grow p-3 border border-gray-300 rounded-lg font-mono"
68          />
69          <button
70            onClick={createPDF}
71            disabled={loading}
72            className="px-6 py-3 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 disabled:bg-gray-400 transition-colors"
73          >
74            {loading ? "Creating PDF..." : "Create PDF"}
75          </button>
76        </div>
77        {error && <p className="text-red-500 mt-4">{error}</p>}
78      </div>
79    </main>
80  );
81}

Step 4: Configure Next.js

Update your next.config.ts to ensure Puppeteer runs correctly:

ts
1import type { NextConfig } from "next";
2
3const nextConfig: NextConfig = {
4  serverExternalPackages: ["@sparticuz/chromium", "puppeteer-core"],
5};
6
7export default nextConfig;

Step 5: Run & Deploy

Run locally:

bash
1npm run dev

Then open http://localhost:3000 to test. Finally, deploy it to Vercel 🚀.


Conclusion

We built and deployed an HTML-to-PDF API with Puppeteer on Vercel, along with a simple frontend interface. This approach is lightweight, serverless, and works both in development and production.