The Node.js path module is a built-in module that helps you work with file system paths in an OS-independent way. The path module is essential if you’re building a CLI tool that supports OSX, Linux, and Windows.
Even if you’re building a backend service that only runs on Linux, the path module is still helpful for avoiding edge cases when manipulating paths.
In this blog post, I’ll describe some common patterns for working with the path module, and why you should use the path module rather than manipulate paths into strings.
The most commonly used function in the path module is path.join()
. The path.join()
function merges one or more path segments into a single string, as shown below.
const path = require('path'); path.join('/path', 'to', 'test.txt'); // '/path/to/test.txt'
You may be wondering why you’d use the path.join()
function instead of using string concatenation.
'/path' + '/' + 'to' + '/' + 'test.txt'; // '/path/to/test.txt' ['/path', 'to', 'test.txt'].join('/'); // '/path/to/test.txt'
There are two main reasons why.
First, for Windows support. Windows uses backslashes (\
) rather than forward slashes (/
) as path separators. The path.join()
function handles this for you because path.join('data', 'test.txt')
returns 'data/test.txt'
on both Linux and OSX, and 'data\\test.txt'
on Windows.
Secondly, for handling edge cases. Numerous edge cases pop up when working with file system paths. For example, you may accidentally end up with a duplicate path separator if you try to join two paths manually. The path.join()
function handles leading and trailing slashes for you, like so:
path.join('data', 'test.txt'); // 'data/test.txt' path.join('data', '/test.txt'); // 'data/test.txt' path.join('data/', 'test.txt'); // 'data/test.txt' path.join('data/', '/test.txt'); // 'data/test.txt'
The path module also has several functions for extracting path components, such as the file extension or directory. For example, the path.extname()
function returns the file extension as a string:
path.extname('/path/to/test.txt'); // '.txt'
Like joining two paths, getting the file extension is trickier than it first seems. Taking everything after the last .
in the string doesn’t work if there’s a directory with a .
in the name, or if the path is a dotfile.
path.extname('/path/to/github.com/README'); // '' path.extname('/path/to/.gitignore'); // ''
The path module also has path.basename()
and path.dirname()
functions, which get the file name (including the extension) and directory, respectively.
path.basename('/path/to/test.txt'); // 'test.txt' path.dirname('/path/to/test.txt'); // '/path/to'
Do you need both the extension and the directory? The path.parse()
function returns an object containing the path broken up into five different components, including the extension and directory. The path.parse()
function is also how you can get the file’s name without any extension.
/* { root: '/', dir: '/path/to', base: 'test.txt', ext: '.txt', name: 'test' } */ path.parse('/path/to/test.txt');
path.relative()
Functions like path.join()
and path.extname()
cover most use cases for working with file paths. But the path module has several more advanced functions, such as path.relative()
.
The path.relative()
function takes two paths and returns the path to the second path relative to the first.
// '../../layout/index.html' path.relative('/app/views/home.html', '/app/layout/index.html');
The path.relative()
function is useful when you’re given paths relative to one directory, but want paths relative to another directory. For example, the popular file system watching library Chokidar gives you paths relative to the watched directory.
const watcher = chokidar.watch('mydir'); // if user adds 'mydir/path/to/test.txt', this // prints 'mydir/path/to/test.txt' watcher.on('add', path => console.log(path));
This is why tools that make heavy use of Chokidar, like Gatsby or webpack, for instance, often also make heavy use of the path.relative()
function internally.
For example, here’s Gatsby using the path.relative()
function to help sync a static files directory.
export const syncStaticDir = (): void => { const staticDir = nodePath.join(process.cwd(), `static`) chokidar .watch(staticDir) .on(`add`, path => { const relativePath = nodePath.relative(staticDir, path) fs.copy(path, `${process.cwd()}/public/${relativePath}`) }) .on(`change`, path => { const relativePath = nodePath.relative(staticDir, path) fs.copy(path, `${process.cwd()}/public/${relativePath}`) }) }
Now, suppose a user adds a new file main.js
to the static
directory. Chokidar calls the on('add')
event handler with path
set to static/main.js
. However, you don’t want the extra static/
when you copy the file to /public
.
Calling path.relative('static', 'static/main.js')
returns the path to static/main.js
relative to static
, which is exactly what you want if you want to copy the contents of static
to public
.
By default, the path module automatically switches between POSIX (OSX, Linux) and Windows modes based on which OS your Node process is running.
However, the path module does have a way to use the Windows path module on POSIX, and vice versa. The path.posix
and path.win32
properties contain the POSIX and Windows versions of the path module, respectively.
// Returns 'path\\to\\test.txt', regardless of OS path.win32.join('path', 'to', 'test.txt'); // Returns 'path/to/test.txt', regardless of OS path.posix.join('path', 'to', 'test.txt');
In most cases, switching the path module automatically based on the detected OS is the right behavior. But using the path.posix
and path.win32
properties can be helpful for testing or applications where you always want to output Windows or Linux-style paths.
For example, some applications use functions like path.join()
and path.extname()
to work with URL paths.
// 'https://api.mydomain.app/api/v2/me' 'https://api.mydomain.app/' + path.join('api', 'v2', 'me');
This approach works on Linux and OSX, but what happens if someone tries to deploy your app on Azure Functions?
You’ll end up with 'https://api.mydomain.app/api\\v2\\me'
, which is not a valid URL! If you’re using the path module to manipulate URLs, you should use path.posix
.
The Node path module is a great tool for working with file system paths, especially when it comes to joining and parsing. While you can manipulate file paths as strings, there are many subtle edge cases when working with paths.
In general, you should use the path module to get file extensions and join paths, because it is easy to make mistakes if you’re manipulating paths as strings.
Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third-party services are successful, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens while a user interacts with your app. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.
LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowToast notifications are messages that appear on the screen to provide feedback to users. When users interact with the user […]
Deno’s features and built-in TypeScript support make it appealing for developers seeking a secure and streamlined development experience.
It can be difficult to choose between types and interfaces in TypeScript, but in this post, you’ll learn which to use in specific use cases.
This tutorial demonstrates how to build, integrate, and customize a bottom navigation bar in a Flutter app.