DEV Community

Cover image for Must-Have Practical Tutorial on Node.js Modules for Badass Beginners
Sandor | tutorialhell.dev
Sandor | tutorialhell.dev

Posted on

Must-Have Practical Tutorial on Node.js Modules for Badass Beginners

It's a practical tutorial that will provide you with everything you need to understand how require() works and how to use it efficiently. It includes detailed instructions and code examples that you can run either locally on your computer or in a prepared online Node environment to quickly get the hang of it. And there will be practical exercises waiting for you at the end of this tutorial.

It's part of my tutorialhell.dev project, aimed at helping beginners learn easily without getting stuck.

In Node.js, a module is a self-contained block of code that doesn't affect other code. JavaScript files are treated as modules in Node.js, where you can export functions, objects, or values from one file and use them in another.

Node.js supports two module systems: CommonJS modules and ECMAScript modules.

  • The CommonJS modules were the original method for packaging JavaScript code in Node.js.
  • The ECMAScript modules standard is utilized by browsers and other JavaScript runtimes and is generally viewed as the preferred approach for being future-proof. However, to get started, we will focus on CommonJS modules since they are still widely used. As we progress, I will introduce you to ECMAScript modules in the upcoming tutorials.

Let's break down the basics, starting with how to create a module in Node.js, how to export from it, and how to use the exported entities in other files.

If you don't have Node.js set up, you can get started using a ready-made Node.js environment in this sandbox.

Creating a Simple Module

Create a File: Every module is a JavaScript file. Create a file named 'myModule.js'.

Write Some Code: Inside 'myModule.js', define a function that you want to make available outside this file.

function sayHello(name) {
  return `Hello, ${name}!`;
}
Enter fullscreen mode Exit fullscreen mode

Exporting from a Module

To make functions, objects, or values available outside the module where they are defined, you need to export them using module.exports.

module.exports is an object included in every JavaScript file in Node.js by default. It is used to export functions, objects, or values from a module. Here is the full code:

function sayHello(name) {
  return `Hello, ${name}!`;
}
module.exports = sayHello;
Enter fullscreen mode Exit fullscreen mode

Importing a Module

To use the exported entities from a module in another file, you need to require that module.

Create Another File: 'index.js'. (It's already there if you are using the sandbox. Simply replace the contents with the code provided below.)

Require the Module: Use the require() function to import the module, passing the path to the module file as an argument:

const sayHello = require('./myModule');
console.log(sayHello('World')); // Outputs: Hello, World!
Enter fullscreen mode Exit fullscreen mode

Explanation:

require('./myModule'): This line imports the module myModule.js. The ./ indicates that the file is located in the same directory as index.js. Node.js reads myModule.js, executes it, and then module.exports from myModule.js is returned by require() and assigned to the variable sayHello in index.js.

sayHello('World'): This line calls the imported function with the argument 'World', and the function returns the string 'Hello, World!'.

Multiple exports

To perform multiple exports from a Node.js module, you can use either the module.exports object or the exports shorthand. Each approach allows you to export multiple items, such as functions, objects, or values, from a single module. Here's how you can do it:

  1. Using module.exports

You can directly assign an object to module.exports containing all the items you want to export. This approach is useful when you want to structure your exports clearly in one place. Create a 'multipleExport1.js' and paste the following code there:

function func1() {
  console.log('Function 1');
}

function func2() {
  console.log('Function 2');
}

module.exports = {
  func1,
  func2,
};
Enter fullscreen mode Exit fullscreen mode
  1. Using exports

The exports shorthand allows you to attach exports directly to the exports object. This is syntactically more concise for exporting multiple items. Create a 'multipleExport2.js' and paste the following code there:

exports.func3 = function () {
  console.log('Function 3');
};

exports.func4 = function () {
  console.log('Function 4');
};
Enter fullscreen mode Exit fullscreen mode

Multiple imports

Similarly, you would import these in another file like this:

const myModule = require('./multipleExport1');
const myModule2 = require('./multipleExport2');
Enter fullscreen mode Exit fullscreen mode

Or, using destructuring:

const { func1, func2 } = require('./multipleExport1');
const { func3, func4 } = require('./multipleExport2');
Enter fullscreen mode Exit fullscreen mode

Replace the contents of the 'index.js' file with this code:

const sayHello = require('./myModule');
console.log(sayHello('World')); // Outputs: Hello, World!

// Approach 1
console.log('------- Approach 1 -------');
const myModule = require('./multipleExport1');
const myModule2 = require('./multipleExport2');

myModule.func1(); // Calls 'Function 1'
myModule.func2(); // Calls 'Function 2'

myModule2.func3(); // Calls 'Function 3'
myModule2.func4(); // Calls 'Function 4'

// Approach 2
console.log('------- Approach 2 -------');
const { func1, func2 } = require('./multipleExport1');
const { func3, func4 } = require('./multipleExport2');

func1(); // Calls 'Function 1'
func2(); // Calls 'Function 2'
func3(); // Calls 'Function 3'
func4(); // Calls 'Function 4'
Enter fullscreen mode Exit fullscreen mode

And you should see the following logged in your terminal:

Node.js modules

You might have noticed that when functions are imported through destructuring, they are assigned unique identifiers like func1, func2, etc. This uniqueness is necessary because two functions with the same name cannot coexist in the same file (module). It requires us to use unique names in different modules, which is not convenient.

However, there is a technique to rename these functions while importing them. This allows functions to have identical names across multiple modules and be renamed when imported into the same file to avoid name collisions. Here is how you do it:

Create a new file named 'multipleExport3.js' and paste the following code there:

exports.func3 = function () {
  console.log('Function 3 with an identical name from a different module');
};

exports.func4 = function () {
  console.log('Function 4 with an identical name from a different module');
};
Enter fullscreen mode Exit fullscreen mode

Then add this code to the bottom of your 'index.js' file:

const {
  func3: renamedFunc3,
  func4: renamedFunc4,
} = require('./multipleExport3');
console.log('------- Destructuring with Renaming -------');
renamedFunc3();
renamedFunc4();
Enter fullscreen mode Exit fullscreen mode

And your updated terminal output will look like this:

Destructuring with Renaming

👉 Working code you can find here.

Choosing Between module.exports and exports

Use module.exports when you want to export a single item, like a function or an object, or when you prefer to define all exports at once in an object.

Use exports to attach multiple exports individually. This can be more straightforward when exporting several items but remember that you cannot assign directly to exports as it will break the reference to module.exports. For example, doing exports = { func1, func2 } will not work as expected.

Both methods are commonly used and choosing between them often comes down to preference and the specific needs of your module.

Here’s a breakdown of the key feature that every beginner should grasp:

require() is Synchronous and Caches Modules

Synchronous Loading: When you use require() to import a module, Node.js reads and executes the entire module file synchronously. This means that Node.js will halt the execution of your code until the entire module is loaded. This behaviour is particularly important to understand because it affects how and where you use require() in your application. Here's a simple code example to demonstrate that require() is synchronous in Node.js, meaning Node.js waits for the module to be fully loaded and executed before moving on to the next line of code.

Or try it out yourself by pasting the following code in the sandbox.

Create a file named 'greet.js' and paste the following code there:

console.log('Loading greet.js...');

function greet() {
  console.log(
    'Hello and welcome to learning web development with www.tutorialhell.dev!'
  );
}

console.log('greet.js loaded successfully.');

module.exports = greet;
Enter fullscreen mode Exit fullscreen mode

Create or update (it's already there in the sandbox) the 'index.js' file and paste the following code there:

console.log('Starting index.js...');

const greet = require('./greet'); // Synchronously loads and executes "greet.js"

console.log('greet.js has been required.');

greet();

console.log('Ending index.js...');
Enter fullscreen mode Exit fullscreen mode

How it works:
When you run 'index.js' using the command node index.js, you will see the logs in the following order, demonstrating the synchronous nature of require():

Demonstrating the synchronous nature of require()

Notice how the execution of 'index.js' pauses until 'greet.js' is fully loaded and executed (evidenced by the logs from 'greet.js' appearing before the continuation of 'index.js').

In simple terms, synchronous means doing things one by one in order, where you have to finish one thing before you can start the next thing.

Caching Mechanism: Node.js caches the first instance of each module you require. If you require() the same module multiple times in different parts of your application, Node.js will not reload or re-execute the module file after the first time it's loaded. Instead, it reuses the cached version. This caching mechanism improves performance by avoiding the overhead of loading and compiling the module's code multiple times. However, it also means that any state maintained in the module is shared across all parts of your application that import it.

In simple terms, caching means storing something in a place where it can be quickly accessed later. It's like saving a shortcut to find information faster without having to go through the whole search process again.

Why It Matters

Performance Implications: Knowing that require() is synchronous helps in structuring your code for optimal performance, especially during the startup phase of an application. It encourages the practice of requiring modules at the beginning of files or in sections of the code where blocking the event loop has minimal impact on the application's responsiveness.

Understanding JavaScript's synchronous, single-threaded nature is key in Node.js to avoid blocking code. Node.js uses techniques like callbacks, promises, and async/await to enable asynchronous operations, preventing the event loop from being blocked. These methods allow Node.js applications to remain efficient and responsive, handling tasks in the background without slowing down the main execution flow.

Single-threaded means that a program or environment, like JavaScript in Node.js, can only execute one operation at a time on a single path of execution. Imagine it as a single-lane road where cars (operations) must go one after another, not side by side. This setup ensures that operations are carried out in sequence, but it also means that one operation must complete before the next one starts, preventing simultaneous execution of multiple operations.

Module State Sharing: Awareness of the caching mechanism is important when designing modules. Since the same instance of a module is shared wherever it's required, any state stored in the module is also shared. This can be leveraged to maintain application-wide state but requires careful management to avoid unintended side effects.

In programming, "state" refers to the current condition or data held by an application or a module at any given time. It's like remembering the score while playing a game; just as the score tells you how well you're doing at that moment, the state in programming keeps track of information like what's been clicked, entered into forms, or saved temporarily as the program runs. This state can change based on what actions are taken within the application, just like the game score changes when you score points or lose points.

It's important to distinguish this concept of "state" from a database (DB). While a database can store data that might represent part of an application's state, such as user profiles or application settings, the "state" in the context of programming generally refers to the in-memory condition of an application at a specific moment. This includes data that the application is currently working with but hasn't necessarily saved to a database.

Debugging: Understanding how require() works can aid in debugging issues related to module loading, such as why changes to a module don't seem to take effect (due to caching) or why an application may be blocking at startup (due to synchronous loading of modules).

Practical Exercises

To reinforce your understanding of the require() functionality and the caching mechanism, I recommend practising. Here is the Node.js sandbox, in case you need it..

Objective:

Your task is to create a simple Node.js application that demonstrates how the state is shared and managed across modules due to Node.js's caching mechanism. You will create a shared state module and two additional modules that modify and display the shared state, respectively.

Instructions:

1. Create the Shared State Module:

  • File Name: 'counterState.js'

  • Task: This module will maintain a counter. Write two functions within this module: one named incrementCounter to increment the counter and another named getCounter to retrieve the counter value. Export both functions.

The counter code will look something like this:

let count = 0;

function incrementCounter() {
  count++;
}

function getCounter() {
  return count;
}

module.exports = { incrementCounter, getCounter };
Enter fullscreen mode Exit fullscreen mode

2. Create the First Additional Module:

  • File Name: 'incrementModule.js'

  • Task: This module will require 'counterState.js' and use its incrementCounter function to increment the counter, and the getCounter function to display the counter's current value. It should then log a message showing the new counter value. Export a function named increment that performs these tasks. You need to write this code yourself.

3. Create the Second Additional Module:

  • File Name: 'displayModule.js'

  • Task: This module will also require 'counterState.js' but will only use the getCounter function to display the current value of the counter. Export a function named display that logs the counter's current value. You need to write this code yourself.

4. Create the Entry Point File:

  • File Name: 'index.js'

  • Task: In this file, you will require both 'incrementModule.js' and 'displayModule.js'. Use their exported functions to increment the counter and display its value, demonstrating the shared state across modules. You need to write this code yourself.

  • Instructions: Increment the counter, display its value, and then increment it again to show the shared and persistent state. Then display the value again to see how it updates in the other file (module) that requires the 'counterState.js' module.

The working code should log the following:

State Management Demo

By completing this homework, you will understand how Node.js caches imported modules, allowing shared state between different parts of your application. This knowledge is fundamental for efficient Node.js development, especially for applications relying on shared data across modules.

Here are the solutions to the above practical exercises.

Transferable Knowledge and Patterns

  • Understanding of Synchronous Operations: Recognising that require() operates synchronously, halting execution until a module is fully loaded, teaches the general principle of synchronous execution in programming. This concept is critical for understanding how different parts of a program can affect execution flow and performance, applicable in various programming languages and environments.

  • Importance of Caching for Performance: The caching mechanism of require() illustrates a fundamental performance optimisation technique used widely in computing. Caching, or temporarily storing data for quick access upon repeated requests, is a concept that can be applied in numerous scenarios beyond module loading, such as web content delivery, database queries, and more.

  • Module State Sharing and Global State Management: The idea that a module's state is shared across all parts of an application that import it underscores the broader concept of global state management. This is a key consideration in software design, relevant for understanding how state is managed across an application.

  • Performance Optimisation Practices: The advice to require modules at the beginning of files or in strategic code sections to minimise performance impact teaches a broader lesson on resource management and optimisation in software development. Prioritising resource-intensive operations and understanding their impact on the application's responsiveness and efficiency are valuable skills.

  • Debugging and Troubleshooting: The emphasis on how understanding require()'s behaviour aids in debugging extends to a general principle in programming: knowing how your tools and frameworks work under the hood can significantly improve your ability to troubleshoot and solve problems. This is a critical skill for any developer, as it applies to debugging in all programming languages and environments.

That said, understanding how to import modules in Node.js opens up a wealth of critical knowledge that you will learn once and use everywhere. It shows you how to avoid getting stuck. 

You are more than welcome to explore these topics independently because it’s the best way to develop problem-solving skills. But keep in mind that we will gradually cover all these topics through my tutorials because that’s why tutorialhell.dev was created.

Learning path from here:

The concepts mentioned in the examples, such as function definitions, template literals, and destructuring, are frequently used, and thus I strongly encourage you to engage in some self-learning as well. Yes, to develop problem-solving skills. I cannot emphasize this enough.

  • Introduction to Objects: Learn about JavaScript objects, including creation, access, methods, property shorthand and computed properties.

  • Function Definitions: Explore different ways to define functions, including function declarations, function expressions, and arrow functions.

  • Template Literals: Learn about string interpolation and multi-line strings in ES6, using backticks to define template literals and ${} for embedding expressions.

  • Destructuring: Understand the destructuring assignment syntax for arrays and objects.

  • File Paths: Learn about relative and absolute file paths to grasp how to navigate and reference files within your projects effectively.


If you like this content, consider following me and subscribing to receive these cool tutorials as soon as they are ready.

The sunfish on the post cover was sourced from the O'Rly Generator.

Top comments (0)