Val Karpov Node.js @BoosterFuels. Open source @mongoosejs @mongodb. Blogger, author x2. Coined MEAN. Find me on GitHub.

Mastering the Node.js path module

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.

Joining path modules in Node

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'


Parsing paths in Node

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');


Using 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'


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.

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.

Cross-OS paths and URLs

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.

Conclusion

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.

200’s only Monitor failed and slow network requests in production

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. https://logrocket.com/signup/

LogRocket is like a DVR for web apps, recording literally everything that happens on your site. 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. .
Val Karpov Node.js @BoosterFuels. Open source @mongoosejs @mongodb. Blogger, author x2. Coined MEAN. Find me on GitHub.