Abhijit Hota

Extremely fallible

A sorry excuse of a blog where I write about development, life and opinions I have

Node.js Import Aliases

  • #nodejs
  • #javascript

The Problem

Oftentimes, as a Node.js codebase grows, this happens:

import { UserModel } from "../../../../db/models/index.js";
import { validate } from "../../../../lib/utils.js";
import { SERVICE_API_KEY } from "../../../../lib/constants.js";

There are a few problems with this:

The Solution

A new field in package.json called imports was stabilized in Node.js v14. It was introduced earlier in Node.js v12. It follows certain rules and lets you “map” certain aliases (custom paths) to a path of your choice and also declare fallbacks.

Here’s the documentation for the same.

We can solve our example problem by adding this to our package.json:

"imports": {
  "#models": "./src/db/models/index.js",
  "#utils": "./src/lib/utils.js",
  "#constants": "./src/lib/constants.js"
}

and use them in your code anywhere like this:

import { UserModel } from "#models";
import { Validate } from "#utils";
import { SERVICE_API_KEY } from "#constants";

Note

You should see your application run fine but your IDE of choice may show some errors. Undesirable red and yellow squiggles are no one’s favorite. It would also auto-import from the actual relative path instead of the path alias. That’s no fun.

jsconfig.json to the rescue. (tsconfig.json if you’re in a TypeScript project.)

In your jsconfig.json, add the following

"compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "#models": ["./src/db/models/index.js"],
      "#utils": ["./src/lib/utils.js"],
      "#constants": ["./src/lib/constants.js"]
    }
}

The above configuration tells your IDE’s LSP to look for code in the given prefixes. Refer to the documentation of the property to know more.

Now we have sweet auto-imports from the desired location:

Screenshot of auto-import working desirably

Fallback dependencies

As seen in the documentation, you can also use this property for conditionally setting up fallback packages or polyfills. From the documentation:

// package.json
{
  "imports": {
    "#dep": {
      "node": "dep-node-native",
      "default": "./dep-polyfill.js"
    }
  },
  "dependencies": {
    "dep-node-native": "^1.0.0"
  }
}

[Here, if the] import #dep does not get the resolution of the external package dep-node-native (including its exports in turn), and instead gets the local file ./dep-polyfill.js relative to the package in other environments.

Frontend projects

I haven’t tried this approach with frontend applications. They generally use a bundling system like Webpack or Rollup which have their own way of resolving aliases. For example, for Vite (which uses Rollup and ESBuild), you should add this to your vite.config.js:

import path from "path";

export default defineConfig({
//   Some other config
	resolve: {
		alias: {
			"#": path.resolve(__dirname, "./src"),
		},
	},
});

and in your jsconfig.json:

{
	"compilerOptions": {
		"baseUrl": ".",
		"paths": {
			"#/*": ["src/*"]
		}
	}
}

The above configuration maps everything starting with # to immediate folders and files below src. YMMV.

A designer knows he has arrived at perfection not when there is no longer anything to add but when there is no longer anything to take away

- Jon Bentley in Programming Pearls