Every web application eventually needs to touch the file system, reading a configuration file at startup, saving an uploaded image, writing a log entry, or generating a report. Node.js provides all of this through the built-in fs moduleWhat is module?A self-contained file of code with its own scope that explicitly exports values for other files to import, preventing name collisions.. Before you reach for the async version (which we will cover in the next lesson), it pays to understand the synchronous APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses., both because you will encounter it in real codebases and because knowing its limitations will help you decide when it is safe to use it.
Think of synchronous file operations like a phone call where you put your entire life on hold until the other person picks up. Your program stops, waits, and only continues once the file operation is done. For a server handling hundreds of simultaneous users, that pause can be catastrophic.
Reading files
The most common operation is reading a file's contents. readFileSync() returns the data either as a Buffer (raw bytes) or as a string if you specify an encoding:
import { readFileSync } from 'fs';
// Read as UTF-8 text - the second argument sets encoding
const content = readFileSync('./data.txt', 'utf-8');
console.log(content);
// Read as a Buffer (raw binary) - useful for images, PDFs, etc.
const buffer = readFileSync('./logo.png');
console.log(`File size: ${buffer.length} bytes`);When reading JSONWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it. configuration files, a common pattern is to parse the result immediately:
const config = JSON.parse(readFileSync('./config.json', 'utf-8'));
console.log(config.port); // 3000readFileSync() throws an error with code ENOENT. Wrap it in a try/catch if the file might be absent, or check first with existsSync().Writing and appending files
writeFileSync() creates the file if it does not exist and overwrites it if it does. Use appendFileSync() to add content at the end without touching existing data:
import { writeFileSync, appendFileSync } from 'fs';
// Create or overwrite
writeFileSync('./output.txt', 'Hello, World!');
// Append a new line (the \n is important for log files)
appendFileSync('./app.log', `[${new Date().toISOString()}] Server started\n`);Both functions accept an options object as the third argument. You can control the file encoding or mode (permissions) there, though the defaults work for most cases.
Working with directories
The fs moduleWhat is module?A self-contained file of code with its own scope that explicitly exports values for other files to import, preventing name collisions. can also create, list, and remove directories. The recursive: true option is particularly useful when creating nested folder structures:
import { mkdirSync, readdirSync, rmdirSync, existsSync } from 'fs';
// Create a single directory
mkdirSync('./uploads');
// Create nested directories in one call - no need to create each level manually
mkdirSync('./project/src/components', { recursive: true });
// List the contents of a directory
const files = readdirSync('./src');
console.log(files);
// ['components', 'utils', 'app.js', 'index.js']
// Check if a path exists before trying to open or create it
if (existsSync('./config.json')) {
const config = JSON.parse(readFileSync('./config.json', 'utf-8'));
}
// Remove an empty directory (throws if it contains files)
rmdirSync('./temp-folder');For removing a directory that has contents, you need rm() with { recursive: true }, which we will see in the async lesson.
Inspecting file metadata
statSync() returns a Stats object packed with information about a file or directory, its size, type, and timestamps:
import { statSync } from 'fs';
const stats = statSync('./package.json');
console.log(stats.isFile()); // true
console.log(stats.isDirectory()); // false
console.log(stats.size); // 1248 (bytes)
console.log(stats.birthtime); // 2024-01-15T09:30:00.000Z
console.log(stats.mtime); // 2024-03-22T14:05:12.000ZThis is handy for build scripts that need to check whether a source file is newer than its compiled output, or for validating file size before processing.
When sync is acceptable
The rule is simple: synchronous file operations are safe when they happen once and before your server begins accepting requests. They are never safe inside code that runs per-request.
// GOOD - loading config once at startup, before the server starts
const config = JSON.parse(readFileSync('./config.json', 'utf-8'));
const app = express();
app.listen(config.port);// GOOD - a build script that runs as a one-off CLI process
const template = readFileSync('./templates/email.html', 'utf-8');
const rendered = template.replace('{{name}}', 'Alice');
writeFileSync('./output/welcome.html', rendered);// BAD - blocks every other user while this one user's file is being read
app.get('/download/:id', (req, res) => {
const data = readFileSync(`./files/${req.params.id}`); // Never do this
res.send(data);
});readFileSync holds that thread hostage reading one user's file, all other incoming requests are queued and waiting. For a small file this might be milliseconds; for a large file it could be seconds, effectively a self-inflicted denial of service.Quick reference
| Method | What it does |
|---|---|
readFileSync(path, encoding) | Read a file, returns string or Buffer |
writeFileSync(path, data) | Write (or overwrite) a file |
appendFileSync(path, data) | Append to the end of a file |
mkdirSync(path, options) | Create a directory |
readdirSync(path) | List directory contents |
existsSync(path) | Check if a path exists (returns boolean) |
statSync(path) | Get file metadata (size, dates, type) |
rmdirSync(path) | Remove an empty directory |