DEV Community

Cover image for A Better Way to Import Local Node.js Modules
janniks
janniks

Posted on

A Better Way to Import Local Node.js Modules

This article is more or less an advertisement for an npm package that I have just released: basetag. I want to share how the package came to be and why I believe it’s pretty awesome.

A Bit of Backstory

Node.js projects — like all software development projects — can get somewhat complex over time: developers often refactor functionality into separate modules, subdirectories, and helper classes; in less stressful times tech debt can be paid off.

As a system evolves, its complexity increases unless work is done to maintain or reduce it.
— Lehman’s Laws of Software Evolution

Also nowadays, the monorepo has become increasingly popular again. This shows that projects and their structures can become very large in scope. Different programming languages have different approaches to working with this. Most modern programming languages use namespaces and modules/packages. Some example are listed below.

Yet, in Node.js we can only import local modules via relative path

Node.js Imports

If you’ve used Node.js, you know this and have seen many statements like the following one.

const myModule = require('./MyModule')
Enter fullscreen mode Exit fullscreen mode

Now that doesn’t seem too bad… But let’s consider a more complex project. Most of the time we will be importing modules that aren’t far away. Yet, it can happen that we have modules that are loosely coupled and far away (in terms of files). Please consider the following directory structure (although it might be a fabricated example and maybe even indicate some code smells).

example/
├── its/
│   ├── …
│   └── baseballs/
│       ├── …
│       └── all/
│           ├── …
│           └── the/
│               ├── …
│               └── way/
│                   ├── …
│                   └── down.js
├── somewhere/
│   ├── …
│   └── deep/
│       ├── …
│       └── and/
│           ├── …
│           └── random.js
├── …
└── index.js
Enter fullscreen mode Exit fullscreen mode

You get the picture — we have a bunch of directories with a bunch of files. Now say we want to reference example/somewhere/deep/and/random.js from example/its/baseballs/all/the/way/down.js. In other languages we could probably import somewhere.deep.and.random as rand, but in Node.js this gets pretty messy and would look like the following import statement.

const randomRelative = require('../../../../../somewhere/deep/and/random')
Enter fullscreen mode Exit fullscreen mode

This has always frustrated me considerably and I started doing some research. It turns out that there are a lot of tweets and blog posts that complain about this problem. But there are also some projects that try to tackle the problem.

One approach (the npm package app-root-path tries to find the root path of a project and lets you import relative to that path. They even include a nifty .require method that you can reuse. This is already pretty cool!

const projectRoot = require('app-root-path');
const random = require(projectRoot + '/somewhere/deep/and/random.js');

// OR using .require

const requireLocal = require('app-root-path').require
const random = requireLocal('somewhere/deep/and/random');
Enter fullscreen mode Exit fullscreen mode

You could even store the requireLocal method into your globals 😱 in your entry file and it would be available in all other executed files. The package is great but I wanted to find something that feels even more native.


Screenshot of basetag repository on GitHub

I continued my search and came across some blog posts that proposed symlinks to reference to the base path of a project.

That’s how the idea for basetag was born.

The basetag package consists only of a postinstall script that adds a symlink $ inside node_modules. That symlink points to the base path of your project. Node.js now basically thinks that there is a $ module installed and you can require submodules of $ (which in turn just point to your project files).

const randomRelative = require('../../../../../somewhere/deep/and/random')

// Using 'basetag' becomes...

const randomBasetag = require('$/somewhere/deep/and/random')
Enter fullscreen mode Exit fullscreen mode

All you need to do is install basetag (e.g. via npm i -S basetag) and you can start using the $/… prefix in require statements.

  • The prefixed require is very readable, simple, and it’s pretty obvious what’s going on.
  • The prefixed require is still mixable with traditional relative requires. Since Node.js is literally using the same files (just routing differently), the imports are cached correctly.
  • The solution works with Node.js versions ≥ v4.x.
  • The package is super simple and has zero dependencies.

Well, that was my path to creating the tiny package basetag. Feel free to check it out and use it in your projects. Be aware that this package is stable but still very young — make sure to have all your files safe in version-control before using it. Due to the simple nature of the project there will probably not be a lot of updates to be expected…

Top comments (4)

Collapse
 
tusharpandey13 profile image
Tushar Pandey

The title says 'import' but the code uses require. I've yet to find a nice method for this that works with es6.

Collapse
 
janniks profile image
janniks

Good point, I’ll try to test this with import statements and rewrite if it doesn’t work

Collapse
 
tusharpandey13 profile image
Tushar Pandey

Btw, you can use aliasing. Ive mentioned it on one of my posts, the guide to es6 one. I don't remember it sorry lol. It lets you use ~ as project root (or some specific directory of your choice) in your import statements.

Collapse
 
adegbengaagoro profile image
Agoro, Adegbenga. B

It is a great package but it breaks every time you install another npm package, I had to uninstall it, install the new npm package and then re-install it so my app could work again.

If there is a fix, do let me know because I am about to uninstall permanently and look at an alternative