JavaScript modules have become the standard way to organize code in modern web development. Whether you are building a project that runs in the browser or in Node.js, modules make your code easier to structure, reuse, and maintain.
In this tutorial, we will go through the basics of ES6 modules, look at examples of how they work, and explain why they are such an important part of today’s JavaScript ecosystem.

A brief history of JavaScript modules
Before native modules became part of the language, developers used different systems to organize code into smaller, reusable parts. Solutions like CommonJS, Require.js, and Browserify made modular code possible before browsers supported it natively.
CommonJS became the foundation of Node’s package management system, npm, which let developers create and share reusable modules. But this worked only on the server side. For browsers, developers used tools such as Require.js and Browserify to simulate the same modular behavior, and later, bundlers like Webpack and Parcel made the process more efficient.
Today, the ecosystem has evolved. Native modules are now part of JavaScript itself, and modern build tools like Vite, Rollup, and esbuild rely on ES Modules as their foundation.
If you want to delve deeper into the details of the history of JavaScript modules, see this article.
Native JavaScript modules
SoIn 2015, the ECMAScript standard officially added support for native modules (also called ES Modules). This addition made it possible to write modular JavaScript code directly in browsers without needing third-party libraries or bundlers.
Native modules are useful for many reasons:
- They allow developers to encapsulate and organize code into smaller, reusable files.
- They help prevent duplicate code and keep projects easier to maintain.
- They improve performance by letting browsers load only the code that is needed at any given time.
- They reduce naming conflicts by keeping variables and functions isolated to their own scope.
- They make testing easier, since individual modules can be tested on their own.
Today, ES Modules are supported in all major browsers and in Node.js, where you can enable them by adding "type": "module" to your package.json file.
With that background covered, let’s now look at how to use JavaScript modules in practice.
Syntax for including JavaScript modules
You can insert any JavaScript module into a web page using almost the same syntax as any other script. But note one small difference:
<body>
...
<script src="index.js" type="module"></script>
</body>
Code language: HTML, XML (xml)
In the above code, I’ve added the <script> element to the bottom of my HTML page. I’m referencing the script as I normally would using the src attribute. But, notice the type attribute. Instead of the customary value of text/javascript, I’ve used a value of module. This tells the browser to enable JavaScript module features in this script rather than treating it like a normal script.
How does a script differ when it’s included as a module?
- The browser automatically defers scripts with
type="module", so using thedeferattribute would not effect this script. - Modules are subject to CORS rules, meaning you can access modules only on the same domain or those on a different domain that you’ve allowed permission to access via CORS. This is unlike regular scripts that you can load from anywhere.
- The browser interprets individual modules in strict mode.
In the example above, I can consider the index.js file as the entry point of my modular application. I could call this file whatever I want, but many developers customarily use index.js as their app entry point.
Now, I’m going to create some modules I’ll use inside index.js. Here’s a look at my folder structure so you can get an idea of how I’m organizing my modules:
index.html
index.js
modules/
one.js
two.js
three.js
Your own app’s folder structure and file/folder names might differ, but the above should be easy enough to understand for demo purposes here. The index.html file would be the one that includes the <script> element that inserts the initial module entry point (index.js).
Notice I’ve added a folder called modules, which will store all of my modules in separate files. In this case, there are three modules.
Syntax for importing and exporting JavaScript modules
Now that I have a module entry point and I’ve set up my folder structure, I’ll show you how you can import any of the modules into the main script.
The one.js file is going to hold the following simple module script:
export function add(a, b) {
return a + b;
}
Code language: JavaScript (javascript)
This is nothing but a simple add() function. The key portion of the script that makes this a usable module is the use of the export statement. This syntax is only usable inside a module. With this in place, I can then import the module inside main.js like this:
import { add } from './modules/one.js';
console.log(add(5, 8)); // 13
Code language: JavaScript (javascript)
The first line of code is where I import the add() function, wrapped in curly braces. I then define from where I want to import the module using the “from” keyword, followed by the module’s location in quotes.
Once I have the module imported, I can use the add() function as I would any function that I have access to.
You can also load modules dynamically using the import() function. This allows you to import code only when needed, which can help improve performance in large web applications.
const { add } = await import('./modules/one.js');
console.log(add(2, 3)); // 5
Code language: JavaScript (javascript)
Dynamic imports work in both browsers and Node.js and return a promise that resolves with the module’s exports.
In this instance, I’m calling the function and passing in two numbers, which get added, and the function returns the value.
I don’t have to export a value at the same time that I define it. For example, earlier, I exported the add() function by putting the export keyword ahead of the function keyword. I could alternatively do:
function add(a, b) {
return a + b;
}
export { add };
Code language: JavaScript (javascript)
Here I’m exporting the reference to add()rather than the declaration of add(). Note the use of curly braces around the add reference, which is required. The same principle would apply to anything I’m exporting – functions, variables, or classes. I can export the reference rather than the declaration.
That’s the simplest way to explain JavaScript modules syntax. There are quite a few other aspects to the code, so I’ll cover those next.
Syntax for multiple exports in a JavaScript module
In a JavaScript module file, I can export any number of values, including anything stored in a variable, a function, and so on. To demonstrate, I’ll add the following code to my two.js module:
export let name = 'Sally';
export let salad = ['lettuce', 'tomatoes', 'onions'];
Code language: JavaScript (javascript)
I’ve exported a simple name variable followed by an array called salad. Now I’ll import them, so my index.js file will look like this:
import { add } from './modules/one.js';
import { name, salad } from './modules/two.js';
console.log(add(5, 8)); // 13
console.log(name); // "Sally"
console.log(salad[2]); // "onions"
Code language: JavaScript (javascript)
Take note of the syntax required for importing multiple values. I’ve placed the name and salad references inside the curly braces and separated them with a comma. This list of comma-separated imports could be any number of references, as long as I properly export them from the two.js file.
Another way I can import multiple exports from a single file is by using the asterisk character during my import. Let’s say I have the following in my module:
export function add(a, b) {
return a + b;
}
export let num1 = 6,
num2 = 13,
num3 = 35;
Code language: JavaScript (javascript)
Notice I’m exporting a function along with three different variables. I can import all of these exports using the following syntax:
import * as mod from './modules/one.js';
console.log( mod.add(3, 4) ); // 7
console.log( mod.add(mod.num1, mod.num2) ); // 19
console.log( mod.num3 ); // 35
Code language: JavaScript (javascript)
Notice I’m exporting the entire module as an object called mod. From there, I can access all the functions, properties, or classes defined in the mod object using the familiar object dot notation.
In any of these examples, once I import the exports, I can then use them however I like in my application code.
In larger projects, you can also use Import Maps in the browser to manage module paths more easily. Import Maps let you define aliases for modules directly in HTML, so you can write cleaner imports without relative paths.
<script type="importmap">
{
"imports": {
"utils": "./modules/utils.js"
}
}
</script>
Code language: HTML, XML (xml)
import { formatDate } from "utils";
Code language: JavaScript (javascript)
Most modern browsers now support this feature natively.
Renaming JavaScript module exports
I can rename a module’s export before it’s exported, using the as keyword, as in the following example:
function add(a, b) {
return a + b;
}
export { add as addFunc };
Code language: JavaScript (javascript)
With that code inside the module, importing would look like this:
import { addFunc } from './modules/one.js';
Code language: JavaScript (javascript)
This allows me to use a different name for the export when it’s used inside the main application code compared to how it’s named in the module itself. I just have to make sure I reference the function as addFunc() when I use it.
I can also use the renaming syntax when doing the import. Assuming I’ve exported the function with its original name, add, I can do the following:
import { add as addFunc } from './modules/one.js';
Code language: JavaScript (javascript)
This is the same basic idea as the previous example; I’m just doing the rename on import rather than on export. Of course, you’d have to be careful when renaming so as not to cause confusion in the code. You should generally have a good reason for renaming the export to make sure the code is still readable and maintainable.
Exporting defaults in JavaScript modules
The way I’ve exported and imported parts of my module code in earlier code examples is using what’s referred to as a named export. The other kind of export is a default export. Exporting a default value from a module is a pattern carried over from third-party module systems that I mentioned earlier. This became part of the ECMAScript standard to be interoperable with those older tools.
I can define a default export as follows:
export default function add(a, b) {
return a + b;
}
Code language: JavaScript (javascript)
This is the same function I exported earlier, except this time I’m using the default keyword after the export keyword. I can also export a default value by reference:
function add(a, b) {
return a + b;
}
export default add;
Code language: JavaScript (javascript)
And I can use the renaming syntax:
function add(a, b) {
return a + b;
}
export { add as default };
Code language: JavaScript (javascript)
In the case of the renaming syntax, I’m simply using the keyword default in place of the export name.
To import any of the above default values, I can do the following:
import add from './modules/one.js';
Code language: JavaScript (javascript)
Notice I’m not using the curly braces around the import name. Non-default exports require curly braces, whereas a default export has no curly braces. Also, I can import any of the above exports using the following:
import addFunction from './modules/one.js';
Code language: JavaScript (javascript)
In this case, I’ve renamed the import. Because this is a default export, I can alter the name as I import it; I’m not restricted to using the exported name. This is also clear from the fact that the as syntax didn’t use a custom name when I exported the default.
The other thing that’s important to understand about default exports is that I can export only one value as the default. So, if I have multiple values I want to export in a module, I would use a similar syntax to the one earlier when I exported multiple items with an asterisk.
Here is my module:
export function add(a, b) {
return a + b;
}
export let num1 = 6,
num2 = 13,
num3 = 35;
Code language: JavaScript (javascript)
And here is my index.js file:
import mod from './modules/one.js';
console.log( mod.add(3, 4) ); // 7
console.log( mod.add(mod.num1, mod.num2) ); // 19
console.log( mod.num3 ); // 35
Code language: JavaScript (javascript)
The main difference here is that I’m not using the asterisk or the as keyword; I’m simply importing the entire module and then working with it as I would any object.
In Node.js, default and named exports work the same way as in the browser when your project uses "type": "module" in package.json. You can also mix CommonJS and ES Modules within the same project, though it’s best to choose one format for consistency.
More tips and facts on JavaScript modules
What I’ve discussed so far covers most of the basics to get you up and running with ES6 modules. Once you get past the basics, there are different subtleties you’ll want to keep in mind as you write your modules, which I’ll cover in this section.
Understanding encapsulation
Firstly, just because some code exists in a module doesn’t mean it’s going to be accessible in your primary script (the one that imports modules). For example, let’s say three.js has the following code:
function subtract (c, d) {
return c - d;
}
export function add (a, b) {
return a + subtract(a, b);
}
Code language: JavaScript (javascript)
I can then import and use the add() function, but notice what happens if I try to use the subtract() function:
import { add } from './modules/three.js';
import { subtract } from './modules/three.js';
// Error: The requested module './modules/three.js' does not provide an export named 'subtract'
console.log(add(23, 16)); // 30
console.log(subtract(30, 34)); // subtract is not defined
Code language: JavaScript (javascript)
Notice I can’t import the subtract() function, nor can I use it. The subtract() function is part of my module’s logic and is necessary for the module to work. But it’s not an exported function, so I don’t have access to it outside of the module unless I explicitly export it.
Relative path names
As shown in multiple examples above, I can import JavaScript modules by referencing a JavaScript file. But notice what happens if I try to use the following syntax:
import { add } from 'modules/three.js';
// Error: Relative references must start with either "/", "./", or "../".
Code language: JavaScript (javascript)
I can use an absolute file path (i.e., a full URL), and that would be no problem (as long as it passes CORS requirements). But if I’m using a relative file reference, the path needs to include a form that has a forward slash at the beginning of the path. This could be any one of the three formats shown in the error message above.
In Node.js projects that use ES Modules, relative imports must include the full file extension (for example, import { add } from './math.js'). This is required by the ESM specification and ensures that imports resolve correctly across environments.
Strict mode in modules
Each encapsulated module works in the same way that code in strict mode works (that is, code in a block that has a 'use strict' statement at the top). So whatever rules apply to strict mode, the same rules apply to code in a module. For example, the value of this in the top level of a module is undefined, which isn’t the case when not in strict mode.
This means you can reference this at the top level inside the file doing the importing or inside any of the modules and the result will be the same: undefined.
// inside a module
function add(a, b) {
return a + b;
}
console.log(this); // undefined
export { add };
Code language: JavaScript (javascript)
This is different from regular scripts in the browser, where this at the top level is a reference to the Window object.
// inside a non-module script not in strict mode
function add(a, b) {
return a + b;
}
console.log(this); // [object Window]
export { add };
Code language: JavaScript (javascript)
Implied const in modules
Another point to keep in mind is that anything I import behaves as if I defined it using const. If you’re familiar with const, this is a way to declare a variable that I can’t change (unless it’s an object, in which case I can change the properties).
To illustrate, suppose my two.js file contains the following:
export let name = 'Sally';
Code language: JavaScript (javascript)
I’m using let to define the variable that’s exported. But notice what happens if I try to change it after import:
import { name } from './modules/two.js';
console.log(name); // Sally
name = "Jimmy";
// Error: Assignment to constant variable.
Code language: JavaScript (javascript)
Even though I didn’t use const, the code import behaves as if I did.
Exporting and importing limitations
When doing imports or exports, I have to have them outside of other statements and functions. For example, the following code inside a module would throw an error:
if (name === 'Sally') {
export { name };
}
// Error: Unexpected token 'export'
Code language: JavaScript (javascript)
The same would result if trying to export inside of a function body.
Importing multiples using default and non-defaults
As mentioned, you can import only one default. But this doesn’t mean you’re limited to importing a single value from a module. For example, here’s my one.js file:
function add(a, b) {
return a + b;
}
export let name = "Sally";
export { add as default };
Code language: JavaScript (javascript)
Notice I’m exporting the add() function as the default, but I’m also exporting a variable.
Now here’s index.js:
import add, { name } from './modules/one.js';
console.log(name); // Sally
console.log(add(10, 8)); // 18
Code language: JavaScript (javascript)
I import the default with no curly braces and I export the variable with curly braces, and both are accessible as expected. When combining the default with non-default imports, I have to list the default first; otherwise, it will throw an error.
The exception would be if I were renaming the default, then I would put both inside the curly braces:
import { default as addFunc, name } from './modules/one.js';
console.log(name); // Sally
console.log(addFunc(10, 8)); // 18
Code language: JavaScript (javascript)
Using the .mjs file extension for JS Modules
You may still see the .mjs extension used for module files, especially in older projects. However, since Node.js v20, you can use .js for ES Modules by declaring "type": "module" in your package.json. The .mjs extension is now mostly optional, though it remains useful when mixing CommonJS and ESM code in the same project.
This small configuration change allows you to write and import modern JavaScript modules in both Node.js and browsers without extra build steps.
If you want to learn more about how .mjs works and the edge cases involved, you can read the detailed reference on MDN.
Wrapping up this tutorial
That wraps up this tutorial on JavaScript modules. There’s more I could talk about, including how modules play an important role in your build tools process and how these tools will minify your modules. But this should be enough to give you a basic framework from which to get started.
Feel free to use the code examples from here to create your own testing ground to try out these features. This way, you can work with some live examples in your personal development environment and become more familiar with how this JavaScript standard works in practice.
…
Don’t forget to join our crash course on speeding up your WordPress site. Learn more below:
How to Speed Up Your WordPress Site
With some simple fixes, you can reduce your loading times by even 50-80% 🚀
By entering your email above, you're subscribing to our weekly newsletter. You can change your mind at any time. We respect your inbox and privacy.









Thanks for sharing was really useful.