Working with the Node.js File System (fs module)

Working with the Node.js File System (fs module)
Reading configuration files, writing logs, processing uploaded files, building CLI tools, generating reports — virtually every server-side application touches the file system. Node.js provides the built-in fs module for all of it, with three flavours: callback-based, promise-based, and synchronous.
This module covers every operation you will actually use in production: reading, writing, appending, deleting, copying, watching files, working with directories, streaming large files, and building safe paths with the path module.
This is Module 7 of the Node.js Full‑Stack Developer course.
The Three fs APIs
Node.js ships three versions of the file system API:
// 1. Callback-based (original)
const fs = require('fs')
fs.readFile('file.txt', 'utf8', (err, data) => { ... })
// 2. Promise-based (modern — use this)
const fs = require('fs/promises')
const data = await fs.readFile('file.txt', 'utf8')
// 3. Synchronous (blocks the event loop — avoid in servers)
const fs = require('fs')
const data = fs.readFileSync('file.txt', 'utf8')Use fs/promises for everything in a server. We will use it throughout this module. The synchronous API is only appropriate for startup tasks before your server begins accepting requests.
The path Module — Safe File Paths
Before touching the file system, always use the path module to build file paths. String concatenation breaks on different operating systems:
const path = require('path')
// ❌ Fragile — breaks on Windows (uses backslashes)
const filePath = __dirname + '/data/' + filename
// ✅ Safe — works on all platforms
const filePath = path.join(__dirname, 'data', filename)Essential path Methods
const path = require('path')
// Join path segments safely
path.join('/users', 'alice', 'docs', 'file.txt')
// → /users/alice/docs/file.txt
// Resolve to an absolute path
path.resolve('src', 'index.js')
// → /current/working/directory/src/index.js
// Get the directory of a file
path.dirname('/users/alice/file.txt')
// → /users/alice
// Get the filename from a path
path.basename('/users/alice/file.txt')
// → file.txt
// Get just the filename without extension
path.basename('/users/alice/file.txt', '.txt')
// → file
// Get the file extension
path.extname('/users/alice/file.txt')
// → .txt
// Parse a path into its parts
path.parse('/users/alice/file.txt')
// → { root: '/', dir: '/users/alice', base: 'file.txt', ext: '.txt', name: 'file' }Always use path.join with __dirname to build file paths relative to the current file. This ensures your code works correctly no matter where it is run from.
Reading Files
Read an Entire File
const fs = require('fs/promises')
const path = require('path')
async function readConfig() {
const filePath = path.join(__dirname, 'config.json')
try {
const data = await fs.readFile(filePath, 'utf8')
return JSON.parse(data)
} catch (err) {
if (err.code === 'ENOENT') {
console.error('Config file not found')
} else {
console.error('Failed to read config:', err.message)
}
throw err
}
}
const config = await readConfig()
console.log(config)The 'utf8' encoding returns a string. Without it, readFile returns a raw Buffer.
Common File Error Codes
| Code | Meaning |
|---|---|
ENOENT | File or directory does not exist |
EACCES | Permission denied |
EISDIR | Expected a file but found a directory |
EEXIST | File already exists (when using exclusive create flags) |
ENOSPC | No space left on device |
Read a JSON File
async function readJSON(filePath) {
const raw = await fs.readFile(filePath, 'utf8')
return JSON.parse(raw)
}
// Usage
const users = await readJSON(path.join(__dirname, 'data', 'users.json'))Check if a File Exists
async function fileExists(filePath) {
try {
await fs.access(filePath)
return true
} catch {
return false
}
}
if (await fileExists('./config.json')) {
const config = await readJSON('./config.json')
}Note: Do not use
fs.exists()— it was deprecated. Usefs.access()inside a try/catch as shown above.
Writing Files
Write a File (overwrites if it exists)
async function writeConfig(config) {
const filePath = path.join(__dirname, 'config.json')
const content = JSON.stringify(config, null, 2) // pretty-print with 2-space indent
await fs.writeFile(filePath, content, 'utf8')
console.log('Config saved')
}
await writeConfig({ port: 3000, db: 'mongodb://localhost/myapp' })Write Flags
You can control write behaviour with the flag option:
// Overwrite (default)
await fs.writeFile('file.txt', 'content', { flag: 'w' })
// Create only — fails if file already exists
await fs.writeFile('file.txt', 'content', { flag: 'wx' })
// Append
await fs.writeFile('file.txt', 'new line\n', { flag: 'a' })Append to a File
async function appendLog(message) {
const logPath = path.join(__dirname, 'logs', 'app.log')
const timestamp = new Date().toISOString()
const line = `[${timestamp}] ${message}\n`
await fs.appendFile(logPath, line, 'utf8')
}
await appendLog('Server started on port 3000')
await appendLog('User alice logged in')Write a File Atomically (Safe Writes)
For critical files (config, data), write to a temp file first, then rename — this prevents corruption if the process crashes mid-write:
const os = require('os')
async function writeFileSafe(filePath, content) {
const tmpPath = path.join(os.tmpdir(), `tmp-${Date.now()}-${path.basename(filePath)}`)
try {
await fs.writeFile(tmpPath, content, 'utf8')
await fs.rename(tmpPath, filePath) // atomic on most OSes
} catch (err) {
// Clean up temp file if rename failed
await fs.unlink(tmpPath).catch(() => {})
throw err
}
}Deleting, Renaming, and Copying Files
Delete a File
await fs.unlink(path.join(__dirname, 'temp.txt'))Rename or Move a File
// Rename
await fs.rename('old-name.txt', 'new-name.txt')
// Move to a different directory (same as rename with full path)
await fs.rename(
path.join(__dirname, 'uploads', 'temp.jpg'),
path.join(__dirname, 'images', 'avatar.jpg')
)Copy a File
// Simple copy (Node 16.7+)
await fs.copyFile('source.txt', 'destination.txt')
// Copy only if destination doesn't already exist
const { COPYFILE_EXCL } = require('fs').constants
await fs.copyFile('source.txt', 'destination.txt', COPYFILE_EXCL)Get File Metadata
const stats = await fs.stat(path.join(__dirname, 'data.json'))
console.log(stats.size) // file size in bytes
console.log(stats.isFile()) // true
console.log(stats.isDirectory()) // false
console.log(stats.mtime) // last modified date
console.log(stats.birthtime) // creation dateWorking with Directories
Create a Directory
// Create a single directory
await fs.mkdir(path.join(__dirname, 'uploads'))
// Create nested directories (like mkdir -p)
await fs.mkdir(path.join(__dirname, 'uploads', '2026', 'may'), { recursive: true })List Directory Contents
// List filenames
const files = await fs.readdir(path.join(__dirname, 'uploads'))
console.log(files) // ['file1.jpg', 'file2.png', ...]
// List with file type info
const entries = await fs.readdir(path.join(__dirname, 'uploads'), { withFileTypes: true })
entries.forEach(entry => {
if (entry.isFile()) console.log('File:', entry.name)
if (entry.isDirectory()) console.log('Dir:', entry.name)
})Remove a Directory
// Remove empty directory
await fs.rmdir(path.join(__dirname, 'empty-folder'))
// Remove directory and all contents (like rm -rf)
await fs.rm(path.join(__dirname, 'uploads'), { recursive: true, force: true })Recursively List All Files in a Directory
async function listAllFiles(dirPath) {
const entries = await fs.readdir(dirPath, { withFileTypes: true })
const files = []
for (const entry of entries) {
const fullPath = path.join(dirPath, entry.name)
if (entry.isDirectory()) {
const nested = await listAllFiles(fullPath) // recurse
files.push(...nested)
} else {
files.push(fullPath)
}
}
return files
}
const allFiles = await listAllFiles(path.join(__dirname, 'src'))
console.log(allFiles)Streaming Large Files
For large files (logs, CSVs, media), loading the entire file into memory with readFile is a bad idea — you could easily exhaust available RAM. Streams read and process data in chunks:
Read a Large File with a Stream
const fs = require('fs') // streams use the callback fs, not fs/promises
function countLines(filePath) {
return new Promise((resolve, reject) => {
let lineCount = 0
const stream = fs.createReadStream(filePath, { encoding: 'utf8' })
stream.on('data', chunk => {
// Count newlines in each chunk
for (let i = 0; i < chunk.length; i++) {
if (chunk[i] === '\n') lineCount++
}
})
stream.on('end', () => resolve(lineCount))
stream.on('error', reject)
})
}
const lines = await countLines('/var/log/large-app.log')
console.log(`Total lines: ${lines}`)Copy a Large File with Streams (pipe)
const fs = require('fs')
function copyLargeFile(src, dest) {
return new Promise((resolve, reject) => {
const readStream = fs.createReadStream(src)
const writeStream = fs.createWriteStream(dest)
readStream.pipe(writeStream)
writeStream.on('finish', resolve)
readStream.on('error', reject)
writeStream.on('error', reject)
})
}
await copyLargeFile('/data/bigfile.csv', '/backup/bigfile.csv')Using stream/promises for Cleaner Stream Code
Node.js 15+ includes stream/promises which makes stream handling async/await friendly:
const { pipeline } = require('stream/promises')
const fs = require('fs')
const zlib = require('zlib')
// Compress a file using streams — memory-efficient even for huge files
async function compressFile(inputPath, outputPath) {
await pipeline(
fs.createReadStream(inputPath),
zlib.createGzip(),
fs.createWriteStream(outputPath)
)
console.log(`Compressed ${inputPath} → ${outputPath}`)
}
await compressFile('large-data.csv', 'large-data.csv.gz')pipeline handles backpressure automatically and cleans up streams on error — always prefer it over manual .pipe().
Watching Files for Changes
const fs = require('fs')
// Watch a single file
fs.watchFile('config.json', { interval: 1000 }, (curr, prev) => {
if (curr.mtime > prev.mtime) {
console.log('config.json was modified — reloading...')
// reload config
}
})
// Watch a directory recursively
const watcher = fs.watch('./src', { recursive: true }, (eventType, filename) => {
console.log(`${eventType}: ${filename}`)
// 'change': file.js, 'rename': newfile.js
})
// Stop watching
watcher.close()fs.watch is faster and more efficient than fs.watchFile for most use cases. Tools like nodemon, webpack, and Vite use this internally.
Practical Example: A Simple File-Based Logger
Combining everything from this module into a real utility:
// lib/logger.js
const fs = require('fs/promises')
const path = require('path')
const LOG_DIR = path.join(__dirname, '..', 'logs')
async function ensureLogDir() {
await fs.mkdir(LOG_DIR, { recursive: true })
}
async function log(level, message) {
await ensureLogDir()
const timestamp = new Date().toISOString()
const logFile = path.join(LOG_DIR, `${new Date().toISOString().slice(0, 10)}.log`)
const line = `[${timestamp}] [${level.toUpperCase()}] ${message}\n`
await fs.appendFile(logFile, line, 'utf8')
// Also print to console
const consoleFn = level === 'error' ? console.error : console.log
consoleFn(line.trim())
}
module.exports = {
info: (msg) => log('info', msg),
warn: (msg) => log('warn', msg),
error: (msg) => log('error', msg),
}// Usage anywhere in your app
const logger = require('./lib/logger')
await logger.info('Server started on port 3000')
await logger.warn('Rate limit approaching for user 42')
await logger.error('Database connection failed')Node.js Full‑Stack Course — Module 7 of 32
You can now confidently read, write, stream, and manage files in Node.js. Continue to Module 8 to master npm and package.json — the foundation of every Node.js project.
Summary
The fs module gives you complete control over the file system from Node.js. Here is what to remember:
- Always use
fs/promisesin servers — never block the event loop with sync methods - Use the
pathmodule to build file paths — never string concatenation readFile/writeFile/appendFilefor standard file operationsmkdir({ recursive: true })for creating nested directories safelyfs.stat()for file metadata;fs.access()to check existence- Use streams (
createReadStream,createWriteStream,pipeline) for large files fs.watch()for monitoring file changes during development- Write critical files atomically with write-to-temp then
rename()
Continue to Module 8: npm & package.json — Dependency Management Mastered →
Content powered by SearchFit.ai — for automated content at scale, visit https://searchfit.ai
