@forevka/wordcanvas/builder is a typed, chainable API that composes real .docx: page-number fields, content controls, footnotes, tables, and a table of contents. Build the document once, render it live, export it to PDF.

Generate Word documents from TypeScript, in the browser or on your server

@forevka/wordcanvas/builder is a typed, chainable API that composes real .docx: page-number fields, content controls, footnotes, tables, and a table of contents. Build the document once, render it live, export it to PDF.

You know the drill for generating a .docx from code. Pick a library, learn its object model, fight its idea of a table, and hope the file opens in Word without a repair prompt. Or drop to raw OOXML and hand-write w:tbl and w:sdt until your eyes cross. Either way, the report your product promised slips a week you did not budget for.

@forevka/wordcanvas/builder takes a different route. You describe the document with a typed, chainable API, call build(), and get a plain-data Document you can render in a canvas editor, save as .docx, or save as PDF. You write it once and choose the output later.

import { DocumentBuilder, inches } from "@forevka/wordcanvas/builder";

const doc = DocumentBuilder.create({ pageSize: "A4" })
  .paragraph("Quarterly Report").withStyle("Title")
  .paragraph("Prepared for Acme Corp").withStyle("Subtitle")
  .table(
    [
      ["Region", "Revenue", "Growth"],
      ["EMEA", "$1.2M", "+8%"],
      ["APAC", "$0.9M", "+14%"],
    ],
    { headerRow: true },
  )
  .build();

That is the whole flow. No XML, no repair prompts, and the types tell you what table and withStyle accept while you write.

One document, wherever you need it

build() hands back the exact Document the WordCanvas editor paints and the exporters write. So you send that one object to three places:

// live canvas editor, in the browser
editor.setDocument(doc);

// bytes, on Node
import { runExport } from "@forevka/wordcanvas/export";
const pdf = await runExport(doc, "pdf");
const docx = await runExport(doc, "docx");

Generate an invoice in your API handler and stream the PDF back. Preview the same invoice in the browser and let a user fix a line before they download it. Both paths read the document you built once.

Start from a designer's template

Your designer owns the letterhead, the fonts, and the numbered-list look. Hand the builder their .docx and keep all of it:

const b = await DocumentBuilder.fromTemplate(templateBytes);
b.paragraph("Statement of Work").withStyle("Heading1");
const doc = b.build();

fromTemplate carries over the template's stylesheet, list definitions, page setup, and header/footer bands, then drops its body so you fill in your own. Reference the brand's Heading1 by name and it looks the way the designer drew it.

It speaks real Word

Many code-to-docx tools cover paragraphs, a table, maybe an image, then stop. This one emits the OOXML that a reader expects from a document authored in Word:

  • headers and footers with live PAGE and NUMPAGES fields
  • footnotes and endnotes, auto-numbered
  • bookmarks and cross-references (REF, PAGEREF)
  • content controls: rich text, checkboxes, drop-downs, date pickers
  • a table of contents from outline levels, plus display and inline equations from LaTeX

Here is a header that numbers every page, and a cross-reference that points at a bookmark, in the same chain:

b.header((h) => {
  h.paragraph().text("Contract ").pageField().text(" / ").numPagesField();
});

b.paragraph().bookmark("terms", "1. Terms")
  .paragraph().text("See ").crossReference("terms").text(" for the full terms.");

Open the result in Word and the page numbers update, the cross-reference jumps, the checkbox toggles. Save it back to .docx and the fields survive the round-trip.

Built for code you ship

The builder runs where your code runs. In a Node request handler it composes and exports with no browser and no headless Office install. In the browser it feeds the same live editor your users touch.

It stays testable. Pass idSeed and every block id comes out deterministic, so a snapshot test of the generated Document matches across runs. When the builder makes a lossy call, an unknown style id or a quirk in a template, it records a warning instead of throwing, so a nightly batch of a thousand contracts finishes and hands you the list of what it skipped.

Try it

npm install @forevka/wordcanvas
import { DocumentBuilder } from "@forevka/wordcanvas/builder";

const doc = DocumentBuilder.create()
  .paragraph("Hello from TypeScript").withStyle("Heading1")
  .paragraph("A real .docx, built in five lines.")
  .build();

Point it at your report or your invoice, and let the types guide the rest.