yazl
yazl is a focused Node.js ZIP writer built around streams. You create a ZipFile, add filesystem paths, Buffers, lazy read streams, or empty directories, call end, and pipe its outputStream somewhere. It compresses with Node's zlib, calculates CRC values, validates archive paths against absolute and parent traversal forms, and turns on ZIP64 automatically when classic ZIP limits are exceeded. It creates archives only; extraction is handled by the companion yauzl package.
yazl is a strong low-level ZIP writer for Node services that care about backpressure, bounded file descriptors, and predictable archive structure. Choose a higher-level package if you need directory discovery, several archive formats, encryption, or a Promise-shaped one-call workflow.
Use it if
- You need to stream a ZIP response or file without buffering every input or the finished archive in memory
- You are packaging many filesystem files and want the library to keep only a constant number of input file descriptors open
- You need ZIP64 to be selected automatically for large files, large archives, or more than 65,534 entries
- You want a small, Node-only ZIP writer and are comfortable composing streams and callbacks
- You need to extract, inspect, append to, or edit an existing archive: yazl only writes new ZIP files, and its README points extraction users to yauzl
- You need password or AES encryption: it is not supported, and an encryption request has remained open since 2020
- You want a Promise-first API, directory recursion, globbing, progress reporting, or archive format choices; yazl deliberately exposes lower-level entries and streams
- Your recipients depend on older ZIP readers: ZIP64 is necessary beyond classic limits, but the README warns that macOS Archive Utility does not understand ZIP64, and old 7-Zip has a data-descriptor bug
- You need bundled TypeScript declarations or ESM: 3.3.1 publishes one CommonJS file with no types, so TypeScript users depend on the separate @types/yazl package
Setup reality
npm install yazl installs one runtime dependency, buffer-crc32. There is no native build, account, credential, binary, or config file. The package is CommonJS and Node-only. TypeScript users need @types/yazl because the npm tarball contains only index.js. The main surprise is that creating a correct archive is a stream lifecycle, not one awaited function. Attach error handlers, connect outputStream to its destination, add every entry, call zipfile.end(), and wait for the destination or pipeline to finish. Forgetting end leaves an incomplete zero-byte-looking archive. addFile runs fs.stat immediately but opens the file later; a file changed or removed between those moments can fail during output. For dynamic streams, prefer addReadStreamLazy so sockets and descriptors are acquired only when yazl is ready. Provide size when known if you need calculatedTotalSizeCallback, and disable compression for every entry when an exact total must be available before processing. yazl validates metadata paths, converts backslashes to slashes, and rejects blank, absolute, drive-letter, parent-segment, or trailing-slash file names. It does not walk directories, so your code must enumerate them and add empty directories deliberately. Compression defaults to DEFLATE level 6. Already compressed media may be faster and sometimes smaller with compression disabled. Version 3.3 adds a UTC Info-ZIP timestamp field; set explicit mtime and mode for reproducible output, and use forceDosTimestamp only for consumers that reject the newer field. ZIP64 is automatic when required, but recipient compatibility is your responsibility. Archive comments use CP437 by default and are safest when limited to printable ASCII. There is no encryption, signing, atomic file replacement, cancellation API, or built-in progress event.
Patterns
Create a ZIP from filesystem fileszip-files
const fs = require('node:fs');
const yazl = require('yazl');
const zip = new yazl.ZipFile();
zip.addFile('reports/january.csv', 'reports/january.csv');
zip.addFile('reports/february.csv', 'reports/february.csv');
zip.outputStream.pipe(fs.createWriteStream('reports.zip'));
zip.end();addFile accepts files only, not directories. The metadata path is the name stored inside the archive.
Wait for the archive with stream pipelineawait-zip-completion
const fs = require('node:fs');
const { pipeline } = require('node:stream/promises');
const yazl = require('yazl');
const zip = new yazl.ZipFile();
const done = pipeline(zip.outputStream, fs.createWriteStream('out.zip'));
zip.addFile('input.txt', 'input.txt');
zip.end();
await done;Await the destination pipeline, not end(). end signals that no more entries are coming but does not return a Promise.
Add generated content from a Bufferadd-buffer
const zip = new yazl.ZipFile();
zip.addBuffer(
Buffer.from(JSON.stringify(manifest, null, 2)),
'manifest.json',
{ mtime: new Date('2024-01-01T00:00:00Z') }
);addBuffer retains the content in memory. Use a lazy read stream for large generated entries.
Open a source stream only when neededadd-lazy-stream
zip.addReadStreamLazy(
'exports/orders.ndjson',
{ size: expectedBytes },
(callback) => {
callback(null, createOrdersStream());
}
);The callback may run later. If stream creation fails, pass the error as the first callback argument so the ZipFile emits it.
Stream a ZIP to an HTTP responsestream-http-response
res.setHeader('Content-Type', 'application/zip');
res.setHeader('Content-Disposition', 'attachment; filename=export.zip');
const zip = new yazl.ZipFile();
zip.outputStream.pipe(res);
zip.addBuffer(Buffer.from(csv), 'orders.csv');
zip.end();Handle errors and client disconnects in the surrounding server. Once response bytes are sent, an archive failure cannot become a clean JSON error.
Preserve an empty directoryadd-empty-directory
zip.addEmptyDirectory('uploads/', {
mtime: new Date('2024-01-01T00:00:00Z'),
mode: 0o40775,
});Parent directories for normal files are implied. Add a directory entry only when an empty directory must survive extraction.
Add a directory tree recursivelywalk-directory
const fs = require('node:fs/promises');
const path = require('node:path');
async function addTree(zip, root, dir = root) {
for (const entry of await fs.readdir(dir, { withFileTypes: true })) {
const real = path.join(dir, entry.name);
const stored = path.relative(root, real).split(path.sep).join('/');
if (entry.isDirectory()) await addTree(zip, root, real);
else if (entry.isFile()) zip.addFile(real, stored);
}
}
await addTree(zip, '/srv/export');This skips symlinks and empty directories intentionally. Decide how both should behave before using recursion on user-controlled trees.
Set stable metadata for reproducible outputmake-reproducible-zip
const stable = {
mtime: new Date('2024-01-01T00:00:00Z'),
mode: 0o100644,
};
zip.addFile('build/app.js', 'app.js', stable);
zip.addBuffer(Buffer.from(version), 'VERSION', stable);Stable timestamps and modes remove common sources of byte differences. Input order and content must also be deterministic.
Store already compressed filesskip-useless-compression
zip.addFile('video.mp4', 'media/video.mp4', {
compress: false,
});JPEG, PNG, MP4, and existing archives often gain little from DEFLATE. Storing them reduces CPU use.
Choose a zlib compression leveltune-compression-level
zip.addFile('database.sql', 'backup/database.sql', {
compressionLevel: 9,
});The default is 6. Level 0 is equivalent to compress: false, and contradictory compress and compressionLevel options are rejected.
Calculate a known archive sizecalculate-content-length
zip.addReadStreamLazy(
'payload.bin',
{ size: payloadSize, compress: false },
(cb) => cb(null, openPayload())
);
zip.end({}, (totalSize) => {
if (totalSize !== -1) res.setHeader('Content-Length', totalSize);
zip.outputStream.pipe(res);
});A guaranteed size generally requires compression disabled and a size for every stream entry. The callback can return -1 when the total cannot be predicted.
Force ZIP64 for compatibility testingforce-zip64
zip.addFile('sample.bin', 'sample.bin', {
forceZip64Format: true,
});
zip.end({ forceZip64Format: true });Normal applications should let yazl enable ZIP64 only when required. Some archive readers, including the macOS utility named in the README, mishandle ZIP64.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| archiver | npm | Use it when directory recursion, globs, progress events, and TAR support are worth a larger abstraction. |
| jszip | npm | Use it for browser compatibility or an in-memory object API when archives are small enough to buffer. |
| zip-stream | npm | Use it for a similarly stream-oriented writer when sequential entry callbacks fit your pipeline. |
| @types/yazl | npm | Add it alongside yazl when TypeScript declarations are the only missing piece and you want to keep yazl's runtime. |