Skip to main content

Command Palette

Search for a command to run...

Writing Modular Code with JavaScript

Updated
6 min read

Find a needle in the haystack. Let us try to take it literally. Suppose Mr Beast came up to you and asked you to find the needle in the Haystack as fast as possible. How would you approach it?

To me, the best option would be to make chunks of the haystack and try to find the needle in those chunks. Right? That is the general divide-and-conquer approach. We humans, cannot keep a lot of things in our minds in one go. So we try to divide our work. For making big engines, we don't just go and make a big engine. We divide the task into small parts and assemble those parts together to form the engine. Code also behaves in the same manner. If we try to find bugs in a single huge file, it would be difficult. Making modifications, tracking the flow, etc., becomes very difficult if all of the code is in a single file. Hence, we use the approach of divide and conquer here, too.

Modular Code with JavaScript

During its initial years, JavaScript was developed to write only small scripts to run some logic on the client side. However, as time passed, Node.js came into existence, and soon entire servers were being written in JavaScript. As we know, it is very difficult to maintain such a huge code base. So developers came up with a solution - 'commonJS'.

Common JS

The syntax of CommonJS was simple. We export what we want, and any other file can get its contents using our file path.

For example:

// file => root/src/operations.js
const sum = (a,b)=>a+b;
const multiply = (a,b)=>a*b;
const value = 34;

module.exports = {value,sum,multiply};
// file in root => root/index.js
const {sum} = require('./src/operations.js');

console.log(sum(2,3));

This syntax allows us to export all the important parts of a file into another.

The main idea is that we create a file that has some logic and data. This data can be exported in an object using a module.exports syntax. Then a new file can take whatever data or logic is required by it and use it. This assures that there is no forceful pollution of the file, and one can take only as much data as is needed.

This functionality was introduced for the node, but it had some issues. There were a lot of problems, like its synchronous nature, only Node.js support, slowness, etc., due to which a core language-level solution was needed. That was when JavaScript implemented modules at the language level, making it usable in any environment via the ES modules. ES modules-based import exports look a little confusing at first. This is because compared to CommonJS, it is much more flexible.

ES modules

The ES modules system consists of the import and export keywords.

We will try to mimic the above example in multiple ways to see the behaviour of each type separately.

// file => root/src/operations.js
export const sum = (a,b)=>a+b;
export const multiply = (a,b)=>a*b;
export const value = 34;

The above code is equivalent to :

// file => root/src/operations.js
const sum = (a,b)=>a+b;
const multiply = (a,b)=>a*b;
const value = 34;

export {sum,multiply,value}

This export syntax sends an object consisting of all the values and functions that we have specified.

// file in root => root/index.js
import {sum} from './src/operations.js';

console.log(sum(2,3));

Here, we could have included any or all the elements of the object, but we only took what we needed. This is the general use of the import and export syntax.

We can also do an interesting thing:

//file => root/test/importAll.js
import * as operations from "../src/operations.js"
//This reads as - import all (as a single object) as operations(name this object operations) from the given path
console.log(operations.value);
//34
console.log(operations.multiply(2,3));
//6

However, while using import and export statements, we must understand the concept of named and default exports.

Named Exports:

First, we would export the data using direct export or grouped export.

// file => root/src/getdata.js
//can also write export const getdata here
const getdata = async (url)=>{
const data = await fetch(url);
return data;
}
export {getdata};

Now, when we import the data from this file, we need to have the exact same name.

// file => root/index.js
import {getdata} from "./src/getdata.js"

const url='www.example.com';
console.log(getdata(url));

Here, we can get the correct function only if we specify the name of the function correctly. This is because there can be many elements that were exported. By specifying the name, we clarify which element we want to export. However, sometimes we might not know the name of the function exported very well. In that case, we can rely on default exports - if they were exported in that manner.

Default Exports:

Sometimes a module can define a default export. Such an export can be only 1 per file. This is typically used when the module has a major functionality, or can be used when the module has only 1 functionality. The syntax is:

// file => root/src/getData.js
//can also write export default const getdata here
const getData = async (url)=>{
const data = fetch(url);
return data;
}
export default getData;

Now, here it is not possible to define any other item as a default export. Since it is a single item, during import, we do not need to de-structure it.

// file => root/index.js
import getData from './src/getData.js'

In fact, since there is only one element to resolve, there is no confusion regarding the name. Therefore, we can choose to name the import anything. Like:

import someDataGettingFunction from './src/getData.js'

Even tho there can only be one default export per file, a file can have both named exports and a default export together. However, for clarity and maintainability, developers prefer to use named exports.

Conclusion

Modular Code is the heart of the software industry. It helps us to create separation of concerns, allow multiple developers to work independently, reuse the code, isolate errors and trace the flow of the code more easily. Even tho scalability and maintainability sound like inflated corporate terms, we should try to understand this with a little empathy. If we remove the big terms, all we are doing is dividing the huge work into small chunks, so we don't get confused. Developers are never afraid of coding. They are afraid of losing the context of what they were doing. If we think rationally, then regaining that context by mapping the flow is far easier in modular code than in a single huge 3000-line file. Now, if we look from this perspective, it is difficult to debug around 300-400 lines of code. Modern software has tens of thousands of lines of code, and modular code is the way to manage all that.