Node.js Interview Prep
Module System

ES Modules in Node.js -- import, export, and Top-Level Await

ES Modules in Node.js -- import, export, and Top-Level Await

LinkedIn Hook

"Why does require is not defined suddenly appear in half the Node.js tutorials you copy from Stack Overflow?"

Because the JavaScript ecosystem is in the middle of a quiet migration. For 13 years, Node.js shipped with exactly one module system -- CommonJS. Then browsers standardized ES Modules, and Node.js had to support both. The result is a module system with two personalities, two file extensions, two loader algorithms, and a growing list of subtle interop rules.

Most developers learn require first and never touch import until a library forces their hand. Then they hit the classic error: SyntaxError: Cannot use import statement outside a module. They add "type": "module" to package.json and suddenly __dirname is gone too.

ES Modules are static, asynchronous, and hoisted. They unlock await at the top level of a file. They let a browser and Node.js run the same source file unchanged. And they are the only module system the TC39 standards body will ever evolve going forward.

In Lesson 2.2, I break down how to enable ESM in Node.js, how import differs from require under the hood, and why interviewers keep asking about import.meta.url.

Read the full lesson -> [link]

#NodeJS #JavaScript #ESModules #BackendDevelopment #InterviewPrep


ES Modules in Node.js -- import, export, and Top-Level Await thumbnail


What You'll Learn

  • How to enable ES Modules in Node.js using .mjs or "type": "module"
  • Named exports vs default exports and when to use each
  • Dynamic import() for conditional and lazy loading
  • Top-level await -- the killer feature that only works in ESM
  • Why ESM is static and asynchronous (and why that matters for tree-shaking)
  • Import assertions / attributes for loading JSON files
  • Interoperability rules between ESM and CommonJS
  • Replacing __dirname and __filename with import.meta.url
  • The "dual package" pattern for publishing libraries that support both worlds

The Library Catalog Analogy -- import vs require

Imagine two ways to borrow a book from a library.

CommonJS (require) is like asking the librarian. You walk up to the desk and say "give me the book on cooking." The librarian disappears into the stacks, finds the book, and hands it to you at that exact moment. The transaction is synchronous -- you wait at the counter. You can ask for a book based on a condition ("if it is Tuesday, give me the cookbook, otherwise give me the novel"). You can even ask for a book whose title is built from variables. But nobody knows ahead of time which book you will borrow.

ES Modules (import) is like a library catalog system. Before the library even opens, every book request has already been registered. The catalog lists exactly which books are needed, in what order. When the doors open, every book is pulled from the shelves in parallel, stamped, and delivered as a batch. You cannot say "give me book X only on Tuesday" at the top of your file -- all imports are declared upfront and hoisted to the top, regardless of where you wrote them.

This difference is not cosmetic. Because ESM imports are static (known before code runs), tools like bundlers can analyze the entire dependency graph without executing a single line. That is how tree-shaking works -- the bundler sees exactly which named exports are used and drops the rest. CommonJS cannot do this reliably because require() is a function call that can take any argument at runtime.

+---------------------------------------------------------------+
|           CommonJS -- "Ask the Librarian"                     |
+---------------------------------------------------------------+
|                                                                |
|  const fs = require('fs');              // synchronous        |
|  if (condition) {                                              |
|    const a = require('./a');            // conditional OK     |
|  }                                                             |
|  const name = 'utils';                                         |
|  const u = require('./' + name);        // dynamic path OK    |
|                                                                |
|  -> require() is a FUNCTION                                    |
|  -> runs top-to-bottom                                         |
|  -> module.exports is an OBJECT                                |
|  -> cannot be statically analyzed                              |
|                                                                |
+---------------------------------------------------------------+

+---------------------------------------------------------------+
|           ES Modules -- "The Catalog System"                  |
+---------------------------------------------------------------+
|                                                                |
|  import fs from 'node:fs';              // hoisted            |
|  import { readFile } from 'node:fs/promises';                  |
|                                                                |
|  -> import is a KEYWORD (syntax, not a function)              |
|  -> all imports hoisted before any code runs                   |
|  -> dependency graph known BEFORE execution                    |
|  -> enables tree-shaking and top-level await                   |
|  -> fully static (paths must be string literals)               |
|                                                                |
+---------------------------------------------------------------+

Napkin AI Visual Prompt: "Dark gradient (#0a1a0a -> #0d2e16). Split comparison: LEFT side labeled 'CommonJS' shows a librarian at a desk handing books one at a time (sequential arrows in gray). RIGHT side labeled 'ES Modules' shows a card catalog with all book slots pre-filled and parallel green arrows pulling books simultaneously. Below the ESM side, an amber clock icon labeled 'top-level await'. White monospace labels throughout. Node green (#68a063) divider between the two sides."


Enabling ES Modules in Node.js

Node.js decides whether a file is CommonJS or ESM using three signals, in order of priority:

  1. File extension: .mjs is always ESM. .cjs is always CommonJS.
  2. "type" field in the nearest package.json: "type": "module" makes .js files ESM. "type": "commonjs" (the default) makes .js files CJS.
  3. --input-type flag: For code passed via -e or stdin.

Option A -- Use the .mjs Extension

The simplest way to opt a single file into ESM without touching package.json:

// math.mjs
// This file is ESM because of the .mjs extension
export function add(a, b) {
  return a + b;
}

export function multiply(a, b) {
  return a * b;
}
// app.mjs
// Import named exports from a sibling .mjs file
// Note the REQUIRED file extension -- ESM does not guess
import { add, multiply } from './math.mjs';

console.log(add(2, 3));       // 5
console.log(multiply(4, 5));  // 20

Run it with:

node app.mjs

Option B -- Set "type": "module" in package.json

The preferred approach for a whole project is to declare the package type once:

{
  "name": "my-app",
  "version": "1.0.0",
  "type": "module",
  "main": "index.js"
}

With this setting, every .js file in the package is treated as ESM. You can still opt individual files back into CommonJS by renaming them to .cjs.

my-app/
|-- package.json        ("type": "module")
|-- index.js            ESM (because of package.json)
|-- legacy.cjs          CommonJS (explicit .cjs)
|-- utils/
|   |-- math.js         ESM (inherits from package.json)

Named Exports and Default Exports

ES Modules support two flavors of exports. You can use both in the same file, but interviewers love to ask when you should pick one over the other.

Named Exports -- Multiple Values per File

// user-service.js  (package.json has "type": "module")
// Named exports attach values to specific identifiers
// You can have as many named exports as you want per file

// Inline named export
export const MAX_USERS = 100;

// Named export on a function declaration
export function createUser(name) {
  return { id: Date.now(), name };
}

// Named export on a class
export class UserValidator {
  validate(user) {
    return typeof user.name === 'string' && user.name.length > 0;
  }
}

// Declare first, export later (grouped)
function hashPassword(pw) {
  return pw.split('').reverse().join(''); // toy example
}
function comparePassword(a, b) {
  return hashPassword(a) === hashPassword(b);
}
export { hashPassword, comparePassword };
// app.js
// Import named exports using curly braces -- names MUST match
import { createUser, UserValidator, MAX_USERS } from './user-service.js';

// Rename on import using 'as'
import { hashPassword as hash } from './user-service.js';

const user = createUser('Alice');
const validator = new UserValidator();

console.log(validator.validate(user));  // true
console.log(MAX_USERS);                  // 100
console.log(hash('secret'));             // 'terces'

Default Exports -- One Primary Value per File

// logger.js
// A module can have AT MOST ONE default export
// The default is imported without curly braces and can be renamed freely

class Logger {
  constructor(prefix) {
    this.prefix = prefix;
  }
  info(msg) {
    console.log(`[${this.prefix}] ${msg}`);
  }
}

// Single default export
export default Logger;
// app.js
// Default imports do NOT use curly braces
// The local name is arbitrary -- no connection to the original name
import Logger from './logger.js';
import MyCustomName from './logger.js';  // same class, different local name

const log = new Logger('app');
log.info('server started');

Mixing Named and Default in One File

// config.js
// You can combine a default export with named exports
const config = {
  port: 3000,
  host: 'localhost',
};

export const VERSION = '1.0.0';
export const ENV = process.env.NODE_ENV || 'development';
export default config;
// app.js
// Import the default and named exports in a single statement
import config, { VERSION, ENV } from './config.js';

console.log(config.port);  // 3000
console.log(VERSION);      // '1.0.0'
console.log(ENV);          // 'development'

Namespace Import -- Grab Everything

// Import ALL exports (named + default) as a single object
import * as Config from './config.js';

console.log(Config.VERSION);   // '1.0.0'
console.log(Config.default);   // the default export lives on .default

Dynamic import() -- Loading Modules on Demand

The static import statement is hoisted and runs before any other code. But sometimes you need conditional or lazy loading -- for example, loading a heavy library only when a specific route is hit. That is what the import() expression is for.

// dynamic-load.js
// import() is a FUNCTION-like expression that returns a Promise
// It works in BOTH ESM and CommonJS (in recent Node.js versions)

async function handleRequest(req) {
  if (req.url === '/export-pdf') {
    // Heavy PDF library loaded only when this route is called
    const { generatePDF } = await import('./pdf-generator.js');
    return generatePDF(req.body);
  }

  if (req.url === '/export-csv') {
    // A different heavy library, also lazy-loaded
    const csvModule = await import('./csv-generator.js');
    return csvModule.default(req.body);  // default export via .default
  }

  return 'OK';
}

// Dynamic import() can take a variable path -- static import cannot
async function loadPlugin(name) {
  const plugin = await import(`./plugins/${name}.js`);
  return plugin.default;
}

The key differences from static import:

  • Returns a Promise -- you must await it or chain .then().
  • Accepts runtime expressions -- the path does not have to be a string literal.
  • Not hoisted -- runs at the exact point in your code where you call it.
  • Default exports live on .default -- not automatically unwrapped.

Top-Level await -- The Killer ESM Feature

Inside CommonJS, await is only legal inside an async function. The top level of the file is synchronous. ESM changed this. Because modules are fundamentally asynchronous (the loader already returns a Promise), you can use await at the top level of an ESM file.

// database.js  (ESM)
// Top-level await -- runs before this module's exports are visible to importers
import { createConnection } from './db-driver.js';

// The import of this module BLOCKS until the connection is established
const db = await createConnection({
  host: 'localhost',
  port: 5432,
});

// Run a startup query before exporting anything
const schemaVersion = await db.query('SELECT version FROM schema_info');
console.log(`Database schema: ${schemaVersion}`);

// Export the ready-to-use connection
export default db;
// app.js
// When this import completes, db is ALREADY connected
// No need to wrap everything in a top-level async IIFE
import db from './database.js';

const users = await db.query('SELECT * FROM users');
console.log(users);

Why this matters for interviews: top-level await is the reason many libraries are now ESM-only. They use it at module initialization to fetch config, warm up caches, or read manifest files -- things that were previously awkward in CommonJS.

The catch: top-level await in a module blocks all of its importers until the await resolves. If you await a 5-second fetch at the top of a module, every module that imports it waits 5 seconds. Use it for essential setup, not optional work.


import.meta.url -- The __dirname Replacement

One of the first surprises when migrating to ESM is that __dirname and __filename are gone. ESM files have no such globals. Instead, you get import.meta.url, which is the file URL of the current module.

// paths.js  (ESM)
// import.meta is an object provided by the ESM loader
// import.meta.url looks like: 'file:///C:/projects/app/paths.js'
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';

// Convert the file URL to a normal OS path
const __filename = fileURLToPath(import.meta.url);

// Get the directory of the current file
const __dirname = dirname(__filename);

// Now use it exactly like in CommonJS
const configPath = join(__dirname, 'config', 'app.json');
console.log(`Reading config from ${configPath}`);

export { __dirname, __filename };

You can also use the newer import.meta.dirname and import.meta.filename helpers available in Node.js 20.11+ and 21.2+:

// Modern Node.js shortcut (Node 20.11+ / 21.2+)
// Skips the fileURLToPath dance entirely
console.log(import.meta.dirname);   // 'C:\projects\app'
console.log(import.meta.filename);  // 'C:\projects\app\paths.js'

If you are writing a library that must support older LTS versions, stick with the fileURLToPath pattern. It works in every ESM-capable Node.js release.


Import Attributes for JSON Files

Importing JSON from an ESM file requires a special syntax called import attributes (previously called "import assertions"). This exists for security -- without the attribute, a malicious server could switch a .json URL to serve JavaScript and trick the loader into executing it.

// Modern syntax -- import attributes (Node.js 22+)
// The 'with' clause tells the loader the expected module type
import config from './config.json' with { type: 'json' };

console.log(config.port);  // 3000
// Older syntax -- import assertions (deprecated but still works)
import data from './data.json' assert { type: 'json' };

You can also read JSON with dynamic import():

// Dynamic import with attributes
const { default: config } = await import('./config.json', {
  with: { type: 'json' },
});

For code targeting older Node.js versions, the most portable approach is to read the file with fs:

// Works in every ESM-capable Node.js version -- no attributes needed
import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';

const __dirname = dirname(fileURLToPath(import.meta.url));
const config = JSON.parse(
  await readFile(join(__dirname, 'config.json'), 'utf8')
);

ESM and CommonJS Interoperability

This is the single most confusing part of the Node.js module system. The rules are simple once you memorize them, but they trip up everyone on first contact.

You CAN import CommonJS from ESM

// CommonJS file: legacy-util.cjs
module.exports = {
  greet(name) {
    return `Hello, ${name}`;
  },
  VERSION: '2.0',
};
// ESM file: app.js
// The entire module.exports becomes the DEFAULT export
import legacy from './legacy-util.cjs';
console.log(legacy.greet('Alice'));  // 'Hello, Alice'
console.log(legacy.VERSION);          // '2.0'

// Node.js ALSO tries to detect named exports via static analysis
// For simple module.exports objects, this usually works:
import { greet, VERSION } from './legacy-util.cjs';
console.log(greet('Bob'));  // 'Hello, Bob'

Named export detection for CJS is heuristic -- it works for obvious cases like module.exports = { a, b, c } but can fail for dynamic constructions like module.exports[name] = value. When in doubt, import the default and destructure manually.

You CANNOT statically import ESM from CommonJS

// CommonJS file: old-app.cjs
// This THROWS: SyntaxError: Cannot use import statement outside a module
// import { something } from './modern.js';  <-- NOT ALLOWED

// You MUST use dynamic import() instead
async function main() {
  const modern = await import('./modern.js');
  modern.doWork();
}
main();

Dynamic import() works from CommonJS because it is an expression, not a syntax form. Starting in Node.js 22 (with the --experimental-require-module flag, on by default in newer releases), you can also require() synchronous ESM graphs -- but rely on this only after checking your minimum supported Node.js version.

+---------------------------------------------------------------+
|           CJS <-> ESM INTEROP RULES                           |
+---------------------------------------------------------------+
|                                                                |
|   From ESM importing CJS:                                      |
|     import cjs from './legacy.cjs';          OK  (default)     |
|     import { x } from './legacy.cjs';        OK  (heuristic)   |
|                                                                |
|   From CJS importing ESM:                                      |
|     const esm = require('./modern.js');      FAILS (usually)   |
|     const esm = await import('./modern.js'); OK                |
|                                                                |
|   ESM <-> ESM:                                                 |
|     import x from './y.js';                  OK                |
|                                                                |
|   CJS <-> CJS:                                                 |
|     const x = require('./y.js');             OK                |
|                                                                |
+---------------------------------------------------------------+

CommonJS vs ES Modules -- Side by Side

+-------------------+-----------------------+-----------------------+
| Feature           | CommonJS              | ES Modules            |
+-------------------+-----------------------+-----------------------+
| Syntax            | require / module.exp  | import / export       |
| Nature            | Dynamic (runtime)     | Static (parse time)   |
| Loading           | Synchronous           | Asynchronous          |
| Hoisting          | No                    | Yes (imports)         |
| Top-level await   | No                    | Yes                   |
| __dirname         | Yes (built-in)        | No (use import.meta)  |
| __filename        | Yes (built-in)        | No (use import.meta)  |
| File extension    | .js or .cjs           | .mjs or .js + "type"  |
| Tree-shakeable    | No                    | Yes                   |
| JSON import       | require('./x.json')   | with { type: 'json' } |
| Dynamic paths     | require(varName)      | await import(varName) |
| Conditional load  | if (x) require(...)   | await import(...)     |
| Browser-compatib. | No                    | Yes                   |
| Default in Node   | Yes (historical)      | No (opt-in)           |
+-------------------+-----------------------+-----------------------+

Dual Packages -- Publishing a Library for Both Worlds

If you publish an npm package, users may be on CommonJS or ESM. Modern libraries ship both builds using conditional exports in package.json:

{
  "name": "my-lib",
  "version": "1.0.0",
  "type": "module",
  "main": "./dist/index.cjs",
  "module": "./dist/index.js",
  "exports": {
    ".": {
      "import": "./dist/index.js",
      "require": "./dist/index.cjs",
      "types": "./dist/index.d.ts"
    }
  }
}

The exports field tells Node.js which file to load based on how the consumer imported the package. An ESM consumer gets index.js, a CommonJS consumer gets index.cjs. This is called a dual package.

The dual package hazard: if your library has internal state (a singleton, a cache), a consumer who imports your package from both CJS and ESM codepaths gets two separate copies with independent state. This is a real foot-gun. The common fix is to put all state in a small CJS core file and wrap it with ESM -- both entry points then share the same underlying CJS instance.


Common Mistakes

1. Forgetting the file extension in import paths. ESM requires explicit extensions. import './utils' fails -- you must write import './utils.js'. Node.js does not perform extension probing for ESM the way CommonJS does. Tools like TypeScript and bundlers paper over this, which makes the raw Node.js behavior feel surprising.

2. The "require is not defined in ES module scope" error. You added "type": "module" to package.json and suddenly all your require() calls break. You have two options: rename the file to .cjs (keeping it CommonJS), or convert the require() calls to import statements. You cannot mix them in the same file.

3. Assuming __dirname and __filename exist. These are CommonJS globals. In ESM they are undefined. Use fileURLToPath(import.meta.url) (or import.meta.dirname on Node.js 20.11+) to get the current file's path.

4. Using require() on an ESM-only package. Many modern packages (like node-fetch v3, chalk v5, p-queue v7+) are ESM-only. Calling require('chalk') from CommonJS throws ERR_REQUIRE_ESM. Either upgrade your project to ESM, pin the last CJS-compatible version, or use dynamic await import('chalk').

5. Expecting top-level await to be free. Top-level await blocks every importer of the module. If module A awaits a slow network call at the top level and module B imports A, then B waits for A before any of B's own code runs. Use it only for essential initialization, and prefer explicit init() functions for optional or slow work.

6. Mixing named and default imports from CommonJS incorrectly. Not every CJS module supports named imports from ESM -- it depends on whether Node.js can statically detect the exports. If import { foo } from './legacy.cjs' returns undefined, fall back to import legacy from './legacy.cjs' and then legacy.foo.


Interview Questions

1. "What are the main differences between CommonJS and ES Modules in Node.js?"

CommonJS is dynamic, synchronous, and was the original Node.js module system. Modules are loaded on the fly via require(), which is a regular function call. ES Modules are static, asynchronous, and were standardized in ES2015. Imports are declarative -- import is a keyword, not a function -- and the entire dependency graph is known before any code executes. This enables top-level await, tree-shaking, and parse-time error detection. ESM uses import/export syntax, requires explicit file extensions, and does not provide __dirname or __filename. CommonJS uses require/module.exports, supports conditional imports naturally, and runs synchronously top-to-bottom. Node.js supports both and picks which to use based on file extension (.mjs/.cjs) or the "type" field in package.json.

2. "How do you use __dirname and __filename in an ES Module?"

They do not exist as globals in ESM. Instead, ESM exposes import.meta.url, which is the file:// URL of the current module. To convert it to a normal path, use fileURLToPath from the node:url module: const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename);. Starting in Node.js 20.11 and 21.2, you can use the shortcuts import.meta.dirname and import.meta.filename directly, which return OS-style path strings without any conversion. For libraries targeting older LTS versions, the fileURLToPath approach is the portable choice.

3. "Explain top-level await. Why does it exist and what are its pitfalls?"

Top-level await lets you use await directly at the top of an ES Module file, outside any async function. It works because ES Modules are fundamentally asynchronous -- the ESM loader already returns a Promise for each module, so the runtime can suspend the evaluation of a module and resume it when the awaited value resolves. This is useful for setup work like connecting to a database or loading a config file before exporting anything. The pitfall is that top-level await blocks every importer of the module. If module A awaits a slow operation at the top level, every module that imports A -- directly or transitively -- is blocked until that operation completes. Long blocking awaits at module load time can dramatically slow startup. Use top-level await for essential, fast initialization and prefer explicit init() functions for optional or slow work.

4. "How does interop between CommonJS and ES Modules work in Node.js?"

The rules are asymmetric. ESM can import CommonJS using either default or named import syntax -- the entire module.exports object becomes the default export, and Node.js also tries to detect named exports via static analysis of the CJS source (this works for simple module.exports = { a, b } patterns but can fail for dynamic constructions). CommonJS cannot use import at all, because import is a syntax form not allowed in CJS files. The only way to load an ES Module from CJS is dynamic await import('./esm-file.js'), which returns a Promise for the module namespace. Recent Node.js versions (22+) also allow require() of synchronous ESM graphs, but this is new enough that you should only rely on it once your minimum Node.js version supports it. This asymmetry is why ESM-only libraries like chalk v5 cannot be require()d from older CommonJS codebases without a dynamic import.

5. "What is a dual package and what is the dual package hazard?"

A dual package is an npm package that ships both a CommonJS and an ES Modules build, typically configured via the exports field in package.json with "import" and "require" conditions pointing to different files. This lets a single package serve users on either module system. The dual package hazard is that if a consumer ends up importing the package through both entry points -- for example, the consumer is ESM but one of its dependencies still uses CommonJS to require the same package -- Node.js loads the CJS and ESM builds as two separate modules with independent state. Singletons, caches, and instance checks (instanceof) all break. The standard mitigation is to keep all stateful logic in a small CommonJS core module, and have the ESM wrapper re-export from that core. Both entry points then share the same underlying CJS instance, so there is only one copy of the state.


Quick Reference -- ES Modules Cheat Sheet

+---------------------------------------------------------------+
|           ES MODULES CHEAT SHEET                              |
+---------------------------------------------------------------+
|                                                                |
|  ENABLE ESM:                                                   |
|    Option 1: rename file to .mjs                               |
|    Option 2: package.json -> "type": "module"                  |
|                                                                |
|  NAMED EXPORTS:                                                |
|    export const X = 1;                                         |
|    export function f() {}                                      |
|    export { a, b as alias };                                   |
|                                                                |
|  DEFAULT EXPORT (max one per file):                            |
|    export default class Foo {}                                 |
|                                                                |
|  NAMED IMPORT:                                                 |
|    import { X, f } from './module.js';                         |
|    import { a as alias } from './module.js';                   |
|                                                                |
|  DEFAULT IMPORT:                                               |
|    import Foo from './module.js';                              |
|                                                                |
|  NAMESPACE IMPORT:                                             |
|    import * as M from './module.js';                           |
|                                                                |
|  DYNAMIC IMPORT:                                               |
|    const m = await import('./module.js');                      |
|                                                                |
|  TOP-LEVEL AWAIT (ESM only):                                   |
|    const data = await fetchData();                             |
|    export default data;                                        |
|                                                                |
|  JSON IMPORT:                                                  |
|    import cfg from './c.json' with { type: 'json' };           |
|                                                                |
|  __dirname REPLACEMENT:                                        |
|    import { fileURLToPath } from 'node:url';                   |
|    import { dirname } from 'node:path';                        |
|    const __dirname = dirname(fileURLToPath(import.meta.url));  |
|    // OR (Node 20.11+): import.meta.dirname                    |
|                                                                |
+---------------------------------------------------------------+

+---------------------------------------------------------------+
|           KEY RULES                                            |
+---------------------------------------------------------------+
|                                                                |
|  1. File extensions are REQUIRED in import paths               |
|  2. import is hoisted and static -- paths must be literals     |
|  3. Dynamic import() is the only runtime-path loader           |
|  4. Top-level await blocks every importer -- use sparingly     |
|  5. ESM imports CJS as default; CJS cannot statically import   |
|     ESM (use dynamic import)                                   |
|  6. No __dirname, __filename, require, module.exports in ESM   |
|  7. JSON imports need 'with { type: "json" }'                  |
|  8. Dual packages risk the dual package hazard                 |
|                                                                |
+---------------------------------------------------------------+
FeatureCommonJSES Modules
Syntax styleFunction callDeclarative keyword
Load timingSynchronousAsynchronous
HoistingNoYes
Top-level awaitNoYes
Tree-shakeableNoYes
__dirname / __filenameBuilt-inVia import.meta.url
JSON importsrequire('./x.json')with { type: 'json' }
Dynamic path loadingrequire(expr)await import(expr)
Browser compatibleNoYes

Prev: Lesson 2.1 -- CommonJS in Node.js Next: Lesson 2.3 -- Module Resolution in Node.js


This is Lesson 2.2 of the Node.js Interview Prep Course -- 10 chapters, 42 lessons.

On this page