\begin{article}
LaTeX PDF Generation in Node.js and TypeScript
Generate professional PDFs from LaTeX in Node.js — no system dependencies, no temp files, just an API call. Full TypeScript client with error handling and Next.js integration.

Node.js and TypeScript are the most common backend environments for web applications, and PDF generation is a nearly universal requirement. The traditional approach — spawning a pdflatex process — works on developer machines but falls apart in serverless environments, Docker containers without TeX Live, and anywhere you cannot install a 4 GB system dependency. The API approach solves all of this with zero system requirements.
The DIY Pain
The subprocess approach in Node.js:
import { exec } from "child_process";
import { promisify } from "util";
import { writeFile, readFile, mkdir, rm } from "fs/promises";
import { join } from "path";
import { tmpdir } from "os";
const execAsync = promisify(exec);
// This breaks in serverless, Docker without TeX Live, and CI without pdflatex
async function compilePdfNaive(latex: string): Promise<Buffer> {
const dir = join(tmpdir(), `latex-${Date.now()}`);
await mkdir(dir, { recursive: true });
try {
const texPath = join(dir, "document.tex");
await writeFile(texPath, latex, "utf-8");
await execAsync(
`pdflatex -interaction=nonstopmode -output-directory "${dir}" "${texPath}"`
);
return await readFile(join(dir, "document.pdf"));
} finally {
await rm(dir, { recursive: true, force: true }); // cleanup
}
}Problems: requires pdflatex installed, single pass (no bibliography), breaks in serverless, error messages are raw TeX logs.
The fetch() Integration
The minimal API call — works in any Node.js environment:
async function compilePdf(latex: string): Promise<Buffer> {
const response = await fetch("https://api.formatex.io/api/v1/compile", {
method: "POST",
headers: {
"X-API-Key": process.env.FORMATEX_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({ content: latex, engine: "pdflatex" }),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.log ?? error.error ?? "Compilation failed");
}
return Buffer.from(await response.arrayBuffer());
}fetch is built into Node.js 18+. No npm package required.
Full TypeScript Client Class
A reusable client with proper typing, error handling, and engine selection:
type Engine = "pdflatex" | "xelatex" | "lualatex" | "latexmk";
interface CompileOptions {
engine?: Engine;
}
interface CompileError {
error: string;
log?: string;
}
class FormaTeXClient {
private readonly baseUrl = "https://api.formatex.io/api/v1";
constructor(private readonly apiKey: string) {}
async compile(latex: string, options: CompileOptions = {}): Promise<Buffer> {
const engine = options.engine ?? "pdflatex";
const response = await fetch(`${this.baseUrl}/compile`, {
method: "POST",
headers: {
"X-API-Key": this.apiKey,
"Content-Type": "application/json",
},
body: JSON.stringify({ content: latex, engine }),
});
if (!response.ok) {
const error: CompileError = await response.json();
throw new LatexError(error.error, error.log);
}
return Buffer.from(await response.arrayBuffer());
}
}
class LatexError extends Error {
constructor(
message: string,
public readonly log?: string
) {
super(message);
this.name = "LatexError";
}
get firstErrorLine(): string | undefined {
return this.log?.split("\n").find((line) => line.startsWith("!"));
}
}
// Usage
const formatex = new FormaTeXClient(process.env.FORMATEX_KEY!);
try {
const pdf = await formatex.compile(latexSource, { engine: "xelatex" });
await writeFile("output.pdf", pdf);
} catch (e) {
if (e instanceof LatexError) {
console.error("LaTeX error:", e.firstErrorLine ?? e.message);
console.error("Full log:", e.log);
}
throw e;
}Error Handling
LaTeX errors return 400 with the TeX compilation log. The log is verbose — extract the first ! line for the human-readable error:
function extractLatexError(log: string): string {
const lines = log.split("\n");
const errorLine = lines.find((l) => l.startsWith("!"));
if (!errorLine) return log.slice(0, 200);
// Also find the line number
const lineNumberLine = lines
.slice(lines.indexOf(errorLine))
.find((l) => /^l\.\d+/.test(l));
return lineNumberLine ? `${errorLine} (${lineNumberLine})` : errorLine;
}Next.js API Route Example
A full Next.js App Router route that generates and streams a PDF:
// app/api/pdf/route.ts
import { NextRequest } from "next/server";
export async function POST(request: NextRequest) {
const { latexSource, engine = "pdflatex" } = await request.json();
if (!latexSource) {
return Response.json({ error: "latexSource is required" }, { status: 400 });
}
const compileResponse = await fetch(
"https://api.formatex.io/api/v1/compile",
{
method: "POST",
headers: {
"X-API-Key": process.env.FORMATEX_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({ content: latexSource, engine }),
}
);
if (!compileResponse.ok) {
const error = await compileResponse.json();
return Response.json(
{ error: error.error, log: error.log },
{ status: 422 }
);
}
// Stream the PDF directly to the client
return new Response(compileResponse.body, {
headers: {
"Content-Type": "application/pdf",
"Content-Disposition": 'attachment; filename="document.pdf"',
},
});
}Streaming the response body directly (compileResponse.body) avoids buffering the entire PDF in memory on your server. For large documents, this significantly reduces memory usage.
Streaming Response
For large PDFs, stream the response to disk rather than buffering:
import { createWriteStream } from "fs";
import { pipeline } from "stream/promises";
import { Readable } from "stream";
async function compileToDisk(latex: string, outputPath: string): Promise<void> {
const response = await fetch("https://api.formatex.io/api/v1/compile", {
method: "POST",
headers: {
"X-API-Key": process.env.FORMATEX_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({ content: latex, engine: "pdflatex" }),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.log ?? error.error);
}
await pipeline(
Readable.fromWeb(response.body!),
createWriteStream(outputPath)
);
}Beyond Sync Compilation
The examples above use the synchronous POST /compile endpoint. FormaTeX also offers:
- Smart Compile (
POST /compile/smart) — AI-powered error detection and auto-fix. If your LaTeX has errors, the AI pipeline fixes them automatically. See Smart Compile guide. - Async Compilation (
POST /compile/async) — submit a job, get a job ID, poll for completion or receive a webhook. Ideal for long documents and batch processing. See Async guide.
Get Started
- Sign up for free — 15 compilations/month, no card required
- API documentation — full endpoint schema and error codes
- Dashboard — API key management and usage stats
\end{article}
\related{posts}




