The August 26 release of Node 26.8.0 (Current) has one change that stands out from the usual minor-version batch: a built-in, in-memory ZIP archive API in node:zlib. Up to now there was no standard-library way to read or write a .zip, so you either pulled in an npm package or shelled out to unzip. That is still the right call for the more complex cases, but the new ZipBuffer, ZipEntry, and ZipFile classes give you a no-dependency path for the common ones.1
The work went through PR #64339 by Philipp Dunkel, reviewed and landed with four approvals including Matteo Collina and James Snell.2 Two points about its status up front: it is experimental (marker 1.0, "early development"), and merely importing node:zlib does not trigger the experimental warning, only actually using one of the classes does.3
The three classes
The API is split into three pieces, each tuned for how you already hold the archive:
-
ZipBufferis a view over an archive you already have in memory as aBuffer(orTypedArray,DataView,ArrayBuffer). It does not copy the archive; it keeps a zero-copy view and reads each entry's bytes lazily and directly from that memory. You can add and remove entries, but additions are held as separate in-memory entries, since the source buffer is a fixed-size view with no room to append into.toBuffer()serializes the current set back out.3 -
ZipEntryrepresents a single file, directory, or symlink inside an archive. Entries come out of aZipBuffer/ZipFile, or you can build them yourself withcreate(),createStream()for streaming a source without buffering it,createSync(), andcreateSymlink().3 -
ZipFileis the file-descriptor-backed variant. It gives random access to an archive, reads members lazily without retaining their content, and writes new members in place, opened withopen()oropenSync(). This is the one to reach for when you want to edit an archive that lives on disk without slurping it all into RAM.3
On top of the classes there are helper functions: createZipArchive()/createZipArchiveSync() serialize a sequence of entries into an archive byte stream, and zipFiles() bundles a list of files. getMaxZipContentSize()/setMaxZipContentSize() bound the default in-memory decompression ceiling. Every operation has both an asynchronous and a synchronous form.1
A small example, straight from the docs, shows the ZipBuffer flow of read, add, delete, and write:
import { ZipBuffer } from 'node:zlib';
import { readFileSync, writeFileSync } from 'node:fs';
import { Buffer } from 'node:buffer';
const zip = new ZipBuffer(readFileSync('archive.zip'));
for (const [name, entry] of zip) {
console.log(name, entry.size);
}
await zip.add('hello.txt', Buffer.from('Hello, world!'));
zip.delete('unwanted.txt');
writeFileSync('archive.zip', await zip.toBuffer());import { ZipBuffer } from 'node:zlib';
import { readFileSync, writeFileSync } from 'node:fs';
import { Buffer } from 'node:buffer';
const zip = new ZipBuffer(readFileSync('archive.zip'));
for (const [name, entry] of zip) {
console.log(name, entry.size);
}
await zip.add('hello.txt', Buffer.from('Hello, world!'));
zip.delete('unwanted.txt');
writeFileSync('archive.zip', await zip.toBuffer());The details that matter
A few behaviors are worth knowing before you build anything on this.
Names are returned verbatim. zipEntry.name is never normalized, and a name containing .., a leading /, a drive letter, or backslashes is neither rewritten nor rejected. Since a ZipBuffer/ZipFile never writes to disk by itself, guarding against path traversal ("zip slip") when you extract is entirely the caller's responsibility.3
The memory ceiling only applies to buffered reads. content() (and contentSync()) default to rejecting any entry that declares more than getMaxZipContentSize() bytes, before allocating anything. That default guards a single large buffer allocation. The streaming path, contentIterator() and ZipFile.stream(), is deliberately not capped that way: it is the bounded-memory way to read arbitrarily large members, and is only bounded per chunk plus whatever explicit maxSize you pass. This split was its own follow-up commit in the merged PR.4
Streaming entries verify late. contentIterator() yields provisional chunks, because CRC-32 verification and the final size check can only run once every byte has been read. A corrupt or truncated entry is reported by the iterator throwing after the last chunk, not before the first. If you must not act on unverified bytes, buffer them or use content().3
Symlinks and timestamps get real support. createSymlink() writes a stored entry whose Unix mode type bits mark it as a symlink, and isSymlink reads that back. For timestamps, ZIP's MS-DOS fields have 2-second resolution and no time zone; when a modified value does not land on a whole 2-second boundary, Node writes an Info-ZIP extended-timestamp extra field so the exact second round-trips.3
Internally the change got a heavy test pass: the merged commit added coverage for edge cases, fuzzing, security hardening, Zip64, interoperability with other archive tools, and a slow path for large members, around 9,665 lines added across 36 files.2
What else shipped in 26.8.0
The other notable changes in the same release:1
crypto: SIV and GCM-SIV modes are now enabled in theCipher/DecipherAPIs, and root certificates were updated to NSS 3.126.diagnostics_channel:TracingChannelis marked stable.util: a non-throwingMIMEType.parse()was added alongside the existing constructor.sqlite:StatementSync.prototype.close()and aSymbol.disposecounterpart let statements be finalized explicitly.net:net.BlockListgot a performance improvement.repl: basic syntax highlighting is now on in the interactive shell.
For a small, dependency-free tool, the Zlib ZIP API means you no longer need a third-party package just to read a member out of a .zip in memory. For anything heavier, well-established third-party ZIP libraries still run circles around an early-development API, and the verbatim-name behavior is a genuine footgun for anyone extracting to disk. If that applies to you, treat the feature as useful but young, and keep your own path-traversal checks.
- Node.js v26.8.0 release notes, https://github.com/nodejs/node/releases/tag/v26.8.0
- PR #64339 "zlib: add ZIP archive support to zlib (ZipFile, ZipBuffer, ZipEntry)", https://github.com/nodejs/node/pull/64339
- Zlib documentation, Node v26.8.1, https://nodejs.org/api/zlib.html
- PR #64339, commit "zlib: do not cap ZIP streaming reads by the content-size ceiling", https://github.com/nodejs/node/pull/64339/commits