DEV Community

Roberto Luna
Roberto Luna

Posted on

Fixing PDFKit Font Tracing in a Next.js Serverless PDF Report

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

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

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

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

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"
+   ]
+ },
Enter fullscreen mode Exit fullscreen mode

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

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

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

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:

  1. Add unit tests for the PDF service using pdfkit’s in‑memory API to catch future regressions.
  2. Cache the generated PDF in a CDN (Vercel Edge) to reduce lambda cold‑start latency for repeated downloads.
  3. 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)