Skip to content
Home
JavaScript Modules: A Complete Guide

JavaScript Modules: A Complete Guide

JavaScript JavaScript 8 min read 1653 words Beginner ExcellentWiki Editorial Team

JavaScript modules let you organize code into reusable, encapsulated files. ES modules (ESM) are the official standard, supported in all modern browsers and Node.js since v12. This guide covers the module ecosystem and practical patterns.

ES Modules (ESM)

ES modules use export and import keywords and are the standard module system for modern JavaScript.

Named Exports

// utils.js
export const PI = 3.14159;

export function double(n) {
    return n * 2;
---

export class Counter {
    constructor() {
        this.count = 0;
    }
    increment() {
        this.count++;
    }
---

// Named exports (alternative syntax)
const PI = 3.14159;
function double(n) { return n * 2; }
export { PI, double };

Named Imports

import { PI, double, Counter } from './utils.js';
console.log(double(PI)); // 6.28318

// Renaming imports
import { double as twice, Counter as MyCounter } from './utils.js';

// Import all as namespace
import * as Utils from './utils.js';
console.log(Utils.PI);

Default Export

Each module can have one default export:

// logger.js
export default function log(message) {
    console.log(`[LOG] ${message}`);
---

// Or for classes
export default class Logger { /* ... */ }

// Importing default
import log from './logger.js';
import Logger from './logger.js';

// Mixing default and named
import log, { LogLevel } from './logger.js';

Default exports are convenient but make renaming implicit — the importer can name the import anything, which reduces consistency. Prefer named exports for libraries and shared code.

Re-exporting

// barrel/index.js — aggregates and re-exports
export { default as Button } from './Button.js';
export { default as Input } from './Input.js';
export { useAuth } from './hooks/useAuth.js';

// Re-export all named exports
export * from './utils.js';

// Re-export with rename
export { double as duplicate } from './utils.js';

Barrel files simplify imports for consumers — they import from the barrel instead of deep paths.

Dynamic Imports

// Static import (evaluated at module parse time)
import { heavyFunction } from './heavy-module.js';

// Dynamic import (evaluated at runtime)
button.addEventListener('click', async () => {
    try {
        const module = await import('./heavy-module.js');
        module.heavyFunction();
    } catch (error) {
        console.error('Failed to load module:', error);
    }
---);

// Import conditionally
const locale = navigator.language;
const { formatDate } = await import(`./locales/${locale}.js`);

Dynamic imports return a promise and enable code splitting — the browser fetches the module only when it is actually needed. This is the foundation of lazy loading in modern web applications.

CommonJS (CJS)

CommonJS is the module system used by Node.js before ESM support. It uses require and module.exports.

// math.js
const PI = 3.14159;

function circleArea(radius) {
    return PI * radius * radius;
---

module.exports = { PI, circleArea };
// or: exports.PI = PI; exports.circleArea = circleArea;

// main.js
const math = require('./math.js');
console.log(math.PI);
console.log(math.circleArea(5));

CommonJS is synchronous — require returns the module content immediately. This works because files are loaded from disk (synchronous in Node), but it is not suitable for browsers where module loading is inherently async.

Key Differences from ESM

FeatureESMCommonJS
Syntaximport / exportrequire / module.exports
LoadingStatic (parse-time)Dynamic (runtime)
AsyncYes (static analysis possible)Sync (require returns immediately)
Top-level thisundefinedmodule.exports
File extension.mjs or .js with "type": "module".cjs or .js without "type": "module"
Tree-shakingSupportedNot supported
Circular depsBetter error messagesPartial loading

Module Resolution

Relative vs Absolute Imports

// Relative — starts with ./
import { helper } from './helpers/utils.js';
import { something } from '../other/index.js';

// Absolute/bare — from node_modules
import lodash from 'lodash';
import { Router } from 'express';

// Absolute with alias (via bundler config)
import { api } from '@/services/api.js';

Import Maps (Browser Native)

<script type="importmap">
{
    "imports": {
        "lodash": "https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js",
        "react": "https://cdn.jsdelivr.net/npm/react@18/cjs/react.production.min.js"
    }
---
</script>
<script type="module">
    import _ from 'lodash';
</script>

Import maps let browsers resolve bare module specifiers (like Node’s node_modules resolution) without a bundler. They are supported in Chrome 89+, Firefox 108+, and Edge 89+.

Circular Dependencies

Circular dependencies happen when module A imports from module B, and B imports from A (directly or transitively).

// a.js
import { b } from './b.js';
export const a = 'A';
console.log(b); // undefined if b.js hasn't finished evaluating

// b.js
import { a } from './a.js';
export const b = 'B';

In ESM, circular imports work if both modules only reference each other after both are fully evaluated. The problem occurs when a module tries to use an import from a circular dependency before that module has finished evaluating its exports.

Best practices:

  • Avoid circular dependencies entirely — they indicate a design problem
  • Use a shared dependency pattern instead
  • If unavoidable, defer accessing the circular import until after module evaluation

Module Bundlers

Bundlers resolve the module graph, apply transformations, and produce optimized output:

// webpack.config.js
module.exports = {
    entry: './src/index.js',
    output: {
        filename: '[name].[contenthash].js',
        path: path.resolve(__dirname, 'dist'),
    },
    module: {
        rules: [
            { test: /\.js$/, use: 'babel-loader' },
        ],
    },
    optimization: {
        splitChunks: {
            chunks: 'all', // code splitting for shared dependencies
        },
    },
---;

Popular bundlers:

  • Webpack — most configurable, largest ecosystem
  • Vite — fast dev server (ESM-native), uses Rollup for production
  • Rollup — optimized tree-shaking, best for libraries
  • esbuild — extremely fast (written in Go), limited plugin ecosystem
  • Parcel — zero-configuration

Package.json and Module Types

{
    "name": "my-library",
    "type": "module",
    "main": "./dist/index.cjs",
    "module": "./dist/index.js",
    "exports": {
        ".": {
            "import": "./dist/index.js",
            "require": "./dist/index.cjs"
        },
        "./utils": {
            "import": "./dist/utils.js",
            "require": "./dist/utils.cjs"
        }
    }
---

The "type": "module" field makes all .js files in the package use ESM by default. The "exports" field provides conditional exports for different environments and sub-path exports for package entry points.

Best Practices

  • Prefer named exports over default exports for consistency and tree-shaking
  • Use barrel files sparingly — they can increase bundle size if used incorrectly
  • Split code at route/page boundaries for optimal loading
  • Keep modules focused on a single responsibility
  • Avoid side effects at module evaluation time
  • Use .mjs for ESM-only files and .cjs for CJS-only files

Module Proxy and Checksum Database

Go uses a module proxy (proxy.golang.org) to cache and serve module versions. This proxy ensures that modules are never removed from the ecosystem once published. The checksum database (sum.golang.org) provides a verifiable log of module checksums, preventing supply-chain attacks.

Configuring Proxies

# Use direct access (no proxy)
GOPROXY=direct go get ./...

# Use multiple proxies with fallback
export GOPROXY=https://proxy.golang.org,direct

# Private modules (skip proxy)
export GOPROXY=https://proxy.golang.org,direct
export GONOSUMCHECK=github.com/mycompany/*
export GOPRIVATE=github.com/mycompany/*

Workspace Mode (Go 1.18+)

For multi-module projects, workspace mode lets you work on multiple modules simultaneously:

go work init ./module1 ./module2
go work use ./local-module
go.work
├── use (
│       ./module1
│       ./module2
│   )
├── replace example.com/old => ./local-fork

Real-World Implementation Tips

Production Considerations

When moving from development to production, several factors become critical. Error handling should be comprehensive — every external call (database, API, file system) should have proper error checking, logging, and retry logic where appropriate. Performance monitoring through metrics and structured logging helps identify bottlenecks before they affect users.

Testing Strategy

A thorough testing approach combines multiple levels:

  • Unit tests verify individual functions and methods in isolation
  • Integration tests validate that components work together correctly
  • Edge case tests cover boundary conditions, empty inputs, and error states
  • Performance tests ensure the system meets latency and throughput requirements

Test data should be realistic but controlled. Mock external dependencies to make tests fast and deterministic. Aim for tests that are independent, repeatable, and fast enough to run on every commit.

Documentation

Good documentation is essential for maintainable code. Follow these principles:

  • Document the “why” not just the “what” — explain design decisions
  • Keep examples up to date with the code
  • Include usage examples for public APIs
  • Document configuration options and their defaults
  • Explain error conditions and recovery strategies

Security Best Practices

Security should be considered throughout development:

  • Validate all inputs at system boundaries
  • Use parameterized queries for database access
  • Store secrets in environment variables or secret managers
  • Keep dependencies updated to patch vulnerabilities
  • Apply the principle of least privilege

Performance Optimization

Optimize based on measured data, not assumptions:

  1. Profile before optimizing — identify actual bottlenecks
  2. Measure the impact of each change
  3. Consider the trade-off between speed and readability
  4. Cache expensive operations with appropriate invalidation
  5. Use connection pooling for database and network resources

Monitoring and Observability

Production systems need visibility:

  • Structured logging with correlation IDs for request tracking
  • Metrics for latency, throughput, error rates, and resource usage
  • Health check endpoints for load balancers and orchestration
  • Distributed tracing for request flows across services
  • Alerts for anomaly detection based on baselines

These patterns apply across all programming languages and frameworks. The specific implementation varies, but the principles remain consistent.

FAQ

Q: What is the difference between static and dynamic imports? A: Static imports (import x from 'y') are evaluated at parse time and must be at the top level. Dynamic imports (import('y')) are runtime-evaluated and can be conditional or lazily loaded.

Q: Can I mix CommonJS and ESM in the same project? A: Yes, but with caveats. ESM can import CJS modules (default import = module.exports). CJS cannot require ESM modules — you must use dynamic import().

Q: What is tree-shaking? A: Tree-shaking is dead code elimination. If you import only one function from a module, the bundler removes the unused code. ESM supports this because imports/exports are static and analyzable.

Q: Do I need a bundler for production? A: Not necessarily, but bundlers optimize code (minification, tree-shaking, code splitting) and handle assets (CSS, images). Native ESM in browsers works for development without a bundler.

Q: What is the difference between main, module, and exports in package.json? A: main is the entry for CJS (require). module is the entry for ESM (import). exports provides conditional entry points for both systems and sub-path exports.

Q: How do I handle environment-specific code in modules? A: Use bundler replacements or conditional exports. Avoid runtime checks for process.env.NODE_ENV in ESM — bundlers handle this at build time through DefinePlugin or equivalent.

For a comprehensive overview, read our article on Javascript Arrays Guide.

For a comprehensive overview, read our article on Javascript Async Await.

Section: JavaScript 1653 words 8 min read Beginner 756 articles in section Report inaccuracy Back to top