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)); // [3, 1, 5, 2, 4]
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

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.