mrkeyoor.com_
Sat 08 Aug 20:58 UTC
npmUtilsupdated 08 Aug 2026

node-stream-zip

node-stream-zip is a Node.js ZIP reader and extractor built for archives that should not be loaded into memory all at once. It reads the archive in chunks, indexes entries, can return a small entry as a Buffer or expose it as a readable stream, and can extract one entry, a folder, or the whole archive. It supports ZIP64, self-extracting archives, deflate through Node's built-in zlib, callback and Promise APIs, and ships with TypeScript declarations. It does not create ZIP files and does not support AES-encrypted archives.

Verdict

A practical Node-only reader for large archives, with no dependency or native-build burden and a useful Promise wrapper. Keep 1.16.0 or newer, close every archive, stream large entries, and choose something else for ZIP creation, browsers, or AES encryption.

API stability4/5The callback API has remained compatible through the 1.x line, and the Promise wrapper added in 1.13 sits alongside it rather than replacing it. The public options and declarations clearly separate the two styles. There are a few historical sharp edges: deflate64 was removed in 1.11, errors changed from thrown strings to Error objects in 1.13, and advanced capabilities do not have matching methods on both surfaces.
Docs4/5The README provides runnable examples for opening, listing, buffering, streaming, extracting one file, extracting folders, extracting everything, and using events in both Promise and callback styles. It also names the AES exclusion, filename issue, validation option, and close requirement. It falls short on defensive size limits, error handling for the async wrapper, import variations in TypeScript, CRC expectations, and the exact behavior of less common options such as chunkSize.
Maintenance4/5Version 1.16.0 was published on July 22, 2026 and the repository was pushed again two days later. That release fixed compressed-size reading after responsible disclosure of a crafted-archive memory-overflow risk, showing that serious reports are acted on. The repository is not archived and reports 18 open issues and pull requests, though the previous feature release was in 2021 and the project remains a compact maintainer-led codebase.
Ecosystem4/5The npm last-week endpoint recorded 5,124,982 downloads, and the package supports the Node primitives applications already use: readable streams, Buffer, file descriptors, fs, and zlib. It has no runtime dependencies or native addons and includes declarations. Its reach is narrower than general archive toolkits because it reads only ZIP, runs only on Node, does not write archives, and intentionally excludes AES encryption.

Use it if

  • You need to inspect or extract large ZIP and ZIP64 files without buffering the entire archive
  • You want a dependency-free Node implementation with no native addon or external unzip binary
  • You need either a Promise API for new code or the older event and callback API for entry-by-entry processing
  • You want entry-name validation against paths such as `../` and Windows absolute paths enabled by default
Skip it if

Setup reality

`npm i node-stream-zip` installs a CommonJS package with no runtime dependencies, peer dependencies, native compilation, credentials, command-line binary, or config file. It uses Node's built-in `fs`, streams, Buffer, and zlib, so it is server-side only. New code should construct `new StreamZip.async({ file: 'archive.zip' })`; the unusual lowercase `.async` is a class exposed as a static property, not an async factory. Opening still has work to do: await `entries()`, `entriesCount`, `entryData()`, `stream()`, or another method before assuming the central directory is ready. Always call `await zip.close()` in a `finally` block after every read or extraction, because the archive file descriptor stays open otherwise. `entryData()` returns a full Buffer for that entry, so the archive can be huge while one huge entry still exhausts memory; use `stream()` and enforce compressed and uncompressed size limits for untrusted input. Extraction creates nested directories, but the README examples create the top-level output directory first. Entry names are checked by default for malicious paths such as `../` and `c:\123`; never enable `skipEntryNameValidation` for user-controlled archives. Version 1.16.0 fixed compressed-size reading after a reported memory-overflow path involving specially crafted ZIPs, so do not pin an older release when handling uploads. AES encryption is explicitly unsupported. Filename decoding defaults to UTF-8 and `nameEncoding` is available for known legacy encodings, but the README still lists non-ASCII filenames as a known issue. Types are included in `node_stream_zip.d.ts`, although they use `export =`, so TypeScript import syntax depends on `esModuleInterop`. The package claims compatibility as far back as Node 0.12, but production users should judge runtime support by their own maintained Node line rather than treat that permissive engine declaration as a recommendation.

Patterns

Open an archive and list its entriesopen-and-list

const StreamZip = require('node-stream-zip');
const zip = new StreamZip.async({ file: 'archive.zip' });

try {
  const entries = await zip.entries();
  for (const entry of Object.values(entries)) {
    console.log(entry.name, entry.isDirectory ? 'directory' : entry.size);
  }
} finally {
  await zip.close();
}

`entries()` stores and returns the complete entry map; for extremely large entry counts, consume the `entry` event instead.

Read one small entry into a Bufferread-entry-buffer

const zip = new StreamZip.async({ file: 'archive.zip' });
try {
  const data = await zip.entryData('docs/readme.txt');
  console.log(data.toString('utf8'));
} finally {
  await zip.close();
}

The archive is chunked, but `entryData()` still buffers the entire selected entry; do not use it for an unbounded large file.

Stream an entry to a writable destinationstream-entry

const { pipeline } = require('node:stream/promises');
const { createWriteStream } = require('node:fs');
const zip = new StreamZip.async({ file: 'archive.zip' });

try {
  const input = await zip.stream('video/demo.mp4');
  await pipeline(input, createWriteStream('demo.mp4'));
} finally {
  await zip.close();
}

Wait for the pipeline before closing the archive; closing early cancels pending reads and can truncate the output.

Extract one entry to a chosen fileextract-one-entry

const zip = new StreamZip.async({ file: 'archive.zip' });
try {
  await zip.extract('docs/readme.txt', './output/readme.txt');
} finally {
  await zip.close();
}

Create or verify the top-level output directory first, and keep default entry-name validation enabled for untrusted archives.

Extract a folder from inside the archiveextract-folder

const { mkdir } = require('node:fs/promises');
await mkdir('./output', { recursive: true });

const zip = new StreamZip.async({ file: 'archive.zip' });
try {
  await zip.extract('assets/images/', './output/images');
} finally {
  await zip.close();
}

Use the archive's exact folder prefix, including its slash, rather than assuming directory entries exist in every ZIP producer.

Extract every entryextract-entire-archive

const { mkdir } = require('node:fs/promises');
await mkdir('./unpacked', { recursive: true });

const zip = new StreamZip.async({ file: 'archive.zip' });
try {
  const count = await zip.extract(null, './unpacked');
  console.log(`Extracted ${count} entries`);
} finally {
  await zip.close();
}

Passing null selects the whole archive. Do not set `skipEntryNameValidation` when the ZIP came from another user.

Reject an oversized entry before buffering itinspect-before-reading

const zip = new StreamZip.async({ file: 'upload.zip' });
try {
  const entry = await zip.entry('payload.json');
  if (!entry) throw new Error('payload.json is missing');
  if (entry.size > 10 * 1024 * 1024) throw new Error('payload.json is too large');
  const data = await zip.entryData(entry);
} finally {
  await zip.close();
}

Metadata checks reduce accidental memory use, but hostile-archive handling should also cap total extracted bytes, entry count, and compression ratios.

Observe entries while the directory is readprocess-entry-events

const zip = new StreamZip.async({ file: 'large.zip', storeEntries: false });
zip.on('entry', (entry) => {
  console.log(entry.name, entry.compressedSize, entry.size);
});

try {
  await zip.entriesCount;
} finally {
  await zip.close();
}

With `storeEntries: false`, later name-based lookup is unavailable; the entry event is the intended low-memory access path.

Decode names from a known legacy encodingset-filename-encoding

const zip = new StreamZip.async({
  file: 'legacy.zip',
  nameEncoding: 'latin1',
});

try {
  console.log(Object.keys(await zip.entries()));
} finally {
  await zip.close();
}

UTF-8 is the default. Set another encoding only when the archive producer's filename encoding is known.

Read from an existing file descriptoropen-file-descriptor

const fs = require('node:fs');
const fd = fs.openSync('archive.zip', 'r');
const zip = new StreamZip.async({ fd });

try {
  console.log(await zip.entriesCount);
} finally {
  await zip.close();
}

The option has accepted a file descriptor since 1.12. Coordinate ownership carefully so surrounding code does not close it during reads.

Use the included TypeScript entry typesuse-typescript

import StreamZip = require('node-stream-zip');

const zip = new StreamZip.async({ file: 'archive.zip' });
try {
  const entry: StreamZip.ZipEntry | undefined = await zip.entry('data.csv');
  if (entry?.isFile) console.log(entry.size);
} finally {
  await zip.close();
}

The declarations use `export =`; default-import syntax usually requires `esModuleInterop` or `allowSyntheticDefaultImports`.

Use the legacy event and callback APIuse-callback-api

const StreamZip = require('node-stream-zip');
const zip = new StreamZip({ file: 'archive.zip' });

zip.on('error', (error) => {
  console.error(error);
  zip.close();
});
zip.on('ready', () => {
  const entry = zip.entry('notes.txt');
  console.log(entry?.size);
  zip.close();
});

The callback class needs an `error` listener; prefer the Promise wrapper unless you need its extra low-level methods or event timing.

Alternatives

PackageRegistryPick it when
unzippernpmChoose it when a pipeline-oriented streaming API and parse or extract streams fit the surrounding Node code
yauzlnpmChoose it when you want a deliberately low-level, lazy ZIP reader and will build extraction policy yourself
adm-zipnpmChoose it when archives are modest and you also need to create or modify ZIP files through a simpler in-memory API