<< Home

Chapter 13: Files, Paths, and Processes

Node gives you tools to work with the file system, manage paths, handle process details, and run other programs. These capabilities form the backbone of servers, build tools, and command line utilities. This chapter surveys the modern fs interfaces, path handling for both CommonJS and ES modules, configuration through environment variables, and ways to run parallel work through child processes and worker threads.

fs modern APIs: promises and streams

The node:fs module provides three styles of interaction: callbacks, promises, and streams. The promise based API lives in node:fs/promises and offers a clean way to write asynchronous file code without callback nesting. Streams handle large files efficiently because they transfer data in chunks rather than loading entire files into memory.

Here is a simple example using the promise API to read and write text files.

import { readFile, writeFile } from "node:fs/promises";

const text = await readFile("input.txt", "utf8");
await writeFile("output.txt", text.toUpperCase());

Streams are useful when you want backpressure, piping between transformations, or incremental parsing. A readable stream emits data events each time a chunk is ready. A writable stream processes those chunks one at a time, respecting its internal queue so memory stays stable.

import { createReadStream, createWriteStream } from "node:fs";

createReadStream("large.txt")
  .pipe(createWriteStream("copy.txt"))
  .on("finish", () => console.log("done"));
💡 Use pipeline from node:stream/promises when you need a single promise that resolves or rejects once the stream chain completes.

Watching files

The fs.watch function observes file system changes. Events can coalesce or behave differently across platforms because each system exposes its own kernel notifications. For cross-platform tooling try to debounce events or monitor entire directory trees when possible.

import { watch } from "node:fs";

for await (const event of watch("src")) {
  console.log(event.eventType, event.filename);
}

path, URL, and ESM file handling

The node:path module resolves, joins, and normalizes path strings. It accounts for platform rules such as drive letters on Windows. When you write scripts that run across different systems, path helpers keep your logic portable. ES modules add another layer because import.meta.url uses URLs rather than plain file paths.

To convert between file URLs and native paths use fileURLToPath and pathToFileURL. This is essential for loaders, configuration scripts, and anything that calculates paths relative to the current module.

import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";

const here = dirname(fileURLToPath(import.meta.url));
const configPath = join(here, "config.json");

Resolving relative paths

path.resolve computes an absolute path by interpreting segments from right to left. path.join concatenates segments and then normalizes them. When writing cross-platform scripts avoid hard coded separators and always use these helpers.

FunctionPurpose
path.joinConcatenate segments then normalize
path.resolveProduce an absolute path based on working directory
path.basenameReturn last path component
path.extnameReturn the extension including the dot
⚠️ A file URL such as file:///path/to/file is not the same as a plain path string. Convert before passing to fs APIs.

process, env, and configuration

The process object exposes information about the running program: environment variables, arguments, current working directory, memory usage, and signals. Many applications load configuration from process.env so that deployments can override settings without modifying code.

This example pulls a port number from the environment with a fallback.

const port = Number(process.env.PORT) || 3000;
console.log("Server will start on port", port);

The process also emits signals. For example, SIGINT fires when the user presses Ctrl+C. A graceful shutdown sequence closes open resources before exiting.

process.on("SIGINT", () => {
  console.log("Shutting down");
  process.exit(0);
});
💡 Call process.exit() sparingly. Let the event loop empty naturally unless you are in a fatal condition that cannot recover.

Command line arguments

The array process.argv holds the command line arguments. Index zero is the Node executable, index one is the script path, and the rest are the arguments. CLI tools often slice the first two positions away and parse the remainder.

const args = process.argv.slice(2);
console.log(args);

For structured parsing consider a library that handles flags and help text. This keeps the command interface predictable and user friendly.

Child processes and worker threads

Node can run other programs or additional JavaScript threads. Child processes execute external commands or scripts through spawn, exec, and fork. Worker threads run JavaScript in parallel within the same process. Each option fits a slightly different situation.

Running external programs

The spawn function starts a program and streams its output. The exec function buffers output then returns the entire result. The fork function launches another Node script with an IPC channel for structured messaging.

import { spawn } from "node:child_process";

const ls = spawn("ls", ["-l"]);

ls.stdout.on("data", (chunk) => {
  process.stdout.write(chunk);
});

Use external commands when the task already exists as a system utility or when you script build pipelines. Handle exit codes carefully so failures do not pass silently.

Worker threads for parallel CPU work

The node:worker_threads module lets you run compute heavy tasks on additional threads. Workers communicate through messages and can share memory using SharedArrayBuffer or typed arrays when appropriate.

// main.js
import { Worker } from "node:worker_threads";

const worker = new Worker(new URL("./task.js", import.meta.url), { type: "module" });
worker.on("message", (msg) => console.log("result", msg));
worker.postMessage({ n: 1000000 });

Worker threads shine when tasks are CPU bound or when you want to isolate parts of an application from each other. They add concurrency without blocking the event loop.

⚠️ Do not spawn workers for short operations. Creating a worker has overhead. Batch tasks or keep the worker alive when possible.

Files, paths, processes, and parallel work form the practical toolkit that most Node applications depend on. With these pieces in place you can move on to networking, HTTP servers, and the design of services that run reliably in production.