Guidelines
Modules

Modules

Prefer ESM over CJS

In JavaScript, there are two primary module systems: CommonJS (CJS) and ECMAScript Modules (ESM). Here are some key differences between the two:

  • Syntax: CJS uses the require function to import modules, while ESM uses the import keyword. CJS uses the module.exports object to expose values from a module, while ESM uses the export keyword.
  • Dynamic loading: CJS modules are loaded dynamically at runtime, while ESM modules are statically analyzable. This means that ESM modules can be optimized by bundlers like webpack, while CJS modules cannot.
  • Support for circular dependencies: CJS has limited support for circular dependencies, while ESM fully supports them. In CJS, a circular dependency can lead to a "maximum call stack size exceeded" error, while in ESM, the dependencies are resolved correctly.
  • Browser support: CJS is not supported in the browser, while ESM is supported in modern browsers.

Overall, ESM is the newer and more modern module system, and is recommended for most applications. However, CJS is still widely used and supported in Node.js, and you may encounter it in older codebases or libraries.

Avoid using mixed module formats

In general, it is a good idea to use a consistent module format throughout your project. Mixing ESM and CJS modules can lead to confusion and may require additional configuration or workarounds.

Export both ESM and CJS modules if you create an open source library

If you are creating an open source library, support both ECMAScript Modules (ESM) and CommonJS (CJS). ESM is the more modern and recommended module format, but CJS is still widely used and supported in the Node.js ecosystem. By supporting both, you can ensure that your library follows best practices and is compatible with a wide range of projects.

Use named exports

Using a default export in your code can be useful in some cases, but it can also be confusing and may have some limitations.

Using named exports can make it easier to understand and use the exports from a module, as it allows you to explicitly specify the names of the values that are being exported and can provide a clear and self-documented interface for consuming the module's exports.

Avoid renaming named imports

Named imports provide more information about the purpose of a module, and renaming them can make your code less self-documented and more difficult to refactor. Use the original names provided by the module whenever possible.

// 🚫 Bad
import { foo as bar } from "foo";
 
// ✅ Good
import { foo } from "foo";

Avoid using wildcard imports

Wildcard imports can be useful in some cases, but they can also make your code less self-documented and more difficult to refactor. Import specific named exports whenever possible.

// 🚫 Bad
import * as foo from "./foo";
 
// ✅ Good
import { foo } from "./foo";

Use absolute imports

Absolute imports are generally considered more robust and easier to refactor than relative imports. Using absolute imports can make it easier to move files around in your project, and can also help to avoid confusion with different levels of relative imports.

// 🚫 Bad
import { foo } from "../foo";
 
// ✅ Good
import { foo } from "@/src/foo";

Consider using a module resolution tool

If you are working on a large project with a complex file structure, you may want to consider using a module resolution tool like paths.alias or resolve.alias in webpack to simplify your import paths.

Here are examples of how you can set up module resolution with webpack, tsconfig.json, and jsconfig.json:

module.exports = {
  // ...
  resolve: {
    alias: {
      // Add an alias that point to the src folder
      "@": path.resolve(__dirname, "src"),
      // Replace all imports of "lodash" with the lodash package
      lodash: "lodash-es",
    },
  },
};

Avoid loading modules inside functions

Avoiding loading modules inside functions can help improve the performance and reliability of your application, as it allows the module to be loaded and initialized once when the application starts up, rather than being loaded and initialized every time the function is called. This can help reduce the overall overhead of the application and can make it more efficient and scalable.

// 🚫 Bad
const doSomethingFrequently = () => {
  // This will cause the module to be loaded multiple times, which is inefficient
  import { myModule } from "./myModule";
 
  myModule.doSomething();
};
 
// ✅ Good
import { myModule } from "./myModule";
 
const doSomethingFrequently = () => {
  myModule.doSomething();
};
Last updated on