Skip to content
aviral gupta

// B2.4 · ~34 min · Beginner

import/export, import.meta.dirname, node: built-ins

After this lesson you can split a program into ES modules that share what they export, load modules and JSON files when you need them, and locate files next to a module.

Lesson 4 of 5 in B2 npm and packages

You will be able to

  • Write named and default exports and import them with a full relative specifier
  • Load modules with import() and JSON with with {type: "json"}, and predict live bindings
  • Use import.meta.dirname and import.meta.filename, and know which built-ins need node:
  1. Warm-up · Activity 1 of 7

    Warm-up: cart.js contains export default function total() { … }. Which line in main.js imports that function?

  2. Predict · Activity 2 of 7

    Predict before you read on. main.js imports count and increment from counter.js and calls increment twice. What does node main.js print?

    // counter.js
    export let count = 0;
    export function increment() {
      count += 1;
    }
    
    // main.js
    import {count, increment} from "./counter.js";
    increment();
    increment();
    console.log(count);
    
  3. Practice · Activity 3 of 7

    cart.js sits next to main.js. Fill in the rest of the specifier so that Node.js finds it.

    import {add} from "./cart____";
    import {add} from "./cart";
  4. Practice · Activity 4 of 7

    cart.js exports add and count by name and total as its default. Match each import to what it gives main.js.

  5. Practice · Activity 5 of 7

    The file /home/ada/shop/src/main.js contains this line. You are in /home/ada/shop and run node src/main.js. What does it print?

    console.log(import.meta.dirname);
  6. Brain teaser · Activity 6 of 7

    Brain teaser. main.js loads greet.js with import(). What does it print?

    // greet.js
    export default function greet(name) {
      return "Hello, " + name + "!";
    }
    
    // main.js
    const greeting = await import("./greet.js");
    console.log(typeof greeting, typeof greeting.default);
    
  7. Apply · Activity 7 of 7

    Mini-task. In a folder whose package.json has "type": "module", write lib/money.js with a named export CURRENCY and a default export money(amount) that formats 3.5 as 3.50 EUR. Add settings.json with a shop name. In main.js, import both from lib/money.js, import settings.json with its import attribute, and print the shop name and money(3.5). Print basename(import.meta.filename) with node:path. Finally load lib/money.js again with import() and check that its default is the same function.

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

A cart in its own module

cart.js keeps a count and a list of items and exports add and count by name, and total as its default. main.js imports all three, adds two items and reads count before and after: the import follows the variable in cart.js. format.js has a default export too, which main.js names money. At the end, import * as cart shows every export of cart.js in one object. It runs in the browser too.

main.js

import total, {add, count} from "./cart.js";
import money from "./format.js";
import * as cart from "./cart.js";

console.log("items at start:", count);
add("tea", 3.5);
add("cake", 4);
console.log("items now:", count);
console.log("total:", money(total()));
console.log("cart.js exports:", Object.keys(cart).join(", "));
console.log("same count:", cart.count === count);

cart.js

// A tiny shopping cart. Its state lives in this module.
export let count = 0;
const items = [];

export function add(name, price) {
  items.push({name, price});
  count += 1;
}

export default function total() {
  return items.reduce((sum, item) => sum + item.price, 0);
}

format.js

export default function format(amount) {
  return amount.toFixed(2) + " EUR";
}

Run it with

node main.js

Output

items at start: 0
items now: 2
total: 7.50 EUR
cart.js exports: add, count, default
same count: true
  • count is 0, then 2: main.js reads the live variable of cart.js, not a copy.
  • format.js calls its function format, but main.js imports the default export as money: the importer picks the name.
  • The module object holds the default export under the key default, next to the named ones.
Change it and run it

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.

Exercises

Exercise 1 of 3

Named and default exports

rates.js is given: it exports STANDARD (0.19) and REDUCED (0.07) by name, and round2 as its default. In main.js, import all three. Then export a default function gross(net, reduced = false) that adds tax at the matching rate and rounds with round2, so gross(10) is 11.9 and gross(10, true) is 10.7. Also export rates, an object {standard, reduced} with both rates, by name.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.

Hints
  1. Hint 1

    One line imports both kinds: import round2, {STANDARD, REDUCED} from "./rates.js";

  2. Hint 2

    net * (1 + STANDARD) is the gross amount; pass it to round2.

  3. Hint 3

    Pick the rate with reduced ? REDUCED : STANDARD, and build rates as {standard: STANDARD, reduced: REDUCED}.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

import round2, {STANDARD, REDUCED} from "./rates.js";

export default function gross(net, reduced = false) {
  return round2(net * (1 + (reduced ? REDUCED : STANDARD)));
}

export const rates = {standard: STANDARD, reduced: REDUCED};
Run it on your computer

Install Node.js 24 LTS or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.js

// Import round2 (default) and STANDARD, REDUCED (named) from ./rates.js here.

export default function gross(net, reduced = false) {
  return net;
}

export const rates = {};

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import * as main from './main.js';

test('the default export is a function', () => {
  assert.equal(typeof main.default, 'function', `the default export is ${typeof main.default}`);
});

test('adds the standard rate and rounds to cents', () => {
  assert.equal(main.default(10), 11.9, `gross(10) returned ${main.default(10)}`);
  assert.equal(main.default(19.99), 23.79, `gross(19.99) returned ${main.default(19.99)}`);
});

test('adds the reduced rate when asked', () => {
  assert.equal(main.default(10, true), 10.7, `gross(10, true) returned ${main.default(10, true)}`);
});

test('exports rates by name', () => {
  assert.deepEqual(main.rates, {standard: 0.19, reduced: 0.07}, `rates is ${JSON.stringify(main.rates)}`);
});

rates.js

// Tax rates as fractions, and a helper that rounds to cents.
export const STANDARD = 0.19;
export const REDUCED = 0.07;

export default function round2(amount) {
  return Math.round(amount * 100) / 100;
}

package.json

{
  "type": "module"
}

package.json tells Node.js that the .js files are modules; keep it in the folder.

Run the program:

node main.js

Run the checks (needs learnrun.js in the same folder):

node --test
Download learnrun.js

Exercise 2 of 3

Load a module by name

greet-en.js and greet-de.js each have a default export greet(name). Write the async function greet(lang, name) in main.js: it loads "./greet-" + lang + ".js" with import() and returns what that module's default export gives for name, so greet("de", "Ada") is "Hallo, Ada!". When no such module exists, import() rejects: catch that and return "No greeting for " + lang. Run the tests with node --test.

This exercise needs Node.js on your computer (the browser version cannot run it). The files and commands are below.

Hints
  1. Hint 1

    import() takes any string, so you can build the specifier: "./greet-" + lang + ".js".

  2. Hint 2

    await the import(); the function is on the result under .default.

  3. Hint 3

    Wrap the import in try/catch: a missing module makes the awaited import() throw.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

// Loads ./greet-<lang>.js when it is needed and greets name with its default export.
export async function greet(lang, name) {
  try {
    const module = await import("./greet-" + lang + ".js");
    return module.default(name);
  } catch {
    return "No greeting for " + lang;
  }
}
Run it on your computer

Install Node.js 24 LTS or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.js

// Loads ./greet-<lang>.js when it is needed and greets name with its default export.
export async function greet(lang, name) {
  return "";
}

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {greet} from './main.js';

test('greets in English with greet-en.js', async () => {
  const got = await greet('en', 'Ada');
  assert.equal(got, 'Hello, Ada!', `greet('en', 'Ada') returned ${JSON.stringify(got)}`);
});

test('greets in German with greet-de.js', async () => {
  const got = await greet('de', 'Ada');
  assert.equal(got, 'Hallo, Ada!', `greet('de', 'Ada') returned ${JSON.stringify(got)}`);
});

test('says so when there is no module for the language', async () => {
  const got = await greet('fr', 'Ada');
  assert.equal(got, 'No greeting for fr', `greet('fr', 'Ada') returned ${JSON.stringify(got)}`);
});

greet-en.js

export default function greet(name) {
  return "Hello, " + name + "!";
}

greet-de.js

export default function greet(name) {
  return "Hallo, " + name + "!";
}

package.json

{
  "type": "module"
}

package.json tells Node.js that the .js files are modules; keep it in the folder.

Run the program:

node main.js

Run the checks (needs learnrun.js in the same folder):

node --test
Download learnrun.js

Exercise 3 of 3

JSON settings next to the module

settings.json sits next to main.js. Import it with its import attribute, and write two functions. banner() returns the name and the version from the file, separated by a space: "shop 1.4.2". settingsPath() returns the absolute path of settings.json, built with join from node:path and import.meta.dirname, so it is right whichever folder you run node from. Run the tests with node --test.

This exercise needs Node.js on your computer (the browser version cannot run it). The files and commands are below.

Hints
  1. Hint 1

    import settings from "./settings.json" with {type: "json"}; gives you the parsed object.

  2. Hint 2

    settings.name + " " + settings.version builds the banner.

  3. Hint 3

    import {join} from "node:path"; then join(import.meta.dirname, "settings.json").

Show a solution

One way to solve it. Yours can look different and still pass the checks.

import {join} from "node:path";
import settings from "./settings.json" with {type: "json"};

export function banner() {
  return settings.name + " " + settings.version;
}

export function settingsPath() {
  return join(import.meta.dirname, "settings.json");
}
Run it on your computer

Install Node.js 24 LTS or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.js

// Import settings.json (it needs an import attribute) and join from node:path here.

export function banner() {
  return "";
}

export function settingsPath() {
  return "";
}

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {banner, settingsPath} from './main.js';

test('banner() shows the name and version from settings.json', () => {
  assert.equal(banner(), 'shop 1.4.2', `banner() returned ${JSON.stringify(banner())}`);
});

test('settingsPath() is settings.json in the folder of main.js', () => {
  const got = settingsPath();
  const folder = import.meta.dirname;
  assert.ok(got.startsWith(folder) && got.endsWith('settings.json') && got.length === folder.length + 'settings.json'.length + 1, `settingsPath() returned ${JSON.stringify(got)}; expected settings.json inside ${folder}`);
});

settings.json

{"name": "shop", "version": "1.4.2"}

package.json

{
  "type": "module"
}

package.json tells Node.js that the .js files are modules; keep it in the folder.

Run the program:

node main.js

Run the checks (needs learnrun.js in the same folder):

node --test
Download learnrun.js

Common mistakes

Leaving out the extension

import {add} from "./cart";

add("tea", 3.5);

What Node.js prints

Error [ERR_MODULE_NOT_FOUND]: Cannot find module

Why, and the fix

In an ES module Node.js takes a relative specifier literally: it looks for a file called cart, finds none, and adds Did you mean to import "./cart.js"? under the error. Write the full name, import {add} from "./cart.js". The same goes for folders: import "./lib/index.js", not "./lib".

Braces around a default export

import {round2} from "./rates.js";

console.log(round2(12.345));

What Node.js prints

SyntaxError: The requested module './rates.js' does not provide an export named 'round2'

Why, and the fix

round2 is the default export of rates.js. Braces ask for a named export, and rates.js has only STANDARD and REDUCED by name. Import the default without braces, import round2 from "./rates.js", or both at once: import round2, {STANDARD} from "./rates.js". The check happens before any line of main.js runs.

Importing JSON without its attribute

import settings from "./settings.json";

console.log(settings.name);

What Node.js prints

TypeError [ERR_IMPORT_ATTRIBUTE_MISSING]: Module

Why, and the fix

The full message ends with needs an import attribute of "type: json". Node.js requires the attribute for every JSON module, so a file cannot pass itself off as JavaScript: import settings from "./settings.json" with {type: "json"}. With import(), pass it as a second argument: await import("./settings.json", {with: {type: "json"}}).

JavaScript in the browser: your browser’s own engine, in a sandboxed worker. Syntax errors are located with acorn 8.18.0, MIT. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

Named and default exports

A module shares only what it exports. Named exports keep their names: export function add() or export const REDUCED = 0.07, imported as import {add, REDUCED} from './cart.js', or renamed with {add as addItem}. A module may also have one default export, export default function total(), which the importer names freely: import total from './cart.js'. Both mix: import total, {add} from './cart.js'. import * as cart gives one object with every export, the default under cart.default. Asking for a name the module does not export stops the program before any line runs: SyntaxError: The requested module './cart.js' does not provide an export named 'totl'.

Specifiers: the extension is part of the name

The string after from is the specifier. A relative one, './cart.js', is resolved from the importing file, and Node.js adds nothing: './cart' stops with ERR_MODULE_NOT_FOUND and a hint to import "./cart.js", and a folder does not load its index.js. A bare specifier such as 'semver' names a package in node_modules. Built-ins load as fs or node:fs, but node:sea, node:sqlite, node:test and node:test/reporters exist only with the prefix, which keeps new built-ins apart from packages that already use the name; module.builtinModules lists them all. import('./cart.js') takes the same specifiers while the program runs and returns a promise of the module object: await it and read .default.

Live bindings, JSON and import.meta

An import is a live, read-only view of the exporting module's variable. When cart.js runs count += 1, every importer sees the new count; count = 5 in the importer fails with TypeError: Assignment to constant variable. A JSON file needs an import attribute: import settings from './settings.json' with {type: 'json'}. Without it Node.js throws ERR_IMPORT_ATTRIBUTE_MISSING, and a JSON module has only a default export. In ES modules, import.meta.url is the module's file: URL, import.meta.filename its absolute path and import.meta.dirname its folder, whichever folder you start node from. Both are no longer experimental since v24.0.0; a .cjs file cannot use import.meta at all.

Sources

Last reviewed September 30, 2026