In this post, I will share my findings and what I learned while trying to understand how to use absolute paths in a node project. I found two different methods for doing so, using NODE_PATH
and package.json
imports, so let’s quickly dive in and hopefully help you out with your own project, too.
Disclaimer!
I hate path resolving, I truly hate it! 😋 It feels so complicated for some reason, especially when you have to decide if you want Typescript, if you want CommonJS, or ESM… it feels that hell breaks loose for something that should be much simpler….! 😡 Now that I got it out of my system, let’s continue.🤭
By the way, if you are here because you are creating a cool side project and want to stop using relative paths hell, this is your post! If, on the other hand, you are a node guru 🧙♂️, and you have already climbed the ladders of node wisdom then this is also a post for you. In your case, though, maybe you could add some comments in the end and share your knowledge with us. Don’t be shy! We need Gandalfs like you to guide us! 🌟
Initializing our project structure
The stack I am using in this post:
- Ubuntu 22.04.4 LTS
- npm 10.5.0
- node 20.12.0
Before explaining how you can change your importing using absolute paths, let’s build a basic project structure so that we are both on the same page when referring to folders/files in our post’s code snippets.
So, nothing fancy here, just an empty folder initialized by npm. I just ran npm init --yes
so that my project is initialized automatically with all default values preselected. After doing that, we’ll need some basic folder files and folders:
📦node-paths
┣ 📁 server
┃ ┗ 📁 src
┃ ┣ 📁 controllers
┃ ┃ ┗ 📜 UserController.js
┃ ┣ 📁 utils
┃ ┃ ┗ 📜 helper.js
┃ ┗ 📜 app.js
┗ 📜 package.json
Typing mkdir -p server/src/controllers
will create all structures at once. The -p flag throws no error if the parent folder doesn’t exist, on the contrary, it creates the parent.
Define our importing challenge
Now that we have completed our project’s structure let’s decide on our challenge. Suppose we want to import one helper – in this case validateEmail
– from server/src/utils/helper.js
function validateEmail(email) {
// ... validating email logic here
}
module.exports = {
validateEmail,
}
JavaScriptto our
file.server/src/controllers/UserController.js
class UserController {
static createUser(userParams) {
const { email } = userParams;
if (!validateEmail(email)) {
throw new Error('Email is not valid');
}
}
}
JavaScriptWith our current settings, we could do it by adding a relative import
const { validateEmail } = require("../utils/helper");
JavaScriptWell, that’s nice, but we could do much better. Let’s challenge ourselves and change our project’s setting so that we can import by writing:
const { validateEmail } = require("src/utils/helper");
JavaScriptNow be honest! Wouldn’t that be so much better? Great then! Challenge accepted!
I’ve created a basic app.js
file which basically imports and calls the createUser
method.
const userController = require("./controllers/UserController.js");
userController.createUser({})
JavaScriptNow that we are ready, I’ll start my node server by running nodemon app.js
. (I am using nodemon
to watch for file changes and make my life easier, instead of using node
).
Using NODE_PATH for absolute paths
One common approach for asking node to search for our imported paths from the root folder is by exploiting NODE_PATH
. Based on node’s documentation, we just need to add our root folder path in the NODE_PATH
variable.
Let’s do that by adding the following lines in our app’s earliest possible point, in our case, the app.js file’s first lines.
process.env.NODE_PATH = process.cwd();
require("module").Module._initPaths();
const userController = require("./controllers/UserController.js");
userController.createUser({})
JavaScriptThere it is! Nodemon restarted our server and the new import path worked like a charm! 🥳🥳
For those of you who would like to know a bit more, let’s see what we did.
process.env.NODE_PATH = process.cwd();
JavaScriptIn this line of code we are configuring the environment variable NODE_PATH
to match our Node process’s current working directory.
NODE_PATH
is used when the require
method is called to import modules, so we are expanding Node’s search scope by adding one more place to search for, when we ask it to load a new module.
require("module").Module._initPaths();
JavaScriptThis line of code serves to initialize the module paths within our Node.js application. The initPaths
method is pivotal in this process, as it handles the initialization.
Using package.json imports for absolute paths
If the first method didn’t suit you or didn’t work, there’s another way to import your files using absolute paths by exploiting imports in your package.json
file.
Open your package.json
file and add the following highlighted lines
"main": "app.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"imports": {
"#helper": "./server/src/utils/helper.js"
}
JSON5then in our app.js add the following import path
const { validateEmail } = require("#helper");
const userController = require("./controllers/UserController.js");
userController.createUser({})
JavaScriptVoila! This is another way to import your paths! 😎
Keep in mind that the “Imports” feature was introduced in version 14.6.0 and must start with a #
.
Package.json imports and IDEs
If you decide to go with “imports” you could see some errors thrown by your IDE. I had the same issue while working with Webstorm 2023.3.6.
This happens because your IDE does not support the imports field, and you need to help your IDE understand your new setting. (You could also be facing the same issue with other IDEs like VScode.)
I managed to help my Webstorm understand what was going on by adding the following jconfig.json
file in the root folder
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"src/*": [
"server/src/*",
]
}
}
}
JSON5