The archiver, tar, tar-fs, tar-stream, and zip-stream packages provide essential tools for creating and extracting compressed archives in Node.js environments. archiver acts as a high-level wrapper that unifies support for both ZIP and TAR formats, streamlining the process of adding files from various sources. The tar family (tar, tar-fs, tar-stream) focuses exclusively on the TAR format, offering solutions ranging from simple file system operations to low-level stream manipulation. zip-stream serves as the underlying engine for ZIP creation, often used internally by archiver but available for direct use when fine-grained control over ZIP specifics is required. Together, these libraries enable developers to handle backup systems, asset bundling, and data transfer protocols efficiently.
When building backend services, deployment pipelines, or data export features in Node.js, handling compressed archives is a common requirement. Whether you are bundling assets for transfer, creating backups, or distributing software, choosing the right library impacts both code clarity and performance. The ecosystem offers several options: archiver, tar, tar-fs, tar-stream, and zip-stream. While they all deal with compression, they solve different problems and operate at different levels of abstraction.
The most critical decision is how much control you need versus how much convenience you want.
archiver sits at the top of the stack. It is a high-level interface that supports multiple formats (ZIP and TAR). It manages the complexity of streaming data from various sources (files, buffers, strings) into a compressed output. You tell it what to add, and it handles the how.
// archiver: High-level API for ZIP creation
const archiver = require('archiver');
const output = fs.createWriteStream('archive.zip');
const archive = archiver('zip', { zlib: { level: 9 } });
output.on('close', () => console.log('Done'));
archive.pipe(output);
// Easily append different types of data
archive.file('path/to/file.txt', { name: 'renamed.txt' });
archive.append('string content', { name: 'note.txt' });
archive.finalize();
zip-stream is the engine under the hood for ZIP files. It is a lower-level stream that expects you to manage the entry headers and data piping manually. It does not automatically handle file system paths or diverse input types as gracefully as archiver.
// zip-stream: Low-level control for ZIP
const ZipStream = require('zip-stream');
const output = fs.createWriteStream('archive.zip');
const zip = new ZipStream();
zip.pipe(output);
// Must manually define entry options and pipe data
zip.entry(fs.createReadStream('file.txt'), { name: 'file.txt' }, (err) => {
if (err) throw err;
zip.finish();
});
tar, tar-fs, and tar-stream focus exclusively on the TAR format. tar provides a balanced API for creating and extracting tarballs. tar-fs simplifies file system interactions specifically for TAR, while tar-stream offers a raw, event-driven approach for constructing archives from scratch.
// tar: Balanced API for TAR creation
const tar = require('tar');
// Create a tarball from a list of files
tar.create(
{
gzip: true,
file: 'archive.tar.gz'
},
['file1.txt', 'file2.txt']
);
Real-world applications rarely archive just a single folder. You often need to mix database exports (strings/buffers) with static assets (files).
archiver excels here. Its append() method accepts buffers, streams, and strings seamlessly. You don't need to convert data types before adding them to the archive.
// archiver: Mixed source types
const archive = archiver('zip');
// Append a buffer directly
const buffer = Buffer.from('Dynamic content');
archive.append(buffer, { name: 'dynamic.txt' });
// Append a readable stream
archive.append(fs.createReadStream('large-file.log'), { name: 'logs/large-file.log' });
tar-stream requires you to manually initiate entries and pipe data. This gives you precise control over when an entry starts and ends, which is useful for generating data on the fly, but it requires more boilerplate.
// tar-stream: Manual entry management
const tarStream = require('tar-stream');
const pack = tarStream.pack();
pack.entry({ name: 'dynamic.txt', size: 15 }, (err, stream) => {
if (err) throw err;
stream.end('Dynamic content');
});
pack.entry({ name: 'file.txt', size: fs.statSync('file.txt').size }, (err, stream) => {
fs.createReadStream('file.txt').pipe(stream);
});
pack.finalize();
tar-fs is optimized for packing directories. It recursively reads folders and handles permissions automatically, but it is less flexible if you need to inject non-file data into the stream without writing to disk first.
// tar-fs: Directory packing
const tarFs = require('tar-fs');
// Pack an entire directory recursively
tarFs.pack('./my-directory', { gzip: true })
.pipe(fs.createWriteStream('backup.tar.gz'));
Reading archives is just as important as creating them. The approach varies significantly between high-level helpers and low-level parsers.
tar provides a straightforward extract() method that handles decompression and file writing automatically. It is the most convenient way to unpack tarballs.
// tar: Simple extraction
tar.extract({
file: 'archive.tar.gz',
cwd: './restored-files'
});
tar-stream treats extraction as a stream of entries. You listen for the entry event, process the data (perhaps filtering specific files or reading content into memory), and then call next() to proceed. This is powerful for processing large archives without saving them to disk.
// tar-stream: Streaming extraction
const extract = tarStream.extract();
extract.on('entry', (header, stream, next) => {
console.log(`Processing: ${header.name}`);
// Pipe to somewhere or consume data
stream.on('end', () => next());
stream.pipe(fs.createWriteStream(header.name));
});
fs.createReadStream('archive.tar').pipe(extract);
archiver is primarily designed for creating archives. It does not provide built-in extraction capabilities. For unzipping, developers typically pair archiver with a dedicated unzip library or use Node.js built-in zlib alongside stream manipulation, whereas tar handles both creation and extraction natively.
The choice often comes down to the required file format: ZIP or TAR.
archiver is the dominant choice here because it wraps zip-stream and adds necessary quality-of-life features. zip-stream alone is viable but verbose.tar package family is the gold standard here.// archiver: Supporting both formats with one API
// Create a ZIP
const zipArchive = archiver('zip');
zipArchive.pipe(fs.createWriteStream('app.zip'));
// Create a TAR
const tarArchive = archiver('tar');
tarArchive.pipe(fs.createWriteStream('app.tar'));
// Same API, different format
files.forEach(file => zipArchive.file(file));
files.forEach(file => tarArchive.file(file));
As of the latest checks, all five packages (archiver, tar, tar-fs, tar-stream, zip-stream) are actively maintained and not deprecated. They are stable dependencies found in many production systems. However, tar-fs and tar-stream are more specialized; if you do not need their specific stream-control features, the main tar package is generally recommended for better long-term support and broader community usage.
| Feature | archiver | tar | tar-fs | tar-stream | zip-stream |
|---|---|---|---|---|---|
| Primary Format | ZIP & TAR | TAR | TAR | TAR | ZIP |
| Abstraction | High | Medium | High (FS specific) | Low | Low |
| Input Types | Files, Buffers, Streams | Files, Paths | Directories/Paths | Streams/Buffers | Streams |
| Extraction | No (Creation only) | Yes | Yes | Yes (Streaming) | No (Creation only) |
| Best Use Case | General purpose archiving | Standard TAR operations | Recursive FS backups | Custom stream pipelines | Custom ZIP generation |
For most application developers, archiver is the best starting point. It removes the friction of handling streams manually and supports both major formats with a consistent API. Use it when you need to generate reports, bundle uploads, or create backups that might need to be opened by end users.
If you are building infrastructure tools, CI/CD pipelines, or working strictly in a Linux environment where TAR is the norm, the tar package family is superior. Use tar for standard operations, tar-fs if you are simply mirroring directories, and tar-stream if you need to construct archives dynamically from non-file sources.
Reserve zip-stream for cases where you are building a custom archiving library yourself and need to avoid the extra dependencies or abstractions of archiver. In 90% of cases, archiver will save you time and reduce bugs.
Choose archiver when you need a unified, high-level API to create both ZIP and TAR archives without worrying about the underlying stream mechanics. It is ideal for general-purpose archiving tasks where you need to append files from mixed sources (strings, buffers, streams, or file paths) with minimal boilerplate. This package is the best starting point for most backend services requiring reliable compression.
Choose tar if your project strictly requires TAR format support and you prefer a balance between ease of use and performance without external dependencies. It is well-suited for extracting or creating tarballs directly from the file system or buffers in modern Node.js environments. Use this when you need a robust, maintained solution that handles POSIX tar standards efficiently.
Choose tar-fs when your primary goal is to pack or unpack entire directories from the file system with minimal code. It simplifies the interaction between the file system and TAR streams, making it perfect for backup scripts or deployment tools that operate directly on disk paths. Avoid this if you need to manipulate in-memory buffers or complex stream pipelines without file system involvement.
Choose tar-stream if you need low-level control to construct or parse TAR archives using custom data sources that aren't necessarily files. It allows you to manually define headers and pipe raw data streams, which is essential for creating dynamic archives in memory or handling non-standard entry types. This is the go-to choice for advanced use cases where tar-fs is too abstract.
Choose zip-stream only if you specifically need to generate ZIP archives and require direct control over the compression stream without the extra features of archiver. It is suitable for scenarios where you are building a custom archiving pipeline and need to manage ZIP headers and data chunks manually. For most applications, archiver is preferred as it wraps this functionality with a simpler interface.
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.