memfs review
memfs 4.68.1 supplies an in-memory implementation of much of Node's fs API, plus independent Volume objects, JSON fixture import and export, streams, promises, watchers, snapshots, and adapters for the browser File System Access API. Tests can inject its fs object into code without touching the host disk. The package intentionally differs from a real filesystem in places: watcher delivery is deterministic, paths are watched semantically, and platform-specific inode and duplicate-event behavior is absent. Version 4.68.1 fixes unlink so it no longer removes an empty directory; 4.68.0 added file birthTime data.
memfs 4.68.1 took 12.8 seconds and 15 MB across 21 packages in our sandbox, with 0 audit findings, while its full browser bundle failed. Install it for injected Node fs unit tests; keep real-disk coverage for watchers, permissions, platform errors, and browser entry points.
We installed it
| Install | ✓ · 12.8s | 21 packages on disk · 15 MB |
| Import | ✓ | ESM import works · require() works · CommonJS package |
| Browser | n/a | could not be bundled for the browser (Node-only code, most likely) |
| Types | ✓ | TypeScript types bundled |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does memfs install cleanly?
Yes. In a fresh container with an empty cache, npm install memfs finished in 13 seconds, leaving 21 packages and 15 MB on disk. npm audit reported no known vulnerabilities.
Can memfs run in a browser?
Not directly: esbuild could not bundle it for the browser in our run, which normally means it depends on Node built-ins. Use it on the server, or find a browser-targeted alternative.
Does memfs work with both ESM and CommonJS?
Yes. Both import 'memfs' and require('memfs') worked in Node 22 in our run. The package is published as CommonJS.
Does memfs include TypeScript types?
Yes, type declarations ship inside the package, so no @types install is needed.
memfs or mock-fs: which should you use?
mock-fs: Choose it when existing tests already expect a process-level fs mock and its global patching tradeoff is acceptable. memfs 4.68.1 took 12.8 seconds and 15 MB across 21 packages in our sandbox, with 0 audit findings, while its full browser bundle failed.
When should you not use memfs?
Correctness depends on permissions, ownership, symlinks across mounts, inode identity, disk pressure, or operating-system error codes
Use it if
- Unit tests can accept an fs-shaped dependency and need fast, isolated file trees with no host-disk cleanup
- Fixtures are easier to describe as path-to-content JSON and compare again after the code runs
- Two filesystem states must coexist in one test for migration, copy, or synchronization behavior
- Node-style streams, promises, file descriptors, or watchers need a controlled fake before a smaller real-filesystem integration suite
- Correctness depends on permissions, ownership, symlinks across mounts, inode identity, disk pressure, or operating-system error codes
- Watcher timing and duplicate events are part of the behavior; memfs fires Node-style watcher callbacks synchronously and does not imitate platform unreliability
- The frontend build imports memfs as one package; our esbuild browser attempt failed, so browser work needs the documented FSA-specific entry points and its own build check
- Patching the process-wide node:fs module is the only integration seam; fs-monkey can do it, but explicit dependency injection avoids shared test state
- A temporary directory already gives enough isolation and the test must exercise native path, stream, or filesystem semantics
Setup reality
We installed memfs 4.68.1 without a cache in a fresh Node 22 Bookworm sandbox. npm finished in 12.8 seconds and left 21 packages using 15 MB. The package itself is 200 KB unpacked, declares 14 direct dependencies and no peers, and carries Apache-2.0. npm audit found 0 known vulnerabilities. Bundled TypeScript declarations were present. The CommonJS package has no exports map; require and ESM import both worked.
No credential or config file is required. Create a new pair with memfs() or Volume when each test needs private state. The exported fs and vol share a singleton, so reset that volume after every case. Passing fs into application code is safer than patching Node's module cache. fs-monkey patches survive vol.reset and must be restored separately.
Version 4.68 watchers do not reproduce real operating-system timing. The Node-style FSWatcher callback runs inside the mutating operation, reports one event per logical change, and never returns a null filename. Delete and recreate keeps the semantic path watched, unlike a POSIX watcher attached to an old inode. Keep at least one test on a real temporary directory for watcher-dependent code.
Our full-package browser bundle failed in esbuild. memfs documents browser File System Access implementations and adapters, but importing the Node-oriented top level is not proof that a frontend bundler can isolate them. Test the exact subpath and browser target. Streams and promises still require normal completion handling, and unionfs can fall through to the real disk when a memory layer misses.
Patterns
Exercise code against the shared volume write-and-read-file
import { fs } from 'memfs'
fs.writeFileSync('/hello.txt', 'Hello')
console.log(fs.readFileSync('/hello.txt', 'utf8'))The exported fs points at a package singleton. Clear its volume after the test so files do not leak into another case.
Load a readable fixture tree seed-json-tree
import { fs, vol } from 'memfs'
vol.fromJSON({
'./package.json': '{"name":"demo"}',
'./src/index.js': 'export const ready = true',
}, '/workspace')
fs.readFileSync('/workspace/src/index.js', 'utf8')The base directory resolves every relative fixture key below one root. Absolute keys ignore that base.
Give one test its own filesystem create-isolated-volume
import { memfs } from 'memfs'
const { fs, vol } = memfs({
'/config.json': '{"mode":"test"}',
})memfs() returns a paired fs and Volume with no dependence on the exported singleton, which suits parallel tests.
Remove singleton files after a test reset-between-tests
import { vol } from 'memfs'
afterEach(() => {
vol.reset()
})reset removes entries from this Volume. It does not undo module patches, external listeners, or state held in a second Volume.
Compare all text files at once assert-final-tree
expect(vol.toJSON()).toEqual({
'/output/report.txt': 'complete\n',
})toJSON is convenient for text. Binary bytes and metadata need Buffer reads or the snapshot utilities instead.
Run code written for fs.promises use-promise-api
const { fs } = memfs()
await fs.promises.mkdir('/cache', { recursive: true })
await fs.promises.writeFile('/cache/value.txt', '42')
const value = await fs.promises.readFile('/cache/value.txt', 'utf8')Await each call as on disk. An in-memory backend still exposes application mistakes caused by missing promise ordering.
Wait for an in-memory stream to finish test-file-streams
import { once } from 'node:events'
const output = fs.createWriteStream('/events.log')
output.write('start\n')
output.end('done\n')
await once(output, 'finish')
const input = fs.createReadStream('/events.log')The bytes stay in memory, but write completion remains event based. Read only after finish when order matters.
Compare two isolated file trees use-separate-volumes
import { Volume } from 'memfs'
const first = Volume.fromJSON({ '/value': 'one' })
const second = Volume.fromJSON({ '/value': 'two' })
first.readFileSync('/value', 'utf8')
second.readFileSync('/value', 'utf8')Each Volume holds its own inode tree. This is useful for migration tests without resetting shared state between phases.
Make the filesystem an explicit parameter inject-fs-dependency
function loadSettings(fsImpl, path) {
return JSON.parse(fsImpl.readFileSync(path, 'utf8'))
}
const { fs } = memfs({ '/settings.json': '{"port":3000}' })
const settings = loadSettings(fs, '/settings.json')Dependency injection limits the fake to this call and leaves the process-wide node:fs module untouched.
Overlay memory on the host filesystem combine-memory-and-disk
import * as realFs from 'node:fs'
import { ufs } from 'unionfs'
ufs.use(realFs).use(vol)
const data = ufs.readFileSync('/virtual.txt', 'utf8')Layer order controls which duplicate path wins. A miss can reach realFs, so this arrangement is not a security sandbox.
Close a recursive watcher with a signal watch-with-abort
const controller = new AbortController()
const watcher = fs.watch('/workspace', {
recursive: true,
signal: controller.signal,
}, (eventType, filename) => {
console.log(eventType, filename)
})
controller.abort()memfs calls this watcher synchronously during mutation and emits deterministic events. Native Node watchers have different timing and platform behavior.
Restore a complete directory state snapshot-directory
import * as snapshot from 'memfs/lib/snapshot'
const saved = snapshot.toSnapshotSync({ fs, path: '/workspace' })
vol.reset()
snapshot.fromSnapshotSync(saved, { fs, path: '/workspace' })This snapshot utility is reached through a deep import in a package with no exports map. Pin and test that path during upgrades.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| mock-fs | npm | Choose it when existing tests already expect a process-level fs mock and its global patching tradeoff is acceptable. |
| unionfs | npm | Choose it when reads must combine an in-memory volume with another filesystem layer. |
| fs-monkey | npm | Choose it only when code cannot accept an injected fs object and Node module patching is unavoidable. |
More testing guides
pytest · chai · vitest · jsdom · playwright · coverage · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

