Skip to content
JavaScript Basics: Locate Module Resources with File URLs

JavaScript Basics: Locate Module Resources with File URLs

October 19, 2026

A relative filesystem path normally depends on the process working directory. An ES module can instead resolve a resource beside its own source using new URL(relative, import.meta.url). File URLs also need proper conversion when filenames contain spaces, percent signs, or fragment characters.

These examples were tested on Linux with Node.js v24.19.0. They use built-in APIs only, require no network access, and include assertions that check the results. See the official Node.js ES module documentation and file URL conversion reference.

Prepare a scratch directory

You need Bash, a text editor, Node.js 24 on your PATH, and the mktemp, rm, and rmdir utilities. Keep using the same terminal. Save each named source file as UTF-8; .mjs selects ES modules. Each example runs independently.

node --version
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1

The tested runtime prints:

v24.19.0

1. Read a sibling resource regardless of the working directory

Save this as read-module-resource.mjs:

import assert from 'node:assert/strict';
import { readFile, writeFile, unlink } from 'node:fs/promises';

const resource = new URL('./module-message.txt', import.meta.url);
const originalDirectory = process.cwd();
await writeFile(resource, 'Hello from beside the module!\n', { flag: 'wx' });
try {
  process.chdir('/');
  const text = await readFile(resource, 'utf8');
  assert.equal(text, 'Hello from beside the module!\n');
  console.log(text.trim());
  console.log(`Still a file URL: ${resource.protocol === 'file:'}`);
} finally {
  process.chdir(originalDirectory);
  await unlink(resource);
}

Run it:

node read-module-resource.mjs

Output:

Hello from beside the module!
Still a file URL: true

Node’s filesystem methods accept file URL objects directly. The module URL provides a stable base even after the working directory changes. This isolated program changes its own process directory only. Exclusive creation with wx refuses to overwrite an existing fixture, and finally removes the file it created.

2. Convert paths without losing special characters

Save this as convert-special-filenames.mjs:

import assert from 'node:assert/strict';
import { pathToFileURL, fileURLToPath } from 'node:url';

const path = '/tmp/report #1 100%.txt';
const url = pathToFileURL(path);
assert.equal(url.href, 'file:///tmp/report%20%231%20100%25.txt');
assert.equal(url.hash, '');
assert.equal(fileURLToPath(url), path);
console.log(url.href);
console.log(fileURLToPath(url));

Run it:

node convert-special-filenames.mjs

Output:

file:///tmp/report%20%231%20100%25.txt
/tmp/report #1 100%.txt

This example converts strings only; it creates no file in /tmp. Use pathToFileURL() to encode a filesystem path and fileURLToPath() to convert back. Hand-building file:// strings can mistake # for a fragment. A URL’s pathname can still contain percent escapes and is not a portable replacement for conversion.

3. Separate a directory URL from a filename

Save this as resolve-module-url.mjs:

import assert from 'node:assert/strict';
import { fileURLToPath } from 'node:url';
import path from 'node:path';

const directory = new URL('.', import.meta.url);
const file = new URL('./settings.json', directory);
assert.equal(directory.protocol, 'file:');
assert.equal(directory.pathname.endsWith('/'), true);
assert.equal(path.dirname(fileURLToPath(file)),
  path.dirname(fileURLToPath(import.meta.url)));
assert.equal(path.basename(fileURLToPath(file)), 'settings.json');
console.log(`Directory URL ends with slash: ${directory.pathname.endsWith('/')}`);
console.log(`Resource filename: ${path.basename(fileURLToPath(file))}`);

Run it:

node resolve-module-url.mjs

Output:

Directory URL ends with slash: true
Resource filename: settings.json

new URL('.', import.meta.url) resolves the containing directory and preserves a trailing slash. A base URL ending in a filename behaves differently from a directory base. Resolution does not check that settings.json exists. Keep resource references trusted: URL resolution alone does not confine arbitrary input to a directory.

Clean up

In the same terminal, remove only the named example files, return to your original directory, and remove the empty scratch directory:

rm -- read-module-resource.mjs convert-special-filenames.mjs resolve-module-url.mjs
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dir

Acknowledgements

This post was created with assistance from AI.

Last updated on