These five packages handle the creation and extraction of tar archives, but they serve different architectural needs. tar is the official, high-performance utility from npm Inc. for standard file system operations. archiver is a streaming interface designed to build zip and tar archives from various data sources, not just the file system. tar-stream provides low-level building blocks to create and parse tar streams manually. tar-fs bridges the gap between tar streams and the file system for easy packing and unpacking. decompress-tar is a plugin specifically for the decompress library to handle tar extraction, often used for simple unzipping tasks.
Handling compressed archives is a common requirement in backend development, from deploying artifacts to generating user downloads. While JavaScript has native fs modules, it lacks built-in tar support. The ecosystem offers five distinct solutions: archiver, decompress-tar, tar, tar-fs, and tar-stream. Each solves the problem with a different level of abstraction and performance profile.
The most important distinction is how much control you need versus how much code you want to write.
tar sits at the foundation. Maintained by npm Inc., it is optimized for speed and correctness. It works directly with the file system and streams but expects you to wire the pipes.
// tar: Direct extraction to disk
const tar = require('tar');
await tar.x({
file: 'archive.tar.gz',
cwd: './destination-folder'
});
tar-fs wraps tar-stream to make file system operations trivial. It turns complex stream logic into two simple function calls: pack and extract.
// tar-fs: Extract with one function
const tarfs = require('tar-fs');
const fs = require('fs');
const readStream = fs.createReadStream('archive.tar');
const extractStream = tarfs.extract('./destination-folder');
readStream.pipe(extractStream);
tar-stream gives you the raw building blocks. You manually create headers and push data. This is powerful but verbose.
// tar-stream: Manually creating an entry
const tar = require('tar-stream');
const pack = tar.pack();
pack.entry({ name: 'hello.txt', size: 11 }, 'Hello World');
pack.finalize();
pack.pipe(fs.createWriteStream('archive.tar'));
archiver focuses on the output side. It unifies the API for creating both .zip and .tar files, handling the streaming complexity for you.
// archiver: Creating a tar archive
const archiver = require('archiver');
const output = fs.createWriteStream('archive.tar');
const archive = archiver('tar');
archive.pipe(output);
archive.directory('./source-folder', false);
archive.finalize();
decompress-tar is a plugin, not a standalone runner. It works inside the decompress library to handle the specific logic of tar files.
// decompress-tar: Used within decompress
const decompress = require('decompress');
const decompressTar = require('decompress-tar');
await decompress('archive.tar', './dist', {
plugins: [decompressTar()]
});
When creating archives, your data source dictates the tool.
If your data lives entirely on the disk, tar and tar-fs are efficient. tar-fs is cleaner for recursive directory packing.
// tar-fs: Pack a whole directory
const pack = tarfs.pack('./source-folder');
pack.pipe(fs.createWriteStream('archive.tar'));
If you need to mix files with dynamic data (like a database query result or a generated string), archiver is superior. It allows appending different data types seamlessly.
// archiver: Mixing files and strings
const archive = archiver('tar');
// Append a file from disk
archive.file('config.json', { name: 'config.json' });
// Append a dynamic string
archive.append('Generated content', { name: 'report.txt' });
archive.pipe(fs.createWriteStream('mixed-archive.tar'));
archive.finalize();
Using tar-stream for mixed sources requires manual header management for every chunk, which increases the risk of bugs.
// tar-stream: Manual mixed content
const pack = tar.pack();
// File entry
pack.entry({ name: 'config.json', size: bufferSize }, fileBuffer);
// String entry
pack.entry({ name: 'report.txt', size: 16 }, 'Generated content');
For simple extraction, decompress-tar (via decompress) and tar offer the easiest APIs.
// decompress-tar: Promise-based simplicity
await decompress('archive.tar', './output', {
plugins: [decompressTar()]
});
// tar: High-performance extraction
await tar.x({ file: 'archive.tar', cwd: './output' });
However, if you need to filter files before they hit the disk or modify permissions on the fly, tar-stream provides the necessary hooks.
// tar-stream: Filtering entries during extract
const extract = tar.extract();
extract.on('entry', (header, stream, next) => {
if (header.name.endsWith('.log')) {
// Skip log files
stream.resume();
next();
return;
}
stream.pipe(fs.createWriteStream(header.name));
stream.on('end', next);
});
fs.createReadStream('archive.tar').pipe(extract);
tar-fs also supports filtering but with a slightly higher-level API.
// tar-fs: Filter during extract
const extractStream = tarfs.extract('./output', {
ignore: (name) => name.endsWith('.log')
});
fs.createReadStream('archive.tar').pipe(extractStream);
tar is generally the fastest for pure file system operations because it avoids unnecessary abstractions. It is actively maintained by the npm team and is the engine behind the npm CLI itself.
archiver introduces a small overhead due to its unified abstraction layer but remains highly performant for most web applications. It is widely used in SaaS platforms for generating user downloads.
decompress-tar should only be used if you are already committed to the decompress ecosystem. Using it as a standalone solution adds an unnecessary dependency wrapper.
tar-stream and tar-fs are stable and reliable but require more careful error handling in your code since you are managing the stream lifecycle manually.
| Feature | tar | tar-fs | tar-stream | archiver | decompress-tar |
|---|---|---|---|---|---|
| Primary Goal | Fast FS operations | FS + Stream bridge | Low-level stream control | Multi-format creation | Simple extraction |
| Create Archives | Yes (verbose) | Yes (easy) | Yes (manual) | Yes (unified) | No |
| Extract Archives | Yes (fast) | Yes (easy) | Yes (custom) | No | Yes (promise) |
| Data Sources | Files/Streams | Files/Streams | Buffers/Streams | Files, Strings, Streams | Files |
| Complexity | Medium | Low | High | Low | Very Low |
| Best For | CLI Tools, Servers | Docker-like bundling | Custom protocols | Dynamic reports | Quick scripts |
For most backend services, tar is the default choice for extraction due to its speed and official status. Use tar-fs if you need to pack directories into streams for upload (e.g., to S3) without writing boilerplate.
If your application generates archives containing dynamic data (strings, buffers) alongside files, archiver is the only logical choice. It saves hours of stream management code.
Reserve tar-stream for advanced cases where you must inspect or modify archive headers in real-time. Avoid decompress-tar for new projects unless you are maintaining legacy code that already relies on the decompress wrapper.
Choose archiver when you need to create archives (zip or tar) from mixed sources like strings, buffers, and files simultaneously. It is the best choice for building dynamic reports or backups where data isn't solely located on the disk. Avoid it if you only need to extract files, as it is focused on creation.
Choose decompress-tar if you are already using the decompress library and need a simple, promise-based way to extract tar files. It is ideal for quick scripts or build tools where you need to unpack a single archive without managing complex streams. Do not use it for creating archives or for high-performance streaming pipelines.
Choose tar for the most reliable, high-performance handling of tar files directly on the file system. It is the standard choice for CLI tools, package managers, and production servers where stability and speed are critical. It supports both creation and extraction but requires more manual stream wiring than higher-level wrappers.
Choose tar-fs when you need to pack a directory into a tar stream or extract a tar stream to a directory with minimal code. It is perfect for Docker-like operations or bundling assets where you want the power of streams without writing boilerplate pipe logic. It is less suitable if you need to manipulate individual headers mid-stream.
Choose tar-stream when you need full control over the tar format, such as modifying headers, filtering entries on the fly, or integrating tar logic into a custom protocol. It is the right tool for building custom archiving tools or middleware. Avoid it for simple file copying tasks where tar or tar-fs would be more concise.
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.