@zip.js/zip.js
@zip.js/zip.js is a ZIP archive reader and writer for browsers, Node.js, Deno, Bun, and React Native. It can stream large inputs instead of holding every file in memory, use workers or native compression, create Zip64 and split archives, read Deflate64, and encrypt entries with AES. The API is built around paired Reader and Writer adapters, so a Blob, string, Uint8Array, HTTP resource, or Web Stream can move through the same archive machinery without a separate package for each runtime.
The strongest general-purpose ZIP choice for modern JavaScript when streaming, workers, encryption, or large archives are real requirements. For tiny in-memory browser archives, JSZip remains easier, and no ZIP library removes the need to defend extraction paths and resource limits yourself.
Use it if
- You need to create or inspect ZIP files in a browser without sending user files to a server
- You handle archives large enough to need streaming, Zip64, split files, or concurrent compression
- One codebase must work across modern browsers, Node 18+, Deno, Bun, or React Native
- You need AES-encrypted ZIP entries, Deflate64 decompression, progress callbacks, or cancellation
- You only need to gzip one response body: the platform CompressionStream API or Node zlib has a much smaller API surface
- Your archive inputs are untrusted and you expect safe extraction by default: strict header checks, overlap checks, output size limits, and path containment are application responsibilities, while several reader safety checks default to false or balanced
- You want the simplest in-memory browser API: JSZip's object model is easier to teach when files are small and advanced streaming features do not matter
- You support Node older than 18: version 2.8.36 declares Node 18 as the minimum engine
- Your deployment has a restrictive Content Security Policy or unusual worker asset pipeline: worker and WASM URLs may need explicit configuration, and the smaller core entry points deliberately do not embed those assets
Setup reality
Installation is one npm package with no runtime dependencies or peer dependencies, and it ships ESM, CommonJS, and TypeScript declarations. The easy demo uses BlobReader and BlobWriter, but production choices start immediately. Node must be 18 or newer. Browser builds may create Web Workers and use a bundled WASM module; a strict Content Security Policy, CDN deployment, or bundler that relocates assets can require configure() with explicit workerURI and wasmURI values. The smaller lib/zip-core.js entry point omits both assets, which saves code only if you host and configure them yourself. Import named exports because importing the namespace keeps the complete read and write paths. BlobWriter buffers the finished archive in memory, so large files should go through WritableStream adapters or a filesystem-backed temporary stream. Concurrent adds can also create temporary buffers. Always await close(), because it writes the central directory and returns the finished output; an unclosed writer does not produce a valid archive. Entry names must use forward slashes, and backslashes are stored literally rather than normalized. For untrusted archives, inspect names before writing to disk, reject absolute paths and parent traversal, impose your own uncompressed byte and entry-count ceilings, and opt into strictness, signature, ambiguity, and overlap checks where appropriate. Password support does not make filenames private, and compatibility may differ between AES and legacy ZipCrypto readers. React Native uses a separate native export, while Deno documentation uses the JSR package name rather than the npm scope.
Patterns
Create a ZIP containing textcreate-text-archive
import { BlobWriter, TextReader, ZipWriter } from '@zip.js/zip.js';
const writer = new ZipWriter(new BlobWriter('application/zip'));
await writer.add('hello.txt', new TextReader('Hello world'));
const zipBlob = await writer.close();close() writes the central directory and returns the Blob; always await it before using or downloading the archive.
List files without extracting themlist-archive-entries
import { BlobReader, ZipReader } from '@zip.js/zip.js';
const reader = new ZipReader(new BlobReader(file));
try {
const entries = await reader.getEntries();
for (const entry of entries) {
console.log(entry.filename, entry.directory, entry.uncompressedSize);
}
} finally {
await reader.close();
}Listing reads archive metadata, not file contents; still set application limits on entry count and declared sizes before extraction.
Read one entry as textextract-entry-text
import { BlobReader, TextWriter, ZipReader } from '@zip.js/zip.js';
const reader = new ZipReader(new BlobReader(file));
try {
const entries = await reader.getEntries();
const config = entries.find((entry) => entry.filename === 'config.json');
if (!config || config.directory) throw new Error('config.json is missing');
const text = await config.getData(new TextWriter());
console.log(JSON.parse(text));
} finally {
await reader.close();
}TextWriter decodes the complete entry into a string, so stream or cap large entries instead of using this pattern blindly.
Extract an entry to a browser Blobextract-entry-blob
import { BlobReader, BlobWriter, ZipReader } from '@zip.js/zip.js';
const reader = new ZipReader(new BlobReader(file));
try {
const entry = (await reader.getEntries()).find((item) => item.filename === 'photo.png');
if (!entry || entry.directory) throw new Error('photo.png is missing');
const blob = await entry.getData(new BlobWriter('image/png'));
const url = URL.createObjectURL(blob);
image.src = url;
} finally {
await reader.close();
}Revoke the object URL when the preview is no longer needed, and do not trust an archive filename as a MIME type.
Add files selected in a browseradd-browser-files
import { BlobReader, BlobWriter, ZipWriter } from '@zip.js/zip.js';
const writer = new ZipWriter(new BlobWriter('application/zip'));
for (const file of fileInput.files) {
const safeName = file.name.replaceAll('\\', '_').replaceAll('/', '_');
await writer.add(safeName, new BlobReader(file), { lastModDate: file.lastModified ? new Date(file.lastModified) : new Date() });
}
const archive = await writer.close();ZIP entry paths require forward slashes. Flatten user-provided names unless directory structure is intentional and validated.
Write an archive to a Web Streamstream-large-archive
import { BlobReader, ZipWriter } from '@zip.js/zip.js';
const stream = new TransformStream();
const response = new Response(stream.readable, {
headers: { 'Content-Type': 'application/zip' },
});
const writer = new ZipWriter(stream.writable);
const writing = (async () => {
await writer.add('video.mp4', new BlobReader(videoFile));
await writer.close();
})();
await Promise.all([writing, consumeResponse(response)]);The readable side must be consumed while writing or backpressure can stall the writer; BlobWriter is simpler but retains the output in memory.
Track compression and extraction progresstrack-progress
await zipWriter.add('data.bin', sourceReader, {
onprogress: (loaded, total) => {
if (total) progress.value = loaded / total;
},
});
await entry.getData(destinationWriter, {
onprogress: (loaded, total) => {
console.log(`${loaded} of ${total} bytes`);
},
});Treat total as potentially unavailable or zero for streaming sources whose size is not known in advance.
Cancel compression with AbortControllercancel-operation
const controller = new AbortController();
cancelButton.addEventListener('click', () => controller.abort());
try {
await zipWriter.add('large.bin', sourceReader, { signal: controller.signal });
} catch (error) {
if (error.name !== 'AbortError') throw error;
}An aborted add can leave a partially written entry; close or discard the output according to the writer state instead of presenting it as complete.
Create an AES-encrypted entryencrypt-entry-aes
const writer = new ZipWriter(new BlobWriter('application/zip'));
await writer.add('private.txt', new TextReader(secret), {
password,
encryptionStrength: 3,
});
const encryptedZip = await writer.close();Strength 3 selects AES-256. ZIP metadata such as filenames remains visible, and recipients need a ZIP tool that supports AES encryption.
Read a password-protected entrydecrypt-entry
const reader = new ZipReader(new BlobReader(encryptedFile));
try {
const entry = (await reader.getEntries()).find((item) => !item.directory);
if (!entry) throw new Error('archive has no file entry');
const value = await entry.getData(new TextWriter(), {
password,
checkSignature: true,
});
console.log(value);
} finally {
await reader.close();
}A wrong password rejects during extraction. Avoid logging the password or accepting unlimited attempts in a public service.
Apply stricter checks to untrusted entriesinspect-untrusted-archive
const reader = new ZipReader(new BlobReader(upload), { strictness: 'strict' });
try {
for (const entry of await reader.getEntries()) {
const name = entry.filename.replaceAll('\\', '/');
if (name.startsWith('/') || name.split('/').includes('..')) {
throw new Error(`unsafe entry path: ${name}`);
}
if (!entry.directory) {
await entry.getData(destinationFor(name), {
strictness: 'strict',
checkSignature: true,
checkOverlappingEntry: true,
});
}
}
} finally {
await reader.close();
}These checks do not impose an uncompressed-size ceiling or prove that destinationFor stays inside a chosen root; enforce both separately.
Configure worker and WASM asset URLsconfigure-worker-assets
import { configure } from '@zip.js/zip.js';
configure({
workerURI: new URL('./zip-web-worker.js', import.meta.url).href,
wasmURI: new URL('./zip-module.wasm', import.meta.url).href,
maxWorkers: 2,
});Use this when a CDN, CSP, or core-only entry point prevents the default embedded assets from loading; copy the matching distribution files during the build.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| jszip | npm | Small browser archives where a simple in-memory object API matters more than streaming and advanced ZIP features |
| fflate | npm | You want a compact, fast compression library and can work with a lower-level synchronous, asynchronous, or streaming API |
| client-zip | npm | You only need to generate ZIP downloads in modern browsers from streams, blobs, or fetched responses |