Skip to main content

pathe vs node:path vs upath: Cross-Platform Path Utilities in Node.js (2026)

·PkgPulse Team

TL;DR

pathe is the UnJS path utility — same API as node:path but always uses forward slashes, works identically on Windows and POSIX, ESM-native. node:path is the built-in Node.js path module — path.posix for forward slashes, path.win32 for backslashes, platform-dependent by default. upath wraps node:path to always return forward slashes — drop-in replacement, normalizes Windows backslashes to forward slashes. In 2026: pathe for cross-platform tools (especially UnJS/Vite ecosystem), node:path for platform-native behavior, upath for simple backslash-to-forward-slash normalization.

Key Takeaways

  • pathe: ~10M weekly downloads — UnJS, always forward slashes, same API as node:path
  • node:path: built-in — platform-native, path.posix for forced forward slashes
  • upath: ~10M weekly downloads — wraps node:path, normalizes to forward slashes
  • The problem: Windows uses \ separators, POSIX uses / — inconsistent paths break tools
  • Forward slashes work everywhere: URLs, glob patterns, import paths, config files
  • pathe and upath solve this by always returning / separators

The Problem

import path from "node:path"

// On macOS/Linux:
path.join("src", "components", "App.tsx")
// → "src/components/App.tsx" ✅

// On Windows:
path.join("src", "components", "App.tsx")
// → "src\\components\\App.tsx" ❌ (backslashes!)

// This breaks:
// - Glob patterns: "src/**/*.tsx" won't match "src\\components\\App.tsx"
// - URL paths: new URL("src\\components\\App.tsx") is wrong
// - Config files: paths in configs should use forward slashes
// - Import specifiers: import "./src\\utils" doesn't work

// Workaround with node:path:
path.posix.join("src", "components", "App.tsx")
// → "src/components/App.tsx" ✅ (but verbose)

pathe

pathe — cross-platform paths:

Drop-in replacement

// Replace:
import path from "node:path"
// With:
import { join, resolve, relative, dirname, basename, extname, normalize } from "pathe"

// Same API, always forward slashes:
join("src", "components", "App.tsx")
// → "src/components/App.tsx" (on ALL platforms)

resolve("src", "utils")
// → "/Users/royce/project/src/utils" (forward slashes)

relative("/Users/royce/project", "/Users/royce/project/src/index.ts")
// → "src/index.ts" (forward slashes)

dirname("/Users/royce/project/src/index.ts")
// → "/Users/royce/project/src"

basename("/Users/royce/project/src/index.ts")
// → "index.ts"

extname("App.tsx")
// → ".tsx"

normalize

import { normalize, toNamespacedPath } from "pathe"

// Normalizes Windows paths:
normalize("src\\components\\App.tsx")
// → "src/components/App.tsx"

normalize("C:\\Users\\royce\\project")
// → "C:/Users/royce/project"

// Handles redundant separators and dots:
normalize("src//components/../utils/./helpers.ts")
// → "src/utils/helpers.ts"

Extra utilities

import { filename, normalizeAliases, resolveAlias } from "pathe/utils"

// filename — basename without extension:
filename("/src/components/App.tsx")
// → "App"

// Alias resolution:
const aliases = { "@": "/Users/royce/project/src", "~": "/Users/royce/project" }
resolveAlias("@/utils/helpers", aliases)
// → "/Users/royce/project/src/utils/helpers"

Why Vite/Nuxt use pathe

In Vite and Nuxt codebases:
  - Config paths must be cross-platform
  - Glob patterns need forward slashes
  - Module IDs use forward slashes
  - Generated import paths must be valid

Using node:path on Windows would break:
  vite.config.ts with resolve.alias paths
  Glob patterns in plugins
  Virtual module IDs
  Source map paths

pathe ensures consistent paths everywhere.

node:path

node:path — built-in path module:

Platform-dependent (default)

import path from "node:path"

// Default — uses platform separators:
path.join("src", "utils")
// macOS/Linux: "src/utils"
// Windows: "src\\utils"

path.resolve("src", "index.ts")
// macOS: "/Users/royce/project/src/index.ts"
// Windows: "C:\\Users\\royce\\project\\src\\index.ts"

path.sep
// macOS/Linux: "/"
// Windows: "\\"

path.posix (forced forward slashes)

import path from "node:path"

// Force POSIX behavior (forward slashes):
path.posix.join("src", "utils")
// → "src/utils" (everywhere)

path.posix.normalize("src//utils/../helpers")
// → "src/helpers"

// But path.posix doesn't normalize Windows paths:
path.posix.normalize("src\\utils\\helpers")
// → "src\\utils\\helpers" (doesn't convert backslashes!)
// You'd need to replace manually: .replace(/\\/g, "/")

path.win32 (forced Windows behavior)

import path from "node:path"

// Force Windows behavior:
path.win32.join("src", "utils")
// → "src\\utils" (everywhere)

path.win32.resolve("C:", "Users", "royce")
// → "C:\\Users\\royce"

When node:path is enough

Use node:path when:
  ✅ Working with actual filesystem paths on the current platform
  ✅ Reading/writing files (OS expects native separators)
  ✅ Process arguments that use native paths
  ✅ You only target one platform (e.g., Linux servers)

Use pathe/upath when:
  ✅ Generating paths for config files, URLs, imports
  ✅ Working with glob patterns
  ✅ Building cross-platform tools
  ✅ Comparing paths across platforms

upath

upath — forward-slash path wrapper:

Drop-in replacement

import upath from "upath"

// Same API as node:path, always forward slashes:
upath.join("src", "components", "App.tsx")
// → "src/components/App.tsx"

upath.resolve("C:\\Users\\royce\\project")
// → "C:/Users/royce/project"

upath.normalize("src\\utils\\..\\components")
// → "src/components"

Extra methods

import upath from "upath"

// toUnix — convert any path to forward slashes:
upath.toUnix("C:\\Users\\royce\\project\\src")
// → "C:/Users/royce/project/src"

// changeExt — change file extension:
upath.changeExt("src/index.ts", ".js")
// → "src/index.js"

// addExt — add extension if missing:
upath.addExt("src/index", ".ts")
// → "src/index.ts"

upath.addExt("src/index.ts", ".ts")
// → "src/index.ts" (already has .ts)

// removeExt — remove extension:
upath.removeExt("src/index.ts", ".ts")
// → "src/index"

// trimExt — remove any extension:
upath.trimExt("src/index.ts")
// → "src/index"

upath vs pathe

upath:
  ✅ Wraps node:path — exact same behavior + forward slashes
  ✅ Extra methods (changeExt, addExt, removeExt, trimExt)
  ✅ Mature — available since 2014
  ❌ CJS-first (ESM wrapper available)
  ❌ Not actively maintained
  ❌ Depends on node:path internals

pathe:
  ✅ ESM-native
  ✅ Zero dependencies
  ✅ Actively maintained (UnJS)
  ✅ Alias resolution utilities
  ❌ No changeExt/addExt/removeExt helpers
  ❌ Slightly different edge cases from node:path

Feature Comparison

Featurepathenode:pathupath
Always forward slashes❌ (use .posix)
Normalize backslashes❌ (.posix won't)
ESM native⚠️ (CJS-first)
TypeScript✅ (@types/node)
Extension helpersextname only✅ (changeExt, etc.)
Alias resolution✅ (utils)
Dependencies0Built-in0
Maintained✅ (UnJS)✅ (Node.js)⚠️
Weekly downloads~10MBuilt-in~10M

When to Use Each

Use pathe if:

  • Building cross-platform tools (CLIs, bundlers, linters)
  • Need consistent forward-slash paths for configs and globs
  • In the UnJS ecosystem (Nuxt, Vite, Nitro)
  • Want modern ESM-native path utility

Use node:path if:

  • Working with actual filesystem operations (read/write)
  • Only targeting one platform
  • Don't need cross-platform path normalization
  • Want zero dependencies (built-in)

Use upath if:

  • Need forward-slash normalization + extension helpers
  • Existing project already using upath
  • Want the closest behavior to node:path with forward slashes

Methodology

Download data from npm registry (weekly average, February 2026). Feature comparison based on pathe v1.x, Node.js 22 path module, and upath v2.x.

Compare path utilities and developer tooling on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.