Course:Node.js & Express/
Lesson

If you have written React, Vue, or any modern frontend JavaScript, you have already used ES ModulesWhat is es modules?The official JavaScript module standard using import and export - enabled in Node.js via "type": "module" in package.json., the import and export statements are part of the same standard. Node.js added full support for ESM starting with version 12, and by Node 18 it became the recommended approach for new projects. The appeal is straightforward: one moduleWhat is module?A self-contained file of code with its own scope that explicitly exports values for other files to import, preventing name collisions. system across the entire JavaScript stack, from browser to server.

Enabling ESMWhat is es modules?The official JavaScript module standard using import and export - enabled in Node.js via "type": "module" in package.json. in Node.js

By default, Node.js still treats .js files as CommonJSWhat is commonjs?Node.js's original module format using require() and module.exports - distinct from and predating the ES module import/export syntax. for backwards compatibility. You have two ways to opt into ES Modules.

Using package.jsonWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it.

Add "type": "module" to your project's package.json. Every .js file in the project is then treated as an ES Module:

json
{
  "name": "my-app",
  "version": "1.0.0",
  "type": "module"
}

If you need to include a CommonJS file inside an ESM project, give it the .cjs extension. The reverse also works: .mjs forces ESM treatment even without the package.json flag.

The .mjs extension is useful for writing quick experimental scripts or for publishing packages that support both module systems simultaneously without needing two separate package.json files.
02

Named exports and imports

Named exports let you expose multiple values from a single moduleWhat is module?A self-contained file of code with its own scope that explicitly exports values for other files to import, preventing name collisions.. Each one gets a label that callers use to import it:

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

export function multiply(a, b) {
  return a * b;
}

export const PI = 3.14159;
// app.js
import { add, multiply, PI } from './math.js';

console.log(add(2, 3));   // 5
console.log(PI);          // 3.14159

Notice the .js extension in the import path. This is mandatory in standard Node.js ESMWhat is es modules?The official JavaScript module standard using import and export - enabled in Node.js via "type": "module" in package.json.. Omitting it causes an error, unlike CommonJSWhat is commonjs?Node.js's original module format using require() and module.exports - distinct from and predating the ES module import/export syntax. which resolved extensions automatically.

03

Default exports and imports

A moduleWhat is module?A self-contained file of code with its own scope that explicitly exports values for other files to import, preventing name collisions. can designate one value as its default exportWhat is default export?A module's single primary export, imported without curly braces and with any local name the caller chooses.. The caller then imports it without curly braces and can choose any local name:

// Logger.js
export default class Logger {
  constructor(name) {
    this.name = name;
  }

  log(message) {
    console.log(`[${this.name}] ${message}`);
  }
}
// app.js
import Logger from './Logger.js';
// The name "Logger" is entirely your choice here
const log = new Logger('App');

You can mix named and default exports in the same file, though many style guides prefer one or the other for consistency.

04

Import aliases and namespace imports

When two modules export a name that collides, or when you just want a shorter label, you can rename on import:

import { add as sum, multiply as product } from './math.js';

// Import everything into a single namespace object
import * as math from './math.js';
math.add(2, 3);
math.PI;

Namespace imports are handy when you want to document clearly which moduleWhat is module?A self-contained file of code with its own scope that explicitly exports values for other files to import, preventing name collisions. a value is coming from.

05

Top-level awaitWhat is top-level await?The ability in ES modules to use await outside of any async function, at the top level of the module.

One of the most practical advantages of ESMWhat is es modules?The official JavaScript module standard using import and export - enabled in Node.js via "type": "module" in package.json. is that you can use await at the top level of a moduleWhat is module?A self-contained file of code with its own scope that explicitly exports values for other files to import, preventing name collisions., outside of any async function:

// config.js
const response = await fetch('https://api.example.com/config');
export const config = await response.json();
// app.js
import { config } from './config.js';
// config is already fully loaded before this line runs
console.log(config.apiUrl);

This makes initialisation sequences much cleaner, no more wrapping everything in an immediately-invoked async function.

06

Dynamic imports

The import() function lets you load a moduleWhat is module?A self-contained file of code with its own scope that explicitly exports values for other files to import, preventing name collisions. at runtimeWhat is runtime?The environment that runs your code after it's written. Some languages need a runtime installed on the machine; others (like Go) bake it into the binary., conditionally or on demand. It returns a PromiseWhat is promise?An object that represents a value you don't have yet but will get in the future, letting your code keep running while it waits.:

// Load a heavy module only when a specific route is hit
async function handleAdminRequest(req, res) {
  if (!req.user.isAdmin) return res.status(403).end();

  const { generateReport } = await import('./reports/admin.js');
  const report = await generateReport(req.query);
  res.json(report);
}

Dynamic imports are useful for code-splitting in large applications, you pay the loading cost only when the feature is actually needed.

07

Replacing __dirname in ESMWhat is es modules?The official JavaScript module standard using import and export - enabled in Node.js via "type": "module" in package.json.

CommonJSWhat is commonjs?Node.js's original module format using require() and module.exports - distinct from and predating the ES module import/export syntax. gives you __dirname (the directory of the current file) and __filename (the full file path) for free. ESM does not include these. The modern replacement uses import.meta.url:

import { fileURLToPath } from 'url';
import { dirname, join } from 'path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

// Now use them just like in CommonJS
const configPath = join(__dirname, '../config.json');

This pattern is boilerplateWhat is boilerplate?Repetitive, standardized code that follows a known pattern and appears in nearly every project - like setting up a server or wiring up database connections.-heavy but necessary. Some teams extract it into a shared utility file.

08

Interoperability with CommonJSWhat is commonjs?Node.js's original module format using require() and module.exports - distinct from and predating the ES module import/export syntax.

Most npm packages are still published in CommonJS format. You can import them from ESMWhat is es modules?The official JavaScript module standard using import and export - enabled in Node.js via "type": "module" in package.json. without any special setup:

// ESM importing a CommonJS package - works fine
import lodash from 'lodash';
import express from 'express';

Going the other direction, importing ESM from CommonJS, is much harder. The only option is the dynamic import() function, which forces the surrounding code to be async:

// CommonJS file needing an ESM-only package
async function loadFeature() {
  const { feature } = await import('esm-only-package');
  return feature;
}
This interoperability friction is why many popular packages now ship dual builds: a .cjs file for CommonJS consumers and an .mjs (or exports map) for ESM consumers. When you publish your own packages, providing both formats broadens your audience.
09

CommonJSWhat is commonjs?Node.js's original module format using require() and module.exports - distinct from and predating the ES module import/export syntax. vs ES modulesWhat is es modules?The official JavaScript module standard using import and export - enabled in Node.js via "type": "module" in package.json. at a glance

AspectCommonJSES modules
Syntaxrequire / module.exportsimport / export
LoadingSynchronousAsynchronous
AnalysisRuntimeStatic (before execution)
Tree-shakingNoYes
Top-level awaitNoYes
File extension in importsOptionalRequired
__dirname availableYesNo (use import.meta.url)
Default in Node.jsYes (legacy)With "type": "module"