6/25/2025 · Portfolio Admin

JavaScript Modules in 2025: ESM, Import Maps & Best Practices

Synced from Medium RSS

JavaScript modules have come a long way since the days of script tags and IIFEs. In 2025, ECMAScript Modules (ESM) are no longer the future — they’re the present. Combined with Import Maps, the modern module system has finally matured across browsers and environments.

Whether you’re working in Node.js, the browser, or building cross-platform tools, this blog will give you a full 360-degree perspective of how to use JavaScript modules the right way in 2025.

The Evolution of JavaScript Modules

1. From IIFE to CommonJS

(function() {
// isolated scope
})();

Then came CommonJS (CJS) in Node.js:

const fs = require('fs');
module.exports = myFunc;

But CJS was server-only, synchronous, and not designed for browsers.

2. Enter ES Modules (ESM)

// math.js
export function add(a, b) { return a + b; }

// app.js
import { add } from './math.js';

Features of ESM:

  • Native support in browsers and Node.js (>= v14 fully stable)
  • Static analysis, tree-shaking support
  • Top-level await support
  • Supports asynchronous loading in browsers

Using ESM in the Browser

<script type="module">
import { init } from './app.js';
init();
</script>

Things to remember:

  • Use .js extensions (relative imports)
  • Modules are deferred by default
  • You can use import.meta.url to resolve relative paths.

Using ESM in Node.js (2025 Way)

Node.js now supports ESM natively without .mjs files if:

  • Your package.json includes { "type": "module" }
  • You use import/export instead of require()/module.exports
// index.js
import express from 'express';

To use bare imports (like lodash), you still need a bundler or an Import Map (see next).

Import Maps: Native Aliases for Modules

Import Maps let you define module paths for the browser without bundling.

<script type="importmap">
{
"imports": {
"lodash": "https://cdn.skypack.dev/lodash-es"
}
}
</script>

<script type="module">
import _ from 'lodash';
console.log(_.chunk([1,2,3,4], 2));
</script>

Import Maps are now supported in all modern browsers including Chrome, Edge, and Safari.

Dynamic Imports

You can load modules conditionally or on demand:

if (userIsAdmin) {
const adminModule = await import('./admin-tools.js');
adminModule.init();
}

Use cases:

  • Lazy loading
  • Code splitting
  • Plugin systems

Mixing CJS and ESM

In Node.js:

  • You can import() CJS modules in ESM
  • You cannot require() ESM from CJS (without dynamic import + async).

Recommendation: Standardize your codebase to ESM in 2025.

Best Practices for 2025

| Practice                           | Description                                      |
|------------------------------------|--------------------------------------------------|
| Use ESM everywhere | Browsers & Node.js now support it natively |
| Avoid deep relative paths | Use Import Maps or aliases |
| Modularize logically | Split code by concern (auth.js, db.js, ui.js) |
| Prefer named exports | More clarity and static checks |
| Keep side-effects out of modules | Makes tree-shaking and testing easier |
| Version your URLs in Import Maps | Avoid breaking changes from CDNs |

Bonus: Top-Level Await

ESM supports top-level await:

const res = await fetch(‘/data.json’);
const data = await res.json();

Just ensure the script/module is in an ESM context (e.g., type="module").

Conclusion

2025 is the year to go all-in on modern ESM:

  • Skip bundling for small apps using Import Maps
  • Move Node.js projects from CJS to ESM
  • Organize projects using logical module boundaries

This is no longer future-proofing. This is the present.

Follow me for more deep dives into modern JavaScript, frontend performance, and real-world dev practices.