Typed-xlsx
Core Concepts

Buffered vs Streaming

When to use createWorkbook versus createWorkbookStream, and how their APIs differ.

typed-xlsx ships two workbook builders. Both support the same schema kinds (report and excel-table), the same formula columns, the same column groups, and the same output targets. The difference is in how row data flows through memory.

Buffered workbook

createWorkbook() holds all rows in memory while building. You pass rows: T[] at the table level and call an output method once. The builder plans every row, computes summaries, resolves formulas, and assembles the XLSX in one pass.

import { 
createExcelSchema
,
createWorkbook
} from "@chronicstone/typed-xlsx";
const
invoiceSchema
=
createExcelSchema
<{
invoiceId
: string;
customer
: string;
amount
: number;
dueDate
: string;
}>() .
column
("invoiceId", {
header
: "Invoice #",
accessor
: "invoiceId" })
.
column
("customer", {
header
: "Customer",
accessor
: "customer" })
.
column
("amount", {
header
: "Amount",
accessor
: "amount",
style
: {
numFmt
: "$#,##0.00" } })
.
column
("dueDate", {
header
: "Due Date",
accessor
: "dueDate" })
.
build
();
const
rows
= [
{
invoiceId
: "INV-001",
customer
: "Acme Corp",
amount
: 4200,
dueDate
: "2025-03-31" },
{
invoiceId
: "INV-002",
customer
: "Globex Inc",
amount
: 8750,
dueDate
: "2025-04-15" },
]; const
workbook
=
createWorkbook
();
workbook
.
sheet
("Invoices").
table
("invoices", {
schema
:
invoiceSchema
,
rows
});
const
buffer
=
workbook
.
toBuffer
();

Use buffered when:

  • The full dataset fits comfortably in memory (rule of thumb: under ~100k rows)
  • You need summary rows (report mode) — their step() reducers run as rows are planned
  • You want the simplest API with no commit lifecycle to manage

Streaming workbook

createWorkbookStream() writes rows to a spool (temporary file on disk) in batches. No rows are held in memory after a commit. The XLSX file is assembled by streaming the spool contents into a ZIP writer.

import { 
createExcelSchema
,
createWorkbookStream
} from "@chronicstone/typed-xlsx";
const
orderSchema
=
createExcelSchema
<{
orderId
: string;
sku
: string;
qty
: number;
unitPrice
: number;
}>() .
column
("orderId", {
header
: "Order ID",
accessor
: "orderId" })
.
column
("sku", {
header
: "SKU",
accessor
: "sku" })
.
column
("qty", {
header
: "Qty",
accessor
: "qty" })
.
column
("unitPrice", {
header
: "Unit Price",
accessor
: "unitPrice",
style
: {
numFmt
: "$#,##0.00" },
}) .
build
();
const
workbook
=
createWorkbookStream
();
const
table
= await
workbook
.
sheet
("Orders").
table
("orders", {
schema
:
orderSchema
});
// Commit in batches — memory stays flat await
table
.
commit
({
rows
: [{
orderId
: "ORD-1",
sku
: "SKU-A",
qty
: 5,
unitPrice
: 29.99 }] });
await
table
.
commit
({
rows
: [{
orderId
: "ORD-2",
sku
: "SKU-B",
qty
: 2,
unitPrice
: 149.99 }] });
await
workbook
.
writeToFile
("./orders.xlsx");

Use streaming when:

  • You are exporting hundreds of thousands of rows
  • Rows come from a database cursor or paginated API
  • You want to keep Node.js heap usage flat regardless of dataset size

API shape differences

createWorkbook()createWorkbookStream()
Row inputrows: T[] on .table().commit({ rows }) on the table handle
Sheet .table() returnWorkbookSheet (chainable sync)Promise<WorkbookTableStream>
Finalize.toBuffer() / .toUint8Array().writeToFile() / .pipeToNode() etc.
Summary rowsYesYes (reducers accumulate across commits)
Excel table modeYesYes
Column groupsYesYes
Formula columnsYesYes

Memory model

The buffered builder holds a PlannerResult for every table in memory until an output method is called. For a table with 100k rows and 20 columns, this is roughly the row data plus per-cell metadata — typically several hundred MB.

The stream builder writes each committed batch to a spool file and discards the in-memory representation. Peak memory is proportional to the largest single .commit() batch, not the total row count. A 1M-row export committed in batches of 1000 rows uses the same peak memory as a 1000-row export.

Summary reducers (init / step / finalize) work identically in both builders — step() is called once per row in each batch, accumulating into a tiny in-memory state object regardless of total row count.

Copyright © 2026 Cyprien Thao. Released under the MIT License.