archiver, compressing, tar, tar-stream, and zip-stream are essential Node.js utilities for handling file compression and archiving, a common requirement in frontend build pipelines for artifact storage, deployment bundling, and asset optimization. archiver acts as a high-level streaming interface that supports multiple formats (ZIP, TAR) and simplifies appending files from various sources. compressing offers a promise-based, high-level API specifically optimized for GZIP, TAR, and ZIP operations with a focus on ease of use. The tar package provides a robust, low-level implementation of the TAR format strictly adhering to POSIX standards, ideal for Unix-like environments. tar-stream focuses on creating and extracting TAR archives as pure streams without temporary files, offering fine-grained control. Finally, zip-stream serves as the underlying engine for ZIP creation within the archiver ecosystem but can be used standalone for custom ZIP streaming needs.
In modern frontend engineering, managing build artifacts is just as critical as writing the code itself. Whether you are bundling assets for deployment, caching dependencies, or distributing libraries, you need reliable tools to compress and archive files. The Node.js ecosystem offers several packages for this: archiver, compressing, tar, tar-stream, and zip-stream. While they all handle compression, they differ significantly in their APIs, streaming models, and intended use cases.
The most immediate difference lies in how these libraries expose their functionality. Frontend developers often prefer async/await for readability, but high-performance build tools frequently rely on Node.js streams to handle large files without bloating memory.
compressing stands out by offering a modern, Promise-based API. It abstracts away the complexity of streams, making it feel like a standard async function call. This is excellent for simple scripts but can be limiting if you need to pipe data through complex transformation chains.
// compressing: Promise-based API
const compressing = require('compressing');
async function createZip() {
await compressing.zip.compressDir('./dist', './archive.zip');
console.log('Compression complete');
}
archiver, tar, tar-stream, and zip-stream all rely on the Node.js Stream API. You must explicitly pipe data or listen to events. This approach is more verbose but provides superior control over backpressure and memory usage during large operations.
// archiver: Stream-based API
const archiver = require('archiver');
const output = require('fs').createWriteStream('./archive.zip');
const archive = archiver('zip', { zlib: { level: 9 } });
output.on('close', () => console.log('Archiver finalized'));
archive.pipe(output);
archive.directory('./dist', false);
archive.finalize();
// tar: Stream-based API (POSIX focused)
const tar = require('tar');
// Creating a tarball
tar.create({
gzip: true,
file: 'archive.tar.gz',
cwd: './dist'
}, ['.']).then(() => console.log('Tar complete'));
Not all archives are created equal. Your choice often depends on whether you need the universal compatibility of ZIP or the Unix-native efficiency of TAR.
archiver is the most versatile, supporting both ZIP and TAR formats through a unified interface. You can switch formats by changing a single string argument, making it easy to support multiple deployment targets without rewriting logic.
// archiver: Switching formats easily
const zipArchive = archiver('zip', { zlib: { level: 9 } });
const tarArchive = archiver('tar', { gzip: true });
// Both use the same .directory() and .file() methods
zipArchive.directory('./dist', false);
tarArchive.directory('./dist', false);
compressing also supports multiple formats (gzip, tar, zip, tar.gz) but treats them as separate modules within the package. This keeps the API clean but requires importing specific sub-modules.
// compressing: Specific modules for formats
const compressing = require('compressing');
// GZIP
await compressing.gzip.compressFile('input.txt', 'input.txt.gz');
// TAR.GZ
await compressing.tar.gz.compressDir('./dist', 'archive.tar.gz');
tar and tar-stream are strictly for the TAR format. They do not support ZIP. If your pipeline requires ZIP specifically (e.g., for Windows compatibility or browser download conventions), these packages are not suitable unless you combine them with a separate GZIP stream.
// tar-stream: Pure TAR streaming
const tarStream = require('tar-stream');
const pack = tarStream.pack();
pack.entry({ name: 'index.js', size: 1024 }, bufferData);
pack.finalize();
zip-stream is dedicated solely to ZIP generation. It is often used internally by archiver but can be used directly if you need a lightweight, dependency-minimal ZIP creator.
// zip-stream: Direct ZIP streaming
const ZipStream = require('zip-stream');
const output = require('fs').createWriteStream('out.zip');
const zip = new ZipStream();
zip.pipe(output);
zip.entry(bufferData, { name: 'index.js' });
zip.finish();
In frontend builds, you often mix static files, generated buffers from bundlers (like Webpack or Vite), and entire directories. How each package handles these sources varies.
archiver excels here with high-level helpers like .directory(), .file(), and .append(). It can seamlessly mix a physical directory with an in-memory buffer in the same archive.
// archiver: Mixing sources
archive.directory('./dist/assets', 'assets');
archive.append(stringBuffer, { name: 'manifest.json' });
archive.file('./config.json', { name: 'settings/config.json' });
compressing focuses on bulk operations. It is great for compressing a whole directory or a single file but less flexible if you need to construct an archive from mixed sources (e.g., adding a virtual file alongside a real folder) without writing temporary files to disk first.
// compressing: Bulk directory compression
await compressing.zip.compressDir('./dist', 'archive.zip');
// Adding a single buffer requires more manual stream handling or temp files
tar-stream gives you the most control but requires the most code. You must manually define every entry, including its size and mode. This is powerful for generating archives from pure memory streams but tedious for simple file copying.
// tar-stream: Manual entry definition
const entry = pack.entry({ name: 'virtual-file.txt', size: buffer.length });
entry.end(buffer);
When deploying to Linux servers or containers, preserving file permissions (chmod) and symlinks is crucial. Losing this metadata can break executables or build scripts.
tar is the industry standard for preserving POSIX metadata. It handles symlinks, hard links, and user/group IDs robustly. If your artifact is destined for a Unix environment, tar is the safest bet.
// tar: Preserving permissions automatically
tar.create({
file: 'backup.tar',
preservePaths: true, // Keeps directory structure
gzip: true
}, ['./src']);
archiver attempts to preserve permissions when creating TAR files, but its ZIP implementation has limitations due to the ZIP format's different permission model. It works well for general use but may require manual attribute setting for edge cases.
// archiver: Setting custom permissions
archive.append(fileStream, {
name: 'script.sh',
mode: 0o755 // Executable permission
});
compressing abstracts much of this away. While convenient, it offers less granular control over specific metadata fields compared to the raw tar package.
Build pipelines must fail loudly and clearly when archiving fails.
tar and tar-stream integrate tightly with Node.js error streams. If a file is missing or permissions deny access, the stream emits an 'error' event that must be caught to prevent unhandled rejections in async contexts.
// tar-stream: Explicit error handling
pack.on('error', (err) => {
throw err; // Must handle explicitly
});
compressing wraps these errors in Promises, making them easier to handle with standard try/catch blocks, which aligns better with typical frontend developer patterns.
// compressing: Try/Catch pattern
try {
await compressing.zip.compressDir('./missing-folder', 'out.zip');
} catch (err) {
console.error('Compression failed:', err.message);
}
| Feature | archiver | compressing | tar | tar-stream | zip-stream |
|---|---|---|---|---|---|
| Primary API | Streams | Promises | Promises/Streams | Streams | Streams |
| Formats | ZIP, TAR | GZIP, TAR, ZIP | TAR (optionally GZ) | TAR | ZIP |
| Memory Usage | Low (Streaming) | Medium | Low | Very Low | Low |
| Metadata Support | Good | Basic | Excellent (POSIX) | Excellent | Basic |
| Ease of Use | High | Very High | Medium | Low | Low |
| Best For | Multi-format pipelines | Quick scripts | Unix deployments | Custom stream logic | Custom ZIP tools |
For most frontend build architectures, archiver strikes the best balance. Its ability to handle both ZIP and TAR with a consistent, stream-based API makes it ideal for complex pipelines where you might need to append generated buffers alongside static directories. It scales well from small projects to enterprise monorepos.
If you are writing simple deployment scripts or utility tools where developer velocity is key and you don't need complex stream manipulation, compressing is the most ergonomic choice thanks to its Promise-based interface.
Reserve tar for backend-heavy tooling where strict POSIX compliance and symlink preservation are non-negotiable. Use tar-stream or zip-stream only if you are building highly specialized tooling that requires direct manipulation of archive bytes without higher-level abstractions.
Choose archiver when you need a flexible, streaming solution that supports multiple archive formats (ZIP and TAR) within a single codebase. It is ideal for build scripts where you need to append files from diverse sources (buffers, streams, disk paths) into an archive on the fly without loading everything into memory.
Choose compressing if your team prefers modern async/await syntax over callback-based streams and needs a straightforward API for standard compression tasks like gzipping assets or creating TAR/ZIP bundles. It is best suited for utility scripts where developer speed and readability are more critical than low-level stream manipulation.
Choose tar when you require strict POSIX compliance and robust handling of Unix file permissions, symlinks, and metadata in a server-side or CI/CD environment. It is the go-to choice for backend tooling that interacts heavily with Linux-based deployment targets where archive integrity is paramount.
Choose tar-stream when you need to construct or parse TAR archives entirely in memory or via pipes without touching the filesystem, such as in serverless functions or browser-like environments using Node.js shims. It provides the most granular control over the stream entries for custom packaging logic.
Choose zip-stream only if you are building a custom archiving tool that specifically requires the ZIP format and need direct access to the compression stream without the extra abstractions found in archiver. For most general use cases, archiver is preferred as it wraps this functionality with a more ergonomic API.
A streaming interface for archive generation
Visit the API documentation for a list of all methods available.
npm install archiver --save
import fs from "fs";
import { ZipArchive } from "archiver";
// create a file to stream archive data to.
const output = fs.createWriteStream(__dirname + "/example.zip");
const archive = new ZipArchive({
zlib: { level: 9 }, // Sets the compression level.
});
// listen for all archive data to be written
// 'close' event is fired only when a file descriptor is involved
output.on("close", function () {
console.log(archive.pointer() + " total bytes");
console.log(
"archiver has been finalized and the output file descriptor has closed.",
);
});
// This event is fired when the data source is drained no matter what was the data source.
// It is not part of this library but rather from the NodeJS Stream API.
// @see: https://nodejs.org/api/stream.html#stream_event_end
output.on("end", function () {
console.log("Data has been drained");
});
// good practice to catch warnings (ie stat failures and other non-blocking errors)
archive.on("warning", function (err) {
if (err.code === "ENOENT") {
// log warning
} else {
// throw error
throw err;
}
});
// good practice to catch this error explicitly
archive.on("error", function (err) {
throw err;
});
// pipe archive data to the file
archive.pipe(output);
// append a file from stream
const file1 = __dirname + "/file1.txt";
archive.append(fs.createReadStream(file1), { name: "file1.txt" });
// append a file from string
archive.append("string cheese!", { name: "file2.txt" });
// append a file from buffer
const buffer3 = Buffer.from("buff it!");
archive.append(buffer3, { name: "file3.txt" });
// append a file
archive.file("file1.txt", { name: "file4.txt" });
// append files from a sub-directory and naming it `new-subdir` within the archive
archive.directory("subdir/", "new-subdir");
// append files from a sub-directory, putting its contents at the root of archive
archive.directory("subdir/", false);
// append files from a glob pattern
archive.glob("file*.txt", { cwd: __dirname });
// finalize the archive (ie we are done appending files but streams have to finish yet)
// 'close', 'end' or 'finish' may be fired right after calling this method so register to them beforehand
archive.finalize();
Archiver ships with out of the box support for TAR and ZIP archives.