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.
Importing fs
Section titled “Importing fs”// The recommended modern approach — promises APIimport { readFile, writeFile, readdir, mkdir } from 'fs/promises'
// Or the full fs objectimport fs from 'fs/promises'Reading a file
Section titled “Reading a file”import { readFile } from 'fs/promises'
const content = await readFile('data.json', 'utf8')console.log(content) // file contents as a stringThe 'utf8' encoding converts the raw buffer to a string. Without it, you get a Buffer object.
Writing a file
Section titled “Writing a file”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 doesReading a directory
Section titled “Reading a directory”import { readdir } from 'fs/promises'
const files = await readdir('./src')console.log(files) // ['index.ts', 'routes', 'middleware', ...]Checking if a file exists
Section titled “Checking if a file exists”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 }}Synchronous vs asynchronous
Section titled “Synchronous vs asynchronous”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 yetconst config = JSON.parse(readFileSync('config.json', 'utf8'))
// ❌ Bad in a request handler — blocks the event loopapp.get('/data', (req, res) => { const data = readFileSync('data.json', 'utf8') // don't do this res.json(JSON.parse(data))})Practical example: loading seed data
Section titled “Practical example: loading seed data”The Bulletin API uses fs during development to load seed data into the SQLite database:
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 )}Exercise
Section titled “Exercise”- Create a
src/utils/config.tsfile that reads aconfig.jsonfrom the project root at startup and exports the parsed config object. - Create a
config.jsonwith{ "port": 3000, "dbPath": "./bulletin.db" }. - Import
configinsrc/index.tsand log the port. - 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.
fsis used for config loading, log writing, and dev utilities — the database handles persistent app data.