DEV Community

Andrew S.
Andrew S.

Posted on

1

Modules in JavaScript, import/export

Overview

Every programme gets big over time. The more code there is in a project, the harder it is to navigate, write and maintain.

Modules help to organise the code in such a way that it is easier and more convenient to work with the project, allow you to reuse the code and structure the program.

A module is a separate part of the system. As a rule, one module is one file. Modules can be perceived as parts of a constructor, from which a programme is assembled.

Everything is hidden in the module by default. Only what the developers intentionally want to provide - exports is provided outside. This functionality can be imported into other modules and used.

Thus, a module is a black box with one input (import) and one output (export) through which this module "communicates" with others.

Modules can be compared to functions: they solve the same problems, but on a larger scale. Functions help to break a large task into smaller ones, while modules help to break a large project into smaller parts. Functions help to structure the architecture of solving a specific task, while modules help to structure the architecture of the whole project.

Benefits of the Modules

The idea and usefulness of modules is primarily to help deal with the complexity of the programme. If we are working with a project of 20 thousand lines, it would be quite inconvenient to store all the code in one file.

Without modules With modules
Navigating and moving around the project: Difficult. To work on two parts of the code, you will have to either open the file twice or jump between sections of the file. Simple. Modules can be opened in parallel and worked on simultaneously.
Full project coverage: Difficult, often impossible. A sheet on several dozens of screens will not allow you to understand how the project is structured, evaluate its structure and interrelationships between programme parts. Much easier. The folder and file structure of a project built on modules helps to cover the structure and understand how the project is organised.
Code reuse: It is difficult, sometimes even impossible. To reuse code from such a mess, you need to separate it, clean it from unnecessary dependencies, test its work separately and build it into a new project. Sometimes it is easier to write everything from scratch. Simple. Modules can be used in multiple projects.

Modules in JavaScript

Originally, modules did not exist in JavaScript. It was thought that scripts connected to pages were very simple, and a module system was unnecessary.

As time went on, JavaScript applications became more and more complex, and the need for modules became obvious. That's when the first attempts to "bring" modules to JavaScript appeared.

Now modules have appeared and are supported, but their "becoming" was slow. There were several versions of module systems: AMD, CommonJS, UMD and ES-modules.

ES-modules are considered to be the modern system. Other module systems are considered obsolete.

ECMAScript or ES Modules

ES modules are a language-level modular system that appeared in the ES2015 specification. Further, when we will talk about modules, we will mean exactly ES-modules.

In ES modules, the export keyword is used for export, and the import keyword is used for import. When the export keyword is added, the expression becomes exported. Not only functions, but also constants can be exported. We can also access the required functionality in another module by importing or importing constants. Note that by listing the names of imports separated by commas, we can access several variables or functions in one import.

If we want to change the name of the function or variable we are importing, we can use the as keyword. The keyword also works with multiple imports. It is also possible to export a functionality even after it has been defined. This is sometimes useful if we want to describe all exports at the end of the file. It also helps to change the names when exporting.

// module1.js

// Exported expression:
export function sum(a, b) {
  return a + b
}

// Exporting constants:
export const SOME_SETTINGS_FLAG = false
export const user = {}
export const books = []

// module2.js

// Access to functionality from the first module:
import { sum } from './module1.js'

// Importing constants:
import { user, books } from './module1.js'

// Changing the name of the function or variable we are importing:
import { user as admin } from './module1.js'

// Changing names in multiple imports:
import { books as library, SOME_SETTINGS_FLAG as turnedOn } from './module1.js'

// Exporting functionality:
const user = {}
export { user }

// Changing names when exporting:
const user = {}
export { user as admin }
Enter fullscreen mode Exit fullscreen mode

When we use the export keyword next to a function or variable, we are exporting a specific function or variable.

Such variables and functions must have a name, because this is the name by which we will access them from other modules. That is why such exports are called named - the exported functionality has a name and we will import it in other modules by the same name.

Default Exports

There are also default exports. When we export some default functionality from a module, we can omit the name, but must use the default keyword after export. The functionality may not have a name because the default export is used. When importing such functionality in another module, we no longer need to use {}. Moreover, we can immediately use a different name when importing.

// sum.js

// Exporting an unnamed function by default:
export default function (a, b) {
  return a + b
}

// other-module.js

// Import functionality in another module:
import sum from './sum.js'

// We'll use a different name right away:
import summator from './sum.js'
Enter fullscreen mode Exit fullscreen mode

The community now considers default exports to be a less good practice. First of all, because named exports are easier to work with: it is easier to rename them, it is easier to work with them using automated refactoring tools.

Capabilities and Limitations of the Modules

Modules are always use strict

Strict mode is always used inside the modules. Because of this, for example, this is not a window, but undefined.

The variables are isolated internally

Modules do not see the "insides" of other modules. To share some functionality, we can use either imports and exports, or global objects like window, global, etc.

Using global objects is not recommended. It clogs up the global scope and can lead to unexpected results.

For the sake of simplicity, we can also compare a module with a function. Nobody has access to the local variables of a function - they are available only inside this function. It is the same with a module - only this module has access to the variables and functionality of this module until this functionality is explicitly exported to the outside.

This not only gets rid of the naming problem (where they might be the same), but also allows you not to worry about "something extra" being available to another module.

The module code is executed only once

The module code is executed once on import. Therefore, creation of some objects (without using factories) will be performed only once:

// module1.js
export const user = { name: 'Alice' }
console.log(user.name)

// module2.js
import { user } from './module1.js' // It'll bring up 'Alice'.
import { user } from './module1.js'// Won't get anything out.
Enter fullscreen mode Exit fullscreen mode

Because of this, it can happen that an object from one module can be changed by other modules. If we delete a field in the first module and try to display it in the next module, it will not be defined. To avoid this situation, it is better to use factories to create objects. In the example, this is the createUser function, which creates objects of the same type. When using this function, we will create a new object each time, thus protecting ourselves from possible changes in the object.

// module1.js
export const user = { name: 'Alice' }

// module2.js
import { user } from './module1.js'
console.log(user.name) // 'Alice'

// Deleting the field:
delete user.name

// module3.js
import { user } from './module1.js'

// Trying to display the deleted field:
console.log(user.name) // 'undefined'

// module1.js, a factory for creating objects:
export function createUser() {
  return { name: 'Alice' }
}

// module2.js
import { createUser } from './module1.js'

// Create a new object:
const user = createUser()

// And remove the field from the newly created object:
delete user.name

// module3.js
import { createUser } from './module1.js'

// So that there will be no more error in the third module:
const user = createUser()
console.log(user.name) // 'Alice'
Enter fullscreen mode Exit fullscreen mode

Features in the browser

In the browser, modules work by connecting scripts with attribute type='module':

<body>
  <script src="module1.js" type="module"></script>
  <script src="module2.js" type="module"></script>
</body>
Enter fullscreen mode Exit fullscreen mode

The operation of modules in the browser has some peculiarities.

Such scripts will always be deferred. This means that loading of modules will not block page rendering, but their execution will start only after the document is fully loaded. In addition, the order of execution will be preserved. In the example above - module1.js will be executed first, and only then module2.js.

External scripts with type='module' will be loaded and executed only once.

Therefore:

<!-- It will load and execute: -->
<script type="module" src="./user.js"></script>

<!--
  It will not start to load and execute,
  because the call has already been declared above:
-->
<script type="module" src="./user.js"></script>
Enter fullscreen mode Exit fullscreen mode

The path to the file must be specified

// Wrong:
import user from 'user'

// There must be either an absolute path:
import user from 'https://some-site.com/js/user.js'

// Either relative:
import user from './user.js'
Enter fullscreen mode Exit fullscreen mode

Modules and assembly

In the browser, the modules themselves are rarely used yet. Nowadays, build tools like Gulp, Webpack, Parcel, Rollup and others are used more often.

Code that uses imports and exports or scripts with type="module" is "run" through this tool, combined into bundles, minified and sent to production.

As a result, we get scripts ready for use in more browsers, while the usefulness of the modules in organising the code base and structuring the project is preserved.

Support ❀️

It took a lot of time and effort to create this material. If you found this article useful or interesting, please support my work with a small donation. It will help me to continue sharing my knowledge and ideas.

Make a contribution or Subscription to the author's content: Buy me a Coffee, Patreon, PayPal.

Neon image

Resources for building AI applications with Neon Postgres πŸ€–

Core concepts, starter applications, framework integrations, and deployment guides. Use these resources to build applications like RAG chatbots, semantic search engines, or custom AI tools.

Explore AI Tools β†’

Top comments (0)

tutorial image

Next.js Tutorial 2025 - Build a Full Stack Social App

In this 4-hour hands-on tutorial, Codesistency walks you through the process of building a social platform from scratch with Next.js (App Router), React, Prisma ORM, Clerk for authentication, Neon for PostgreSQL hosting, Tailwind CSS, Shadcn UI, and UploadThing for image uploads.

Watch the video β†’

πŸ‘‹ Kindness is contagious

Engage with a wealth of insights in this thoughtful article, valued within the supportive DEV Community. Coders of every background are welcome to join in and add to our collective wisdom.

A sincere "thank you" often brightens someone’s day. Share your gratitude in the comments below!

On DEV, the act of sharing knowledge eases our journey and fortifies our community ties. Found value in this? A quick thank you to the author can make a significant impact.

Okay