Module Resolution or Import Alias: The Final Guide

A guide how to enable this feature in (almost) any JS/TS project.

Published at Nov 29, 2020

Hello, devs.

This is might be an old topic but I think it still can be a bit confused when you try to do this configuration:

How can I add aliases to my imports?

The idea here is to do not stitch in any specific framework/tool but to give you an idea of knowing what and how to do, based on your application setup.

First, let's check what problem we're trying to solve, a solution in a more abstract way and how to configure your project to support that.

Table of Content

Problem

In well-structured JavaScript applications it's common we organize our codebase in a way that makes explicit what these files do or each domain they belong to.

Notwithstanding we don't have a standard of "how to structure", we always try to organize like components, helpers, models, etc. and sometimes, creating subfolders inside these folders and as consequence, creating a deeply nested structure.

Let's see an example of a tiny and not too nested app folder structure:

.
├── package.json
├── src
│   ├── components
│   │   └── Button
│   │       ├── Button.js
│   │       └── index.js
│   ├── helpers
│   │   └── text.js
│   └── index.js
└── yarn-error.log

This is a very basic setup where:

  1. We have our src (source) which holds our app code;
  2. src/components which holds all our app components;
  3. src/helpers, which holds all our helpers/utilities which can be used anywhere in our code

Now let's say we to import a function called sanitizeText from our helper text inside our Button component. We would do something like:

import React from "react";
import { sanitizeText } from "../../helpers/text";

export const Button = ({ text }) => {
  return <button>{sanitizeText(text)}</button>;
};

It's not so bad, but as soon as you start having more and more imports from different folders and levels, it starts to become more confusing.

Also, you always need to guess how many levels you have to go up and down to import your code. Of course, modern code editors can help you out with that by just showing you which level you are and sometimes even completing it for you, but still.

Another problem is that if you eventually create a subfolder (for whatever reason you might have), you need to fix ALL imports by adding another "go up" level in the path.

That's not too much work but isn't by far optimal. We have a better way to do that and it's via module resolution or import alias

Solution

Module resolution or import alias is a way we can emulate the same way we import node_modules but with our internal code.

We can say to the tool we're using:

Hey, when you encounter "helpers/text", could you consider "./src/helpers/text" please?

In the same example above we would have some code like this:

import React from "react";
import { sanitizeText } from "helpers/text";

export const Button = ({ text }) => {
  return <button>{sanitizeText(text)}</button>;
};

Much cooler, right?

You'll import from helpers and it does not matter how deep you are in your app structure. Even if you move around this file, the imports will be always correct.

At the end of the day it's like import a library in our project. Imagine if you have to every time go up and down inside node_modules until you find your dependency:

import React from "react";
import { sanitizeText } from "helpers/text";
import { camelCase } from '../../../node_modules/lodash'

export const Button = ({ text }) => {
  return <button>{camelCase(sanitizeText(text))}</button>;
};

This would be hell. A lot of waste of energy having to navigate to the code.

Configuration

JavaScript itself does not allow us doing that those imports natively. But luckily we are always using a tool that supports that (e.g. Babel, Webpack, etc).

You might think:

Nice, I only need to do once this configuration!

And the answer is: it depends... but probably not.

The more tooling you introduce in your application, the more complicated it becomes to get it configured. Here some examples where this could be tricky:

  • If you're working on a JS project only with Babel and add jest to test your app, you'll need to add aliases in both places.
  • If you're working in a JS project with some built-in Webpack config and you add Storybook, you'll need to add an alias in both webpack.config.js and also customize Storybook babel.config.js.
  • If you're working on a JS project with all these configurations and want to move to TypeScript, you'll need to keep this configuration everywhere plus configure the tsconfig.json file to let TS aware of how to resolve those aliases.

As you can see this can be puzzling but here I want to give you an understanding of each possible tool. After that, you'll check how your project works and do all configurations needed to enable this feature into your project.

Editor completion

Before we dive deep into the configs, let's talk about Developer Experience (DX).

If you use VSCode, you probably already notice that when you need to import something, because VSCode uses TS, and it does a lot of inference, usually it's possible either to automatically imports the method you're trying to use or have an autocompletion for the imports, right?

When you just configure alias in Babel, for example, you kinda lost that and that's sucks.

If we want to let VSCode aware of how to suggest these modules, we need to create a file called jsconfig.json (in root level), which it's a JavaScript version of tsconfig.json and also declare those alias there:

jsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "*": [
        "./src"
      ],
      // Your custom aliases here
      "helpers/*": [
        "helpers"
      ],
      "models/*": [
        "models"
      ]
    }
  }
}

Every time you add a new alias to your project, all you need to do is come to this file and add your new alias.

If you use Typescript you don't need this file because this configuration will be made there.

Now, let's dive deep into the specifics.

Babel

Description

If your project has a babel.config.js or .babelrc file at the root level, you'll probably need to do this configuration.

Configuration

First, you need to install the plugin babel-plugin-module-resolver:

yarn install -D babel-plugin-module-resolver

Then, add this plugin into your babel config file:

.babelrc
{
  "presets": [
    // ....
  ],
  "plugins": [
    // ....
    [
      "babel-plugin-module-resolver",
      {
        "root": [
          "./src"
        ],
        "alias": {
          // Add your aliases here
          "helpers": "./src/helpers",
          "models": "./src/models"
        }
      }
    ]
  ]
}

After this, you'll be able to import files from helpers/<file> and models/<file>.

Resources

Rollup

Description

Rollup is one of the most annoying tools to do this configuration. That's because the official plugin to do that isn't smart enough to recognize index.js imports.

If you try to do something like models/User/ the plugin will complain saying that "models/User" is a folder, not a file. In that sense, you'll need to give full import, like models/User/index.

After some tests, I've realized that isn't necessarily the plugin's fault. If we try to enable this feature via @rollup/plugin-babel, we still need to import index at the end of our import. It seems that that's the way Rollup handles import and we cannot do anything about it.

Configuration

The first step is installing @rollup/plugin-alias.

After that, in your rollup.config.js you import it and set it up:

rollup.config.js
import alias from "@rollup/plugin-alias";
import path from "path";

export default {
  input: "src/index.js",
  output: {
    format: "cjs",
    dir: "dist",
  },
  plugins: [
    alias({
      /**
       * For custom files extension you might want to add "customerResolver"
       * https://github.com/rollup/plugins/tree/master/packages/alias#custom-resolvers
       *
       * By doing that this plugin can read different kind of files.
       */
      entries: [
        {
          find: "models",
          replacement: path.resolve(__dirname, "src/models"),
        },
        {
          find: "helpers",
          replacement: path.resolve(__dirname, "src/helpers"),
        },
      ],
    }),
  ],
};

Remember: imports from index.js files DOES NEED to be full imports

Resources

Webpack

Description

Webpack allows us to do the same as Babel but via its configuration. In case you're working on a project which already had a webpack.config.js somewhere in the code, it's there you'll need to tweak.

Configuration

webpack.config.js
const path = require('path');

module.exports = {
  // ...
  resolve: {
    alias: {
      // Your custom aliases here
      // __dirname will stands for your root directory level
      // path.resolve will concatenate your project folder location with the aliased folder
      helpers: path.resolve(__dirname, 'src/helpers/'),
      models: path.resolve(__dirname, 'src/models/')
    }
  }
};

If you're working with a framework/platform which works with Webpack under the hood and allows you to extend its configuration, the solution will be slightly different but the same concept.

For example, I'll show you a next.config.js file from NextJS which allow us to extend their configuration:

next.config.js
const path = require('path');

module.exports = {
  webpack: (config) => {
    // Mutate the alias configuration
    config.resolve.alias = {
      // Spread everything to avoid remove any alias they might have
      ...config.resolve.alias,
      // Your custom aliases here
      helpers: path.resolve(__dirname, 'src/helpers/'),
      models: path.resolve(__dirname, 'src/models/')
    }

    // Important: return the modified config
    return config
  },
}

Resources

Jest

Description

Now that we already have our aliases working in our regular environment, let's see how can we make Jest aware of how to resolve our aliases

Unfortunately, their API for that is by far one of the most complicated ones. That's because they use for stub out mocks and their test stuff and not for our organized code. But luckily we can grasp it.

To do that, first, you need to understand where your jest config is. It can be inside of package.json or in a jest.config.js file in the root level of your directory.

Configuration

jest.config.js
module.exports = {
  // ...
  moduleNameMapper: {
    // Your custom aliases here
    "^helpers/(.*)": "<rootDir>/src/helpers/$1",
    "^modules/(.*)": "<rootDir>/src/modules/$1",
  },
};

Resources

TypeScript

Description

For TS projects, we usually have to attack in two ways:

  1. Configure the transpiler/compiler/bundler tool (e.g. Webpack, Babel)
  2. Configure tsconfig.json

That's because those tools use a lot of internal tools AND Typescript to generate your final files and for Typescript, what matters is your tsconfig.json file.

Also, you might want to add tsc --noEmit as a fancy linter to be sure that at least your code has no errors and will compile right.

Configuration

Open your tsconfig.json file and set a baseUrl and paths:

tsconfig.json
{
  "compilerOptions": {
    // ...

    /* Base URL is a MUST. */
    "baseUrl": ".",
    /* Your aliases will live here */
    "paths": {
      "models/*": [
        "src/models/*"
      ],
      "helpers/*": [
        "src/helpers/*"
      ],
    }
  }
}

Tip: If you're using VSCode, usually you'll probably need to restart it to be able to navigate these aliased imports.

Just to be clear, baseUrl: "." will tell Typescript to consider the root level of your project to build the paths. In that sense, TS will find "models" and consider something like <root>/src/models/*.

It's also common for people defining baseUrl to be src. If you do so, you can remove src from your paths:

tsconfig.json
{
  "compilerOptions": {
    // ...

    /* Base URL is a MUST. */
    "baseUrl": "src",
    /* Your aliases will live here */
    "paths": {
      "models/*": [
        "models/*"
      ],
      "helpers/*": [
        "helpers/*"
      ],
    }
  }
}

Resources

Other tools

I tried to focus on the most common tools we use independently a specific framework but each one can have some specificities on how to extend or do this in an easy way.

My suggestion for you to figure this out is: always search at Google for <framework-or-tool-name> import alias. Usually, you'll find the answer in one of the top 3 results.

Prefix Strategy

In all examples, I just use regular names for our aliases but it's also a common practice is adding a prefix (a character before) to them.

Personally, I'm very fan of the prefix @ just because it's really nice to read import something "at" components but some people don't like this strategy because this special character is very popular for orgs, like @babel, @rollup, and it can mislead developers to think that this import is from an external resource.

As you notice, this is optional. Feel free to use or not whatever special char to give more clarity about internal X external imports.

Conclusion

I hope at the end of this article you feel comfortable to tweak your configs and enable this feature if you feel useful somehow.