Course:Node.js & Express/
Lesson

Every time you work with the file system, you need file paths. It is tempting to just write './src/components/' + filename and move on, but this approach breaks the moment someone runs your code on a different operating system. Windows uses backslashes as path separators; macOS and Linux use forward slashes. A path you build by hand will silently produce the wrong result, or throw an error outright, on the other platform.

The built-in path moduleWhat is module?A self-contained file of code with its own scope that explicitly exports values for other files to import, preventing name collisions. solves this. It is a small collection of functions that handle path construction, inspection, and normalizationWhat is normalization?Organizing database tables to reduce duplicated data by splitting information into related tables connected by foreign keys. for you, correctly on every OS. Professional Node.js projects treat raw string concatenation for paths as a code smellWhat is code smell?A pattern in code that isn't a bug but signals a design problem that will make future changes harder..

Building paths safely

path.joinWhat is join?A SQL operation that combines rows from two or more tables based on a shared column, letting you query related data in one request.()

path.join() takes any number of path segments and joins them with the correct separator for the current operating system:

import { join } from 'path';

// Produces 'src/components/Button.js' on Unix
// Produces 'src\components\Button.js' on Windows
const componentPath = join('src', 'components', 'Button.js');

// Handles redundant slashes and '..' segments gracefully
join('/foo', 'bar', '../baz');
// '/foo/baz'

// Combine with __dirname for an absolute path to your project files
join(__dirname, 'public', 'index.html');

Think of path.join() as the safe version of string concatenation. You provide the logical segments; it handles the OS-specific glue.

path.resolve()

While path.join() just connects segments, path.resolve() acts like running cd commands in a terminalWhat is terminal?A text-based interface where you type commands to interact with your computer. Also called the command line or shell., it builds an absolute path anchored to the current working directory (or to a specific starting point you provide):

import { resolve } from 'path';

// Resolves relative to the process's current working directory
resolve('src', 'index.js');
// '/Users/alice/project/src/index.js'

// Starting from a specific absolute directory
resolve('/var/app', '../config', 'default.json');
// '/var/config/default.json'
path.resolve() processes segments right-to-left. If any segment is an absolute path, everything to its left is discarded. resolve('/tmp', '/etc', 'hosts') returns '/etc/hosts', not '/tmp/etc/hosts'. This surprises people, so remember it.
02

Inspecting paths

Once you have a path, whether it came from user input, a config file, or the OS, you often need to extract specific parts of it. The path moduleWhat is module?A self-contained file of code with its own scope that explicitly exports values for other files to import, preventing name collisions. gives you several tools for this.

Extracting components

import { parse, basename, dirname, extname } from 'path';

const filePath = '/home/alice/project/src/app.js';

// parse() gives you everything at once
parse(filePath);
// {
//   root: '/',
//   dir: '/home/alice/project/src',
//   base: 'app.js',
//   ext: '.js',
//   name: 'app'
// }

// Individual helpers for when you need just one piece
basename(filePath);           // 'app.js'
basename(filePath, '.js');    // 'app'  (strips the extension)
dirname(filePath);            // '/home/alice/project/src'
extname(filePath);            // '.js'

basename() with a second argument is particularly useful when you need just the file's name without its extension, for example, when generating an output file name from an input path.

Checking and converting

import { isAbsolute, relative, normalize } from 'path';

// Is this path absolute or relative?
isAbsolute('/home/alice/app.js');  // true
isAbsolute('./utils.js');          // false

// Get the relative path between two absolute paths
relative('/home/alice/project', '/home/alice/project/src/app.js');
// 'src/app.js'

// Clean up a messy path
normalize('/foo/bar//baz/../qux');
// '/foo/bar/qux'

path.relative() comes up often in build tools, for example, when you want to display a short file path in a warning message rather than the full absolute path.

03

__dirname in ESMWhat is es modules?The official JavaScript module standard using import and export - enabled in Node.js via "type": "module" in package.json.

CommonJSWhat is commonjs?Node.js's original module format using require() and module.exports - distinct from and predating the ES module import/export syntax. automatically provides __dirname (directory of the current moduleWhat is module?A self-contained file of code with its own scope that explicitly exports values for other files to import, preventing name collisions.) and __filename (full path of the current file). ES Modules do not include these, but you can reconstruct them from import.meta.url:

import { fileURLToPath } from 'url';
import { dirname, join } from 'path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

Once you have __dirname, you use it exactly as you would in CommonJS:

// Build robust absolute paths relative to the current file
const paths = {
  root:    join(__dirname, '..'),
  src:     join(__dirname, '../src'),
  public:  join(__dirname, '../public'),
  uploads: join(__dirname, '../uploads'),
  config:  join(__dirname, '../config.json')
};

import { readFile } from 'fs/promises';
const config = JSON.parse(await readFile(paths.config, 'utf-8'));

Many teams put this boilerplateWhat is boilerplate?Repetitive, standardized code that follows a known pattern and appears in nearly every project - like setting up a server or wiring up database connections. in a shared paths.js utility so they only write it once and import the pre-built path constants wherever they are needed.

04

Building a practical paths config

Rather than scattering path.join(__dirname, '...') calls throughout your codebase, a clean pattern is to define all your project's key paths in one place:

// src/config/paths.js
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';

const __dirname = dirname(fileURLToPath(import.meta.url));

export const ROOT     = join(__dirname, '../..');
export const SRC      = join(ROOT, 'src');
export const PUBLIC   = join(ROOT, 'public');
export const UPLOADS  = join(ROOT, 'uploads');
export const LOGS     = join(ROOT, 'logs');
export const CONFIG   = join(ROOT, 'config.json');

Then anywhere in your application:

import { UPLOADS, LOGS } from './config/paths.js';
import { mkdir } from 'fs/promises';

await mkdir(UPLOADS, { recursive: true });
await mkdir(LOGS, { recursive: true });

This approach means that if you ever move your project's directory structure, you fix one file instead of hunting down string literals across dozens of source files.

05

Quick reference

FunctionWhat it does
path.join(...segments)Join segments with the OS separator
path.resolve(...segments)Build an absolute path from segments
path.parse(path)Break a path into root, dir, base, ext, name
path.basename(path, ext?)Get the file name (optionally strip extension)
path.dirname(path)Get the parent directory
path.extname(path)Get the file extension including the dot
path.isAbsolute(path)Returns true if path is absolute
path.relative(from, to)Get the relative path between two absolute paths
path.normalize(path)Clean up redundant slashes and .. segments
path.sepThe OS path separator (/ or \\)