Skip to main content
Technology & EngineeringDocument Generation Services312 lines

Latex Node

LaTeX with Node.js: compile LaTeX documents programmatically, template-based PDF generation, mathematical typesetting, academic papers

Quick Summary10 lines
You are an expert in using LaTeX from Node.js for high-quality document generation.

## Key Points

- Always escape user-supplied text with a dedicated function before interpolating it into LaTeX source to prevent compilation errors and injection of arbitrary LaTeX commands.
- Use `xelatex` or `lualatex` when you need Unicode characters or custom OpenType fonts; reserve `pdflatex` for simpler ASCII-only documents where speed matters.
- Run at least two compilation passes when the document uses `\tableofcontents`, `\ref`, `\cite`, or any cross-referencing — the first pass collects labels and the second resolves them.
- Not escaping special characters (`&`, `%`, `$`, `#`, `_`, `{`, `}`, `\`, `~`, `^`) in dynamic content — a single unescaped ampersand will crash the entire compilation with a cryptic error.
skilldb get document-generation-services-skills/Latex NodeFull skill: 312 lines
Paste into your CLAUDE.md or agent config

LaTeX with Node.js — Document Generation

You are an expert in using LaTeX from Node.js for high-quality document generation.

Core Philosophy

Overview

LaTeX is the gold standard for typesetting academic papers, technical reports, and documents with complex mathematical notation. By driving LaTeX from Node.js you can template documents programmatically, compile them to PDF, and integrate the workflow into web services or CI pipelines. This approach is ideal when you need publication-quality output with precise control over typography, equations, bibliographies, and cross-references that no HTML-to-PDF engine can match.

Setup & Configuration

# Install a LaTeX distribution (required on the host)
# Ubuntu/Debian:
sudo apt-get install texlive-full latexmk

# macOS:
brew install --cask mactex

# Windows: install MiKTeX from https://miktex.org/download

# Node.js dependencies
npm install node-latex tmp-promise
npm install -D @types/tmp
// latex-compiler.ts
import { exec } from "child_process";
import { promisify } from "util";
import { readFile, writeFile, mkdir } from "fs/promises";
import { join } from "path";
import { dir as tmpDir } from "tmp-promise";

const execAsync = promisify(exec);

interface CompileOptions {
  /** Number of compilation passes (needed for references/TOC) */
  passes?: number;
  /** LaTeX engine: pdflatex, xelatex, or lualatex */
  engine?: "pdflatex" | "xelatex" | "lualatex";
  /** Additional files to copy into the build directory */
  assets?: Array<{ name: string; content: Buffer }>;
  /** BibTeX file content for bibliography */
  bibContent?: string;
}

async function compileLatex(
  texSource: string,
  options: CompileOptions = {}
): Promise<Buffer> {
  const { passes = 2, engine = "pdflatex", assets = [], bibContent } = options;

  const tmpResult = await tmpDir({ unsafeCleanup: true });
  const workDir = tmpResult.path;
  const texFile = join(workDir, "document.tex");
  const pdfFile = join(workDir, "document.pdf");

  try {
    await writeFile(texFile, texSource, "utf-8");

    // Copy any assets (images, style files) into the work directory
    for (const asset of assets) {
      await writeFile(join(workDir, asset.name), asset.content);
    }

    // Write bibliography if provided
    if (bibContent) {
      await writeFile(join(workDir, "references.bib"), bibContent, "utf-8");
    }

    // Run the LaTeX engine the required number of passes
    for (let i = 0; i < passes; i++) {
      await execAsync(
        `${engine} -interaction=nonstopmode -output-directory="${workDir}" "${texFile}"`,
        { cwd: workDir, timeout: 60_000 }
      );

      // Run bibtex after the first pass if we have a bibliography
      if (i === 0 && bibContent) {
        await execAsync(`bibtex document`, { cwd: workDir, timeout: 30_000 });
      }
    }

    return await readFile(pdfFile);
  } catch (error: any) {
    // Read the log file for diagnostics
    const logFile = join(workDir, "document.log");
    try {
      const log = await readFile(logFile, "utf-8");
      const errorLines = log
        .split("\n")
        .filter((l) => l.startsWith("!") || l.includes("Error"))
        .slice(0, 10);
      throw new Error(
        `LaTeX compilation failed:\n${errorLines.join("\n")}\n\nOriginal: ${error.message}`
      );
    } catch {
      throw error;
    }
  } finally {
    await tmpResult.cleanup();
  }
}

export { compileLatex, CompileOptions };

Core Patterns

Template-based document generation

// template-engine.ts

/** Escape special LaTeX characters in user-provided strings */
function escapeLatex(text: string): string {
  const replacements: Record<string, string> = {
    "\\": "\\textbackslash{}",
    "{": "\\{",
    "}": "\\}",
    "&": "\\&",
    "%": "\\%",
    $: "\\$",
    "#": "\\#",
    _: "\\_",
    "~": "\\textasciitilde{}",
    "^": "\\textasciicircum{}",
  };
  return text.replace(/[\\{}&%$#_~^]/g, (ch) => replacements[ch]);
}

interface InvoiceItem {
  description: string;
  quantity: number;
  unitPrice: number;
}

interface InvoiceData {
  invoiceNumber: string;
  date: string;
  clientName: string;
  clientAddress: string;
  items: InvoiceItem[];
  notes?: string;
}

function generateInvoiceLatex(data: InvoiceData): string {
  const itemRows = data.items
    .map(
      (item) =>
        `${escapeLatex(item.description)} & ${item.quantity} & \\$${item.unitPrice.toFixed(2)} & \\$${(item.quantity * item.unitPrice).toFixed(2)} \\\\`
    )
    .join("\n    \\hline\n    ");

  const total = data.items.reduce(
    (sum, item) => sum + item.quantity * item.unitPrice,
    0
  );

  return `
\\documentclass[11pt,a4paper]{article}
\\usepackage[margin=2cm]{geometry}
\\usepackage{booktabs}
\\usepackage{array}
\\usepackage{graphicx}
\\usepackage{xcolor}
\\usepackage{fancyhdr}

\\definecolor{primary}{HTML}{2563EB}

\\pagestyle{fancy}
\\fancyhf{}
\\rhead{Invoice \\#${escapeLatex(data.invoiceNumber)}}
\\rfoot{Page \\thepage}

\\begin{document}

\\begin{flushright}
{\\Large\\bfseries\\color{primary} INVOICE}\\\\[4pt]
\\#${escapeLatex(data.invoiceNumber)}\\\\
${escapeLatex(data.date)}
\\end{flushright}

\\vspace{1cm}

\\textbf{Bill To:}\\\\
${escapeLatex(data.clientName)}\\\\
${escapeLatex(data.clientAddress)}

\\vspace{1cm}

\\begin{tabular}{|p{7cm}|c|r|r|}
\\hline
\\textbf{Description} & \\textbf{Qty} & \\textbf{Unit Price} & \\textbf{Total} \\\\
\\hline
${itemRows}
\\hline
\\multicolumn{3}{|r|}{\\textbf{Grand Total}} & \\textbf{\\$${total.toFixed(2)}} \\\\
\\hline
\\end{tabular}

${data.notes ? `\\vspace{1cm}\n\\textbf{Notes:} ${escapeLatex(data.notes)}` : ""}

\\end{document}
`;
}

// Usage
async function generateInvoicePdf(data: InvoiceData): Promise<Buffer> {
  const tex = generateInvoiceLatex(data);
  return compileLatex(tex, { engine: "pdflatex", passes: 1 });
}

Mathematical documents with XeLaTeX and custom fonts

function generateMathReport(title: string, sections: Array<{ heading: string; content: string; equations: string[] }>): string {
  const body = sections
    .map(
      (s) => `
\\section{${escapeLatex(s.heading)}}
${s.content}
${s.equations.map((eq) => `\\begin{equation}\n  ${eq}\n\\end{equation}`).join("\n")}`
    )
    .join("\n");

  return `
\\documentclass[12pt]{article}
\\usepackage{fontspec}
\\setmainfont{Latin Modern Roman}
\\usepackage{amsmath,amssymb,amsthm}
\\usepackage{hyperref}
\\usepackage[margin=2.5cm]{geometry}

\\title{${escapeLatex(title)}}
\\author{Generated Report}
\\date{\\today}

\\newtheorem{theorem}{Theorem}[section]
\\newtheorem{lemma}[theorem]{Lemma}

\\begin{document}
\\maketitle
\\tableofcontents
\\newpage

${body}

\\end{document}
`;
}

// Compile with xelatex for Unicode/font support, 2 passes for TOC
// compileLatex(source, { engine: "xelatex", passes: 2 });

Express endpoint for on-demand PDF generation

import express from "express";

const app = express();
app.use(express.json());

app.post("/api/generate-report", async (req, res) => {
  try {
    const texSource = generateMathReport(req.body.title, req.body.sections);
    const pdf = await compileLatex(texSource, {
      engine: "xelatex",
      passes: 2,
    });

    res.setHeader("Content-Type", "application/pdf");
    res.setHeader("Content-Disposition", `attachment; filename="report.pdf"`);
    res.send(pdf);
  } catch (error: any) {
    res.status(500).json({ error: error.message });
  }
});

Best Practices

  • Always escape user-supplied text with a dedicated function before interpolating it into LaTeX source to prevent compilation errors and injection of arbitrary LaTeX commands.
  • Use xelatex or lualatex when you need Unicode characters or custom OpenType fonts; reserve pdflatex for simpler ASCII-only documents where speed matters.
  • Run at least two compilation passes when the document uses \tableofcontents, \ref, \cite, or any cross-referencing — the first pass collects labels and the second resolves them.

Common Pitfalls

  • Not escaping special characters (&, %, $, #, _, {, }, \, ~, ^) in dynamic content — a single unescaped ampersand will crash the entire compilation with a cryptic error.
  • Running LaTeX in a container or serverless function without a complete TeX distribution installed — missing packages cause silent failures. Use texlive-full or explicitly install every required package in the Docker image.

Anti-Patterns

Using the service without understanding its pricing model. Cloud services bill differently — per request, per GB, per seat. Deploying without modeling expected costs leads to surprise invoices.

Hardcoding configuration instead of using environment variables. API keys, endpoints, and feature flags change between environments. Hardcoded values break deployments and leak secrets.

Ignoring the service's rate limits and quotas. Every external API has throughput limits. Failing to implement backoff, queuing, or caching results in dropped requests under load.

Treating the service as always available. External services go down. Without circuit breakers, fallbacks, or graceful degradation, a third-party outage becomes your outage.

Coupling your architecture to a single provider's API. Building directly against provider-specific interfaces makes migration painful. Wrap external services in thin adapter layers.

Install this skill directly: skilldb add document-generation-services-skills

Get CLI access →