Skip to content

Modules and Package Management

💡
Before you start

You need Node installed, and to know how to run a .js file. If node --version in a terminal prints a version number, you are ready. If it does not — or if you have never opened a terminal — do Getting Started with JavaScript first; it installs Node and runs your first program in about fifteen minutes. Nothing else is needed.

Why Modules?

As JavaScript applications grow, putting all your code in a single file becomes unmanageable. Modules let you split your code into separate files, each with its own scope and responsibilities. This brings several benefits:

  • Organization: Group related functions and data together in logical files
  • Reusability: Import the same module wherever it is needed instead of copying code
  • Encapsulation: Each module has its own scope — internal variables do not leak into other files
  • Dependency management: Clear imports show exactly what each file depends on
  • Team collaboration: Different developers can work on different modules simultaneously
// Without modules — everything in one file, globals everywhere
// app.js (500+ lines...)
var users = [];
var API_URL = "/api";
function fetchUsers() { /* ... */ }
function renderUsers() { /* ... */ }
function validateEmail() { /* ... */ }
function formatDate() { /* ... */ }
// All variables are global — name conflicts are inevitable

// With modules — clean separation of concerns
// api.js         — API functions
// render.js      — DOM rendering
// validators.js  — Input validation
// utils.js       — Helper functions
// app.js         — Main application logic (imports from others)

ES Modules (import/export)

ES Modules (ESM) are the official standard for JavaScript modules, supported natively in all modern browsers and Node.js. They use the import and export keywords.

Exporting from a Module

// math.js — a module that exports math utilities

// Named exports — export individual items
export const PI = 3.14159265359;

export function add(a, b) {
    return a + b;
}

export function subtract(a, b) {
    return a - b;
}

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

// You can also export at the end of the file
function divide(a, b) {
    if (b === 0) throw new Error("Cannot divide by zero");
    return a / b;
}

function power(base, exponent) {
    return Math.pow(base, exponent);
}

export { divide, power };

Importing into Another Module

// app.js — importing from math.js

// Import specific items by name
import { add, subtract, PI } from "./math.js";

console.log(add(5, 3));      // 8
console.log(subtract(10, 4)); // 6
console.log(PI);              // 3.14159265359

// Import everything as a namespace
import * as math from "./math.js";

console.log(math.add(5, 3));      // 8
console.log(math.multiply(4, 5)); // 20
console.log(math.PI);             // 3.14159265359

// Rename imports to avoid conflicts
import { add as mathAdd, subtract as mathSubtract } from "./math.js";

Using Modules in HTML

// To use ES modules in the browser, add type="module" to the script tag:
// <script type="module" src="app.js"></script>

// Modules are automatically in strict mode
// Modules have their own scope (no global pollution)
// Modules are deferred by default (load after HTML parsing)
💡
ES Modules vs CommonJS

ES Modules (import/export) are the modern standard used in browsers and modern Node.js. CommonJS (require()/module.exports) is the older system used in Node.js. Most new projects use ES Modules. If you see require() in older tutorials or Node.js code, that is CommonJS. The two systems work differently under the hood but serve the same purpose.

Named vs Default Exports

Modules can have two types of exports: named exports (any number per file) and a default export (one per file).

Named Exports

// utils.js — multiple named exports
export function formatDate(date) {
    return date.toLocaleDateString();
}

export function formatCurrency(amount) {
    return `$${amount.toFixed(2)}`;
}

export function capitalize(str) {
    return str.charAt(0).toUpperCase() + str.slice(1);
}

// Importing named exports — must use curly braces
import { formatDate, formatCurrency } from "./utils.js";

Default Exports

// User.js — one default export (typically a class or main function)
export default class User {
    constructor(name, email) {
        this.name = name;
        this.email = email;
    }

    greet() {
        return `Hello, I'm ${this.name}`;
    }
}

// Importing default exports — no curly braces, choose any name
import User from "./User.js";
import MyUser from "./User.js"; // Same thing, different name

const alice = new User("Alice", "alice@example.com");
console.log(alice.greet()); // "Hello, I'm Alice"

Mixing Named and Default Exports

// api.js — one default + several named exports
const API_BASE = "/api/v1";

export function get(endpoint) {
    return fetch(`${API_BASE}${endpoint}`).then(r => r.json());
}

export function post(endpoint, data) {
    return fetch(`${API_BASE}${endpoint}`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(data)
    }).then(r => r.json());
}

// Default export — the main thing this module provides
export default {
    getUsers: () => get("/users"),
    getUser: (id) => get(`/users/${id}`),
    createUser: (data) => post("/users", data)
};

// Import both
import api, { get, post } from "./api.js";

// Use the default export
const users = await api.getUsers();

// Or use the named exports directly
const data = await get("/posts");

Re-exporting

// index.js — barrel file that re-exports from multiple modules
// This lets other files import everything from one place

export { formatDate, formatCurrency } from "./utils.js";
export { default as User } from "./User.js";
export { get, post } from "./api.js";

// Now consumers can do:
import { formatDate, User, get } from "./lib/index.js";

Dynamic Imports

Static import statements load modules before any code runs. Dynamic imports let you load modules on demand at runtime using the import() function, which returns a Promise.

// Load a module only when needed
async function loadChart() {
    const { default: Chart } = await import("./chart.js");
    const chart = new Chart("#container");
    chart.render(data);
}

// Triggered by user action — not loaded until needed
document.querySelector("#show-chart").addEventListener("click", loadChart);

// Conditional imports
async function loadLocale(language) {
    const translations = await import(`./locales/${language}.js`);
    return translations.default;
}

// Import with error handling
try {
    const module = await import("./optional-feature.js");
    module.init();
} catch (error) {
    console.log("Optional feature not available");
}

// Code splitting — load heavy libraries only when needed
async function handlePDFExport() {
    const { jsPDF } = await import("./vendor/jspdf.js");
    const doc = new jsPDF();
    doc.text("Hello World", 10, 10);
    doc.save("document.pdf");
}

Dynamic imports are essential for performance. Instead of loading everything up front, you load code only when the user actually needs it. This is called code splitting.

Node.js and npm

Node.js is a runtime that lets you run JavaScript outside the browser. npm (Node Package Manager) is its package manager — a massive registry of reusable code packages that you can install into your projects.

1
Install Node.js

Download and install from nodejs.org. This includes both Node.js and npm.

# Verify installation
node --version    # v20.x.x or later
npm --version     # 10.x.x or later
2
Initialize a project
# Create a new project directory
mkdir my-project
cd my-project

# Initialize with default settings
npm init -y

# This creates package.json
3
Install packages
# Install a package (adds to dependencies)
npm install lodash

# Install a dev-only package (testing, building, etc.)
npm install --save-dev jest

# Install globally (CLI tools)
npm install -g typescript

# Install all dependencies from package.json
npm install
4
Use the installed package
// In your JavaScript file
import _ from "lodash";

const numbers = [1, 2, 3, 4, 5];
console.log(_.shuffle(numbers)); // e.g. [3, 1, 5, 2, 4] — a different order every run
console.log(_.sum(numbers));     // 15

package.json and Dependencies

package.json is the configuration file for your project. It tracks metadata, dependencies, and scripts.

{
    "name": "my-project",
    "version": "1.0.0",
    "description": "A sample JavaScript project",
    "main": "index.js",
    "type": "module",

    "scripts": {
        "start": "node index.js",
        "dev": "node --watch index.js",
        "test": "jest",
        "build": "vite build",
        "lint": "eslint src/"
    },

    "dependencies": {
        "express": "^4.18.2",
        "lodash": "^4.17.21"
    },

    "devDependencies": {
        "jest": "^29.7.0",
        "eslint": "^8.56.0"
    }
}

Key Fields

  • "type": "module" — enables ES Module syntax (import/export) in Node.js
  • "scripts" — custom commands you can run with npm run <name>
  • "dependencies" — packages your app needs to run in production
  • "devDependencies" — packages needed only during development (testing, building, linting)
# Running scripts
npm start         # Runs the "start" script
npm test          # Runs the "test" script
npm run dev       # Runs the "dev" script
npm run build     # Runs the "build" script
npm run lint      # Runs the "lint" script

The node_modules Directory

# node_modules/ contains all installed packages
# It can be very large (hundreds of MB) — NEVER commit it to git

# .gitignore should always include:
# node_modules/

# To recreate node_modules from package.json:
npm install

# package-lock.json tracks exact versions — DO commit this file
# It ensures everyone gets the same versions

Version Ranges

// In package.json, version ranges control which updates are allowed:
// "^4.18.2" — compatible updates: 4.18.2, 4.18.3, 4.19.0, but NOT 5.0.0
// "~4.18.2" — patch updates only: 4.18.2, 4.18.3, but NOT 4.19.0
// "4.18.2"  — exact version only

// Update packages
// npm update           — update within version ranges
// npm outdated         — check for newer versions
// npm install pkg@latest — install the latest version

Popular JavaScript Packages

The npm registry has over two million packages. Here are some of the most widely used ones organized by category:

Frontend Frameworks

  • React — Component-based UI library by Meta (most popular)
  • Vue.js — Progressive framework with gentle learning curve
  • Svelte — Compiler-based framework with minimal runtime overhead
  • Angular — Full-featured framework by Google

Backend / Server

  • Express — Minimalist web server framework for Node.js
  • Fastify — High-performance alternative to Express
  • Next.js — Full-stack React framework with server-side rendering

Utility Libraries

  • Lodash — Utility functions for arrays, objects, strings
  • date-fns — Modern date utility library (alternative to Moment.js)
  • Axios — HTTP client with better error handling than fetch
  • Zod — Runtime type validation and schema parsing

Development Tools

  • TypeScript — Adds static types to JavaScript
  • ESLint — Identifies and fixes code quality issues
  • Prettier — Automatic code formatting
  • Jest / Vitest — Testing frameworks

Bundlers (Webpack, Vite)

Browsers can load ES Modules natively, but for production applications you typically use a bundler. Bundlers combine your modules into optimized files, handle dependencies, and provide development tools.

Why Use a Bundler?

  • Performance: Combines many files into fewer HTTP requests
  • Optimization: Minifies code, removes unused exports (tree shaking)
  • Compatibility: Transpiles modern syntax for older browsers
  • Assets: Handles CSS, images, fonts, and other non-JS files
  • Dev server: Hot module replacement for instant feedback during development

Vite (Recommended for New Projects)

Vite is a modern build tool that is fast and simple. It uses native ES Modules during development and Rollup for production builds.

# Create a new Vite project
npm create vite@latest my-app
cd my-app
npm install
npm run dev     # Start development server (usually localhost:5173)

# Project structure:
# my-app/
#   index.html         — Entry point
#   package.json
#   vite.config.js     — Vite configuration
#   src/
#     main.js          — JavaScript entry
#     style.css        — Global styles
#     components/      — Your modules
// vite.config.js — basic configuration
import { defineConfig } from "vite";

export default defineConfig({
    root: "src",
    build: {
        outDir: "../dist",
        minify: true
    },
    server: {
        port: 3000,
        open: true // Auto-open browser
    }
});

// Build for production
// npm run build   — outputs optimized files to dist/

Webpack (Industry Standard)

Webpack is the most widely used bundler. It is more configurable than Vite but also more complex to set up.

// webpack.config.js — basic configuration
const path = require("path");

module.exports = {
    entry: "./src/index.js",
    output: {
        filename: "bundle.js",
        path: path.resolve(__dirname, "dist")
    },
    module: {
        rules: [
            {
                test: /\.css$/,
                use: ["style-loader", "css-loader"]
            },
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: "babel-loader"
            }
        ]
    },
    devServer: {
        static: "./dist",
        hot: true
    }
};
# Install Webpack
npm install --save-dev webpack webpack-cli webpack-dev-server

# Build
npx webpack

# Development server
npx webpack serve

When to Use Which

  • Vite: New projects, simpler setup, faster development experience
  • Webpack: Complex projects, legacy codebases, maximum configurability
  • No bundler: Small projects, learning exercises, simple static pages

Now Do It Yourself: Five Steps

You will build a two-file project and grow it: one file that exports, one that imports, then a default export, then everything a module offers at once, then a module loaded only at the moment it is needed. Use the .mjs ending throughout — it tells every version of Node, old or new, that the file is a module, with no package.json needed. Every output below is exactly what Node printed.

1
Split one program into two files

Go: open a terminal, make a folder with mkdir modules-demo, and move into it with cd modules-demo.

Do: save these three lines as math.mjs.

export function add(a, b) {
  return a + b;
}

Do: save these two lines as app.mjs, in the same folder.

import { add } from "./math.mjs";
console.log(add(2, 3));

Do: run it.

node app.mjs

You should see: 5. Two files, one program. export is a doorway out of math.mjs; nothing else in that file is visible to anyone. The braces in { add } mean “the export called add”, and the ./ means “a file next to this one”.

If not: Error [ERR_MODULE_NOT_FOUND]: Cannot find module …/maths.mjs is a filename typo — the message prints the full path it looked for, so read that path rather than your import line. Drop the ./ and you get a different message, Cannot find package 'math.mjs': without it Node stops looking at your folder and starts looking for an installed package of that name.

2
Export more than one thing

Go: stay in the same folder.

Do: save this as the whole of math.mjs.

export function add(a, b) {
  return a + b;
}

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

Do: save this as the whole of app.mjs.

import { add, multiply } from "./math.mjs";
console.log(add(2, 3));
console.log(multiply(2, 3));

Do: run it again.

node app.mjs

You should see: 5 then 6. These are named exports: a file may have as many as it likes, and you list the ones you want inside the braces, separated by commas. Anything you leave out simply stays private to math.mjs.

If not: misspell one and Node tells you precisely: SyntaxError: The requested module './math.mjs' does not provide an export named 'mutiply'. Note that it is a Syntax error, raised before a single line of your program runs — the names are checked when the file is loaded, not when you call the function.

3
Add a default export — the one without braces

Go: same folder, new file.

Do: save these three lines as greet.mjs.

export default function (name) {
  return `Hello, ${name}!`;
}

Do: save this as the whole of app.mjs.

import { add, multiply } from "./math.mjs";
import sayHello from "./greet.mjs";
console.log(add(2, 3));
console.log(multiply(2, 3));
console.log(sayHello("Sam"));

Do: run it again.

node app.mjs

You should see: 5, 6, then Hello, Sam!. A file may have exactly one default export, and it arrives without braces. Because the export has no name of its own, the name is yours to choose: sayHello here, but greet or hi would work identically. That freedom is the difference — a named import must match the export exactly, a default import never does.

If not: put braces around it and you get SyntaxError: The requested module './greet.mjs' does not provide an export named 'sayHello', which reads as if the file is wrong when in fact the braces are. Braces mean “by name”; a default has no name to match.

4
Take the whole module in one go

Go: same folder.

Do: save this line at the end of math.mjs, after the two functions.

export const PI = 3.14159;

Do: save this as the whole of app.mjs.

import { add, multiply } from "./math.mjs";
import sayHello from "./greet.mjs";
import * as math from "./math.mjs";
console.log(add(2, 3));
console.log(multiply(2, 3));
console.log(sayHello("Sam"));
console.log(math.PI, math.add(1, 1));

Do: run it again.

node app.mjs

You should see: the three earlier lines, then 3.14159 2. import * as math gathers every named export into one object, so you reach them through a prefix instead of listing them. Functions are not the only thing a module can export — PI is a plain constant. Use the star form when you want a clear prefix at the call site; use the braces when you want just two or three things.

If not: if math.PI prints undefined, the new line did not land in math.mjs, or it is missing the word export. A value without export stays private, and asking for a private name through the star object is not an error — it is simply undefined, which is why this one is easy to stare past.

5
Load a module only when you need it

Go: same folder.

Do: save this as the whole of app.mjs — the last two lines are the new ones.

import { add, multiply } from "./math.mjs";
import sayHello from "./greet.mjs";
import * as math from "./math.mjs";
console.log(add(2, 3));
console.log(multiply(2, 3));
console.log(sayHello("Sam"));
console.log(math.PI, math.add(1, 1));
const later = await import("./greet.mjs");
console.log("loaded on demand:", later.default("Alex"));

Do: run it one last time.

node app.mjs

You should see: the four earlier lines, then loaded on demand: Hello, Alex!. import(...) with brackets is a function that returns a promise, so it can sit inside an if, run only when a user clicks something, and be awaited like any other slow job. That is how a site avoids shipping a large chart library to every visitor who never opens the chart.

If not: notice later.default rather than plain later. A dynamic import hands you the whole module as an object, so the default export lives on a property literally called default. Forgetting it gives TypeError: later is not a function — the object is not callable, the function inside it is.

🎉
Check yourself before moving on

Without scrolling up: you write import { sayHello } from "./greet.mjs"; and Node says the module does not provide an export named sayHello — yet the function is plainly there. What is wrong? Answer: it is the file’s default export, and braces ask for a named one. Drop the braces and the name becomes yours to choose.

Now do it without the page: add a third file, strings.mjs, exporting a function shout that returns its argument in capitals, and call it from app.mjs. One export, one import with braces, one console.log — and if the name in the braces does not match the name after export, you already know exactly which message you will get.

Summary

  • Modules split code into separate files with clear responsibilities and dependencies
  • ES Modules use export to make code available and import to use it
  • Named exports allow multiple exports per file; default exports provide one main export
  • Dynamic import() loads modules on demand for better performance
  • Node.js runs JavaScript outside the browser; npm manages package dependencies
  • package.json tracks project metadata, scripts, and dependencies
  • The npm ecosystem has millions of packages for every need
  • Bundlers like Vite and Webpack optimize modules for production deployment
🎉
Modules and package management mastered!

You now understand how professional JavaScript projects are organized, how to use the npm ecosystem, and how bundlers prepare your code for production. These skills are essential for building real-world applications.