Course:Node.js & Express/
Lesson

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); // 3000
If the file does not exist, readFileSync() throws an error with code ENOENT. Wrap it in a try/catch if the file might be absent, or check first with existsSync().
02

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.

03

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.

04

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.000Z

This 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.

05

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);
});
The reason the last example is so harmful: Node.js runs on a single thread. While 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.
06

Quick reference

MethodWhat 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