In this article, we will explore how to create an HTML-to-PDF API on Vercel using Puppeteer
Start by creating a new Next.js project:
bash1npx create-next-app@latest html-to-pdf-on-vercel --typescript --tailwind --app
Install the required packages for Puppeteer:
bash1npm install puppeteer-core @sparticuz/chromium
bash1npm install -D puppeteer
Create a new file at app/api/pdf/route.ts:
ts1import { 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.
To interact with the API, replace the content of app/page.tsx:
tsx1"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}
Update your next.config.ts to ensure Puppeteer runs correctly:
ts1import type { NextConfig } from "next"; 2 3const nextConfig: NextConfig = { 4 serverExternalPackages: ["@sparticuz/chromium", "puppeteer-core"], 5}; 6 7export default nextConfig;
Run locally:
bash1npm run dev
Then open http://localhost:3000 to test. Finally, deploy it to Vercel 🚀.
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.