Different forms of modularization have been around in the JavaScript ecosystem for years. Developers have used well-defined specifications such as AMD or CommonJS as well as simple coding patterns like the revealing module pattern to gain the benefits of a well-modularized solution.
Modules can be used on the client side in browsers or on the server side in Node.js. Sometimes code is transpiled from one module format to another using tools like Babel. All of this makes for a messy and complex JavaScript module state.
Enter ES modules — more specifically, ES modules in Node.js.
Tip: This article focuses on ES modules in Node.js. Check out “CommonJS vs AMD vs RequireJS vs ES6 Modules” for an excellent comparison of module systems not specific to Node.js.
Let’s look at some of the key milestones for ES module support:
Tip: The Node.js Modules team has provided a more detailed set of guiding principles for the new implementation.
For a couple reasons. For one thing, all major browsers already support ES modules — you can see for yourself here. Supporting ES modules on the server side in Node.js out of the box will allow full-stack developers to naturally write modular, reusable JavaScript for both the client and server.
For another thing, experimental features in Node.js are subject to non-backward-compatible changes or removal in future versions. That being said, experimental ES module support has been around in Node for a few years and is not expected to change dramatically before October 2019.
The de facto standard for modules in Node.js currently (mid-2019 at the time of writing) is CommonJS. CommonJS modules are defined in normal .js files using module.exports
. Modules can be used later within other .js files with the require()
function. For example:
// foo.js module.exports = function() { return 'Hello foo!'; } // index.js var foo = require('./foo'); console.log(foo()); // Hello foo!
Use Node to run this example with node index.js
.
Since Node v8.5, developers have been able to run variations of support for the ES modules specification using the --experimental-modules
flag. As of Node v12.4, modules can be defined in .mjs files (or .js files under certain circumstances). For example:
// foo.mjs export function foo() { return 'Hello foo!'; } // index.mjs import { foo } from './foo.mjs'; console.log(foo()); // Hello foo!
Use Node to run this example with node --experimental-modules index.mjs
.
In some ways, supporting ES modules in browsers may have been a little simpler than supporting ES modules in Node because Node already had a well-defined CommonJS module system. Luckily, the community has done a fantastic job of ensuring developers can work with both types of modules at the same time and even import from one to the other.
For example, let’s say we have two modules. The first is a CommonJS module, and the second is an ES module (note the different file extensions):
// cjs-module-a.js module.exports = function() { return 'I am CJS module A'; }; // esm-module-a.mjs export function esmModuleA() { return 'I am ESM Module A'; }; export default esmModuleA;
To use the CommonJS module in an ES module script (note the .mjs extension and use of the import
keyword):
// index.mjs import esmModuleA from './esm-module-a.mjs'; import cjsModuleA from './cjs-module-a.js'; console.log(`esmModuleA loaded from an ES Module: ${esmModuleA()}`); console.log(`cjsModuleA loaded from an ES Module: ${cjsModuleA()}`);
Use Node to run this example with node --experimental-modules index.mjs
.
To use the ES module in a standard CommonJS script (note the .js extension and use of the require()
function):
// index.js // synchronously load CommonJS module const cjsModuleA = require('./cjs-module-a'); console.log(`cjsModuleA loaded synchronously from an CJS Module: ${cjsModuleA()}`); // asynchronously load ES module using CommonJS async function main() { const {esmModuleA} = await import('./esm-module-a.mjs'); console.log(`esmModuleA loaded asynchronously from a CJS module: ${esmModuleA()}`); } main();
These examples provide a basic demonstration of how to use CommonJS and ES modules together in the same application. Check out “Native ES Modules in NodeJS: Status and Future Directions, Part I” by Gil Tayar for a deeper dive into CommonJS and ES Module interoperability.
At the time of writing, the new module implementation plan is in its third and final phase. Phase 3 is planned to be completed at the same time that Node 12 LTS is released and when ES module support will be available without the -experimental-modules
flag.
Phase 3 will likely bring a few big improvements to round out the ES module implementation.
Developers expect module loading systems to be flexible and full-featured. Here are a few of the key features in development in the Node.js module loader solution:
You can view the full list here.
"exports"
object in package.json
While the naming and syntax is not final, the idea here is to have an object somewhere in the package.json
file that allows packages to provide “pretty” entry points for different components within the package. Take this package.json
as an example of a possible implementation:
{ "name": "@myorg/mypackage", "version": "1.0.0", "type": "module", "main": "./dist/index.js", "exports": { ".": "./src/mypackage.mjs", "./data": "./data/somedir/someotherdir/index.mjs" } }
Developers would be able to import the data component of @myorg/mypackage
like this:
import { MyModule } from '@myorg/mypackage/data
When referencing one module from another module within the same package, you may end up with a lot of backtracking that looks like this:
import coolcomponent from '../../../coolcomponent/module.js
If this change is implemented, then backtracking can be replaced with a reference to the package’s name as defined in package.json
. The new code would look like this:
import coolcomponent from 'mypackage/coolcomponent/module.js
Allowing an npm package to contain CJS and ES modules alongside each other is important to ensure there is a backwards-compatible, developer-friendly path to migrate from CommonJS to ES modules. This has often been referred to as “dual-mode” support.
The status quo approach to dual-mode support is for the existing main
entry point in package.json
to point to a CommonJS entry point. If an npm package contains ES modules and the developer wants to use them, they need to use deep imports to access those modules (e.g., import 'pkg/module.mjs'
). This is the dual-mode solution that is likely to ship with Node.js 12 LTS.
There were some other proposals for dual-mode support. This widely debated proposal includes some options for making it easier for developers to ship packages with two separate implementations (ESM and CJS), but this proposal failed to reach consensus.
A newer proposal for require
of ESM suggests a different approach that allows developers to resolve ES modules with require()
. This proposal is still open but has went silent and is unlikely to be included in Node 12 LTS.
While the goal is for ES modules to eventually replace CommonJS modules in Node.js, no one knows what the future holds — nor how long before CommonJS module support disappears. But one thing is for sure: Node developers have spent considerable time and effort ensuring a seamless transition to a future without CommonJS.
They have done a fantastic job striking a balance between ensuring both module types interoperate with each other while trying not to introduce too many new dual-mode APIs that would become useless once the critical mass has migrated and it comes time to remove support for CommonJS from Node.
So when will CommonJS be removed from Node.js? Let’s make a wild, baseless prediction and say Node 18 with an --experimental-no-commonjs-modules
and Node 20 for the final sunset. The future of modular JavaScript across browsers, servers and everywhere else JavaScript runs is an exciting one!
Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third-party services are successful, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens while a user interacts with your app. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.
LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowDesign React Native UIs that look great on any device by using adaptive layouts, responsive scaling, and platform-specific tools.
Angular’s two-way data binding has evolved with signals, offering improved performance, simpler syntax, and better type inference.
Fix sticky positioning issues in CSS, from missing offsets to overflow conflicts in flex, grid, and container height constraints.
From basic syntax and advanced techniques to practical applications and error handling, here’s how to use node-cron.
5 Replies to "ES modules in Node.js 12, from experimental to release"
Thank you for clarification! And one question is still not covered: why .mjs? Does it mean we should have both identical. js and mjs files in a package for browsers/babel and node?
From what I understand, NodeJS developers considered several options for being able to suppor ES modules alongside existing CommonJS modules including not using the .mjs extension. At the end of the day, I believe the decision was made to use the .mjs extension because it was a simply way to identify an ES module over plain JavaScript or CommonJS modules. It also sort of fits in the JavaScript community with other frameworks and tools using file extensions as meta-data (ex. typescript uses .ts, react has .jsx, etc.). Here’s a great write up on other options that were considered: https://2ality.com/2017/05/es-module-specifiers.html.
Browsers will be able to load modules from .mjs files as well but the .mjs extension will not identify that a script contains an ES module. In the browser, you’ll need to include the type=”module” attribute on your script tags, for example:
http://./script.mjs
Probably the most noticeable change in Node 12’s new modules implementation was supporting ES modules syntax in .js files, via `”type”: “module”` in `package.json`. It’s explained in the announcement blog post: https://github.com/nodejs/modules/blob/master/doc/announcement.md#import-and-export-syntax-in-js-files
Thank you for that!
I’ve added an example using with Typescript if someone needs it: https://github.com/Urigo/typescript-node-es-modules-example
Now nodev12 is in stable, but we still need experimental-modules to enable esm/:(