How to import modules via absolute paths on Node.js (with TypeScript and ESLint) back-end with Babel

I started to learn Node.js for back-end development with Express. I soon realized that Node.js didn’t have the capability of importing modules via absolute paths by default. After spending several hours googling, I found that there was no standard solution for it as of Node v12 (reference).

On this Stack overflow post, I saw so many approaches. Some of those were still using “require”, which I didn’t want to use.

One solution I found was to use Babel, which I know about for React app development but didn’t know that this was used for back-end development too (reference). This is a record of how I implemented it.

tl;dr

Here’s my solution. I used @babel/node. There are several other solutions found on Stack overflow which allow you to use absolute paths. If it’s necessary to use ES6+ and make it available in an older environment (or, experiment with the latest JavaScript features), this is the way to go.

(Another solution I found was to use webpack. This seems neat and simple, so I’d like to try this in the future)

yarn add -D @babel/core @babel/cli @babel/node @babel/preset-env babel-plugin-module-resolver

# NOTE:
# @babel/cli isn't necessary for dev environment. This will be used for building the production version.
# @babel/node used to be part of babel-cli until babel v6. Now this is used for dev environment only. For prodcution, check out : https://github.com/babel/example-node-server
# babel-plugin-module-resolver is the one that will make absolute paths availabe.


# If you're using TypeScript, add the following
yarn add -D @babel/preset-typescript @babel/types

# If you're using ESLint, add the following
yarn add -D eslint-import-resolver-babel-module eslint-plugin-import

# Finally, if you're using eslint with TypeScript, add the following
yarn add -D eslint-import-resolver-typescript @typescript-eslint/parser

I use ESLint and TypeScript, so I had to install all of them above.

NOTE: If you’re using Sublime Text, you may have to change the configure eslint-plugin-import a bit differently than the example below (reference).

#.babelrc
{
  "presets": ["@babel/preset-env", "@babel/preset-typescript"],
  "plugins": [
    [
      "module-resolver",
      {
        "root": ["./"],
        "extensions": [".ts"]
      }
    ]
  ]
}
#.eslintrc.json
# Note: I'm just putting the properties here which were necessary for using absolute paths with Babel. You would need other properties for your app to run (e.g. "env", "parserOptions" etc)
# I'm using React for front-end, which is why you see jsx/tsx below

{
  "extends": [
    "eslint:recommended",
    "plugin:react/recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:import/errors",
    "plugin:import/warnings",
    "plugin:import/typescript"
  ],
  "parser": "@typescript-eslint/parser",
  "plugins": ["react", "@typescript-eslint", "import"],
  "rules": {
    "import/no-unresolved": "error",
    "import/named": 2,
    "import/namespace": 2,
    "import/default": 2,
    "import/export": 2,
    "import/extensions": "off"
  },
  "settings": {
    "import/resolver": {
      "node": {
        "extensions": [".js", ".jsx", ".ts", ".tsx"]
      },
      "typescript": {
        "project": "./tsconfig.json"
      }
    },
    "import/parsers": {
      "@typescript-eslint/parser": [".ts", ".tsx"]
    }
  }
}
# package.json
# "yarn run server" will start the Express server and the TS code will be compiled by babel-node.
# Please note that I'm using nodemon.

  "scripts": {
    "server": "nodemon --exec babel-node ./server.ts --extensions \".ts\""
  }

Here’s the folder structure.

- client
- config
  | - db.ts
- models 
  | - index.ts
  | - user.models.ts
- server.ts
- .babelrc
- .eslint.json
- package.json
- tsconfig.json
- yarn.lock

The models/index.ts above used to have…

import { dbConfig } from '../config/db.config';
import { User } from './user.model';

Now those lines are…

import { dbConfig } from 'config/db';
import { User } from 'models/user.model';

The following is a note for myself. Honestly, I didn’t really remember how to work with Babel (despite having been working for front-end with React for 2+ years). So, I had to begin with what Babel was actually doing.

Why is Babel?

Babel is a toolchain that is mainly used to convert ECMAScript 2015+ code into a backwards compatible version of JavaScript in current and older browsers or environments. 

https://babeljs.io/docs/en/

I used to think that Babel is used for front-end only. This wasn’t right.

Some features of ECMAScript may not be available for an older Node.js (Here is a comprehensive list of it), just like some ECMAScript features are not available for an older browser.

According to Node.js’ doc, developers working for Node are making sure that new features from JavaScript ECMA-262 specification are available to Node.js by “keeping up-to-date with the latest releases of ” V8. V8 is an engine that is created by Google, and Node.js was built against it (reference).

Note: Next.js is using Babel with a preset called “next/babel”

What are ECMA-262, ECMAScript, and V8?

  • ECMA-262 is the specification of ECMAScript. This specification is “meant to ensure the interoperability of Web pages across different Web browsers.” (reference)
  • ECMAScript is a scripting language that the basis of JavaScript (ECMAScript isn’t JavaScript, but JavaScript can be as a type of ECMAScript) (reference)
  • V8 is Google’s open-source JavaScript/WebAssembly engine, used in Chrome and in Node.js (reference)

In other words, when ECMA-262 is updated, JavaScript may have new features. However, V8 is not necessarily updated because it’s developed by Google (independent from ECMA-262). When V8 is updated, people working for Node.js will try to make sure that all new features specified by ECMA-262 are available on Node.js.

Then, do I need Babel for Node.js back-end?

Probably not. The reasons are:

  1. I usually make sure that the dev environment is using the same version of Node as the production environment (What’s the point of using different versions?). So, I wouldn’t need to worry about if my applications won’t be working on production due to missing ECMAScript features.
  2. I think introducing Babel often comes with a lot of complexities. For example, you have to determine what “stage” of the ECMAScript should be targeted, which libraries and “presets” you need (@babel/present-evn, babel-loader, etc) and so on.

The only use case where Babel is needed is if you want to try an ECMAScript feature that hasn’t been supported by the latest Node.js. However, I’m usually not very keen to try the latest ECMAScript, and I’m rather concerned about the size and maintainability of my application.

So, after spending several hours, I concluded that what I need wasn’t Babel’s core feature itself (converting ES6+ JavaScript code so that it works on an older environment). I just wanted to use the module resolver.

Let’s begin the experiment

So, I installed babel-plugin-module-resolver. Then, by following this article, I also installed @babel/core, @babel/node, @babel/preset-env as dev dependencies. After that, I also set up .babelrc and updated the npm command for starting the app into nodemon --exec babel-node ./server.ts.

Note: --exec option of nodemon is to execute and monitor other programs

Then, I got this error:

C:\app\server.ts:1                                                                           import express from 'express'; 
       ^^^^^^^                

SyntaxError: Unexpected identifier
    at Module._compile (internal/modules/cjs/loader.js:703:23)
    at Module._extensions..js (internal/modules/cjs/loader.js:770:10)
    at Object.newLoader [as .js] (C:\app\node_modules\pirates\lib\index.js:104:7)
    at Module.load (internal/modules/cjs/loader.js:628:32)

I had to google again.

Some people say that you have to use @babel/preset-env. Others say babel-node is expecting the node style module syntax (i.e. module.exports = ... instead of export class ...), or you have to add .babel.config.js and run babel-node with --config-file .babel.config.js. None of them worked for me.

Then, I stumbled on this issue on Github. The poster saw a similar issue, which was solved by adding --extensions '.ts' to babel-node command.

I followed this, and the error was gone.

Ok… So, was this issue caused by TypeScript??

How does Babel work with TypeScript?

Apparently, yes. To make Babel work with TypeScript, I had to install @babel/preset-typescript, set it as a preset in .babelrc, and add the flag to babel-node command as below.

You will need to specify --extensions ".ts" for @babel/cli & @babel/node cli’s to handle .ts files.

https://babeljs.io/docs/en/babel-preset-typescript

What were presets in Babel?

Presets are collections of plugins. Here is the definition found on Babel’s doc.

Presets can act as an array of Babel plugins or even a sharable options config.

https://babeljs.io/docs/en/presets

Ok, @babel/preset-typescript and .babelrc are ready. On top of that, according to babel-plugin-module-resolver’s Github page, “you should use eslint-plugin-import, and eslint-import-resolver-babel-module to remove falsy unresolved modules”.

Still, however, VS code complains that my folders can’t be found via statements, even though the compilation itself is successfully done. After searching the internet for hours and hours, it turned out that the final piece of the puzzle was eslint-import-resolver-typescript. Once again, I updated .eslintrc.json (as described at the beginning of this article).

Miscellaneous

Until I made Babel work with TypeScript for my Node app, I repeated yarn install and yarn remove. During that time, I sometimes saw the following error.

C:\app\e-wallet\server.ts:1                                                                           import express from 'express';                                                                               ^^^^^^^                                                                                                                                                                                              SyntaxError: Unexpected identifier
at Module._compile (internal/modules/cjs/loader.js:703:23)                                            at Object.Module._extensions..js(internal/modules/cjs/loader.js:770:10)                              at Module.load (internal/modules/cjs/loader.js:628:32)

Strangely, the error was gone after I removed node_modules and yarn.lock, and then ran yarn install. It seems that some Babel dependencies won’t work if you install them in the wrong order.

Leave a Reply

Your email address will not be published. Required fields are marked *