You Should be Using esm

bennypowers profile image Benny Powers ๐Ÿ‡ฎ๐Ÿ‡ฑ๐Ÿ‡จ๐Ÿ‡ฆ Updated on ใƒป4 min read


You can use JavaScript modules in node today without transpiling, just npm -S esm and run your app with node -r esm foo.js. add "esm": "auto" to the top level of your package.json to make loading modules and cjs in the same app effortless and transparent.

If you've stuck around this far, keep reading for an opinionated history of how we've come to this point.

The History
The Problem
esm: A Better Solution

The History

Historically, JavaScript was browser-only. Developers used a number of techniques to structure their code, all of which were basically abstractions over global variables. Among those solutions, a crowd-favourite called CommonJS (or 'cjs') emerged.

const { foo } = require('./bar')

const baz = foo + "qux"

module.exports = {
  quux: [baz]

CJS gained traction among JS developers mostly because it was the module system that NodeJS used. Front-end developers could bundle cjs-based apps with tools like webpack into single-file scripts that browsers could load and run.

The notion that one codebase could (with a certain amount of tool-wrangling) run on the server as well as the client lead to things like server-side rendering, NativeScript/React Native and the proliferation of tools like webpack, babel, and others as non-negotiable prerequisites to JS development.

In 2015, ECMAScript version 6 was published, which included a syntax for language-level modules.

import { foo } from './bar.js'

const baz = foo + "qux"

export const quux = [baz]

These modules were static and top-level-only, meaning you couldn't do things like the following.

const moduleName = "foo" + "bar"
if (baz) {
  // nope!
  import { quz } from moduleName

Which CJS users had become used to. On the other hand, js modules were statically analyzable, meaning that a new breed of tools like Rollup could analyze the js files to do useful things like tree-shaking, which is a process that removes unused code from bundles. This helped developers to ship less code which made sites load faster for users.

The specifics of how modules would be loaded and module graphs (logical structures representing the functional relationship between modules) were left to implementers, i.e. browser vendors and the node maintainers.

Browser vendors took the lead and wrote up the loader specification, but the situation for node, which already had a module system, was more complex, and as of today, no final plan has emerged, although they are close.


When the ES2015 spec (then called ES6 or "harmony") was published, a project called 5-to-6, later renamed Babel, came along as a way to let JS programmers write their apps using the awesome new ES6 features, while shipping code that older browsers and Internet Explorer could support.

This process of translating one language or version of a language to another is called transpiling, a portmanteau of *trans*lating and com*piling.

Babel has since evolved into a sort of JavaScript Swiss army knife. It can take a variety of JavaScript versions or even separate languages entirely and whip them into code that runs in the browser.

The Problem

Babel has done tremendous good for web developers. It's enabled new or proposed features to be explored en masse before they were implemented by browsers, which helped expose edge cases with those features, leading to better specifications. It also played a huge part in the sea change which web development is currently undergoing from an OOP/Proceedural paradigm to a more functional paradigm. Babel also forms the basis for a wide variety of tools and products available to web developers today...

...but it doesn't have to, and that can sometimes be a bit of a problem.

The Cost of Transpiling

Developer Jamie K. put it nicely:

The business case for delivering a large, one-size-fits all bundle to modern browsers and ie8 alike is rapidly eroding. Modern techniques like differential serving let us serve optimized, slimmed-down ES2018 to capable browsers, while reserving bloated, transpiled bundles for those less-so. Beyond that, for apps where IE11 support is not an absolute business necessity, it would actually be irresponsible to support that old, insecure browser, when users can and should be using the latest and greatest.

Principles and Cognitive Load

In the node world, transpiling comes with its costs as well. Maintaining a babel configuration isn't always the simplest task. More than that, though, transpiling subtly communicates that "this code isn't ok by itself, it needs extra processing to be OK", and we shouldn't want to say that about native modules, even if CJS had a head start.

esm: A Simple Solution

esm is an excellent package by Microsoft developer John-David Dalton of lodash fame, et al. It's a module loader that transforms es modules at run time instead of transpiling them.

With esm, the elusive 'interop' Just Worksโ„ข. You can mix and match native and CJS modules without batting an eye.

You can even use most command line node apps! For example, the excellent tape testing library doesn't come with module support out of the box, but you can easily add it like so:

npx tape -r 'esm' './**/*.test.js'


Next time you have a node.js project, before you start writing a babel config just to transpile modules, give esm a try.

Posted on by:

bennypowers profile

Benny Powers ๐Ÿ‡ฎ๐Ÿ‡ฑ๐Ÿ‡จ๐Ÿ‡ฆ


Coding is as much a matter of personal growth as it is of logic and control-flow. I keep patience, curiosity, & exuberance in the same toolbox as vim and git. *Opinions posted are my own*


markdown guide

Babel is also a very good way to scare beginners from using latest JS. Backwards compatibility is important but there isn't a need to include it in every single tutorial out there. Let people get a taste of new JS then introduce them to Babel, especially now when most up-to-date browsers support it.


I can attest to this. My first dev foray was into JS and while I was impressed by the breadth of the ecosystem and the enthusiasm of its torch-bearers, I quickly came to find myself in a sea of chaos.

Sea of Chaos is my title for JS because something that has been around for so long should not be nearly so complicated... all to support versions of browsers that less than 5% of people use.

TypeScript looks nice... but Transcrypt has always looked better since embracing Python.


And I thought that CoffeeScript was the closest to Python. Good find!


A previous version of this post said that the "esm": "auto" package.json configuration is required for interop, which is incorrect. esm will let you load cjs in modules out of the box, you just need to install and load. The post has been edited.


Not about the content, but tapping the ID links towards the top doesn't seem to work for me. My address bar updates, but nothing else happens.


Thanks for the notes. I used GFM-style anchor links, maybe they don't work the same on dev.to.

issue for tracking

Markdown Headers Should Have IDs for Linking #682

Feature Request or Task

When users create headings in markdown, the headings should have ids added to enable linking throughout the document

[The History](#the-history)
[The Problem](#the-problem)
[`esm`: A Better Solution](#esm-a-better-solution)

## tl;dr:

## The History

## Transpiling

## The Problem

### The Cost of Transpiling

### Principles and Cognitive Load

## `esm`: A Simple Solution

## Summary

User Story / Description

Users with medium-to-long-form content might want to add a table of contents, or a link to a summary or tl;dr.

Definition of Done

When the links in the above snippet work as expected: clicking on one scrolls to the relevant header.


Thanks for the detailed look into this! Dealing with imports is one of the few remaining annoying bits to deal with.