Writing Cleaner JavaScript with Modules

Do you want to write cleaner code? This article delves into some techniques you can use to write maintainable and easy-to-understand JavaScript.

Modules are one of the most commonly used JavaScript features because most JavaScript frameworks and libraries leverage the module feature for organization and componentization. Some developers think that the import and export keywords are ReactJS features.

In this article, I will explain how to encapsulate code using modules to make your projects cleaner. Let's take a look at what encapsulation is in the next section.

Encapsulation

In programming, encapsulation refers to bundling related code in a single source. The code can include associated functions and variables inside files or related files inside a folder.

Encapsulation is used to restrict direct access to data and implementations of the bundled and related code from the code that uses them. Thus, the implementation of the functionalities is hidden, can't be manipulated by other parts of the code, and will only change when you want to change it. For example, in a blog application, when you bundle all the post properties and methods in a single unit, you only need to give the code that needs to interact with it: the name of the functions and the values they need to work.

Encapsulation is necessary because it makes your code cleaner, maintainable, and easier to understand, reuse, and test.

Next, I'll explain what modules are in JavaScript.

Modules

A module is an independent, self-contained, and detachable unit of a program. JavaScript allows you to structure large programs and codebases by creating modules of code that hold related functions and properties that can be exported in multiple other files that need these properties and functions.

Code organization is the major reason a framework is a go-to option for most developers when building medium to large applications. I'll show you how to structure your code with JavaScript modules. First, let's look at the syntax of JavaScript modules.

Exports

Using modules makes functionalities available for other modules; the export keyword makes this possible. You can make a function accessible by other modules:

export function verifyUser(email, password) {
 // do all necessary checks
 return "Successfully verified user!"
}

Note: Files that imports or export code are known as a module.

The above function is ready to be used in other modules that need the verifyUser function. This is called a named export.

Exporting Multiple Properties

Using the export keyword, you can export anything from variables to classes. To export multiple properties from a module, you need to prefix the declaration with the export keyword:

export const userName = "Lee";

export const userAge = 30;

export const user = {
 name: "Lee",
 age: 30
};

The code above exports all the declarations in the module, but you don't need to export everything in a module; you can have declarations that are only available for use inside the module:

const apiKey = "12345";

export function getApiKey() {
 return apiKey;
}

Note: A declaration is a function, class, variable, or anything declared inside a module.

The code above exports only the getApiKey function, which returns the apiKey variable declared above the function.

A cleaner way to export multiple properties is using the curly brackets notation:

// user sign in
function userSignIn() {
 console.log("User signed in");
}
// user sign out
function userSignOut() {
 console.log("User signed out");
}
// delete task
function deleteTask(id) {
 console.log(`Task ${id} deleted`);
}
//add task
function addTask(task) {
 console.log(`Task ${task.id} added`);
}
//edit task
function editTask(id, changes) {
 console.log(`Task ${id} edited`);
}
//complete task
function completeTask(id) {
 console.log(`Task ${id} completed`);
}

You can export all the functions in the block of code above:

export {
 userSignIn,
 userSignOut,
 deleteTask,
 addTask,
 editTask,
 completeTask
};

The above block of code exports all the declarations inside the curly braces and is available for imports in other modules, so now the module will look like this:

// user sign in
function userSignIn() {
 console.log("User signed in");
}
// user sign out
function userSignOut() {
 console.log("User signed out");
}
// delete task
function deleteTask(id) {
 console.log(`Task ${id} deleted`);
}
//add task
function addTask(task) {
 console.log(`Task ${task.id} added`);
}
//edit task
function editTask(id, changes) {
 console.log(`Task ${id} edited`);
}
//complete task
function completeTask(id) {
 console.log(`Task ${id} completed`);
}
export {
 userSignIn,
 userSignOut,
 deleteTask,
 addTask,
 editTask,
 completeTask
};

Note: The export {...} block of code is typically placed at the bottom of the module for readability, but you can be put it anywhere inside the module.

Default Exports

Sometimes, you might have a function over 100 lines of code and want to place it alone in a single file. To make importing it into other modules easier, you can make it a default export:

// google sign in
export default function googleSignIn() {
 // 100 lines of checking and getting details
 console.log("User signed in with Google");
}

The above function is being exported as the default from the module. I will show you how to use the import keyword in the next section.

Note: A module can have only one default export.

Import

In the previous section, you learned about using the export keyword to make properties of a module available for other modules. In this section, I'll teach you how to use the import keyword to get code from other modules.

Importing

To use code from other modules, you can import them using the import keyword:

import { userSignIn, userSignOut } from "./filePath.js";

Note: The "./filePath" is a relative path to the directory route.

The code above imports the userSignIn and userSignOut functions from the declared module. You can import one or more declarations; the only requirement is to ensure the property is defined in the module from which you are importing.

Importing default Exports

In the "Default Exports" section above, you learned how to export functions as default from modules. This section will explain how to import exported declarations as default.

You can import a default export using the following:

import googleSignIn from "./filePath.js";

The above code imports the googleSignIn function, which was exported as default in the previous section. Because a module can have only one default export, you can omit the name of the function declaration, and the above code will still work; this means you can declare the function without a name:

// google sign in
export default function () {
 // 100 lines of checking and getting details
 console.log("User signed in with Google");
}

The above code will work because it is a default export.

The only difference between importing a default and named export is the curly braces:

// default export, no braces
import googleSignIn from  "./filePath.js";

// named export, must use braces
import { userSignIn, userSignOut } from "./filePath.js";

Namespace Import

Sometimes, you have a module containing many different utility functions and want to use a single name to access them; this name is called a namespace.

For example, you have defined all user related functions in a module:

function getUserName() {
 return userName;
}
function getUserAge() {
 return userAge;
}
function getUser() {
 return user;
}
function getApiKey() {
 return apiKey;
}
function userSignIn() {
 console.log("User signed in");
}
function userSignOut() {
 console.log("User signed out");
}
export {
 getUserName,
 getUserAge,
 getUser,
 getApiKey,
 userSignIn,
 userSignOut,
};

Then, the module that will use the function will import it:

import * as userFuncs from './filePath.js';

The above code uses a special character * to import all the declarations in the module on top of the userFuncs. You can access the getUserName in the module:

import * as userFuncs from './filePath.js';

// use getUserName function
userFuncs.getUserName(); 

Renaming Declarations

To help developers avoid naming collisions, JavaScript modules use the as keyword to rename declarations.

Renaming Exports

Sometimes, you might have a declaration named login, and you're using another library that has a function named login; you can export your login function as myLogin:

function login(email, password) {
// check if your email and password are valid
 return "User logged in";  
}
// export as myLogin
export { login as myLogin };

The code above declares a login function but exports it as myLogin. You can import the function as myLogin:

import { myLogin } from "./filePath.js";

The above function imports the myLogin function. Next, I'll show you how to rename imports.

Renaming Imports

When working on a large project, you will import from multiple modules, making it easier to mix up declaration names. For example, you might be working with two different libraries, one for Twitter authentication and the other for Google authentication, and both have their own login function. To avoid naming collisions, in this case, you can import them with different names:

// import twitter login
import login as twitterLogin from 'twitter-auth';
// import google login
import login as googleLogin from 'google-auth';

The above code imports the login function of two different libraries with specific names. This way, it's easier to avoid bugs and helps other developers understand your code.

Next, I'll show you how to re-export a declaration.

Re-exporting

Although it’s not commonly used, JavaScript modules allow you to re-export a module you previously imported:

// import the login function
import { login  } from './filePath.js';
// re-export the login function
export { login };

The above code imports the login function and then re-exports it.

Now that you know how to use import and export, I'll show you how to structure applications using modules.

Structuring Code with Modules

In the previous sections, you learned how to use the import and export keywords to make code available in different and multiple modules. In this section, I'll explain the benefits of using modules and how they help in structuring your code and applications.

Reusability

Whether you're a beginner, intermediate, or advanced developer, you've probably seen the term "DRY" or "Don't Repeat Yourself" on the internet.

What this means is that most times, you can reuse functions multiple times in different parts of the code. As you have learned in the previous sections, modules make this easier because all you need to do is write the code, export it, and then use it in other modules that need the particular function.

A few of the benefits of this approach are as follows:

  • Saves time.
  • Increases the maintainability and portability of the code.
  • Increases the productivity of developers.
  • Reduces redundancy.

These are just a few benefits of reusability that using modules helps you achieve.

Composability

Composability allows you to break functionality into pieces and bring them together to form the whole function, as well as allow you to reuse the parts of the function in other parts of the application.

An example of this is when creating an addComment function, you might want to make some checks inside the function:

  • Is this user allowed to comment?
  • Remove prohibited characters like <h1></h1> from input.
  • Is this input length greater than the allowed characters?
  • Add input to the database.

Then, you can create these four different functions, for example:

// check if user if allowed to comment
export default function canComment(user) {
 // make checks here
 return user.signedIn;
}

// check if input contains html tags
export function containsHTML(input) {
 return /<[a-z][\s\S]*>/i.test(input);
}

// check if input is not longer than maxLength
export function isTooLong(input, maxLength) {
 return input.length > maxLength;
}

// add input to the database
export function addToDatabase(input) {
 console.log(`${input} added to database`);
}

The above functions can now be combined to create the addComment function:

function addComment(user, comment) {
 if (canComment(user) && !containsHTML(comment) && !isTooLong(comment)) {
    addToDatabase(comment);
    }
}

Each function that makes up the addComment function can also be used independently in other parts of the program.

The benefits of composability include the following:

  • It makes your code cleaner.
  • It makes it easier to reuse existing code.
  • It makes it easier to separate concerns.
  • It makes code easy to understand.

Isolation

Understanding the whole project can be difficult for new team members working on a large project.

Because modules allow you to build the application by composing small, focused functions, each of these functions can be created, repaired, and thought of in isolation.

Using the example in the previous section, to change the implementation to check whether the user can comment, you only need to modify the canComment function. The rest can remain unchanged.

Isolation makes it easier to understand, modify, and test your code.

Readability

Using modules in your code makes it easier to read. This is especially necessary when working on large applications, and it's almost impossible to explain to each developer on the team what you're trying to do with a function.

For example, without going into each file to see the implementation, a developer almost automatically knows what the following function does:

function addComment(user, comment) {
 if (canComment(user) && !containsHTML(comment) && !isTooLong(comment)) {
  addToDatabase(comment);
   }
}

The code above can be read as, "If the user can comment, the comment does not contain HTML, and the comment is not too long, add the comment to the database." This makes it easier for new team members to start contributing to the project, which saves time.

Organization

When using modules, organization occurs almost automatically because each part of the code is isolated.

For example, you might have all the functions used to check the type of declarations inside a typeUtils.js:

// check if input is a string
export function isString(input) {
 return typeof input === "string";
}

// check if input is a number
export function isNumber(input) {
 return typeof input === "number";
}

// check if input is an array
export function isArray(input) {
 return Array.isArray(input);
}

// check if input is an object
export function isObject(input) {
 return typeof input === "object";
}

// check if input is a function
export function isFunction(input) {
 return typeof input === "function";
}

// check if input is a boolean
export function isBoolean(input) {
 return typeof input === "boolean";
}

// check if input is null
export function isNull(input) {
 return input === null;
} 

Without giving it much thought, the above code is organized, as they are all related and independent of one another.

Conclusion

I hope you enjoyed this tutorial! Hopefully, you better understand how using modules in JavaScript can improve your code. In this article, you learned what encapsulation is, what modules are and how they function, as well as explored how export and import keywords work and how to rename declarations. Finally, you learned how modules can help structure your code.

What to do next:
  1. Sign up for a FREE Honeybadger account
    Honeybadger helps you find and fix errors before your users can even report them. Get set up in minutes and check monitoring off your to-do list.
    Get started free
  2. Get the Honeybadger newsletter
    Each month we share news, best practices, and stories from the DevOps & monitoring community—exclusively for developers like you.
    author photo

    Adebayo Adams

    Being a self taught developer himself, Adams understands that consistent learning and doing is the way to become self sufficient as a software developer. He loves learning new things and firmly believes that learning never stops.

    “We've looked at a lot of error management systems. Honeybadger is head and shoulders above the rest and somehow gets better with every new release.” 
    Michael Smith
    Try Honeybadger Free for 15 Days
    Are you using Sentry, Rollbar, Bugsnag, or Airbrake for your monitoring? Honeybadger includes error tracking with a whole suite of amazing monitoring tools — all for probably less than you're paying now. Discover why so many companies are switching to Honeybadger here.
    Try Honeybadger Free for 15 Days
    Stop digging through chat logs to find the bug-fix someone mentioned last month. Honeybadger's built-in issue tracker keeps discussion central to each error, so that if it pops up again you'll be able to pick up right where you left off.
    Try Honeybadger Free for 15 Days
    "Wow — Customers are blown away that I email them so quickly after an error."
    Chris Patton
    Try Honeybadger Free for 15 Days