Typed-xlsx
Streaming

Output Targets

All five finalization methods — file, Node writable, Web writable, Node readable, Web readable.

The stream builder supports five finalization methods, all mutually exclusive — only one output can be started per workbook instance.

Write to file

await 
workbook
.
writeToFile
("./output/report.xlsx");

The file is created (or overwritten) at the given path. Intermediate directories must exist.

Pipe to a Node.js writable

const 
dest
=
createWriteStream
("./report.xlsx");
await
workbook
.
pipeToNode
(
dest
);

Any NodeJS.WritableStream is accepted. The stream is ended automatically when the workbook finishes.

Express response:

import { 
createExcelSchema
,
createWorkbookStream
} from "@chronicstone/typed-xlsx";
import type {
Request
,
Response
} from "express";
const
schema
=
createExcelSchema
<{
name
: string;
amount
: number }>()
.
column
("name", {
accessor
: "name" })
.
column
("amount", {
accessor
: "amount",
style
: {
numFmt
: "#,##0.00" } })
.
build
();
export async function
downloadReport
(
req
:
Request
,
res
:
Response
) {
res
.setHeader(
"Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", );
res
.setHeader("Content-Disposition", 'attachment; filename="report.xlsx"');
const
workbook
=
createWorkbookStream
({
tempStorage
: "file" });
const
table
= await
workbook
.
sheet
("Report").
table
({
id
: "data",
schema
});
for await (const
batch
of fetchRows()) {
await
table
.
commit
({
rows
:
batch
});
} await
workbook
.
pipeToNode
(
res
);
}

Fastify response:

import { 
createExcelSchema
,
createWorkbookStream
} from "@chronicstone/typed-xlsx";
import type {
FastifyReply
} from "fastify";
const
schema
=
createExcelSchema
<{
name
: string }>().
column
("name", {
accessor
: "name" }).
build
();
export async function
handler
(
_req
: unknown,
reply
:
FastifyReply
) {
reply
.header("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
reply
.header("Content-Disposition", 'attachment; filename="report.xlsx"');
const
workbook
=
createWorkbookStream
();
const
table
= await
workbook
.
sheet
("Data").
table
({
id
: "rows",
schema
});
await
table
.
commit
({
rows
: [{
name
: "Ada" }] });
const
stream
=
workbook
.
toNodeReadable
();
return
reply
.send(
stream
);
}

Pipe to a Web Streams writable

const { 
writable
} = new
TransformStream
<
Uint8Array
,
Uint8Array
>();
await
workbook
.
pipeTo
(
writable
);

Hono / Cloudflare Workers response:

import { 
createExcelSchema
,
createWorkbookStream
} from "@chronicstone/typed-xlsx";
const
schema
=
createExcelSchema
<{
name
: string }>().
column
("name", {
accessor
: "name" }).
build
();
export default { async
fetch
(
_req
:
Request
):
Promise
<
Response
> {
const
workbook
=
createWorkbookStream
({
tempStorage
: "memory" });
const
table
= await
workbook
.
sheet
("Data").
table
({
id
: "rows",
schema
});
await
table
.
commit
({
rows
: [{
name
: "Ada" }] });
const {
readable
,
writable
} = new
TransformStream
<
Uint8Array
>();
workbook
.
pipeTo
(
writable
); // don't await — starts async
return new
Response
(
readable
, {
headers
: {
"Content-Type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "Content-Disposition": 'attachment; filename="report.xlsx"', }, }); }, };

Get a Node.js Readable

const 
readable
=
workbook
.
toNodeReadable
();
// pipe to any further transform or upload
readable
.
pipe
(
process
.
stdout
);

Get a Web ReadableStream

const 
readable
=
workbook
.
toReadableStream
();

One output per workbook

Calling any finalization method a second time throws an error.

await 
workbook
.
writeToFile
("./a.xlsx");
// Calling again would throw: "Workbook stream output has already started."

If you need the same data in two places, finalize once and copy the file, or pipe through a PassThrough/tee stream.

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