Read and write files without blocking request handling
The fs module works with file contents and directories. The path module constructs and examines filesystem paths; it does not read a file. Use promise-based filesystem calls for ordinary server I/O. Synchronous calls block the main JavaScript thread and are better limited to deliberate startup or small command-line operations.
Build a local progress file
Save this as files.mjs in a practice folder. Its data folder is located beside the script, even if the command is launched from another directory.
import { mkdir, writeFile, readFile, appendFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
const here = path.dirname(fileURLToPath(import.meta.url));
const folder = path.join(here, 'data');
const report = path.join(folder, 'progress.json');
try {
await mkdir(folder, { recursive: true });
const progress = { student: 'Asha', completed: 3 };
await writeFile(report, JSON.stringify(progress, null, 2), 'utf8');
const saved = JSON.parse(await readFile(report, 'utf8'));
console.log(saved.student, saved.completed);
await appendFile(path.join(folder, 'activity.log'),
'Progress saved\n', 'utf8');
} catch (error) {
console.error('File operation failed:', error.code ?? error.name);
process.exitCode = 1;
}
Run node files.mjs. Expected console output is Asha 3. progress.json contains a readable JSON object; activity.log gains one line per run. writeFile replaces existing contents by default, while appendFile adds to the end. Omitting the text encoding from readFile returns a Buffer instead of a string.
Paths and errors
path.join handles platform-specific separators. path.extname('report.json') returns .json and path.basename('/tmp/report.json') returns report.json. ENOENT commonly means the file or a parent directory is missing; EACCES means access is denied. Handle the operation's error rather than checking existence and assuming the file cannot change before the next call.
Important server limitations
Do not concatenate a visitor's filename into an arbitrary filesystem path. path.join or path.resolve alone does not prevent directory traversal, and symbolic links introduce additional risks. Prefer server-generated file IDs with a fixed directory and controlled permissions. Do not let public upload routes overwrite application files.
This JSON example is a learning exercise, not a multi-user database. Concurrent writes can overwrite each other. Large files should be streamed rather than fully loaded into memory, and stream errors must be handled. Avoid logging personal answers or credentials in an unprotected activity file.
Practice checkpoint
Add a read-only command that prints “No progress saved yet” for ENOENT but reports other errors. Test it before the first write and after a successful write. Explain why running files.mjs twice changes the log differently from the JSON file. Do not solve a permission error by giving everyone unrestricted access.