write
write is a small CommonJS helper for putting strings, buffers, or Uint8Arrays into files from Node.js. Its useful trick is creating missing parent directories before it writes, whether you use its promise, callback, synchronous, or writable-stream entry point. It can also add a final newline, refuse to replace an existing file, or choose an incremented filename such as report (2).txt. Think of it as a convenience layer over Node's fs module, not a storage system or a durability tool.
It still does its tiny job, especially in old CommonJS generators that use every API form. For new code, the built-in fs promises are usually clearer, typed, and one fewer unmaintained dependency; use write-file-atomic when durability is the actual requirement.
Use it if
- You maintain CommonJS code that repeatedly needs to create parent directories and then write a file
- You need the same small helper to expose promise, callback, synchronous, and writable-stream forms
- You generate reports, logs, or exports and want automatic numbered filenames instead of replacing an existing file
- You want an optional trailing newline without repeating end-of-file checks around every write
- You are starting a modern Node project: node:fs/promises plus mkdir({ recursive: true }) is built in and avoids an old dependency whose last release was in 2019
- You need crash-safe or concurrent writes: the source writes directly to the destination and its overwrite and increment checks are not atomic, so two processes can race
- You need TypeScript declarations or an ESM export: version 2.0.0 ships CommonJS JavaScript with no types field or declaration files
- You expect append, JSON serialization, file copying, permissions management, or removal helpers: the public API only writes data and creates parent directories
- You need clear failure reporting for directory creation: the source deliberately discards mkdir errors and lets the later file operation report whatever failure follows
Setup reality
Installation is only npm install write, with no native build, credentials, configuration file, or peer dependency. Version 2.0.0 requires Node 10 or newer and is a CommonJS package, so require('write') is the documented path; an ESM project has to rely on Node's CommonJS interop. There are no bundled TypeScript declarations, which means strict TypeScript projects need a local declare module shim or their own small wrapper. Every API creates parent directories automatically and replaces the destination by default. Set overwrite: false when replacement must be rejected, but do not mistake that check for concurrency control because the source checks existence before opening the file. The increment option also chooses a name through a separate filesystem check, so simultaneous writers may still select the same destination. The asynchronous function resolves to an object containing path and data; the README prose calls the second property contents, but the version 2.0.0 source actually uses data. The stream method creates directories synchronously before returning a normal fs.WriteStream, so listen for error and wait for finish or close before treating output as complete. The newline option only adds a line feed when the data is a string or Buffer and lacks one already. Finally, maintenance is the main adoption cost: the npm release and last repository push both date to September 2019, so test it against your supported Node versions and do not expect modern module or typing improvements.
Patterns
Write a file and create its parent directorieswrite-file
const write = require('write');
const result = await write('dist/reports/today.txt', 'ready');
console.log(result.path, result.data);Missing dist and reports directories are created recursively. The returned object uses data, although the README description calls that property contents.
Use the callback formwrite-with-callback
const write = require('write');
write('dist/status.txt', 'ok', (error, result) => {
if (error) {
console.error(error);
return;
}
console.log('wrote', result.path);
});Providing a callback makes the function return undefined instead of a promise. Handle the callback error before reading the result.
Write binary data from a Bufferwrite-buffer
const write = require('write');
const bytes = Buffer.from([0x89, 0x50, 0x4e, 0x47]);
await write('dist/image.bin', bytes);Strings, Buffers, and Uint8Arrays are accepted. Do not enable newline for binary data unless an added line-feed byte is intentional.
Ensure a text file ends with a newlineensure-newline
const write = require('write');
await write('dist/generated.conf', 'enabled=true', {
newline: true,
});newline: true adds one line feed only when the string or Buffer does not already end in one. It does not normalize CRLF line endings.
Reject an existing destinationprevent-overwrite
const write = require('write');
try {
await write('exports/report.csv', csv, { overwrite: false });
} catch (error) {
if (!String(error.message).startsWith('File already exists:')) throw error;
}The existence check is not atomic. Use exclusive fs flags or an atomic-write package when multiple processes can target the same path.
Keep both files by incrementing the new nameincrement-filename
const write = require('write');
const result = await write('exports/report.csv', csv, {
increment: true,
});
console.log(result.path); // exports/report (2).csv when report.csv existsAlways use result.path after an incremented write because it may differ from the requested path. Name selection is not safe against simultaneous writers.
Write during a synchronous build stepwrite-synchronously
const write = require('write');
const result = write.sync('dist/meta/version.txt', '2.4.0', {
newline: true,
});
console.log(result.path);sync blocks the event loop and creates directories synchronously. Keep it to startup scripts and short build tasks, not request handlers.
Pipe a readable stream into a nested filepipe-to-file
const fs = require('node:fs');
const write = require('write');
const output = write.stream('backup/docs/README.md');
output.on('error', console.error);
output.on('close', () => console.log('closed'));
fs.createReadStream('README.md').pipe(output);write.stream returns a standard fs.WriteStream and creates its parent directory before returning. Listen for errors on both input and output in production.
Pass fs options through to the writerset-file-mode
const write = require('write');
await write('dist/run.sh', '#!/bin/sh\necho ok\n', {
mode: 0o755,
});Options are forwarded to fs.createWriteStream. On an existing file, mode does not necessarily replace its current permissions; use chmod when that distinction matters.
Write text with an explicit encodingchoose-encoding
const write = require('write');
await write('dist/latin1.txt', 'café', {
encoding: 'latin1',
});The package defaults encoding to utf8. Explicit non-UTF-8 encodings only apply when the input is a string.
Wait for a streaming write to finishawait-stream-finish
const { finished } = require('node:stream/promises');
const write = require('write');
const output = write.stream('dist/events.log');
output.end('started\n');
await finished(output);Creating or ending the stream does not mean bytes are flushed. Await finished before moving, reading, or announcing the file.
Replace an existing file explicitlyreplace-existing-file
const write = require('write');
await write('dist/latest.json', JSON.stringify(payload, null, 2), {
overwrite: true,
newline: true,
});Replacement is already the default. This direct write is not atomic, so a crash can leave a partial file visible to readers.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| fs-extra | npm | Choose it when you also need copy, move, remove, JSON, and outputFile helpers in one maintained filesystem toolkit |
| write-file-atomic | npm | Choose it when readers must never observe a partially written destination and atomic replacement matters |
| output-file-sync | npm | Choose it for a narrowly focused synchronous helper that creates ancestor directories before writing |