Originally published on abhijithota.me
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";
Note
- The entries in the
imports
field ofpackage.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:
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 packagedep-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
Top comments (0)