Node.jsBackendFull-Stack

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

TT
TopicTrick Team
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:

javascript
// 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:

javascript
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

javascript
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

javascript
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

CodeMeaning
ENOENTFile or directory does not exist
EACCESPermission denied
EISDIRExpected a file but found a directory
EEXISTFile already exists (when using exclusive create flags)
ENOSPCNo space left on device

Read a JSON File

javascript
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

javascript
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. Use fs.access() inside a try/catch as shown above.


Writing Files

Write a File (overwrites if it exists)

javascript
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:

javascript
// 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

javascript
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:

javascript
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

javascript
await fs.unlink(path.join(__dirname, 'temp.txt'))

Rename or Move a File

javascript
// 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

javascript
// 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

javascript
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 date

Working with Directories

Create a Directory

javascript
// 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

javascript
// 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

javascript
// 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

javascript
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

javascript
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)

javascript
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:

javascript
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

javascript
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:

javascript
// 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),
}
javascript
// 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/promises in servers — never block the event loop with sync methods
    • Use the path module to build file paths — never string concatenation
    • readFile / writeFile / appendFile for standard file operations
    • mkdir({ recursive: true }) for creating nested directories safely
    • fs.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