Modules: import and export

A module is a file that shares only what it exports; another file imports it by name (a named export) or under a name it picks (the default export), and a package.json in Node or type="module" on a page makes it run.

Apprentice · JavaScript · Software Development · Updated

After this lesson you can

  • Share code between files with export and import, using named and default exports
  • Set up modules to run in Node with package.json and on a web page with type=module

Key takeaways

  • Each module is one file with its own scope. Nothing in it can be used from another file unless it's exported.
  • A named export is imported with its exact name in curly braces. A file can have as many as it needs.
  • A file has at most one default export, imported with no braces under any name you choose.
  • The path in an import starts with ./ and ends with .js, such as "./units.js".
  • In Node, a package.json with type set to module says your files are modules. On a page, the script tag needs type="module" and an http:// address.

Every exercise in this course so far has had the word export in front of its functions, with a promise that it would be explained later. This is that lesson. Real programs aren't one long file: a game has a file for the player, one for the enemies and one for the score; a shopping site has hundreds. Every package you'll ever install, and every test file in your exercises, uses the same idea, modules, to keep files apart and still let them share. When a project grows past one screen of code, this is how you keep it in order.

One file, one job

A cookbook is split into chapters: breakfast, soups, desserts. Each chapter does one job, and you can find things. A program can be split the same way, into files that each do one job. In JavaScript, each of those files is a module: a file that keeps its code to itself and shares only what it chooses to share. (The modern kind, with import and export, is called an ES module, after ECMAScript, the official name of the JavaScript language.)

For the recipe scaler, one module can know about kitchen units and another about scaling recipes:

recipe-scaler/
├── package.json     says these files are modules
├── units.js         cups, tablespoons and milliliters
├── scale.js         scales a recipe to new servings
└── main.js          the program you run: uses the other two

export and import, by name

A module shares something by putting export (read "export") in front of it. Here units.js shares two functions and keeps ML_PER_CUP to itself:

// units.js
const ML_PER_CUP = 240;

export function cupsToMl(cups) {
  return cups * ML_PER_CUP;
}

export function mlToCups(ml) {
  return ml / ML_PER_CUP;
}

Another file uses them with import (read "import"). The names go in curly braces, and the path to the file comes after from:

// main.js
import { cupsToMl, mlToCups } from "./units.js";

console.log(`1.5 cups of milk is ${cupsToMl(1.5)} ml.`);
console.log(`A 600 ml carton of cream is ${mlToCups(600)} cups.`);

Run node main.js in the folder (with the package.json from the section below), and it prints:

1.5 cups of milk is 360 ml.
A 600 ml carton of cream is 2.5 cups.

These are named exports: each one is imported by its exact name, spelled the same, capital letters included. The path starts with ./ (read "dot slash": in this same folder) and ends with .js. Leave either off, and Node can't find the file. You'll also see named exports listed once at the end of a file, export { cupsToMl, mlToCups };, which does the same thing.

The default export

Sometimes a module has one main thing to share. It can mark that one as its default export with export default:

// scale.js
export default function scaleAmount(amount, fromServings, toServings) {
  return (amount / fromServings) * toServings;
}

A default export is imported without curly braces, and the importing file picks the name. A file can import from as many modules as it needs:

// main.js
import scaleAmount from "./scale.js";
import { cupsToMl } from "./units.js";

const milkCups = scaleAmount(1.5, 4, 10);
console.log(`Milk for 10: ${milkCups} cups, or ${cupsToMl(milkCups)} ml.`);
Milk for 10: 3.75 cups, or 900 ml.

A file has at most one default export, and as many named exports as it likes. Braces or no braces is the whole difference when you import, and mixing them up is the most common module error:

You write It works when the module has If it doesn't, Node says
import { cupsToMl } from "./units.js"; export function cupsToMl does not provide an export named 'cupsToMl'
import scaleAmount from "./scale.js"; export default does not provide an export named 'default'

Each file has its own scope

A module is like a room with its own door: what's declared inside stays inside, and only what's exported goes out through the door. units.js didn't export ML_PER_CUP, so main.js can't see it:

// main.js
import { cupsToMl } from "./units.js";

console.log(cupsToMl(2));
console.log(ML_PER_CUP);

It prints 480, then stops with ReferenceError: ML_PER_CUP is not defined. That's a feature: two modules can each have their own total or count without clashing, and a module's helpers can't be changed by accident from another file. It's the same idea as the scope rules in Scope: Where a Variable Can Be Seen, one level up: each file gets its own scope.

Try it. Make a folder recipe-scaler with package.json (below), units.js and main.js from this lesson, and run node main.js. Then add export in front of const ML_PER_CUP, import it in main.js, and print it.

Making modules run: Node and a page

In Node, a file named package.json in the project folder describes the project. One line in it, "type": "module", tells Node that the .js files in this folder are ES modules:

{
  "name": "recipe-scaler",
  "type": "module"
}

Without it, Node 24 guesses: it tries the older way first, sees import, and runs the file as a module after all, sometimes with a warning telling you to add "type": "module". With it, there's no guessing. Every exercise in this course comes with this line, and Node's own tools are modules too: the import { test } from "node:test"; at the top of your exercises' test files imports Node's test runner.

On a web page, the <script> tag needs type="module":

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Recipe Scaler</title>
  <script type="module" src="main.js"></script>
</head>
<body>
  <h1>Recipe Scaler</h1>
</body>
</html>

Without type="module", the browser's Console shows Cannot use import statement outside a module. And the page must be opened from a web address (http://...), not by double-clicking the file: browsers refuse to load modules from a file:// address. VS Code's Live Preview serves the page at an http:// address for you.

Check yourself

2 questions to try on the lesson page.

So what

Every real project, from a game to a bank's website, is split into modules that each do one job and share only what they export. Now the export in your exercises makes sense, and you can organize a program of your own the same way.

Key terms

Cheat sheets

Related