If you're new to JavaScript, you may have noticed that there are two ways to include code from another file: the import statement and the require() function. These come from two separate module systems. How do you know which one you should use? Let's get into it!

Why are there two methods?

JavaScript is a language with humble beginnings. When it appeared in 1995, it provided a way to add dynamic behaviour to a web page for the first time. Before JavaScript, web pages had to be completely static. There was no alternative. But JavaScript has historically been viewed as a “plaything” by real programmers gatekeeping arseholes. They felt it lacked the features necessary for “serious” programming.

JavaScript has grown a lot in a short space of time! Fast forward to 2009, when Ryan Dahl realised he could use Chrome's V8 JavaScript engine to run JavaScript on the server side. I'm talking about Node.js. This opened up a world of possibilities for JavaScript developers! It meant we could use a single programming language on the client side and the server side, rather than having to learn another language like PHP.

The real programmers gatekeeping arseholes did have a point, though. Some aspects of server-side programming are arguably more complex than client-side programming, although the lines are blurred today. As our JavaScript programs got bigger and more complex, we needed a way to split our code into multiple files. This is a feature that JavaScript didn't support when Node.js came out, and that's why there are two module systems.

CommonJS modules

Let's start with CommonJS, the module system that uses the require() function. We'll use an example from the Node.js documentation. Imagine we have a file called circle.js which exports a couple of helper functions for working with circles.

Note that CommonJS modules are only supported in Node.js, not in the browser.

Named exports in CommonJS modules

To export multiple things from a CommonJS module, we can make them properties of the module.exports object:

module.exports.area = function (radius) {
return Math.PI * radius ** 2;
};

module.exports.circumference = function (radius) {
return 2 * Math.PI * radius;
};

We could also write this more succinctly. Node.js provides a variable called exports which is assigned the value of the module.exports property. This means we can write exports.thing instead of module.exports.thing, since the exports variable and the module.exports property reference the same object:

exports.area = function (radius) {
return Math.PI * radius ** 2;
};

exports.circumference = function (radius) {
return 2 * Math.PI * radius;
};

We could also reassign the module.exports property with a brand new object. It's good practice to reassign the exports variable too, otherwise it will become out of sync with the module.exports property. Note that this code makes use of ES6 shorthand method names:

module.exports = exports = {
area(radius) {
return Math.PI * radius ** 2;
},
circumference(radius) {
return 2 * Math.PI * radius;
},
};

I think reassigning the module.exports property is “cleaner” when we're exporting things we've already declared. Note that this code makes use of ES6 shorthand property names:

function area(radius) {
return Math.PI * radius ** 2;
}

function circumference(radius) {
return 2 * Math.PI * radius;
}

module.exports = exports = { area, circumference };

Either way, we could import our code in another file in one of two ways. We could either import the entire object via a namespace, in this case circle:

const circle = require("./circle.js");

const radius = 4;

const area = circle.area(radius); // 50.26548245743669
const circumference = circle.circumference(radius); // 25.132741228718345

Or we could import the named exports using destructuring assignment:

const { area, circumference } = require("./circle.js");

const radius = 4;

const myArea = area(radius); // 50.26548245743669
const myCircumference = circumference(radius); // 25.132741228718345

Demo: Named exports in CommonJS.

Default export in CommonJS modules

If we only need to export one thing from a CommonJS module, we can reassign the module.exports property with that instead of an object literal. For example, let's export a Circle class with the existing functions as instance methods:

module.exports = exports = class Circle {
constructor(radius) {
this.radius = radius;
}

area() {
return Math.PI * this.radius ** 2;
}

circumference() {
return 2 * Math.PI * this.radius;
}
};

We could also declare the class first before exporting it:

class Circle {
constructor(radius) {
this.radius = radius;
}

area() {
return Math.PI * this.radius ** 2;
}

circumference() {
return 2 * Math.PI * this.radius;
}
}

module.exports = exports = Circle;

Either way, we could import it using whatever name we like. It makes sense to use the same name as the class itself, in this case Circle:

const Circle = require("./circle.js");

const circle = new Circle(4);

const area = circle.area(); // 50.26548245743669
const circumference = circle.circumference(); // 25.132741228718345

Demo: Default export in CommonJS.

ES modules

ECMAScript (ES) modules are the official standard for working with modules in JavaScript code. They're supported in all modern browsers and also in Node.js with some extra configuration.

Named exports in ES modules

To export multiple things from an ES module, we put the export keyword in front of each thing we want to export. These are known as named exports:

export function area(radius) {
return Math.PI * radius ** 2;
}

export function circumference(radius) {
return 2 * Math.PI * radius;
}

We could also put the named exports inside curly braces. Although it looks similar, this is not the same thing as an object literal. The functions are still exported individually, not as properties of an object see next week's post. This is different from CommonJS modules:

function area(radius) {
return Math.PI * radius ** 2;
}

function circumference(radius) {
return 2 * Math.PI * radius;
}

export { area, circumference };

To import everything via a namespace, we write it like import * as x from "./y.js", where x is the namespace and y is the filename:

import * as circle from "./circle.js";

const radius = 4;

const area = circle.area(radius); // 50.26548245743669
const circumference = circle.circumference(radius); // 25.132741228718345

To import the named exports individually, we use curly braces. This is not the same thing as destructuring assignment, even though it looks similar. This is because the named exports were exported individually, not as properties of an object see next week's post. This is different from CommonJS modules:

import { area, circumference } from "./circle.js";

const radius = 4;

const myArea = area(radius); // 50.26548245743669
const myCircumference = circumference(radius); // 25.132741228718345

Demo: Named exports in ES modules.

Default export in ES modules

If we only need to export one thing from an ES module, we can use a default export. This means we put export default in front of the thing we want to export:

export default class Circle {
constructor(radius) {
this.radius = radius;
}

area() {
return Math.PI * this.radius ** 2;
}

circumference() {
return 2 * Math.PI * this.radius;
}
}

We could also declare the class first before exporting it:

class Circle {
constructor(radius) {
this.radius = radius;
}

area() {
return Math.PI * this.radius ** 2;
}

circumference() {
return 2 * Math.PI * this.radius;
}
}

export default Circle;

We could even write it a third way, although beware that export default thing is different to export { thing as default }:

class Circle {
constructor(radius) {
this.radius = radius;
}

area() {
return Math.PI * this.radius ** 2;
}

circumference() {
return 2 * Math.PI * this.radius;
}
}

export { Circle as default };

We can import the default export like so, using whatever name we like. Again, it makes sense to use the same name as the class itself, in this case Circle:

import Circle from "./circle.js";

const circle = new Circle(4);

const area = circle.area(); // 50.26548245743669
const circumference = circle.circumference(); // 25.132741228718345

Demo: Default export in ES modules.

Common issues

Reassigning the exports variable in CommonJS modules

If you reassign the exports variable without also reassigning the module.exports property, it won't work:

// The `Circle` class will NOT be exported!
exports = class Circle {
constructor(radius) {
this.radius = radius;
}

area() {
return Math.PI * this.radius ** 2;
}

circumference() {
return 2 * Math.PI * this.radius;
}
};

Before the module is evaluated, Node.js assigns the value of the module.exports property to the exports variable. At this point, they're referencing the same object. If you only reassign the exports variable, then it will reference the thing you're trying to export, but the module.exports property will still reference an empty object. This is why it's a good idea to reassign the module.exports property and the exports variable at the same time, to keep them in sync:

module.exports = exports = class Circle {
constructor(radius) {
this.radius = radius;
}

area() {
return Math.PI * this.radius ** 2;
}

circumference() {
return 2 * Math.PI * this.radius;
}
};

ES modules in the browser

If you want to use ES modules in the browser, you have to add the type="module" attribute to your <script> element:

<script type="module">
import Circle from "./circle.js";

const circle = new Circle(4);

const area = circle.area(); // 50.26548245743669
const circumference = circle.circumference(); // 25.132741228718345
</script>

You also have to run your code on a server when developing locally.

ES modules in Node.js

If you want to use ES modules in Node.js, you have a few options:

  1. Use the .mjs extension instead of .js
  2. Add a "type": "module" field to your package.json file
  3. Use the --input-type=module flag via the command line interface (CLI)

The first option is great when you have a mix of CommonJS and ES modules in your project. This could be useful for an incremental adoption strategy. If you know that every file in your project uses ES modules, then the second option would be better, because you could keep using the regular .js extension. The third option is less common, but it's useful to know it exists.

Module bundlers

A module bundler is a tool that takes a number of JavaScript modules and bundles them into a single file. This is useful when you're writing code that needs to run in older browsers that don't support ES modules, for example. Because they transform your code before it runs in the browser, module bundlers let you do things that are impossible to do natively.

For example, some module bundlers let you import code without specifying the file extension, e.g. import Circle from "./circle". This is invalid if you're writing code directly for the browser without running it through a module bundler. You can do this in CommonJS, however.

Some module bundlers also let you import other types of asset, such as images and CSS files. Here's an example from Create React App, which uses Webpack:

import logo from "./logo.svg";
import "./App.css";

This doesn't work natively. It only works because Webpack does some clever stuff. The logo import works out the final path to the logo.svg file after it's been bundled, while the CSS import expresses that this JavaScript file depends on the App.css file. The contents of the SVG and CSS files don't actually get included inside the JavaScript file, which would obviously be invalid.

Which method should you use?

Use CommonJS modules for Node.js and ES modules for the browser. If you're working as part of a team, be consistent with whatever your team uses. One might argue that you should use ES modules for new Node.js code, but I've found that they don't always play nicely. For example, you might be using a package from npm that doesn't support ES modules. My stance on this might change in the future, though, especially as the number of projects using ES modules increases.

Next week

Next week, we'll learn about some of the more advanced things we can do with ES modules, such as importing them dynamically.