mrkeyoor.com_
Thu 06 Aug 01:00 UTC
npmUtilsupdated 05 Aug 2026

env-paths

env-paths answers one question: where should my app store its files on this OS? You call envPaths('MyApp') and get back five directory strings: data, config, cache, log, and temp, each pointing at the platform-correct location. That means Library/Application Support, Preferences, Caches, and Logs on macOS, APPDATA and LOCALAPPDATA subfolders on Windows, and the XDG base directory spec on Linux, including respect for XDG_DATA_HOME, XDG_CONFIG_HOME, XDG_CACHE_HOME, and XDG_STATE_HOME overrides. It only builds path strings; it never touches the filesystem, and by default it appends a -nodejs suffix to your app name to avoid colliding with a native app of the same name.

Verdict

The correct 30 lines of platform knowledge you should not rewrite yourself, which is why it sees roughly 80M weekly downloads from 452 stars. Use it directly for paths, or use conf when you realize paths were only step one of your config problem.

API stability5/5One function, same shape since 2016; the breaking majors were packaging and platform floors (ESM-only in v3, Node >=20 plus name validation in v4), not API redesigns.
Docs4/5The README covers the entire surface with example locations for every path on every OS, which is all this library needs; there is no separate site and edge details like the temp path live only in those examples.
Maintenance4/5Maintained by Sindre Sorhus with a clean tracker (0 open issues and PRs) and a v4 release in January 2026; it is effectively finished software, but it is one person's package.
Ecosystem4/5Around 80M weekly downloads as the path layer under a large share of CLI tooling and config stores; there is no plugin ecosystem because there is nothing to extend.

Use it if

  • You ship a CLI or desktop tool and want config and cache files in the places each OS expects instead of dumping dotfiles into the user's home directory
  • You want Linux XDG environment variable overrides handled correctly for free, which most hand-rolled path logic gets wrong
  • You need the five standard locations (data, config, cache, log, temp) from one tiny dependency with bundled TypeScript types
  • You are already avoiding heavier config stores and just want the paths, keeping file formats and IO under your own control
Skip it if

Setup reality

npm install env-paths and one function call; there is genuinely almost nothing to set up. The catches: it is pure ESM, so CommonJS projects need a dynamic import or have to stay on the ancient v2. Since v4 the name you pass is validated with is-safe-filename and throws on path separators or reserved characters, so version 4 can reject input that version 3 silently accepted. It returns strings only, so you must create directories yourself with fs.mkdir recursive before writing, and the docs remind you of that because everyone forgets it once. On Linux the temp path nests under your username in /tmp, which surprises people reading logs.

Patterns

Get the five standard directoriesget-standard-paths

import envPaths from 'env-paths';

const paths = envPaths('MyApp');

console.log(paths.data);   // ~/.local/share/MyApp-nodejs on Linux
console.log(paths.config); // ~/.config/MyApp-nodejs on Linux
console.log(paths.cache);
console.log(paths.log);
console.log(paths.temp);

Note the -nodejs suffix in every path: it is appended by default to avoid clashing with a native app of the same name.

Create the directories before writingcreate-directories

import fs from 'node:fs/promises';
import envPaths from 'env-paths';

const paths = envPaths('MyApp');
await fs.mkdir(paths.config, { recursive: true });
await fs.mkdir(paths.cache, { recursive: true });

env-paths only generates strings and never creates anything. Writing a file into paths.config without mkdir first fails with ENOENT on a fresh machine.

Read and write a JSON config filejson-config-file

import fs from 'node:fs/promises';
import path from 'node:path';
import envPaths from 'env-paths';

const configDir = envPaths('mytool').config;
const configFile = path.join(configDir, 'config.json');

async function loadConfig() {
  try {
    return JSON.parse(await fs.readFile(configFile, 'utf8'));
  } catch {
    return {};
  }
}

async function saveConfig(config) {
  await fs.mkdir(configDir, { recursive: true });
  await fs.writeFile(configFile, JSON.stringify(config, null, 2));
}

If you find yourself adding defaults, validation, and atomic writes here, that is the point where the conf package earns its install.

Cache downloaded artifactscache-downloads

import fs from 'node:fs/promises';
import path from 'node:path';
import envPaths from 'env-paths';

const cacheDir = envPaths('mytool').cache;

async function cachedFetch(name, download) {
  const file = path.join(cacheDir, name);
  try {
    return await fs.readFile(file);
  } catch {
    const data = await download();
    await fs.mkdir(cacheDir, { recursive: true });
    await fs.writeFile(file, data);
    return data;
  }
}

The cache directory is for regenerable data the OS or user may wipe at any time; never keep the only copy of anything there.

Put log files where the OS expects themlog-file-location

import fs from 'node:fs';
import path from 'node:path';
import envPaths from 'env-paths';

const logDir = envPaths('mytool').log;
fs.mkdirSync(logDir, { recursive: true });

const logStream = fs.createWriteStream(
  path.join(logDir, 'mytool.log'),
  { flags: 'a' }
);
logStream.write('started\n');

On Linux this maps to XDG_STATE_HOME (default ~/.local/state), not ~/.cache, following the XDG spec's placement for logs.

Drop or change the -nodejs suffixdisable-name-suffix

import envPaths from 'env-paths';

const plain = envPaths('MyApp', { suffix: '' });
// ~/.config/MyApp instead of ~/.config/MyApp-nodejs

const branded = envPaths('MyApp', { suffix: 'cli' });
// ~/.config/MyApp-cli

The README warns against disabling the suffix unless you must, because a native app named MyApp would collide with your directories. Decide before you ship; changing later strands existing user data.

Use a namespaced temp directorytemp-workspace

import fs from 'node:fs/promises';
import envPaths from 'env-paths';

const tempDir = envPaths('mytool').temp;
await fs.mkdir(tempDir, { recursive: true });

// ... write scratch files ...

await fs.rm(tempDir, { recursive: true, force: true });

On Linux the path includes the username (/tmp/USERNAME/mytool-nodejs), and on macOS it lands in the per-user folder under /var/folders, so do not hardcode /tmp expectations in tests.

Redirect paths in tests via XDG variablesrespect-xdg-overrides

import test from 'node:test';
import assert from 'node:assert';
import envPaths from 'env-paths';

test('config path honors XDG_CONFIG_HOME', () => {
  process.env.XDG_CONFIG_HOME = '/tmp/test-config';
  const paths = envPaths('mytool');
  assert.ok(paths.config.startsWith('/tmp/test-config'));
});

On Linux the XDG variables are read from process.env when envPaths() is called, so setting them in a test before the call works. This only applies to Linux; macOS and Windows paths ignore XDG variables.

Load it from a CommonJS projectuse-from-commonjs

// index.cjs
async function main() {
  const { default: envPaths } = await import('env-paths');
  const paths = envPaths('mytool');
  console.log(paths.config);
}

main();

require('env-paths') throws ERR_REQUIRE_ESM for v3 and v4. Dynamic import works from CJS; the old CJS v2 lacks the log path fix and current name validation.

Handle the v4 safe-name checkvalidate-app-name

import envPaths from 'env-paths';

// Fine
const ok = envPaths('my-tool');

// Throws since v4: path separators are not safe filenames
try {
  envPaths('my/tool');
} catch (error) {
  console.error('invalid app name:', error.message);
}

v4 runs the name through is-safe-filename and throws on separators and reserved characters, where v3 would happily build a nested or broken path. Validate user-provided names before passing them in.

Alternatives

PackageRegistryPick it when
confnpmYou want the whole config problem solved (correct location plus load, save, schema, migrations) rather than just path strings.
xdg-basedirnpmYou only target Linux and want raw XDG base directories without app-name joining or macOS and Windows logic.
platformdirsPyPIYou need the same OS-correct directories idea in a Python project.