How to use the File System in Node.js
Web applications don’t always need to write to the file system, but Node.js provides a comprehensive application programming interface (API) for doing so. It may be essential if you’re outputting debugging logs, transferring files to or from a server, or creating command line tools.
Reading and writing files from code is not necessarily difficult, but your application will be more robust if you do the following:
Ensure it’s cross-platform
Windows, macOS, and Linux handle files differently. For example, you use a forward slash
/
to separate directories in macOS and Linux, but Windows uses a backslash\
and bans certain file name characters such as:
and?
.Double-check everything!
Users or other apps could delete a file or change access permissions. Always check for such issues and handle errors effectively.
The Node.js fs
Module
The Node.js fs module provides methods for managing files and directories. If you’re using other JavaScript runtimes:
Deno provides its own file system APIs as well as support for node:fs APIs.
Bun provides optimized file I/O APIs as well as node:fs APIs.
Browsers run in a sandbox and can’t directly communicate with the OS or underlying file system. That said, you can upload files and permit limited access via the File System API. It’s conceptually different and beyond the scope of this tutorial.
All JavaScript runtimes run on a single processing thread. The underlying operating system handles operations such as file reading and writing, so the JavaScript program continues to run in parallel. The OS then alerts the runtime when a file operation is complete.
The fs documentation provides a long list of functions, but there are three general types with similar functions, which we’ll look at next.
1. Callback functions
These functions take a completion callback function as an argument. The following example passes an inline function which outputs the content of myfile.txt
. Assuming no errors, its content displays after end of program
appears in the console:
import { readFile } from 'node:fs';
readFile('myfile.txt', { encoding: 'utf8' }, (err, content) => {
if (!err) {
console.log(content);
}
});
console.log('end of program');
Note: the { encoding: 'utf8' }
parameter ensures Node.js returns a string of text content rather than a Buffer object of binary data.
This becomes complicated when you need to run one after another and descend into nested callback hell! It’s also easy to write callback functions which look correct but cause memory leaks that are difficult to debug.
In most cases, there’s little reason to use callbacks today. Few of the examples below use them.
2. Synchronous functions
The “Sync” functions effectively ignore Node’s non-blocking I/O and provide synchronous APIs like you’d find in other programming languages. The following example outputs the content of myfile.txt
before end of program
appears in the console:
import { readFileSync } from 'node:fs';
try {
const content = readFileSync('myfile.txt', { encoding: 'utf8' });
console.log(content);
}
catch {}
console.log('end of program');
It looks easier, and I’d never say don’t use Sync … but, erm … don’t use Sync! It halts the event loop and pauses your application. That may be fine in a CLI program when loading a small initialization file, but consider a Node.js web application with 100 concurrent users. If one user requests a file which takes one second to load, they wait one second for a response — and so do all the other 99 users!
There’s no reason to use synchronous methods when we have promise functions.
3. Promise functions
ES6/2015 introduced promises. They’re syntactical sugar on callbacks to provide a sweeter, easier syntax, especially when used with async
/await
. Node.js also introduced a ‘fs/promises’ API which looks and behaves in a similar way to the synchronous function syntax but remains asynchronous:
import { readFile } from 'node:fs/promises';
try {
const content = await readFile('myfile.txt', { encoding: 'utf8' });
console.log(content);
}
catch {}
console.log('end of program');
Note the use of the 'node:fs/promises'
module and the await
before readFile()
.
Most examples below use the promise-based syntax. Most do not include try
and catch
for brevity, but you should add those blocks to handle errors.
ES module syntax
The examples in this tutorial also use ES Modules (ESM) import
rather than the CommonJS require
. ESM is the standard module syntax supported by Deno, Bun, and browser runtimes.
To use ESM in Node.js, either:
- name your JavaScript files with a
.mjs
extension - use an
--import=module
switch on the command line — such asnode --import=module index.js
, or - if you have a project
package.json
file, add a new"type": "module"
setting
You can still use CommonJS require
should you need to.
Reading Files
There are several functions for reading files, but the simplest is to read a whole file into memory using readFile, as we saw in the example above:
import { readFile } from 'node:fs/promises';
const content = await readFile('myfile.txt', { encoding: 'utf8' });
The second options
object can also be a string. It defines the encoding: set 'utf8'
or another text format to read the file content into a string.
Alternatively, you can read lines one at a time using the readLines() method of the filehandle object:
import { open } from 'node:fs/promises';
const file = await open('myfile.txt');
for await (const line of file.readLines()) {
console.log(line);
}
There are also more advanced options for reading streams or any number of bytes from a file.
Handling File and Directory Paths
You’ll often want to access files at specific absolute paths or paths relative to the Node application’s working directory. The node:path module provides cross-platform methods to resolve paths on all operating systems.
The path.sep
property returns the directory separator symbol — \
on Windows or /
on Linux or macOS:
import * as path from 'node:path';
console.log( path.sep );
But there are more useful properties and functions. join([…paths]) joins all path segments and normalizes for the OS:
console.log( path.join('/project', 'node/example1', '../example2', 'myfile.txt') );
/*
/project/node/example2/myfile.txt on macOS/Linux
\project\node\example2\myfile.txt on Windows
*/
resolve([…paths]) is similar but returns the full absolute path:
console.log( path.resolve('/project', 'node/example1', '../example2', 'myfile.txt') );
/*
/project/node/example2/myfile.txt on macOS/Linux
C:\project\node\example2\myfile.txt on Windows
*/
normalize(path) resolves all directory ..
and .
references:
console.log( path.normalize('/project/node/example1/../example2/myfile.txt') );
/*
/project/node/example2/myfile.txt on macOS/Linux
\project\node\example2\myfile.txt on Windows
*/
relative(from, to) calculates the relative path between two absolute or relative paths (based on Node’s working directory):
console.log( path.relative('/project/node/example1', '/project/node/example2') );
/*
../example2 on macOS/Linux
..\example2 on Windows
*/
format(object) builds a full path from an object of constituent parts:
console.log(
path.format({
dir: '/project/node/example2',
name: 'myfile',
ext: 'txt'
})
);
/*
/project/node/example2/myfile.txt
*/
parse(path) does the opposite and returns an object describing a path:
console.log( path.parse('/project/node/example2/myfile.txt') );
/*
{
root: '/',
dir: '/project/node/example2',
base: 'myfile.txt',
ext: '.txt',
name: 'myfile'
}
*/
Getting File and Directory Information
You often need to get information about a path. Is it a file? Is it a directory? When was it created? When was it last modified? Can you read it? Can you append data to it?
The stat(path) function returns a Stats object containing information about a file or directory object:
import { stat } from 'node:fs/promises';
const info = await stat('myfile.txt');
console.log(info);
/*
Stats {
dev: 4238105234,
mode: 33206,
nlink: 1,
uid: 0,
gid: 0,
rdev: 0,
blksize: 4096,
ino: 3377699720670299,
size: 21,
blocks: 0,
atimeMs: 1700836734386.4246,
mtimeMs: 1700836709109.3108,
ctimeMs: 1700836709109.3108,
birthtimeMs: 1700836699277.3362,
atime: 2023-11-24T14:38:54.386Z,
mtime: 2023-11-24T14:38:29.109Z,
ctime: 2023-11-24T14:38:29.109Z,
birthtime: 2023-11-24T14:38:19.277Z
}
*/
It also provides useful methods, including:
const isFile = info.isFile(); // true
const isDirectory = info.isDirectory(); // false
The access(path)
function tests whether a file can be accessed using a specific mode set via a constant. If the accessibility check is successful, the promise fulfills with no value. A failure rejects the promise. For example:
import { access, constants } from 'node:fs/promises';
const info = {
canRead: false,
canWrite: false,
canExec: false
};
// is readable?
try {
await access('myfile.txt', constants.R_OK);
info.canRead = true;
}
catch {}
// is writeable
try {
await access('myfile.txt', constants.W_OK);
info.canWrite = true;
}
catch {}
console.log(info);
/*
{
canRead: true,
canWrite: true
}
*/
You can test more than one mode, such as whether a file is both readable and writeable:
await access('myfile.txt', constants.R_OK | constants.W_OK);
Writing Files
writeFile() is the simplest function to asynchronously write a whole file replacing its content if it already exists:
import { writeFile } from 'node:fs/promises';
await writeFile('myfile.txt', 'new file contents');
Pass the following arguments:
- the file path
- the file content — can be a String, Buffer, TypedArray, DataView, Iterable, or Stream
- an optional third argument can be a string representing the encoding (such as
'utf8'
) or an object with properties such asencoding
andsignal
to abort the promise.
A similar appendFile() function adds new content to the end of the current file, creating that file if it does not exist.
For the adventurous, there is a file handler write() method which allows you to replace content inside a file at a specific point and length.
Creating Directories
The mkdir() function can create full directory structures by passing an absolute or relative path:
import { mkdir } from 'node:fs/promises';
await mkdir('./subdir/temp', { recursive: true });
You can pass two arguments:
- the directory path, and
- an optional object with a
recursive
Boolean andmode
string or integer
Setting recursive
to true
creates the whole directory structure. The example above creates subdir
in the current working directory and temp
as a subdirectory of that. If recursive
were false
(the default) the promise would reject if subdir
were not already defined.
The mode
is the macOS/Linux user, group, and others permission with a default of 0x777
. This is not supported on Windows and ignored.
The similar .mkdtemp() function is similar and creates a unique directory typically for temporary data storage.
Reading Directory Contents
.readdir() reads the content of a directory. The promise fulfills with an array containing all file and directory names (except for .
and ..
). The name is relative to the directory and does not include the full path:
import { readdir } from 'node:fs/promises';
const files = await readdir('./'); // current working directory
for (const file of files) {
console.log(file);
}
/*
file1.txt
file2.txt
file3.txt
index.mjs
*/
You can pass an optional second parameter object with the following properties:
encoding
— the default is an array ofutf8
stringsrecursive
— settrue
to recursively fetch all files from all subdirectories. The file name will include the subdirectory name(s). Older editions of Node.js may not provide this option.withFileType
— settrue
to return an array of fs.Dirent objects which includes properties and methods including.name
,.path
,.isFile()
,.isDirectory()
and more.
The alternative .opendir() function allows you to asynchronously open a directory for iterative scanning:
import { opendir } from 'node:fs/promises';
const dir = await opendir('./');
for await (const entry of dir) {
console.log(entry.name);
}
Deleting Files and Directories
The .rm() function removes a file or directory at a specified path:
import { rm } from 'node:fs/promises';
await rm('./oldfile.txt');
You can pass an optional second parameter object with the following properties:
force
— settrue
to not raise an error when the path does not existrecursive
— settrue
to recursively delete a directory and contentsmaxRetries
— make a number of retries when another process has locked a fileretryDelay
— the number of milliseconds between retries
The similar .rmdir() function only deletes directories (you can’t pass a file path). Similarly, .unlink() only deletes files or symbolic links (you can’t pass a directory path).
Other File System Functions
The examples above illustrate the most-used options for reading, writing, updating, and deleting files and directories. Node.js also provides further, lesser-used options such as copying, renaming, changing ownership, changing permissions, changing date properties, creating symbolic links, and watching for file changes.
It may be preferable to use the callback-based API when watching for file changes, because it’s less code, easier to use, and can’t halt other processes:
import { watch } from 'node:fs';
// run callback when anything changes in directory
watch('./mydir', { recursive: true }, (event, file) => {
console.log(`event: ${ event }`);
if (file) {
console.log(`file changed: ${ file }`);
}
});
// do more...
The event
parameter received by the callback is either 'change'
or 'rename'
.
Summary
Node.js provides a flexible and cross-platform API to manage files and directories on any operating system where you can use the runtime. With a little care, you can write robust and portable JavaScript code that can interact with any file system.
For more information, refer to the Node.js fs and path documentation. Other useful libraries include:
- OS to query operating oystem information
- URL to parse a URL, perhaps when mapping to and from file system paths
- Stream for handling large files
- Buffer and TypedArray objects to handle binary data
- Child processes to spawn a subprocess to handle long-running or complex file manipulation functions.
You can also find higher-level file system modules on npm, but there’s no better experience than writing your own.
FAQs on Accessing the File System in Node.js
The File System module, often referred to as fs
, is a core module in Node.js that provides methods and functionality for interacting with the file system, including reading and writing files.
fs
module in a Node.js script? You can include the fs
module by using the require
statement, like this: const fs = require('fs');
. This makes all fs
methods available in your script.
Synchronous file operations block the Node.js event loop until the operation is completed, while asynchronous operations do not block the event loop, allowing your application to remain responsive. Asynchronous operations are typically recommended for I/O tasks.
fs
module? You can use the fs.readFile()
method to read the contents of a file. Provide the file path and a callback function to handle the data once it’s read.
fs
module in Node.js? Callback functions in fs
operations are used to handle the result of asynchronous file operations. They are called when the operation is complete, passing any errors and data as arguments.
fs
module? You can use the fs.existsSync()
method to check if a file exists at a specified path. It returns true
if the file exists and false
if it doesn’t.
fs.createReadStream()
method, and when is it useful? fs.createReadStream()
is used for reading large files efficiently. It creates a readable stream for the specified file, allowing you to read and process data in smaller, manageable chunks.
fs
module to create and write to a new file in Node.js? Yes, you can use the fs.writeFile()
or fs.createWriteStream()
methods to create and write to a new file. These methods allow you to specify the file path, content, and options for writing.
fs
module in Node.js? You should always handle errors by checking the error parameter in the callback function provided to asynchronous fs
methods or by using try/catch blocks for synchronous operations.
fs
module in Node.js? Yes, you can use the fs.unlink()
method to delete a file. Provide the file path and a callback function to handle the result.
fs
module to work with directories and folder structures in Node.js? Yes, the fs
module provides methods to create, read, and manipulate directories, including creating directories, listing their contents, and removing directories.