Node.js Import Aliases

2 minute read for ~600 words

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:

  • Sensitivity to folder structure changes: A good IDE or editor can auto-import but not all of them are without errors. Also, what if you change something outside your general IDE?
  • Clutter: It just simply looks bad

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";


  • The entries in the imports field of package.json must be strings starting with # to ensure they are disambiguated from package specifiers like @.

  • The values should be relative paths from the root of the project. The root is where your package.json is.

    In the above example, we assumed package.json was at the root and all the relevant files were inside a src directory.

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