Skip to content

The fs Module

The fs (file system) module is one of Node.js’s most important built-ins. It lets your code read configuration files, write logs, load seed data, and work with any file on disk.

// The recommended modern approach — promises API
import { readFile, writeFile, readdir, mkdir } from 'fs/promises'
// Or the full fs object
import fs from 'fs/promises'
import { readFile } from 'fs/promises'
const content = await readFile('data.json', 'utf8')
console.log(content) // file contents as a string

The 'utf8' encoding converts the raw buffer to a string. Without it, you get a Buffer object.

import { writeFile } from 'fs/promises'
await writeFile('output.txt', 'Hello from Node.js\n', 'utf8')
// Creates the file if it doesn't exist, overwrites if it does
import { readdir } from 'fs/promises'
const files = await readdir('./src')
console.log(files) // ['index.ts', 'routes', 'middleware', ...]
import { access } from 'fs/promises'
import { constants } from 'fs'
async function fileExists(path: string): Promise<boolean> {
try {
await access(path, constants.F_OK)
return true
} catch {
return false
}
}

The fs module has synchronous versions of every function (readFileSync, writeFileSync). These block the event loop and should be avoided in server code. Use them only at startup — before the server starts handling requests:

import { readFileSync } from 'fs'
// ✅ OK at startup time — server isn't handling requests yet
const config = JSON.parse(readFileSync('config.json', 'utf8'))
// ❌ Bad in a request handler — blocks the event loop
app.get('/data', (req, res) => {
const data = readFileSync('data.json', 'utf8') // don't do this
res.json(JSON.parse(data))
})

The Bulletin API uses fs during development to load seed data into the SQLite database:

src/db/seed.ts
import { readFile } from 'fs/promises'
import { join } from 'path'
import { db } from './index.js'
const seedData = JSON.parse(
await readFile(join(process.cwd(), 'src/db/seed.json'), 'utf8')
)
for (const post of seedData.posts) {
db.prepare('INSERT INTO posts (title, body, user_id) VALUES (?, ?, ?)').run(
post.title, post.body, post.userId
)
}
  1. Create a src/utils/config.ts file that reads a config.json from the project root at startup and exports the parsed config object.
  2. Create a config.json with { "port": 3000, "dbPath": "./bulletin.db" }.
  3. Import config in src/index.ts and log the port.
  4. What happens if the file doesn’t exist? Add error handling.
  • import { readFile, writeFile } from 'fs/promises' — the modern async API.
  • readFile(path, 'utf8') returns the file content as a string.
  • writeFile(path, content) creates or overwrites a file.
  • Use async versions in request handlers; synchronous versions are only acceptable at server startup.
  • fs is used for config loading, log writing, and dev utilities — the database handles persistent app data.