The complete Node.js path guide

By

Learn Node.js path handling from join() and resolve() to Windows paths, file URLs, safe containment checks, extensions, parsing, and common mistakes.

~~~

File paths look like strings, but treating them as plain strings creates bugs.

Windows uses different separators. Relative paths depend on the current working directory. ES modules identify files with URLs. User input can also escape the directory you intended to use.

Node provides the built-in node:path module to handle those details.

Import the path module

You do not need to install anything.

Use this in an ES module:

import path from 'node:path'

Or use CommonJS:

const path = require('node:path')

The node: prefix makes it clear that this is a built-in Node module.

Paths are not URLs

A file-system path and a file URL can point to the same file, but they are different values.

/Users/flavio/project/config.json
file:///Users/flavio/project/config.json

The path module works with path strings. Node’s URL utilities convert between paths and file URLs.

We will see that conversion later.

POSIX and Windows paths

macOS and Linux normally use / as the separator:

/Users/flavio/notes/today.txt

Windows normally uses \ and can include a drive letter:

C:\Users\flavio\notes\today.txt

By default, node:path follows the operating system running the program.

If you need predictable behavior for a specific path style, use path.posix or path.win32:

path.posix.basename('/tmp/report.txt')
// 'report.txt'

path.win32.basename('C:\\tmp\\report.txt')
// 'report.txt'

This is useful when a Linux server processes Windows paths received from another system.

Join path segments with path.join()

Use path.join() to build a path from separate pieces:

const file = path.join('data', 'customers', 'anna.json')

Node inserts the correct separator and normalizes the result.

On macOS and Linux, the result is:

data/customers/anna.json

On Windows, it is:

data\customers\anna.json

Do not build paths by concatenating strings:

const file = folder + '/' + name

That assumes a separator and makes duplicate slashes easy to introduce.

Turn a path into an absolute path

Use path.resolve() when you need an absolute path:

const file = path.resolve('data', 'customers.json')

If the process runs from /Users/flavio/app, the result is:

/Users/flavio/app/data/customers.json

resolve() processes arguments from right to left. It stops when it finds an absolute path:

path.resolve('/Users/flavio/app', '/tmp', 'report.txt')
// '/tmp/report.txt'

The /tmp segment resets the path because it is absolute.

path.join() vs path.resolve()

This distinction is easy to remember:

Use join() when you are assembling a path. Use resolve() when the final location must be absolute.

path.join('uploads', 'photo.jpg')
// 'uploads/photo.jpg'

path.resolve('uploads', 'photo.jpg')
// '/current/folder/uploads/photo.jpg'

The current working directory

Relative paths are resolved from process.cwd():

console.log(process.cwd())

This is the folder where the Node process was started. It is not necessarily the folder containing the current module.

Imagine this command:

cd /Users/flavio
node projects/app/server.js

Inside server.js, process.cwd() is /Users/flavio.

This difference matters when loading files distributed beside a module.

Get the current module folder in ES modules

ES modules expose import.meta.url, which is a file URL:

console.log(import.meta.url)
// 'file:///Users/flavio/app/server.js'

Convert it to a path with fileURLToPath():

import path from 'node:path'
import { fileURLToPath } from 'node:url'

const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)

You can now build a path relative to the module:

const configPath = path.join(dirname, 'config.json')

Modern Node also provides import.meta.dirname and import.meta.filename. The conversion above remains useful in code supporting older Node releases.

Read how to fix __dirname in ES modules for a focused explanation.

Convert a path to a file URL

Use pathToFileURL() instead of manually adding file://:

import { pathToFileURL } from 'node:url'

const url = pathToFileURL('/Users/flavio/My Notes/today.txt')

console.log(url.href)
// 'file:///Users/flavio/My%20Notes/today.txt'

The utility handles spaces, special characters, Windows drives, and separators correctly.

Get the file name with path.basename()

path.basename() returns the final part of a path:

path.basename('/Users/flavio/notes/today.txt')
// 'today.txt'

Pass a suffix to remove it:

path.basename('/Users/flavio/notes/today.txt', '.txt')
// 'today'

The suffix comparison is case-sensitive, even on systems where the file system is not.

Get the parent folder with path.dirname()

path.dirname() removes the final part:

path.dirname('/Users/flavio/notes/today.txt')
// '/Users/flavio/notes'

This only transforms the string. It does not check that the folder exists.

Get a file extension with path.extname()

path.extname() returns the last extension:

path.extname('archive.tar.gz') // '.gz'
path.extname('notes.txt') // '.txt'
path.extname('README') // ''
path.extname('.gitignore') // ''

A file can have several suffixes, but extname() only returns the final one.

Do not use the extension alone to decide whether an uploaded file is safe. A file name does not prove the file’s real content.

Split a path with path.parse()

path.parse() returns every important part:

const parts = path.parse('/Users/flavio/notes/today.txt')

The result looks like this:

{
  root: '/',
  dir: '/Users/flavio/notes',
  base: 'today.txt',
  ext: '.txt',
  name: 'today'
}

This is useful when you want to change one part of a file name.

Build a path with path.format()

path.format() performs the opposite operation:

path.format({
  dir: '/Users/flavio/notes',
  name: 'tomorrow',
  ext: '.md',
})
// '/Users/flavio/notes/tomorrow.md'

If you provide base, it takes priority over name and ext.

Normalize a path

path.normalize() removes redundant separators and resolves . and .. segments:

path.normalize('/Users/flavio/notes/../images//photo.jpg')
// '/Users/flavio/images/photo.jpg'

Normalization does not access the file system. It does not resolve symbolic links or prove that a path is safe.

Calculate a relative path

path.relative() tells you how to move from one path to another:

path.relative(
  '/Users/flavio/project/src',
  '/Users/flavio/project/public/logo.svg'
)
// '../../public/logo.svg'

The method resolves both arguments before comparing them.

If both paths point to the same location, it returns an empty string.

Check if a path is absolute

Use path.isAbsolute():

path.isAbsolute('/Users/flavio/notes.txt') // true
path.isAbsolute('./notes.txt') // false

This only checks the shape of the path. It does not check whether the file exists, and it does not protect against path traversal.

Work with PATH-like environment variables

path.delimiter is the separator used between paths in environment variables.

It is : on POSIX systems and ; on Windows:

const folders = process.env.PATH.split(path.delimiter)

path.sep is the separator inside one path:

console.log(path.sep)
// '/' on macOS and Linux
// '\\' on Windows

Keep user paths inside a base directory

Suppose users can request a file inside an uploads directory. This is dangerous:

const file = path.join(uploadDirectory, userInput)

An input such as ../../config.json can escape the directory.

Resolve the candidate and verify its relative position:

function resolveInside(baseDirectory, userPath) {
  const base = path.resolve(baseDirectory)
  const candidate = path.resolve(base, userPath)
  const relative = path.relative(base, candidate)

  if (
    relative === '..' ||
    relative.startsWith(`..${path.sep}`) ||
    path.isAbsolute(relative)
  ) {
    throw new Error('Path escapes the base directory')
  }

  return candidate
}

This blocks lexical .. traversal.

Symbolic links can still point outside the base directory. For a security boundary, resolve real paths with the file-system APIs and apply the same containment check to those results.

Common path mistakes

These are the problems I see most often:

My advice is to keep paths as path strings, URLs as URL objects, and convert only at clear boundaries.

The official Node.js path documentation lists every method and its exact platform behavior.

Tagged: Node.js · All topics
~~~

Related posts about node: