In short
Logs, uploads, config files, generated reports: backends touch the disk all the time. Node's fs module handles it all, as long as you pick the right flavour and build paths properly.
Node's built-in module for reading, writing and manipulating files on disk — both synchronous and asynchronous (Promise-based) variants.

Think of it like this
The college record room
The record room clerk can fetch a file, add a line to a register, open a new folder or list what's in an almirah. You just hand over a request slip. Node's fs module is that clerk for your server's disk, and fs/promises lets you wait for him politely.
Watch it happen
Writing a log file
mkdir logs, recursive
1/7Create the folder. recursive: true means no error if it already exists.
The big ideas
What you need to know
Three flavours
Callback (fs.readFile), sync (fs.readFileSync) and promise (fs/promises). Use promises in servers, sync only at startup.
Paths need care
Relative paths start from where you ran node, not from the file. Build them with import.meta.dirname and path.join.
Write vs append
writeFile replaces the whole file. appendFile adds to the end, which is what you want for logs.
Errors have codes
Check err.code: ENOENT means not found, EEXIST already exists, EACCES permission denied.
See it in code
Walk through the code
1import { mkdir, appendFile, readFile, readdir, stat } from "node:fs/promises";2import path from "node:path";3 4const logDir = path.join(import.meta.dirname, "logs");5const logFile = path.join(logDir, "app.log");6await mkdir(logDir, { recursive: true });7await appendFile(logFile, `${new Date().toISOString()} server started\n`);8 9console.log(await readdir(logDir)); // [ 'app.log' ]10console.log((await stat(logFile)).size, "bytes");11 12try {13 await readFile(path.join(logDir, "missing.log"), "utf8");14} catch (err) {15 if (err.code === "ENOENT") console.log("No such file, and that's fine");16}1/4Build paths from the script's own folder, so they don't depend on where you ran node.
Save as fs-demo.mjs and run it twice: the log grows by one line each time. import.meta.dirname (Node 20.11+) keeps paths working whichever folder you run it from.
Save yourself the pain
Common mistakes
Oops
Using "./data.json" and getting ENOENT when the app is started from another folder.
Do this instead
Relative paths resolve from the current working directory. Build paths with import.meta.dirname and path.join.
Oops
Calling fs.existsSync before every read or write.
Do this instead
The file can change between the check and the use. Just try the operation and handle ENOENT or EEXIST in catch.
Your mission
Build a tiny logger
Quick check
You run node scripts/report.mjs from the project root. The script reads "./template.html", which sits next to it in scripts/. It fails with ENOENT. Why?
Go deeper
- 1Read and write a file using both `fs.promises` and the callback-based API.
- 2Practice watching a directory for changes with `fs.watch`.
- 3Learn why you should almost always prefer the async `fs` methods on a server.
More topics at this stop
Stuck on this topic? Ask an Insider
Get 1:1 guidance from people who've walked this exact path — on the InsideEdge app.