How to avoid relative paths in nodejs typescript project

Updated on: Sun Aug 08 2021

Hello! Here I'll explain how to avoid relative path's hell in nodejs based typescript project. To illustrate this I'll use some super duper tiny example.

All code related to this article could be found in this github repo.

First of all what's the problem? Here it is:

copied ts/tsx

// src/some/deep/inside/of/your/codebase/some-module.ts
import someFunction from '../../../../../utils/common/someFunction';

// src/not/deep/module.ts
import someFunction from '../../utils/common/someFunction';

It's hard, not readable, quite annoying and not cool to have this '../../../..' relative paths in a codebase. So we wanna achieve something like:

copied ts/tsx

// src/some/deep/inside/of/your/codebase/some-module.ts
import someFunction from '@src/utils/common/someFunction';

// src/not/deep/module.ts will have same import
import someFunction from '@src/utils/common/someFunction';

To achieve this we have to do 2 things:

  • Teach typescript to follow treat @src properly during development process and build time
  • Trick nodejs runtime to resolve our modules from proper locations

First thing achievable via some improvements in typescript configuration. Let's assume we have following tsconfig.json:

copied json

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "outDir": "./dist",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

If we'll run tsc in our root directory typescript will compile our source files into javascript and put compiled bundles into dist folder. But this will not work if we want to start do absolute imports via aliases somewhere in our codebase. This code will trigger a compilation error:

copied ts/tsx

import someFunction from '@src/lib/someFunction'

Let's fix that leveraging baseUrl and paths config options in tsconfig.json:

copied json

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

Now typescript compiler knows what @src alias means and resolves it to files in ./src folder relative to baseUrl which itself calculates from current tsconfig.json file location on filesystem.

Let's run tsc command again. Whoa! No compilation errors and we now see all our javascript code compiled in dist folder.

But it's not the time to open champagne! Let's try to run our compiled javascript via node:

copied javascript

node dist/index.js

We'll see that node runtime can not resolve module in @src/lib/someFunction. And this makes us sad, because code written well but doesn't bring any value. This happens because typescript does no magic with those imports and if we'll take a look at our compiled javascript bundles in dist folder we'll still see that aliased import @src/lib/someFunction.

To teach runtime resolve this import properly we have to use some tricks. Most well known and widely used is module-alias nodejs package. To utilise it let's install it first:

copied bash

yarn add -D module-alias

Then we have to add just 2 things. Add alias mapping of @src to the dist folder where all bundles are located in package.json via:

copied json

{
  ...
  "_moduleAliases": {
    "@src": "dist"
  },
  ...
}

And second we have to run module-alias's register code as a first thing in our app. So let's add it's import right in the top of our root index.ts file:

copied ts/tsx

import 'module-alias/register';
import express from 'express'; // or something
...
// the rest of your app

Basically module-alias tweaks a bit nodejs resolve module logic. It's well and concisely explained in the package's readme.

And now if we'll try to run our built code everything will be fine!