We use cookies to enhance your experience on the site
CodeWorlds

CommonJS (require/module.exports)

In the previous exercise we learned about ES Modules — JavaScript's modern module system. Now we'll dive into another, older but still very important module system: CommonJS. Although our Jurassic Park needs the latest technologies to monitor dinosaurs in the browser, its solid server-side infrastructure is built on Node.js, where CommonJS dominates.

What is CommonJS?

CommonJS is a module system created primarily with server-side environments in mind, and it has become the standard in the Node.js ecosystem. It provides a way to organize JavaScript code into separate modules, similar to ES Modules, but with different syntax and behavior.

Unlike ES Modules, which were added to JavaScript in ES6 (2015), CommonJS existed earlier and became the de facto standard for Node.js. Even now, despite official ES Modules support in Node.js, CommonJS remains the dominant format for Node.js modules and in the npm ecosystem.

CommonJS Basics

The main CommonJS concepts are:

  1. The
    require()
    function for importing modules
  2. The
    module.exports
    or
    exports
    object for exporting elements

Let's see how these work in the context of our Jurassic Park system.

Exporting Elements in CommonJS

1. Exporting by Assignment to module.exports

The simplest way to export is by assigning a value to

module.exports
:

1// dinosaur.js - Dinosaur model
2class Dinosaur {
3  constructor(name, species, diet, dangerLevel) {
4    this.name = name;
5    this.species = species;
6    this.diet = diet;
7    this.dangerLevel = dangerLevel;
8    this.health = 100;
9    this.isContained = true;
10  }
11
12  feed() {
13    this.health = Math.min(100, this.health + 20);
14    return `${this.name} has been fed. Health level: ${this.health}%`;
15  }
16
17  checkStatus() {
18    return {
19      name: this.name,
20      health: this.health,
21      isContained: this.isContained,
22      warning: this.dangerLevel > 7 ? "HIGH DANGER LEVEL" : ""
23    };
24  }
25}
26
27// Export the entire class
28module.exports = Dinosaur;

2. Exporting an Object with Multiple Elements

We can also export multiple elements by assigning an object to

module.exports
:

1// securityUtils.js - Security utility functions
2const SECURITY_LEVELS = {
3  GREEN: "normal",
4  YELLOW: "elevated",
5  ORANGE: "high",
6  RED: "critical"
7};
8
9function checkFenceStatus(sectorId) {
10  // Simulate checking fence status in a given sector
11  const status = Math.random() > 0.1 ? "active" : "failure";
12  return {
13    sector: sectorId,
14    status,
15    timestamp: new Date().toISOString()
16  };
17}
18
19function triggerAlarm(sector, reason) {
20  console.log(`ALARM! Sector ${sector}: ${reason}`);
21  // Alarm activation logic
22  return {
23    success: true,
24    alarmId: "ALR-" + Math.floor(Math.random() * 10000)
25  };
26}
27
28function getSecurityStatus() {
29  // Returns overall park security status
30  return {
31    level: SECURITY_LEVELS.GREEN,
32    activeAlarms: 0,
33    lastChecked: new Date().toISOString()
34  };
35}
36
37// Export multiple elements as an object
38module.exports = {
39  SECURITY_LEVELS,
40  checkFenceStatus,
41  triggerAlarm,
42  getSecurityStatus
43};

3. Incrementally Adding to exports

We can also add exports one by one using the

exports
object (which is a reference to
module.exports
):

1// parkStats.js - Jurassic Park statistics
2let totalVisitors = 0;
3let revenue = 0;
4const incidents = [];
5
6// Export first function
7exports.addVisitors = function(count) {
8  totalVisitors += count;
9  return totalVisitors;
10};
11
12// Export second function
13exports.addRevenue = function(amount) {
14  revenue += amount;
15  return revenue;
16};
17
18// Export third function
19exports.recordIncident = function(type, location, description) {
20  const incident = {
21    id: incidents.length + 1,
22    type,
23    location,
24    description,
25    timestamp: new Date().toISOString()
26  };
27  incidents.push(incident);
28  return incident;
29};
30
31// Export fourth function
32exports.getDailyStats = function() {
33  return {
34    totalVisitors,
35    revenue,
36    incidentCount: incidents.length
37  };
38};

NOTE: Remember that

exports
is just a reference to
module.exports
. If you assign something directly to
module.exports
, you lose that reference, so you cannot mix these styles in one file:

1// This will NOT work
2exports.someFunction = function() { /* ... */ };
3module.exports = SomeClass; // This overwrites the previous export
4
5// This will work
6exports.someFunction = function() { /* ... */ };
7exports.SomeClass = SomeClass;
8
9// This will work too
10module.exports.someFunction = function() { /* ... */ };
11module.exports.SomeClass = SomeClass;
12
13// And this will work (assigning the whole object)
14module.exports = {
15  someFunction: function() { /* ... */ },
16  SomeClass: SomeClass
17};

Importing Modules in CommonJS

1. Importing an Entire Module

1// app.js
2const Dinosaur = require('./dinosaur.js');
3
4// Create new dinosaur instances
5const trex = new Dinosaur("Rex", "Tyrannosaurus", "carnivore", 9);
6const brachi = new Dinosaur("Longneck", "Brachiosaurus", "herbivore", 4);
7
8console.log(trex.checkStatus());
9console.log(brachi.feed());

2. Importing and Destructuring

1// securityMonitor.js
2const security = require('./securityUtils.js');
3
4// Use imported functions and constants from the object
5function monitorSecurity() {
6  const sectors = ["A1", "B2", "C3", "D4"];
7
8  // Check each sector
9  sectors.forEach(sector => {
10    const fenceStatus = security.checkFenceStatus(sector);
11
12    if (fenceStatus.status === "failure") {
13      const alarmResult = security.triggerAlarm(
14        sector,
15        "Electric fence failure!"
16      );
17      console.log(`Alarm ${alarmResult.alarmId} triggered in sector ${sector}`);
18
19      // Update security status
20      console.log("Changing security level to:", security.SECURITY_LEVELS.RED);
21    }
22  });
23}
24
25setInterval(monitorSecurity, 60000); // Check every minute

3. Destructuring on Import

In newer Node.js versions we can also use destructuring directly on import:

1// dashboard.js
2const { checkFenceStatus, triggerAlarm, SECURITY_LEVELS } = require('./securityUtils.js');
3
4// Now we can use functions directly
5function checkAllSectors() {
6  ["A1", "B2", "C3", "D4"].forEach(sector => {
7    console.log(`Checking sector ${sector}: `, checkFenceStatus(sector));
8
9    // Use the imported constant
10    if (Math.random() < 0.1) {
11      console.log(`Setting security level: ${SECURITY_LEVELS.ORANGE}`);
12    }
13  });
14}

Differences Between CommonJS and ES Modules

It's important to understand the key differences between CommonJS and ES Modules:

| Feature | CommonJS | ES Modules | |---------|----------|------------| | Import syntax |

require()
|
import ... from ...
| | Export syntax |
module.exports = ...
or
exports.x = ...
|
export ...
or
export default ...
| | Loading | Synchronous | Asynchronous (loaded before execution) | | Execution | Code executed on each import | Code executed only once, then module is cached | | Static analysis | Not supported (dynamic paths) | Supported (paths are static) | | Browser support | Not natively supported | Supported in modern browsers | | Hoisting | Imports are not hoisted | Imports are hoisted (usable before declaration) | | Tree-shaking | Harder to implement | Easier due to static analysis |

Node.js Project Structure (Jurassic Park Backend)

Let's look at how we might organize the backend of our Jurassic Park management system using Node.js and CommonJS:

1jurassic-park-backend/
2├── config/
3│   ├── database.js     # Database configuration
4│   ├── environment.js  # Environment variables
5│   └── security.js     # Security settings
6├── models/
7│   ├── dinosaur.js     # Dinosaur model
8│   ├── employee.js     # Employee model
9│   ├── facility.js     # Facility model
10│   └── index.js        # Model aggregator
11├── controllers/
12│   ├── dinosaurController.js  # Dinosaur controller
13│   ├── securityController.js  # Security controller
14│   └── employeeController.js  # Employee controller
15├── routes/
16│   ├── api.js          # Main API router
17│   ├── dinosaurs.js    # Dinosaur-related routes
18│   └── security.js     # Security-related routes
19├── services/
20│   ├── monitoringService.js   # Monitoring service
21│   ├── notificationService.js # Notification service
22│   └── loggingService.js      # Logging service
23├── utils/
24│   ├── logger.js       # Logging utility
25│   └── validators.js   # Data validators
26├── middleware/
27│   ├── auth.js         # Authentication middleware
28│   └── errorHandler.js # Error handling
29├── app.js              # Main application
30└── server.js           # Server startup file

Let's look at example implementations of these modules:

Configuration File (config/database.js)

1// config/database.js
2module.exports = {
3  host: process.env.DB_HOST || 'localhost',
4  port: process.env.DB_PORT || 27017,
5  name: process.env.DB_NAME || 'jurassic_park',
6  user: process.env.DB_USER || 'admin',
7  password: process.env.DB_PASSWORD || 'secret',
8
9  // Helper function to build the connection URI
10  getConnectionString: function() {
11    return `mongodb://${this.user}:${this.password}@${this.host}:${this.port}/${this.name}`;
12  }
13};

Model (models/dinosaur.js)

1// models/dinosaur.js
2const mongoose = require('mongoose');
3const Schema = mongoose.Schema;
4
5// Dinosaur schema
6const dinosaurSchema = new Schema({
7  name: { type: String, required: true },
8  species: { type: String, required: true },
9  diet: { type: String, enum: ['carnivore', 'herbivore', 'omnivore'], required: true },
10  dangerLevel: { type: Number, min: 1, max: 10, required: true },
11  enclosureId: { type: Schema.Types.ObjectId, ref: 'Enclosure' },
12  health: { type: Number, default: 100 },
13  status: {
14    isContained: { type: Boolean, default: true },
15    lastCheck: { type: Date, default: Date.now }
16  },
17  createdAt: { type: Date, default: Date.now },
18  updatedAt: { type: Date, default: Date.now }
19});
20
21// Schema methods
22dinosaurSchema.methods.feed = function() {
23  this.health = Math.min(100, this.health + 20);
24  this.updatedAt = Date.now();
25  return this.save();
26};
27
28dinosaurSchema.methods.checkStatus = function() {
29  return {
30    name: this.name,
31    health: this.health,
32    isContained: this.status.isContained,
33    warning: this.dangerLevel > 7 ? "HIGH DANGER LEVEL" : ""
34  };
35};
36
37// Static methods
38dinosaurSchema.statics.findDangerous = function() {
39  return this.find({ dangerLevel: { $gte: 8 } });
40};
41
42// Pre-save hook
43dinosaurSchema.pre('save', function(next) {
44  this.updatedAt = Date.now();
45  next();
46});
47
48// Create the model
49const Dinosaur = mongoose.model('Dinosaur', dinosaurSchema);
50
51// Export the model
52module.exports = Dinosaur;

Controller (controllers/dinosaurController.js)

1// controllers/dinosaurController.js
2const Dinosaur = require('../models/dinosaur');
3const logger = require('../utils/logger');
4
5// Controller object with multiple methods
6const dinosaurController = {
7  // Get all dinosaurs
8  getAllDinosaurs: async (req, res) => {
9    try {
10      const dinosaurs = await Dinosaur.find().populate('enclosureId');
11      res.json(dinosaurs);
12    } catch (error) {
13      logger.error('Error fetching dinosaurs:', error);
14      res.status(500).json({ error: 'Server error' });
15    }
16  },
17
18  // Get dinosaur by ID
19  getDinosaurById: async (req, res) => {
20    try {
21      const dinosaur = await Dinosaur.findById(req.params.id).populate('enclosureId');
22
23      if (!dinosaur) {
24        return res.status(404).json({ error: 'Dinosaur not found' });
25      }
26
27      res.json(dinosaur);
28    } catch (error) {
29      logger.error(`Error fetching dinosaur ID=${req.params.id}: `, error);
30      res.status(500).json({ error: 'Server error' });
31    }
32  },
33
34  // Add a new dinosaur
35  createDinosaur: async (req, res) => {
36    try {
37      const { name, species, diet, dangerLevel, enclosureId } = req.body;
38
39      const newDinosaur = new Dinosaur({
40        name,
41        species,
42        diet,
43        dangerLevel,
44        enclosureId
45      });
46
47      await newDinosaur.save();
48      logger.info(`Added new dinosaur: ${name} (${species})`);
49
50      res.status(201).json(newDinosaur);
51    } catch (error) {
52      logger.error('Error creating dinosaur:', error);
53      res.status(400).json({ error: error.message });
54    }
55  },
56
57  // Feed a dinosaur
58  feedDinosaur: async (req, res) => {
59    try {
60      const dinosaur = await Dinosaur.findById(req.params.id);
61
62      if (!dinosaur) {
63        return res.status(404).json({ error: 'Dinosaur not found' });
64      }
65
66      await dinosaur.feed();
67      logger.info(`Fed dinosaur: ${dinosaur.name}`);
68
69      res.json({
70        message: `${dinosaur.name} has been fed. Health level: ${dinosaur.health}%`,
71        health: dinosaur.health
72      });
73    } catch (error) {
74      logger.error(`Error feeding dinosaur ID=${req.params.id}: `, error);
75      res.status(500).json({ error: 'Server error' });
76    }
77  },
78
79  // Find dangerous dinosaurs
80  getDangerousDinosaurs: async (req, res) => {
81    try {
82      const dangerousDinos = await Dinosaur.findDangerous();
83      res.json(dangerousDinos);
84    } catch (error) {
85      logger.error('Error fetching dangerous dinosaurs:', error);
86      res.status(500).json({ error: 'Server error' });
87    }
88  }
89};
90
91// Export the entire controller object
92module.exports = dinosaurController;

Routes (routes/dinosaurs.js)

1// routes/dinosaurs.js
2const express = require('express');
3const router = express.Router();
4const dinosaurController = require('../controllers/dinosaurController');
5const authMiddleware = require('../middleware/auth');
6
7// Dinosaur-related routes
8router.get('/', dinosaurController.getAllDinosaurs);
9router.get('/dangerous', dinosaurController.getDangerousDinosaurs);
10router.get('/:id', dinosaurController.getDinosaurById);
11
12// Routes requiring authentication
13router.post('/', authMiddleware.verifyToken, dinosaurController.createDinosaur);
14router.post('/:id/feed', authMiddleware.verifyToken, dinosaurController.feedDinosaur);
15
16// Export the router
17module.exports = router;

Main Application File (app.js)

1// app.js
2const express = require('express');
3const mongoose = require('mongoose');
4const cors = require('cors');
5const morgan = require('morgan');
6const helmet = require('helmet');
7
8// Config and middleware imports
9const dbConfig = require('./config/database');
10const errorHandler = require('./middleware/errorHandler');
11const logger = require('./utils/logger');
12
13// Route imports
14const apiRouter = require('./routes/api');
15const dinosaurRoutes = require('./routes/dinosaurs');
16const securityRoutes = require('./routes/security');
17
18// Initialize Express app
19const app = express();
20
21// Middleware
22app.use(helmet()); // Security
23app.use(cors()); // Cross-Origin Resource Sharing
24app.use(morgan('dev')); // HTTP request logging
25app.use(express.json()); // JSON parsing
26app.use(express.urlencoded({ extended: false })); // Form parsing
27
28// Connect to database
29mongoose.connect(dbConfig.getConnectionString(), {
30  useNewUrlParser: true,
31  useUnifiedTopology: true
32})
33.then(() => {
34  logger.info('Connected to MongoDB database');
35})
36.catch(err => {
37  logger.error('Database connection error:', err);
38  process.exit(1);
39});
40
41// Routes
42app.use('/api', apiRouter);
43app.use('/api/dinosaurs', dinosaurRoutes);
44app.use('/api/security', securityRoutes);
45
46// Handle 404 errors
47app.use((req, res, next) => {
48  res.status(404).json({ error: 'Not found' });
49});
50
51// Centralized error handling
52app.use(errorHandler);
53
54// Export the application
55module.exports = app;

Server Startup File (server.js)

1// server.js
2const http = require('http');
3const app = require('./app');
4const logger = require('./utils/logger');
5
6// Port settings
7const port = process.env.PORT || 3000;
8app.set('port', port);
9
10// Create HTTP server
11const server = http.createServer(app);
12
13// Listen for connections
14server.listen(port, () => {
15  logger.info(`Jurassic Park server started on port ${port}`);
16});
17
18// Handle server errors
19server.on('error', (error) => {
20  if (error.syscall !== 'listen') {
21    throw error;
22  }
23
24  logger.error(`Server error: ${error}`);
25  process.exit(1);
26});

Circular Dependencies

One challenge when working with CommonJS (and modules in general) is circular dependencies, which occur when module A imports module B and module B imports module A:

1// dinosaurUtils.js
2const Dinosaur = require('./dinosaur');
3
4function isDangerous(dino) {
5  return dino.dangerLevel > 7;
6}
7
8module.exports = { isDangerous };
9
10// dinosaur.js
11const dinosaurUtils = require('./dinosaurUtils'); // Circular dependency!
12
13class Dinosaur {
14  // ... class implementation
15
16  isHighRisk() {
17    return dinosaurUtils.isDangerous(this);
18  }
19}
20
21module.exports = Dinosaur;

CommonJS handles circular dependencies, but it can lead to strange behavior like incomplete objects. To avoid such problems:

  1. Restructure the code to avoid circular dependencies
  2. Use the dependency injection pattern
  3. Move shared code to a third module that both import from

Transpiling CommonJS to ES Modules and Back

In many projects we need to handle both CommonJS and ES Modules. For example, the frontend of our Jurassic Park might use ES Modules while the backend uses CommonJS. What do we do when we want to share code?

Babel for Transpilation

We can use Babel to transpile between formats:

1// babel.config.js for ES Modules -> CommonJS
2module.exports = {
3  presets: [
4    ['@babel/preset-env', {
5      targets: {
6        node: 'current'
7      },
8      modules: 'commonjs' // Convert import/export to require/exports
9    }]
10  ]
11};

Dual Package Hazard

Another approach is to create packages that support both module formats. In package.json you can define:

1{
2  "name": "jurassic-park-utils",
3  "version": "1.0.0",
4  "main": "dist/index.js",
5  "module": "dist/index.mjs",
6  "exports": {
7    "import": "./dist/index.mjs",
8    "require": "./dist/index.js"
9  }
10}

Modern Approach: Node.js with ES Modules

Newer versions of Node.js (from 13.2.0) officially support ES Modules. We can enable this support in a few ways:

  1. Using the
    .mjs
    extension for ES Module files
  2. Setting
    "type": "module"
    in package.json

However, many npm packages still use CommonJS, so you'll likely need to work with both systems.

Practical Example: Migrating from CommonJS to ES Modules

Let's say we want to migrate part of our backend from CommonJS to ES Modules. Here's how such a migration might look:

Original CommonJS Version (dinosaurController.js)

1// controllers/dinosaurController.js (CommonJS)
2const Dinosaur = require('../models/dinosaur');
3const logger = require('../utils/logger');
4
5function getAllDinosaurs(req, res) {
6  // implementation
7}
8
9function getDinosaurById(req, res) {
10  // implementation
11}
12
13module.exports = {
14  getAllDinosaurs,
15  getDinosaurById
16};

Migration to ES Modules (dinosaurController.mjs)

1// controllers/dinosaurController.mjs (ES Modules)
2import Dinosaur from '../models/dinosaur.mjs';
3import logger from '../utils/logger.mjs';
4
5export function getAllDinosaurs(req, res) {
6  // implementation
7}
8
9export function getDinosaurById(req, res) {
10  // implementation
11}
12
13// We can also export a default object with functions
14export default {
15  getAllDinosaurs,
16  getDinosaurById
17};

Compatibility Wrapper

Sometimes we need to create a wrapper that allows importing an ES Module from CommonJS code:

1// controllers/dinosaurControllerWrapper.js (CommonJS)
2// This file loads an ES Module and exports it as CommonJS
3const importESM = async () => {
4  const controller = await import('./dinosaurController.mjs');
5  return controller.default; // or selected functions
6};
7
8// Export a function that returns a Promise with the controller
9module.exports = importESM();

Summary

CommonJS is a widely used module system in Node.js that allows organizing code into separate modules, which is critical for building scalable applications like the backend of our Jurassic Park management system.

The main CommonJS concepts are:

  • The
    require()
    function for importing modules
  • The
    module.exports
    or
    exports
    object for exporting elements
  • Synchronous module loading
  • Essential module caching for performance

Despite the growing popularity of ES Modules, CommonJS remains an important module system, especially in the Node.js ecosystem. Understanding the differences between CommonJS and ES Modules is key to effectively developing JavaScript applications that can run both in the browser and on the server.

In our Jurassic Park, we use CommonJS for the backend (monitoring, database, security control) and ES Modules for the frontend (user interface, dashboards, notifications). This combination gives us the best of both worlds and lets us efficiently manage a system that must be reliable — after all, the safety of the park and its guests depends on our code!

In the next exercise we'll look at dynamic module importing, which provides even greater flexibility in loading code on demand, which can be critical in emergency situations at Jurassic Park.

Go to CodeWorlds