JavaScript Module System Explained

The modules is the last core concept in JavaScript we must discuss before moving on to more practical problems. After this lesson, we are going to discuss how JavaScript works in a web application, as both the frontend and the backend.

So far, all of the examples we've seen can be placed in one single JavaScript file, but as your application gets bigger, that will put a lot of pressure on future maintenance. It is better to split your application into multiple files based on their specific purposes or functionalities. These files are called modules.

A module may contain classes, functions, objects, variables, and so on. These resources should be exported, so that they can be imported into the files that depend on them.

A brief history

The module system for JavaScript has been missing for a long time. Because initially, JavaScript was just designed to build small scripts that run in the browser, there wasn't need for a module system. But later, the language evolved into something much more powerful.

In 2015, a built-in module system was still missing in JavaScript, but people were already building large-scale applications with it. The solution back then was CommonJS. A CommonJS module is loaded with the function require().

javascript
1const module = require("package");

Later, a new module system was introduced to the JavaScript standard called ES Modules, which allows you to import modules using the keyword import.

javascript
1import { module } from "package";

Their main difference is that CommonJS modules are loaded synchronously. Because require() is just a standard function, it is executed where it is placed in the file. However, for ES Modules, JavaScript will analyze all the import statements first, and the modules will be loaded asynchronously, allowing better optimizations.

The transition from CommonJS to ES Modules has been slow. Even today, before we can start using the ES Modules in Node.js, the functionality has to be enabled first. It is disabled by default to avoid conflict with the old CommonJS.

To enable ES Modules, run the following command:

cmd
1npm init

You will be prompted to type in some information about your project. For now, you can just keep everything to default.

After that, a package.json should be generated at the root directory of your project. This is a configuration file for your Node.js project, and inside this file, add the "type": "module" key.

json
1{
2  "name": "temp",
3  "version": "1.0.0",
4  "description": "",
5  "type": "module",
6  "main": "index.js",
7  "scripts": {
8    "test": "echo \"Error: no test specified\" && exit 1"
9  },
10  "author": "",
11  "license": "ISC"
12}

This key enables the use of ES Modules in your project.

Export

Next, let's talk about how to export a resource from a module. Assuming you have the following file structure:

text
1.
2├── index.js
3├── module.js
4└── package.json

You can export almost anything using the keyword export. For example,

javascript
1export const YEAR = 2022;
2
3export let array = [1, 2, 3];
4
5export function example() {
6  return array;
7}
8
9export class User {
10  constructor(name) {
11    this.name = name;
12  }
13}

Or you can declare the resource and then have a list of exports, which gives a cleaner syntax and is easier to maintain:

javascript
1const YEAR = 2015;
2
3let array = [1, 2, 3];
4
5function example() {
6  return array;
7}
8
9class User {
10  constructor(name) {
11    this.name = name;
12  }
13}
14
15export { YEAR, array, example, User };

Import

And then, go to index.js, and let's import the resources from module.js. You can specify the resources you wish to import inside the curly braces.

javascript
1import { YEAR, array, example, User } from "./module.js";
2
3console.log(YEAR); // -> 2022
4
5console.log(array); // -> [1, 2, 3]
6
7console.log(example()); // -> [1, 2, 3]
8
9let user = new User("John Doe");
10
11console.log(user); // -> User { name: 'John Doe' }
text
12015
2[ 1, 2, 3 ]
3[ 1, 2, 3 ]
4User { name: 'John Doe' }

Sometimes, you may encounter situations where you need to change the name of the imports. For example, you may be importing from two different modules, and they happen to have two functions with the same name.

In this case, you can change the name of the imports using the keyword as.

javascript
1import {
2  YEAR as year,
3  array as myArray,
4  example as e,
5  User as CustomUser,
6} from "./module.js";
7
8console.log(year);
9
10console.log(myArray);
11
12console.log(e());
13
14let user = new CustomUser("John Doe");
15
16console.log(user);

If you wish to simply import everything, use the wildcard selector (*). In this case, you have to name the imported module using as.

javascript
1import * as module from "./module.js";
2
3console.log(module.YEAR);
4
5console.log(module.array);
6
7console.log(module.example());
8
9let user = new module.User("John Doe");
10
11console.log(user);

Export default

In most cases, it is best to organize your project so that each module only exports a single resource, which is usually a class. For example, your project could have the following structure:

text
1.
2├── comment.js
3├── index.js
4├── package.json
5├── post.js
6└── user.js

The user.js file could contain properties and methods related to users, which are placed inside a User class. You could mark this class as default when exporting it.

javascript
1export default class User {
2  constructor(name) {
3    this.name = name;
4  }
5  . . .
6}

This way, when importing the User class, the curly braces can be omitted.

javascript
1import User from "./user.js";

The default keyword also allows you to automatically assign an alias when importing the module, even without using as. For example,

javascript
1import myUser from "./user.js";

This example also works, and User will be imported as myUser in this case.