From CommonJS to HTTP: The Evolution of JavaScript Module Specifiers
Share this article
The Road to a Multi‑Protocol Import System
JavaScript’s module import syntax has been a moving target for a decade. What began as a simple require() call in Node.js has evolved into a web‑native, protocol‑aware system that lets developers pull code from a variety of sources with a single import statement. This evolution reflects both the growing needs of developers and the tension between legacy compatibility and forward‑looking innovation.
1. CommonJS Roots
When Node.js first shipped, the de‑facto standard for loading modules was CommonJS:
const axios = require('axios');
This approach bundled modules into a local node_modules directory and relied on a synchronous require() lookup. It worked for a single runtime, but it was not designed for the web or for multiple runtimes.
2. ECMAScript Modules (ESM) and the Node: Protocol
Node 12 introduced native ESM support, allowing developers to use the modern import syntax:
import axios from 'axios';
Soon after, Node added the node: protocol in 2021 to disambiguate core modules from third‑party packages:
import os from 'node:os';
“The
node:protocol guarantees that core modules are never confused with user‑space modules that may share a name.” – Node.js documentation
The protocol’s benefits are twofold: it prevents naming collisions with NPM packages and makes it explicit when a module comes from the runtime itself. Lint rules such as useNodejsImportProtocol encourage this explicitness.
3. Deno’s HTTPS Imports (2018)
Ryan Dahl, the original creator of Node.js, launched Deno as a response to what he saw as Node’s shortcomings. Deno’s first major departure was its insistence on HTTPS imports:
import { serve } from "https://deno.land/[email protected]/http/server.ts";
Deno only accepts relative or absolute URLs, eliminating the need for a local package registry. This design made the runtime lightweight and secure by default, but it also introduced a new dependency on network availability.
4. Deno’s npm: Protocol (2022)
To compete with Node’s massive NPM ecosystem, Deno added the npm: protocol in 2022:
import { chalk } from "npm:chalk@5";
This allowed developers to import any NPM module by specifying its name and version directly in the URL. However, Deno still lacked a standard package.json file, so dependency resolution was handled by an internal lockfile.
5. Package.json Support (2023)
Deno’s 2023 release finally added package.json support, aligning its dependency management with Node’s conventions. After this change, a simple import could work in both runtimes:
import { chalk } from "chalk"; // Node & Deno with package.json
Deno still maintained the npm: protocol for explicit versioning, but the presence of a package.json made the ecosystem more familiar to existing developers.
6. JSR and the Shift Away from HTTPS (2024)
In 2024, Deno introduced JSR, a registry aimed at being an alternative to NPM. Modules could be imported via:
import * as chalk from "jsr:@nothing628/chalk";
JSR offered a clearer namespace and versioning scheme, but it struggled to overcome NPM’s network effects. Simultaneously, Deno began moving away from raw HTTPS imports, arguing that they were a “wrong” approach for long‑term sustainability.
7. The Current Landscape (December 2025)
Below is a snapshot of how each runtime currently interprets common import specifiers:
| Specifier | Deno | Node | Bun | Val Town (axios example) |
|---|---|---|---|---|
axios (with deno.lock) |
✅ | ✅ | ✅ | ⛔️ |
npm:axios |
✅ | ⛔️ | ⛔️ | ✅ |
https://esm.sh/axios |
✅ (discouraged) | 🟠 (in userspace) | ⛔️ | ✅ |
jsr:axios |
✅ | ⛔️ | ⛔️ | ✅ |
node:fs |
✅ | ✅ | ✅ | ✅ |
The table illustrates that while Node remains the “safe subset” for most tooling, Deno’s richer protocol set offers flexibility at the cost of tooling friction. Bun, meanwhile, is still in the early stages of adopting these protocols.
8. Implications for Developers
- Portability vs. Familiarity – Developers must decide whether to adopt Deno’s multi‑protocol imports for their projects or stick with Node’s simpler, well‑supported ecosystem.
- Tooling Compatibility – TypeScript, bundlers, and linters still lag behind Deno’s protocol support, making migration a non‑trivial effort.
- Security – HTTPS imports reduce the attack surface of local dependencies but introduce network latency and reliability concerns.
- Ecosystem Lock‑in – The dominance of NPM makes it difficult for alternatives like JSR to gain traction, even if they offer cleaner semantics.
9. Looking Forward
The trajectory suggests that runtimes will continue to experiment with import protocols to balance performance, security, and developer ergonomics. For now, the key takeaway is that explicitness matters: whether you’re importing node:fs or npm:chalk@5, the specifier tells the runtime exactly where to fetch code from, eliminating ambiguity and making dependency management more predictable.
As the ecosystem matures, we can expect tooling to catch up, and the choice of protocol will become a matter of project requirements rather than a source of friction. For developers, staying informed about these protocol changes—and understanding how they map to your build pipeline—will be essential for maintaining robust, future‑proof codebases.
Source: Tom MacWright, “Module Specifiers and Protocols” (2025‑12‑08).