The Node.js file system module (fs) gives your Express server the ability to read files, write files, create directories, and check if paths exist. In MERN development you will use the file system module more than you might expect โ reading .env files, writing logs, handling uploaded images saved to disk, serving static assets, and reading email templates from HTML files. In this lesson you will learn the modern fs/promises API, understand when to use it, and build practical patterns you will reuse throughout the series.
fs vs fs/promises โ Which to Use
| API | Style | Use When |
|---|---|---|
fs (callback) |
Old-style callbacks | Legacy codebases โ avoid in new code |
fs (Sync methods) |
Synchronous โ blocks event loop | One-time startup tasks only (reading config before server starts) |
fs/promises |
Promise-based โ non-blocking | Everything else โ use this in Express route handlers and middleware |
require('fs/promises') is available from Node.js 14 onwards and gives you the same functions as fs but returning Promises instead of using callbacks. Combined with async/await this makes file operations read almost like synchronous code while remaining fully non-blocking. Always prefer fs/promises in Express route handlers.path.join() when building file paths โ never concatenate strings manually with /. path.join(__dirname, 'uploads', filename) works correctly on both Windows (which uses backslashes) and Unix (which uses forward slashes). String concatenation breaks on one platform or the other.../../.env (path traversal attack) to read sensitive files outside your intended directory. Use path.basename(userFilename) to strip any directory components from a user-supplied filename before using it in a file path.Core fs/promises Operations
const fs = require('fs/promises');
const path = require('path');
// โโ Read a file โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
async function readConfig() {
try {
const filePath = path.join(__dirname, 'config.json');
const raw = await fs.readFile(filePath, 'utf8');
return JSON.parse(raw);
} catch (err) {
if (err.code === 'ENOENT') {
console.error('Config file not found');
return null;
}
throw err;
}
}
// โโ Write a file โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
async function writeLog(message) {
const logPath = path.join(__dirname, 'logs', 'app.log');
const entry = `[${new Date().toISOString()}] ${message}\n`;
await fs.appendFile(logPath, entry, 'utf8');
}
// โโ Check if a file exists โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
async function fileExists(filePath) {
try {
await fs.access(filePath);
return true;
} catch {
return false;
}
}
// โโ Create a directory (and any missing parents) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
async function ensureUploadDir(dirPath) {
await fs.mkdir(dirPath, { recursive: true });
// recursive: true โ no error if directory already exists
}
// โโ Delete a file โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
async function deleteUpload(filename) {
const filePath = path.join(__dirname, 'uploads', path.basename(filename));
await fs.unlink(filePath);
}
Reading Email Templates from Files
// A practical MERN use case โ loading HTML email templates from disk
// server/src/utils/emailTemplate.js
const fs = require('fs/promises');
const path = require('path');
/**
* Loads an HTML template file and replaces {{VARIABLE}} placeholders.
* @param {string} templateName - filename without extension e.g. 'welcome'
* @param {object} variables - e.g. { NAME: 'Jane', LINK: 'https://...' }
*/
async function loadTemplate(templateName, variables = {}) {
const templatePath = path.join(__dirname, '../templates', `${templateName}.html`);
let html = await fs.readFile(templatePath, 'utf8');
for (const [key, value] of Object.entries(variables)) {
html = html.replaceAll(`{{${key}}}`, value);
}
return html;
}
module.exports = { loadTemplate };
Handling File Uploads โ Saving to Disk
// When Multer saves an uploaded file, you sometimes need to rename or move it
const fs = require('fs/promises');
const path = require('path');
async function saveUploadedFile(tempPath, originalName, userId) {
// Create user-specific upload directory
const uploadDir = path.join(__dirname, '..', 'uploads', userId.toString());
await fs.mkdir(uploadDir, { recursive: true });
// Sanitise the filename โ remove directory traversal characters
const safeName = path.basename(originalName);
const finalPath = path.join(uploadDir, `${Date.now()}-${safeName}`);
// Move from temp location to final destination
await fs.rename(tempPath, finalPath);
return finalPath;
}
path Module โ Essential Methods
| Method | Example | Result |
|---|---|---|
path.join() |
path.join(__dirname, 'uploads', file) |
Cross-platform path string |
path.resolve() |
path.resolve('./src/config') |
Absolute path from cwd |
path.basename() |
path.basename('/uploads/img.jpg') |
'img.jpg' |
path.extname() |
path.extname('photo.jpg') |
'.jpg' |
path.dirname() |
path.dirname('/src/routes/posts.js') |
'/src/routes' |
__dirname |
__dirname |
Absolute path of current file’s directory |
__filename |
__filename |
Absolute path of current file |
Common Mistakes
Mistake 1 โ Using fs.existsSync() in route handlers
โ Wrong โ synchronous existence check blocks the event loop:
if (fs.existsSync(uploadPath)) { // blocks!
fs.unlinkSync(uploadPath); // also blocks!
}
โ Correct โ use async versions:
try {
await fs.access(uploadPath); // throws if file does not exist
await fs.unlink(uploadPath); // delete it
} catch { /* file did not exist โ that is fine */ }
Mistake 2 โ Building paths with string concatenation
โ Wrong โ hardcoded forward slashes break on Windows:
const filePath = __dirname + '/uploads/' + filename; // breaks on Windows
โ Correct โ use path.join() for cross-platform compatibility:
const filePath = path.join(__dirname, 'uploads', filename); // works everywhere โ
Mistake 3 โ Not handling ENOENT errors
โ Wrong โ letting a missing file throw an unhandled error to the client:
const data = await fs.readFile(path);
// If file is missing โ UnhandledPromiseRejection โ server crash or 500 error
โ Correct โ check the error code and respond appropriately:
try {
const data = await fs.readFile(filePath, 'utf8');
res.json({ data });
} catch (err) {
if (err.code === 'ENOENT') return res.status(404).json({ message: 'File not found' });
next(err); // unexpected error โ pass to global error handler
}
Quick Reference
| Task | fs/promises code |
|---|---|
| Read file as string | await fs.readFile(path, 'utf8') |
| Write file | await fs.writeFile(path, data, 'utf8') |
| Append to file | await fs.appendFile(path, data) |
| Delete file | await fs.unlink(path) |
| Create directory | await fs.mkdir(path, { recursive: true }) |
| List directory | await fs.readdir(path) |
| Check file exists | await fs.access(path) (throws if missing) |
| Move / rename file | await fs.rename(oldPath, newPath) |
| Get file info | await fs.stat(path) |