Skillversitas

49. JavaScript Modules: Modern Code Organization

Introduction

JavaScript Modules is a way to split code into separate files that can be imported from and exported to each other. Modules help organize code, avoid naming conflicts, and enable code splitting for better performance.

There are two main module systems: ES Modules (native browser/Node.js) and CommonJS (Node.js). The two work differently but complement each other.

Why Are Modules Important?

Without modules, all JavaScript code ends up in a single file, or you have to use sequential script tags. This causes:
- Global scope pollution (variable conflicts)
- Difficulty maintaining large codebases
- No way to lazy load specific parts
- Manual dependency management

With modules, each file has its own scope and can clearly define what is exported and imported.

ES Modules (ESM)

Named Export

// file: utils/math.js
// Named export - you can export many things from one file

export const PI = 3.14159;

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

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

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

export function bagi(a, b) {
    if (b === 0) throw new Error('Cannot divide by zero');
    return a / b;
}

// Export at the end of the file
const MAX_RETRY = 5;
const TIMEOUT = 3000;

export { MAX_RETRY, TIMEOUT };
// file: utils/string.js
export function capitalize(str) {
    return str.charAt(0).toUpperCase() + str.slice(1);
}

export function truncate(str, maxLength) {
    if (str.length <= maxLength) return str;
    return str.slice(0, maxLength) + '...';
}

export function slugify(str) {
    return str
        .toLowerCase()
        .replace(/\s+/g, '-')
        .replace(/[^\w-]/g, '');
}

// Re-export from another file
export { add, subtract } from './math.js';

Default Export

// file: models/Student.js
// Default export - one per file
export default class Student {
    constructor(name, studentId) {
        this.name = name;
        this.studentId = studentId;
        this.grade = [];
    }

    addGrade(grade) {
        this.grade.push(grade);
    }

    get average() {
        if (this.grade.length === 0) return 0;
        return this.grade.reduce((a, b) => a + b, 0) / this.grade.length;
    }

    static fromJSON(json) {
        const data = typeof json === 'string' ? JSON.parse(json) : json;
        const student = new Student(data.name, data.studentId);
        student.grade = data.grade || [];
        return student;
    }
}
// file: services/AuthService.js
import Student from '../models/Student.js';

class AuthService {
    constructor() {
        this.currentUser = null;
        this.token = localStorage.getItem('token');
    }

    async login(studentId, password) {
        const response = await fetch('/api/auth/login', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ studentId, password })
        });

        if (!response.ok) throw new Error('Login failed');

        const data = await response.json();
        this.token = data.token;
        this.currentUser = Student.fromJSON(data.user);

        localStorage.setItem('token', this.token);
        return this.currentUser;
    }

    logout() {
        this.currentUser = null;
        this.token = null;
        localStorage.removeItem('token');
    }

    isAuthenticated() {
        return !!this.token;
    }
}

export default new AuthService(); // Singleton

Import

// file: app.js
// Import default
import Student from './models/Student.js';

// Import named
import { PI, add, subtract } from './utils/math.js';
import { capitalize, truncate } from './utils/string.js';

// Import with an alias
import { add as add } from './utils/math.js';

// Import everything as a namespace
import * as MathUtils from './utils/math.js';
console.log(MathUtils.PI);
console.log(MathUtils.add(1, 2));

// Import default + named
import AuthService, { MAX_RETRY } from './services/AuthService.js';

Dynamic Import

// Lazy load a module based on a condition
async function loadAdminPanel() {
    const { AdminDashboard } = await import('./admin/AdminDashboard.js');
    const admin = new AdminDashboard();
    admin.render();
}

// Lazy load based on user action
document.getElementById('load-charts').addEventListener('click', async () => {
    const { Chart } = await import('./charts/Chart.js');
    const chart = new Chart('canvas-id');
    chart.render();
});

// Dynamic import inside an async function
async function getModule(condition) {
    if (condition) {
        const module = await import('./ModuleA.js');
        return module.default;
    } else {
        const module = await import('./ModuleB.js');
        return module.default;
    }
}

// Preload a module
const modulePromise = import('./HeavyModule.js');
// ...do some other things...
const { default: HeavyModule } = await modulePromise;

Re-export

// file: components/index.js
// Re-export everything from different modules
export { default as Button } from './Button.js';
export { default as Input } from './Input.js';
export { default as Modal } from './Modal.js';
export { default as Card } from './Card.js';

// Re-export named
export { validate, sanitize } from './utils.js';

// Re-export with a rename
export { Button as PrimaryButton } from './Button.js';

// Export everything
export * from './utils.js';
export * from './components.js';

CommonJS (CJS)

// file: utils.js (Node.js)
// CommonJS syntax - synchronous loading
const PI = 3.14159;

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

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

// Export with module.exports
module.exports = {
    PI,
    add,
    subtract
};

// Or export individually
exports.PI = PI;
exports.add = add;
exports.subtract = subtract;
// file: app.js (Node.js)
// Import with require
const math = require('./utils');
console.log(math.PI);
console.log(math.add(1, 2));

// Destructure when requiring
const { PI, add, subtract } = require('./utils');

// Import node modules
const fs = require('fs');
const path = require('path');
const express = require('express');

ES Modules vs CommonJS

// === ES Modules ===
// 1. Different syntax
import { something } from './module.js';
export const value = 42;

// 2. Asynchronous loading (in the browser)
// 3. File extension .js or .mjs
// 4. Static analysis - can be tree-shaken
// 5. Works in the browser without a bundler
// 6. Strict mode by default

// === CommonJS ===
// 1. Different syntax
const something = require('./module');
module.exports = { value: 42 };

// 2. Synchronous loading
// 3. File extension .js (Node.js context)
// 4. Dynamic - require can be used anywhere
// 5. Only runs in Node.js (without a bundler)
// 6. Not strict by default
// === Node.js configuration for ESM ===
// package.json
{
    "name": "campus-app",
    "type": "module",
    "scripts": {
        "start": "node app.js"
    }
}

// Or use the .mjs extension
// file: app.mjs
import express from 'express';
const app = express();

Real-World Example: Modular Code for a Campus Application

// =============================================
// Folder structure of a campus application
// =============================================
// src/
//   ├── modules/
//   │   ├── auth/
//   │   │   ├── AuthModule.js
//   │   │   ├── AuthService.js
//   │   │   └── AuthController.js
//   │   ├── student/
//   │   │   ├── StudentModule.js
//   │   │   ├── StudentService.js
//   │   │   └── StudentController.js
//   │   └── course/
//   │       ├── CourseModule.js
//   │       ├── CourseService.js
//   │       └── CourseController.js
//   ├── shared/
//   │   ├── Database.js
//   │   ├── Logger.js
//   │   └── Validator.js
//   └── app.js

// =============================================
// file: src/shared/Database.js
// =============================================
class Database {
    #connection;
    #config;

    constructor(config) {
        this.#config = config;
    }

    async connect() {
        this.#connection = await createConnection(this.#config);
        console.log('Database connected');
    }

    async query(sql, params = []) {
        const [rows] = await this.#connection.execute(sql, params);
        return rows;
    }

    async disconnect() {
        if (this.#connection) {
            await this.#connection.end();
        }
    }
}

export default Database;

// =============================================
// file: src/shared/Logger.js
// =============================================
class Logger {
    #context;

    constructor(context) {
        this.#context = context;
    }

    #format(level, message) {
        const timestamp = new Date().toISOString();
        return `[${timestamp}] [${level}] [${this.#context}] ${message}`;
    }

    info(message) {
        console.log(this.#format('INFO', message));
    }

    warn(message) {
        console.warn(this.#format('WARN', message));
    }

    error(message, error = null) {
        console.error(this.#format('ERROR', message));
        if (error) console.error(error.stack);
    }
}

export default Logger;

// =============================================
// file: src/shared/Validator.js
// =============================================
export function validateStudentID(studentId) {
    return /^\d{10}$/.test(studentId);
}

export function validateEmail(email) {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

export function validateGPA(gpa) {
    return typeof gpa === 'number' && gpa >= 0 && gpa <= 4;
}

export function sanitizeInput(input) {
    return input.replace(/[<>]/g, '');
}

// =============================================
// file: src/modules/auth/AuthService.js
// =============================================
import Logger from '../../shared/Logger.js';
import { validateStudentID } from '../../shared/Validator.js';

const logger = new Logger('AuthService');

export default class AuthService {
    #db;
    #currentUser;

    constructor(database) {
        this.#db = database;
        this.#currentUser = null;
    }

    async login(studentId, password) {
        if (!validateStudentID(studentId)) {
            throw new Error('Invalid StudentID');
        }

        const user = await this.#db.query(
            'SELECT * FROM student WHERE studentId = ?',
            [studentId]
        );

        if (!user.length) {
            logger.warn(`Login failed for StudentID: ${studentId}`);
            throw new Error('User not found');
        }

        const isValid = await this.#verifyPassword(password, user[0].password);
        if (!isValid) {
            throw new Error('Incorrect password');
        }

        this.#currentUser = user[0];
        logger.info(`Login successful: ${studentId}`);

        return this.#createToken(user[0]);
    }

    async #verifyPassword(input, hashed) {
        return bcrypt.compare(input, hashed);
    }

    #createToken(user) {
        return jwt.sign(
            { studentId: user.studentId, role: user.role },
            process.env.JWT_SECRET,
            { expiresIn: '24h' }
        );
    }

    isAuthenticated() {
        return !!this.#currentUser;
    }

    getCurrentUser() {
        return this.#currentUser;
    }
}

// =============================================
// file: src/modules/auth/AuthController.js
// =============================================
import AuthService from './AuthService.js';

export default class AuthController {
    #service;

    constructor(authService) {
        this.#service = authService;
    }

    async handleLogin(req, res) {
        try {
            const { studentId, password } = req.body;
            const token = await this.#service.login(studentId, password);
            res.json({ success: true, token });
        } catch (error) {
            res.status(401).json({ success: false, error: error.message });
        }
    }

    handleLogout(req, res) {
        this.#service.logout();
        res.json({ success: true });
    }
}

// =============================================
// file: src/modules/auth/AuthModule.js
// =============================================
import AuthService from './AuthService.js';
import AuthController from './AuthController.js';

export default class AuthModule {
    #service;
    #controller;

    constructor(database) {
        this.#service = new AuthService(database);
        this.#controller = new AuthController(this.#service);
    }

    get routes() {
        return {
            'POST /api/auth/login': this.#controller.handleLogin.bind(this.#controller),
            'POST /api/auth/logout': this.#controller.handleLogout.bind(this.#controller)
        };
    }

    get service() {
        return this.#service;
    }
}

// =============================================
// file: src/modules/student/StudentService.js
// =============================================
import Logger from '../../shared/Logger.js';
import { validateStudentID, validateGPA } from '../../shared/Validator.js';

const logger = new Logger('StudentService');

export default class StudentService {
    #db;

    constructor(database) {
        this.#db = database;
    }

    async getAllStudents(filters = {}) {
        let sql = 'SELECT * FROM student WHERE 1=1';
        const params = [];

        if (filters.majorCode) {
            sql += ' AND majorCode = ?';
            params.push(filters.majorCode);
        }

        if (filters.year) {
            sql += ' AND year = ?';
            params.push(filters.year);
        }

        if (filters.search) {
            sql += ' AND (name LIKE ? OR studentId LIKE ?)';
            params.push(`%${filters.search}%`, `%${filters.search}%`);
        }

        sql += ' ORDER BY name ASC';

        return this.#db.query(sql, params);
    }

    async getStudentByStudentID(studentId) {
        if (!validateStudentID(studentId)) throw new Error('Invalid StudentID');

        const students = await this.#db.query(
            'SELECT * FROM student WHERE studentId = ?',
            [studentId]
        );

        return students[0] || null;
    }

    async updateGPA(studentId, gpa) {
        if (!validateStudentID(studentId)) throw new Error('Invalid StudentID');
        if (!validateGPA(gpa)) throw new Error('Invalid GPA');

        await this.#db.query(
            'UPDATE student SET gpa = ? WHERE studentId = ?',
            [gpa, studentId]
        );

        logger.info(`GPA updated for ${studentId}: ${gpa}`);
    }

    async getStudentGrades(studentId) {
        return this.#db.query(
            `SELECT mk.name as course, n.grade, n.credits
             FROM grade n
             JOIN course mk ON n.mk_id = mk.id
             WHERE n.studentId = ?
             ORDER BY n.academic_year DESC`,
            [studentId]
        );
    }
}

// =============================================
// file: src/modules/course/CourseService.js
// =============================================
import Logger from '../../shared/Logger.js';

const logger = new Logger('CourseService');

export default class CourseService {
    #db;

    constructor(database) {
        this.#db = database;
    }

    async getAllCourses() {
        return this.#db.query(
            'SELECT * FROM course ORDER BY name ASC'
        );
    }

    async getCoursesByStudent(studentId) {
        return this.#db.query(
            `SELECT mk.* FROM course mk
             JOIN enrollment e ON mk.id = e.mk_id
             WHERE e.studentId = ?
             ORDER BY e.academic_year DESC`,
            [studentId]
        );
    }

    async enrollStudent(studentId, mkId) {
        // Check the quota
        const course = await this.#db.query(
            'SELECT kuota FROM course WHERE id = ?',
            [mkId]
        );

        const enrolled = await this.#db.query(
            'SELECT COUNT(*) as count FROM enrollment WHERE mk_id = ?',
            [mkId]
        );

        if (enrolled[0].count >= course[0].kuota) {
            throw new Error('Class quota is full');
        }

        // Check if already enrolled
        const existing = await this.#db.query(
            'SELECT id FROM enrollment WHERE studentId = ? AND mk_id = ?',
            [studentId, mkId]
        );

        if (existing.length > 0) {
            throw new Error('Already enrolled in this course');
        }

        await this.#db.query(
            'INSERT INTO enrollment (studentId, mk_id, status) VALUES (?, ?, ?)',
            [studentId, mkId, 'active']
        );

        logger.info(`Student ${studentId} enrolled in course ${mkId}`);
    }

    async dropCourse(studentId, mkId) {
        await this.#db.query(
            'UPDATE enrollment SET status = ? WHERE studentId = ? AND mk_id = ?',
            ['dropped', studentId, mkId]
        );

        logger.info(`Student ${studentId} dropped course ${mkId}`);
    }
}

// =============================================
// file: src/app.js - Main Application
// =============================================
import Database from './shared/Database.js';
import AuthModule from './modules/auth/AuthModule.js';
import StudentService from './modules/student/StudentService.js';
import CourseService from './modules/course/CourseService.js';

class CampusApp {
    #db;
    #modules;

    constructor() {
        this.#db = new Database({
            host: process.env.DB_HOST,
            user: process.env.DB_USER,
            password: process.env.DB_PASSWORD,
            database: process.env.DB_NAME
        });

        this.#modules = {};
    }

    async init() {
        await this.#db.connect();

        // Initialize the modules
        this.#modules.auth = new AuthModule(this.#db);
        this.#modules.student = new StudentService(this.#db);
        this.#modules.course = new CourseService(this.#db);

        // Setup routes
        this.setupRoutes();

        console.log('Campus app initialized');
    }

    setupRoutes() {
        const express = require('express');
        const app = express();

        app.use(express.json());

        // Auth routes
        const authRoutes = this.#modules.auth.routes;
        for (const [route, handler] of Object.entries(authRoutes)) {
            const [method, path] = route.split(' ');
            app[method.toLowerCase()](path, handler);
        }

        // Student routes
        app.get('/api/students', async (req, res) => {
            try {
                const students = await this.#modules.student.getAllStudents(req.query);
                res.json(students);
            } catch (error) {
                res.status(500).json({ error: error.message });
            }
        });

        app.get('/api/students/:studentId', async (req, res) => {
            try {
                const student = await this.#modules.student.getStudentByStudentID(req.params.studentId);
                if (!student) {
                    return res.status(404).json({ error: 'Student not found' });
                }
                res.json(student);
            } catch (error) {
                res.status(500).json({ error: error.message });
            }
        });

        // Course routes
        app.get('/api/courses', async (req, res) => {
            try {
                const courses = await this.#modules.course.getAllCourses();
                res.json(courses);
            } catch (error) {
                res.status(500).json({ error: error.message });
            }
        });

        app.post('/api/enroll', async (req, res) => {
            try {
                const { studentId, courseId } = req.body;
                await this.#modules.course.enrollStudent(studentId, courseId);
                res.json({ success: true });
            } catch (error) {
                res.status(400).json({ error: error.message });
            }
        });

        return app;
    }
}

// Run the application
const app = new CampusApp();
app.init().catch(console.error);

Example: Frontend Modules

// =============================================
// file: src/api/ApiClient.js
// =============================================
class ApiClient {
    #baseUrl;
    #headers;

    constructor(baseUrl) {
        this.#baseUrl = baseUrl;
        this.#headers = {
            'Content-Type': 'application/json'
        };
    }

    setToken(token) {
        this.#headers['Authorization'] = `Bearer ${token}`;
    }

    async #request(method, path, data = null) {
        const options = {
            method,
            headers: this.#headers
        };

        if (data) {
            options.body = JSON.stringify(data);
        }

        const response = await fetch(`${this.#baseUrl}${path}`, options);

        if (!response.ok) {
            const error = await response.json().catch(() => ({}));
            throw new Error(error.message || `HTTP ${response.status}`);
        }

        return response.json();
    }

    get(path) { return this.#request('GET', path); }
    post(path, data) { return this.#request('POST', path, data); }
    put(path, data) { return this.#request('PUT', path, data); }
    delete(path) { return this.#request('DELETE', path); }
}

export default new ApiClient('/api');

// =============================================
// file: src/components/StudentList.js
// =============================================
import ApiClient from '../api/ApiClient.js';

class StudentList {
    #container;
    #students;

    constructor(containerId) {
        this.#container = document.getElementById(containerId);
    }

    async render(filters = {}) {
        this.#students = await ApiClient.get('/students');

        let filtered = this.#students;

        if (filters.search) {
            const search = filters.search.toLowerCase();
            filtered = filtered.filter(s =>
                s.name.toLowerCase().includes(search) ||
                s.studentId.includes(filters.search)
            );
        }

        this.#container.innerHTML = `
            <div class="student-table">
                <table>
                    <thead>
                        <tr>
                            <th>StudentID</th>
                            <th>Name</th>
                            <th>Major</th>
                            <th>GPA</th>
                            <th>Action</th>
                        </tr>
                    </thead>
                    <tbody>
                        ${filtered.map(s => `
                            <tr>
                                <td>${s.studentId}</td>
                                <td>${s.name}</td>
                                <td>${s.majorCode}</td>
                                <td>${s.gpa}</td>
                                <td>
                                    <button class="btn-detail" data-studentId="${s.studentId}">
                                        Details
                                    </button>
                                </td>
                            </tr>
                        `).join('')}
                    </tbody>
                </table>
            </div>
        `;

        this.#container.querySelectorAll('.btn-detail').forEach(btn => {
            btn.addEventListener('click', () => {
                this.showDetail(btn.dataset.studentId);
            });
        });
    }

    async showDetail(studentId) {
        const student = await ApiClient.get(`/students/${studentId}`);
        // Render detail view...
    }
}

export default StudentList;

// =============================================
// file: src/app.js - Frontend Entry
// =============================================
import StudentList from './components/StudentList.js';
import ApiClient from './api/ApiClient.js';

// Setup auth
const token = localStorage.getItem('auth_token');
if (token) {
    ApiClient.setToken(token);
}

// Initialize the components
const studentList = new StudentList('student-container');
await studentList.render();

// Setup search
document.getElementById('search-input').addEventListener('input', (e) => {
    studentList.render({ search: e.target.value });
});

Code Explanation

ES Modules

  • export / export default - Export values from a module
  • import - Import values into a module
  • import() - Dynamic import (asynchronous)
  • export { x } from './module' - Re-export

CommonJS

  • module.exports / exports.x - Export values
  • require('./module') - Import values (synchronous)

Main Differences

  • ES Modules: asynchronous, static, works in browser & Node.js
  • CommonJS: synchronous, dynamic, Node.js only
  • ES Modules: strict mode by default
  • ES Modules: can be tree-shaken for bundle optimization

Best Practices

  • One default export per file for the main export
  • Named exports for utilities
  • Use index.js to re-export from a folder
  • Relative paths for local imports, bare specifiers for packages
  • Always include the .js extension in import paths

Conclusion

JavaScript Modules is the foundation for organizing modern code. With ES Modules, you can split a campus application into separate modules: an auth module, a student module, a course module. Each module has a clear responsibility and can be imported as needed. For Node.js, you can choose between CommonJS and ES Modules depending on your project's needs. What matters most is being consistent with one system and following good naming conventions.