Fixing PDFKit Font Tracing in a Next.js Serverless PDF Report
TL;DR: PDFKit’s built‑in fonts weren’t being bundled in the Vercel serverless build, causing 500 errors on the /api/reports/tv-issues endpoint. Adding pdfkit to serverExternalPackages and extending outputFileTracingIncludes to trace the standard-fonts/*.cjs files solved the issue.
The Problem
The TV‑issues dashboard needed an exportable PDF report (fuera de servicio, sin TV, sin MAC, sin Chromecast). Locally the /api/reports/tv-issues route worked, but in production it returned 500 Internal Server Error. The logs showed a generic “Error generating PDF report” without the underlying cause because PDFKit silently failed when it couldn’t locate its font files.
Typical error seen in Vercel logs:
Error generando reporte PDF de TV: Error: Cannot find module '/tmp/.../node_modules/pdfkit/js/data/Helvetica.afm'
PDFKit loads its standard fonts from node_modules/pdfkit/js/data/*.afm and node_modules/pdfkit/js/standard-fonts/*.cjs. When Next.js builds a serverless bundle, only files that are explicitly traced get copied into the lambda. By default, only JavaScript files are traced; the font files were omitted, so PDFKit threw at runtime.
What I Tried First
My initial instinct was to treat it as a missing dependency:
npm install @types/pdfkit
I also added a simple try/catch around the PDF generation to surface the error:
export async function GET() {
try {
const report = await getTvIssuesReport();
// ... generate PDF
} catch (err) {
console.error("Error generando reporte PDF de TV:", err);
return new Response("PDF generation failed", { status: 500 });
}
}
That gave me the real stack trace (the Cannot find module …Helvetica.afm message) but didn’t fix the underlying bundling problem. I also attempted to copy the font files manually in a post‑install script, but that added unnecessary complexity and still conflicted with Vercel’s immutable build cache.
The Implementation
1. Mark pdfkit as an external package
Next.js 13+ allows us to tell the serverless compiler that a package should be treated as an external dependency, bypassing the default tracing logic. Adding it to serverExternalPackages also fixes the #imports sub‑path map resolution that PDFKit uses.
// next.config.ts (before)
const nextConfig: NextConfig = {
// …other options
};
export default withSentryConfig(nextConfig);
// next.config.ts (after)
import type { NextConfig } from "next";
import { withSentryConfig } from "@sentry/nextjs";
const nextConfig: NextConfig = {
// PDFKit reads its own files via #imports, so we keep it external.
serverExternalPackages: ["pdfkit"],
// Ensure the font files are bundled.
outputFileTracingIncludes: {
// Include both .afm and .cjs font resources.
"/api/reports/tv-issues": [
"node_modules/pdfkit/js/data/*.afm",
"node_modules/pdfkit/js/standard-fonts/*.cjs",
],
},
// …other Next.js options
};
export default withSentryConfig(nextConfig);
Why this works: By declaring pdfkit as external, Next.js no longer tries to bundle it as a normal module. Instead, it copies the entire package (including non‑JS assets) into the lambda’s node_modules. The outputFileTracingIncludes entry explicitly tells the tracer to include the font files that would otherwise be ignored.
2. Extend the tracing includes for font files
The original outputFileTracingIncludes only captured data/*.afm files. PDFKit also ships standard-fonts/*.cjs, which contain the compiled font definitions used by the PDF renderer. Missing those caused the runtime crash.
- outputFileTracingIncludes: {
- "/api/reports/tv-issues": ["node_modules/pdfkit/js/data/*.afm"]
- },
+ outputFileTracingIncludes: {
+ "/api/reports/tv-issues": [
+ "node_modules/pdfkit/js/data/*.afm",
+ "node_modules/pdfkit/js/standard-fonts/*.cjs"
+ ]
+ },
3. Add TypeScript typings for PDFKit
The project uses strict TypeScript, so importing PDFKit without types raised compile errors. Adding @types/pdfkit to package.json resolved that.
// package.json (excerpt)
{
"dependencies": {
// …other deps
"@types/pdfkit": "^0.17.6"
}
}
The lockfile (package-lock.json) was automatically updated; no code change required.
4. Clean up the route handler
With the bundling issue fixed, the route can focus on its core responsibility: building the PDF. I removed the temporary debug console and left a concise error logger.
// src/app/api/reports/tv-issues/route.ts
import { PDFDocument } from "pdfkit";
import { getTvIssuesReport } from "@/services/tvIssuesReport.service";
export async function GET() {
try {
const report = await getTvIssuesReport();
const doc = new PDFDocument({ size: "A4", margin: 50 });
// ... draw headers, tables, footers ...
const chunks: Buffer[] = [];
doc.on("data", (chunk) => chunks.push(chunk));
doc.on("end", () => {
const pdfBuffer = Buffer.concat(chunks);
return new Response(pdfBuffer, {
status: 200,
headers: { "Content-Type": "application/pdf" },
});
});
// Trigger PDF generation
doc.end();
} catch (err) {
console.error("Error generando reporte PDF de TV:", err);
return new Response("PDF generation failed", { status: 500 });
}
}
5. Verify locally and in a preview deployment
Running npm run dev still works as before. The crucial test is a Vercel preview build:
vercel --prebuilt
The preview URL returned the PDF correctly, confirming the font files were present in the lambda bundle.
Key Takeaway
When using libraries that load non‑JavaScript assets (fonts, templates, etc.) in a serverless environment, explicitly include those assets in outputFileTracingIncludes and, if needed, mark the library as an external package. Relying on the default tracing will silently drop required files, leading to runtime 500 errors that are hard to diagnose.
What's Next
I plan to:
-
Add unit tests for the PDF service using
pdfkit’s in‑memory API to catch future regressions. - Cache the generated PDF in a CDN (Vercel Edge) to reduce lambda cold‑start latency for repeated downloads.
- Expose a streaming endpoint that streams the PDF directly to the client instead of buffering the whole file in memory.
Roberto Luna Osorio – Full Stack Developer & Project Lead
Playa del
Part of my Build in Public series — sharing the real process of building SaaS projects from Playa del Carmen, México.
Repo: zaerohell/tvview · 2026-09-11
#playadev #buildinpublic
Top comments (0)