Modules
Modules are a method of organizing and structuring the code in your program by dividing it into smaller pieces that can be imported and exported as needed. In this module, you will learn how to create and organize modules in JavaScript using the ES6 import and export standards. You will also learn about other methods for handling modules, such as CommonJS and AMD.
Why modules
In the original ECMA Script, you could only encapsulate data and functionality in functions, which could make a larger code base very complex to develop. Therefore, possibilities were sought to modularize ES, and several patterns/formats/standards have been created:
- Closures and functions in “pure” ES
- CommonJS
- AMD (Asynchronous Module Definition)
- UMD (Universal Module Definition)
The last three require a transpiler like WebPack, Browserify, or Parcel to work in a browser, and Node has chosen to use CommonJS as the standard.
Over the past few years, ES6 modules have become an option in all browsers and Node, so it is likely the way modules will be developed in the future. This course focuses on ES6 modules, but it’s important to know the legacy formats exist in older codebases.
ES6 modules
Adopting a modular structure in large JavaScript applications involves grouping related functionalities into modules, ensuring each module has a single responsibility. Best practices recommend using ES6 modules for clarity and maintainability, emphasizing the importance of naming conventions, clear and consistent export/import patterns, and the modularization of code based on functionality. Leveraging these practices facilitates code reuse, testing, and collaboration, while reducing complexity and improving the scalability of applications.
In ES6, a .js file is a module that in itself is a container where everything is private. If you want others to have access to data and functionality, it must be exported.
Example for node
The following examples can be executed through Node, which uses .mjs files to define modules. You can use .js files if there is a package.json file (use npm –init) with the type set to “module”.
Named exports
module1.js
or
module1.js
Named imports
These can then be imported into another module:
app.js
or
app.js
import * as Utils from "./module1.js";
console.log(Utils.a);
console.log(Utils.b());
let c = new Utils.C();
Default export
There is also a “default” export that does not require a specific named import:
Named vs Default exports
- Named exports: Use when exporting multiple items. Enforces consistent naming across imports.
- Default export: Use when module exports a single main item. Allows flexible naming on import.
- You can mix both in the same module, but it’s often clearer to stick to one style per module.
Example for browser
Here is an example for execution in a browser (note app.js is dependent on module1 which depends on module2):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Demo</title>
</head>
<body>
<h1>Header from HTML</h1>
<script src="app.js" type="module"></script>
</body>
</html>
app.js
module1.js
module2.js
Module scripts in browsers
When using type="module" in script tags:
- Scripts are deferred by default (wait for HTML parsing)
- Scripts run in strict mode automatically
- Each module has its own scope (no global pollution)
- CORS applies - modules must be served from same origin or with CORS headers
Modern Module Features
Dynamic imports
Dynamic imports in JavaScript are a powerful feature that allow you to load modules on demand. This can significantly improve performance in larger applications by reducing the initial load time. Dynamic imports use the import() function, which returns a promise. This makes it particularly useful for conditionally loading modules or when modules are needed during runtime.
Basic usage:
if (condition) {
import('./module.js')
.then(module => {
// Use module here
module.doSomething();
})
.catch(err => {
console.error('Module failed to load', err);
});
}
With async/await:
async function loadModule() {
try {
const module = await import('./module.js');
module.doSomething();
} catch (err) {
console.error('Module failed to load', err);
}
}
// Conditional loading
if (userWantsFeature) {
const feature = await import('./feature.js');
feature.initialize();
}
This pattern is invaluable for applications that require splitting their codebase into smaller chunks, optimizing resource loading as needed.
Top-level await (ES2022)
Top-level await allows you to use await at the top level of a module without wrapping it in an async function. This is particularly useful when a module needs to perform asynchronous setup before it can be used.
// config.js - fetch configuration before module is ready
const response = await fetch('/api/config');
const config = await response.json();
export default config;
// app.js - config is already loaded when imported
import config from './config.js';
console.log(config); // Configuration is ready to use
Top-level await considerations
- Only works in ES modules (not in scripts)
- Blocks other modules from executing until the await completes
- Be mindful of performance - avoid long-running awaits at module level
- Not supported in all environments yet - check compatibility
Import assertions
Import assertions allow you to import non-JavaScript resources like JSON files with type safety:
// Import JSON file
import data from './data.json' assert { type: 'json' };
console.log(data); // Parsed JSON object
Import assertions browser support
Import assertions are a newer feature with limited browser support. Check caniuse.com for current compatibility. For older browsers, you may need to fetch JSON manually:
Best Practices
Module organization:
- Keep modules small and focused (single responsibility)
- Use clear, descriptive file names that match exports
- Group related modules in directories
- Use index.js files for directory-level exports
Export patterns:
- Prefer named exports for better IDE support and refactoring
- Use default exports only for single-purpose modules
- Don’t mix too many named exports with a default export
- Export at the bottom of the file for better overview
Import patterns:
- Import only what you need (avoid
import *when possible) - Keep imports at the top of the file
- Group imports logically (third-party, then local modules)
- Use consistent path styles (relative vs absolute)
Performance:
- Use dynamic imports for large features loaded conditionally
- Avoid circular dependencies between modules
- Be cautious with top-level await - it blocks module loading
Summary
Modules are essential for organizing JavaScript code into maintainable, reusable pieces. ES6 modules are the standard for modern JavaScript development.
Key Takeaways
Module Systems:
- Legacy formats exist: CommonJS (Node), AMD, UMD
- ES6 modules are the modern standard for browsers and Node
- Focus on ES6 modules for new projects
Exports:
- Named exports:
export const x = ...orexport { a, b, c } - Default export:
export default ...(one per module) - Mix both if needed, but prefer consistency within a module
Imports:
- Named imports:
import { a, b } from './module.js' - Namespace import:
import * as Utils from './module.js' - Default import:
import Utils from './module.js' - Always include file extension in browsers (.js)
Modern Features:
- Dynamic imports:
import('./module.js')returns a Promise - load on demand - Top-level await (ES2022): Use
awaitat module top level for async setup - Import assertions: Import JSON and other resources with type safety
- All require ES module context to work
Browser Usage:
- Use
<script type="module">in HTML - Modules are deferred and run in strict mode automatically
- Each module has its own scope (no global pollution)
- CORS applies - serve from same origin or configure CORS headers
Best Practices:
- Keep modules small and focused
- Use named exports for better IDE support
- Import only what you need
- Group related modules in directories
- Use dynamic imports for conditional/large features
- Avoid circular dependencies
Modules enable you to write scalable, maintainable JavaScript applications by organizing code into logical, reusable units. Master ES6 modules to build modern web applications effectively.