The path Module
Paths look like the most boring topic in this course and are responsible for a startling share of the bugs that appear only after deployment. The reason is that a path is a string, so JavaScript will happily let you build one by gluing pieces together — and gluing works on your laptop and then fails on the server.
The first problem the path module solves is separators. Windows writes C:\Users\prem\project with backslashes; Linux and macOS write /home/prem/project with forward slashes. Nearly every deployment target is Linux, so a student developing on Windows who writes folder + '\\' + file ships code that cannot find anything in production. path.join inserts whichever separator the current system uses, so the same source works everywhere.
The second problem is the small ugly cases. Join 'data/' and '/users.json' by hand and you get data//users.json. Join 'src' and '../config' and you get a literal src/../config that some tools accept and others do not. path.join normalises all of it: duplicate separators collapse, and .. segments are resolved away, so what comes out is a clean canonical path.
path.join and path.resolve are frequently confused. join simply concatenates the pieces and tidies them; the result can be relative. resolve guarantees an absolute path, and it does so by walking the arguments from right to left until it has one — which means a leading slash in any argument throws away everything before it. Use join when you are combining a known base with a filename, and resolve when you need something absolute regardless of where the program was started.
The rest of the module is inspection rather than construction: basename for the filename, dirname for the folder above it, extname for the extension, and parse for all of them at once as an object. These are what you use to work out a file's type before serving it, or to strip an extension when generating a thumbnail name.
const path = require('path');
// Join path segments safely
const filePath = path.join(__dirname, 'data', 'users.json');
console.log(filePath);
// /home/user/project/data/users.json
// Resolve to an absolute path
const absolute = path.resolve('src', 'index.js');
console.log(absolute);
// /home/user/project/src/index.js
// Extract parts of a path
const file = '/home/user/project/app.js';
console.log(path.basename(file)); // 'app.js'
console.log(path.dirname(file)); // '/home/user/project'
console.log(path.extname(file)); // '.js'
// Parse a path into an object
console.log(path.parse(file));
// { root: '/', dir: '/home/user/project',
// base: 'app.js', ext: '.js', name: 'app' } path.join(a, b)— combine and tidy; may stay relativepath.resolve(a, b)— always absolute; a leading/in any part discards what came beforepath.basename(p)— the filename; pass an extension to strip itpath.dirname(p)— the folder containing itpath.extname(p)— the extension, including the dotpath.parse(p)— root, dir, base, name and ext as one objectpath.sep— the separator this operating system uses, if you ever need it literally
- Never build a path with
+or a template string. It is the kind of code that passes every test on a Windows laptop and then breaks the moment it is deployed to a Linux server, which is the worst possible time to discover it.
__dirname vs process.cwd() — the Real Source of ENOENT
Two different "here" exist in a Node program, and confusing them is the most common cause of ENOENT: no such file or directory.
__dirname is the folder containing the source file you are writing in. It never changes, no matter how the program was launched. process.cwd() is the current working directory: the folder your terminal was sitting in when you typed the command. A bare relative path such as 'data/users.json' is resolved against process.cwd(), not against your file.
Here is how that bites. Your project has src/app.js, which reads data/users.json. You test it by running node app.js from inside src/, and everything works — the working directory is src. Later you add an npm script and run npm start from the project root. Nothing about your code changed, but the working directory is now the root, Node looks for <root>/data/users.json, and you get ENOENT on a file you can see with your own eyes. The same failure appears when a deployment platform starts your app from a different folder.
The fix is to anchor every path to the file that uses it: path.join(__dirname, 'data', 'users.json'). Now the location is fixed relative to your source code, and the program behaves identically whether it was started from the root, from src, or by a process manager on a server.
The one legitimate use of process.cwd() is when the user's location is genuinely the point — a command-line tool that formats "the files in whichever folder you ran me from" should use the working directory, because that is what the user meant.
const path = require('node:path');
const fsp = require('node:fs/promises');
// FRAGILE — depends on where the terminal happened to be
await fsp.readFile('data/users.json', 'utf8');
// ROBUST — anchored to this source file, works from anywhere
const usersPath = path.join(__dirname, 'data', 'users.json');
await fsp.readFile(usersPath, 'utf8');
// Print both when a path bug confuses you
console.log('cwd :', process.cwd()); // where node was STARTED
console.log('__dirname:', __dirname); // where THIS FILE lives
// Common anchored paths in a real project
const PUBLIC_DIR = path.join(__dirname, '..', 'public');
const UPLOADS_DIR = path.join(__dirname, '..', 'uploads');
const VIEWS_DIR = path.join(__dirname, 'views'); __dirname— the folder of the current source file; fixed, predictable, almost always what you want__filename— the same but including the filenameprocess.cwd()— where the terminal was when the program started; changes with how you launch it- Relative paths passed to
fsresolve againstprocess.cwd(), not against your file - Use
process.cwd()only when the user's own location is the intended meaning
- When a file read fails and you are certain the file exists, print
process.cwd()before anything else. In most cases the answer appears immediately and you have saved yourself twenty minutes of staring at a correct-looking filename.
Paths in ES Modules
If your project uses ES modules — that is, "type": "module" in package.json — then __dirname and __filename do not exist. They were never part of the JavaScript language; they were variables CommonJS injected into every module. Reference them in an ES module and you get ReferenceError: __dirname is not defined, which is startling the first time because the code was working an hour ago under CommonJS.
What ES modules give you instead is import.meta.url, the location of the current module expressed as a URL string such as file:///home/prem/project/src/app.js. It is a URL rather than a path, so you cannot hand it to fs directly — on Windows especially, the two formats differ enough to matter. The fileURLToPath helper from the built-in node:url module converts it properly, and once converted, path.dirname gives you the folder.
Three lines at the top of the file recreate both variables, and that is the conventional fix you will see in every ESM Node project. Write them once per file that needs them and carry on as before.
Do not try to shortcut this by stripping file:// from the string yourself. It works on Linux, which is why people ship it, and then produces a path like /C:/Users/... on Windows that no file operation will accept.
// Top of an ES module that needs paths
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Now use them exactly as in CommonJS
import fs from 'node:fs/promises';
const config = JSON.parse(
await fs.readFile(path.join(__dirname, 'config.json'), 'utf8')
);
// What NOT to do — breaks on Windows
// const dir = import.meta.url.replace('file://', ''); // gives /C:/Users/... - The same conversion is needed anywhere a URL meets the file system. If a library hands you a
file:URL andfsrejects it,fileURLToPathis almost certainly the missing step.
Never Join a User-Supplied Path Directly
The moment a path segment comes from a request, paths stop being a convenience topic and become a security one. The attack has a name — path traversal — and it is one of the easiest serious vulnerabilities to introduce by accident.
Suppose you serve uploaded files with path.join(UPLOADS_DIR, req.params.filename). That looks safe: everything is inside the uploads folder. But path.join resolves .. segments, and a request for ../../.env therefore produces a path outside the uploads folder. Your server, doing exactly what you told it, reads your environment file and sends the database password and JWT secret to whoever asked. The same trick reaches configuration files, source code, and on a badly configured Linux box, system files.
There are two dependable defences and you should use both. First, strip the directory portion of whatever the user sent with path.basename, which turns ../../.env into .env — a name that will simply not be found in your uploads folder. Second, resolve the final path and confirm it really is inside the folder you intended, using path.relative to check that escaping it would require a ... The second check is what protects you when the first is not enough, for example against symbolic links or URL-encoded separators.
Better still, do not let users choose filenames at all. Store uploads under an identifier you generate, such as crypto.randomUUID(), and keep the original name in your database as a label. Then no request can name a file on disk, and the entire class of attack disappears rather than being defended against.
This matters for a student project more than it seems. A public repository plus a deployed demo plus one traversal bug is enough to leak a real API key, and automated scanners look for exactly this pattern.
const path = require('node:path');
const UPLOADS_DIR = path.join(__dirname, '..', 'uploads');
// VULNERABLE — a request for ../../.env escapes the folder
app.get('/files/:name', (req, res) => {
res.sendFile(path.join(UPLOADS_DIR, req.params.name));
});
// SAFER — strip any directory part the user tried to send
const safeName = path.basename(req.params.name);
// SAFEST — resolve, then prove it is still inside UPLOADS_DIR
function resolveInside(baseDir, userInput) {
const target = path.resolve(baseDir, path.basename(userInput));
const rel = path.relative(baseDir, target);
if (rel.startsWith('..') || path.isAbsolute(rel)) {
throw new Error('Invalid file path');
}
return target;
}
app.get('/files/:name', (req, res, next) => {
try {
res.sendFile(resolveInside(UPLOADS_DIR, req.params.name));
} catch {
res.status(400).json({ error: 'Invalid file name' });
}
}); path.basename()on user input removes any directory the attacker tried to includepath.resolve()plus apath.relative()check proves the result stayed inside your folder- Generate your own filenames for uploads and keep the original name only as a label
- Reject rather than sanitise silently — a request containing
..is not an honest mistake - The same rule applies to any path built from a query string, a header or a JSON body
- Express's
res.sendFilerejects relative paths and requires either an absolute path or arootoption, which helps — but it is not a substitute for validating the segment yourself. Treat the framework's check as a second line of defence, not the first.
Practical Path Examples
In real applications, you frequently use path with fs to read configuration files, serve static assets, or manage upload directories. Using path.join with __dirname ensures your file references work regardless of where the script is run from.
const path = require('path');
const fs = require('fs').promises;
async function loadConfig() {
// Always resolve relative to the current file
const configPath = path.join(__dirname, 'config', 'settings.json');
const raw = await fs.readFile(configPath, 'utf8');
return JSON.parse(raw);
}
// Serving static files from a 'public' folder
const publicDir = path.join(__dirname, 'public');
// Get file extension to set Content-Type
function getContentType(filePath) {
const ext = path.extname(filePath).toLowerCase();
const types = {
'.html': 'text/html',
'.css': 'text/css',
'.js': 'application/javascript',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg'
};
return types[ext] || 'application/octet-stream';
} - In ES Modules, __dirname is not available. Use import.meta.url with fileURLToPath instead: const __dirname = path.dirname(fileURLToPath(import.meta.url));
