mrkeyoor.com_
Sat 08 Aug 22:00 UTC
npmUtilsupdated 08 Aug 2026

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.

Verdict

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.

API stability4/5The ZipFile constructor, addFile, addBuffer, outputStream, and end contract has remained recognizable across years of use. Version 3 made dependency and runtime changes, then 3.1 added addReadStreamLazy without removing the older stream method. The callback API is conservative, though the README marks dateToDosDateTime for removal in 4.0 and notes subtle error-handling changes were not well tested.
Docs5/5The README documents every option, byte and path limit, default compression behavior, timestamp encoding, ZIP64 thresholds, stream timing, calculated-size conditions, file descriptor strategy, and compatibility exception. It also contains a detailed change history. Error handling and complete modern async examples are the notable gaps, reflected by an open documentation issue.
Maintenance4/5GitHub reports a push in March 2026, and 3.3.1 shipped in November 2024 after a concentrated series of 3.x releases fixing lazy-stream timing, compression levels, timestamps, and dependency age. The project is maintained by a small group and has long-lived feature requests, but its narrow mission and recent correctness work make it healthier than release cadence alone suggests.
Ecosystem4/5yazl recorded 3,457,909 downloads in the measured week, has a matching extraction-side sibling in yauzl, community TypeScript declarations, and standard Node stream behavior that composes with HTTP and filesystem destinations. It has far fewer convenience integrations than archiver, and its CommonJS callback surface feels dated in ESM and Promise-first applications.

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
Skip it if

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

PackageRegistryPick it when
archivernpmUse it when directory recursion, globs, progress events, and TAR support are worth a larger abstraction.
jszipnpmUse it for browser compatibility or an in-memory object API when archives are small enough to buffer.
zip-streamnpmUse it for a similarly stream-oriented writer when sequential entry callbacks fit your pipeline.
@types/yazlnpmAdd it alongside yazl when TypeScript declarations are the only missing piece and you want to keep yazl's runtime.