NodeJSWorking with File Paths

Working with File Paths

Filesystem code lives and dies by getting paths right. This page focuses on the practical path patterns that surround fs work — anchoring to the current module, resolving user input safely, and avoiding the classic cross-platform and security mistakes. It builds on the API reference in The path Module.

The cwd trap
Relative paths resolve against where `node` was launched
`fs.readFile('data.json')` looks in `process.cwd()` — the directory the *user* ran `node` from — **not** the folder your script lives in. Run the script from anywhere else and it breaks. This is the single most common path bug in Node. Always anchor file paths to the module's own location.

JS
import { readFile } from 'node:fs/promises'
import { join } from 'node:path'

// ✗ Fragile — depends on the caller's working directory
await readFile('config/default.json', 'utf8')

// ✓ Robust — relative to THIS file, wherever node runs
await readFile(join(import.meta.dirname, 'config', 'default.json'), 'utf8')
__dirname in both module systems

CommonJS

ES Modules

Directory of this file

__dirname

import.meta.dirname (Node 20.11+)

This file’s full path

__filename

import.meta.filename

As a URL

import.meta.url

ESM, older Node: reconstruct __dirname

JS
import { fileURLToPath } from 'node:url'
import { dirname, join } from 'node:path'

const __dirname = dirname(fileURLToPath(import.meta.url))
const dataPath = join(__dirname, 'data.json')

Why these names exist (and differ between CJS and ESM) is explained in The Module Wrapper Function.

join vs resolve, for files

join glues segments; resolve produces a guaranteed absolute path. For reading bundled assets, join(import.meta.dirname, …) is the workhorse. For turning user-supplied paths into absolutes, resolve — but then you must validate (next section):

JS
import { join, resolve } from 'node:path'

join('src', 'lib', 'x.js')          // 'src/lib/x.js'   (relative, normalized)
resolve('src', 'lib', 'x.js')       // '/cwd/src/lib/x.js' (absolute)
resolve('/etc', 'passwd')           // '/etc/passwd'    (absolute arg wins)
Path traversal — a real security hole
Never join untrusted input onto a base path without checking
If a user controls part of a path — `join(uploadsDir, req.params.filename)` — they can send `../../etc/passwd` and escape your directory, reading or writing arbitrary files. This is **path traversal**, a top web vulnerability. After resolving, verify the result still lives inside the intended directory.

Safe: confine resolved paths to a base directory

JS
import { join, resolve, sep } from 'node:path'

function safeJoin(baseDir, userPath) {
  const base = resolve(baseDir)
  const target = resolve(base, userPath)
  // target must be base itself or live under base + separator
  if (target !== base && !target.startsWith(base + sep)) {
    throw new Error('Path traversal blocked')
  }
  return target
}

safeJoin('/srv/uploads', 'photo.jpg')        // ok → /srv/uploads/photo.jpg
safeJoin('/srv/uploads', '../../etc/passwd') // throws
Cross-platform pitfalls
  • Build paths with path.join/resolve, never string concatenation — separators differ (/ vs \\).

  • Use path.sep and path.delimiter instead of hardcoding / or :.

  • For URLs, use path.posix or the URL class — web paths are always /, even on Windows.

  • Case sensitivity differs: Linux is case-sensitive, macOS/Windows usually are not — don’t rely on case to distinguish files.

Dissecting and rebuilding paths

JS
import { basename, dirname, extname, parse } from 'node:path'

const p = '/home/ada/report.final.pdf'
basename(p)            // 'report.final.pdf'
basename(p, '.pdf')   // 'report.final'
dirname(p)            // '/home/ada'
extname(p)            // '.pdf'
parse(p)              // { root, dir, base, name: 'report.final', ext: '.pdf' }
Quick reference

Need

Use

Path relative to this file

join(import.meta.dirname, …)

Force an absolute path

resolve(...)

Confine user input to a dir

safeJoin pattern (validate after resolve)

Filename / dir / extension

basename / dirname / extname

Cross-platform URL path

path.posix or URL

Next
React to files changing on disk in real time: [Watching Files for Changes](/nodejs/watching-files).