How to export multiple functions in a Node.js module

How to export multiple functions in a Node.js module

When you want to expose multiple functions from a module in Node.js, the pattern revolves around assigning an object to module.exports</>. Each property of that object represents a function you want to share. This approach keeps your module's API clean and explicit, making it easier to reason about what’s available for import.

Here’s a simpler example to illustrate the concept:

function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

module.exports = {
  add,
  subtract
};

Notice how the functions are declared first, then grouped inside an object literal assigned to module.exports</>. This is preferable to incrementally adding properties like exports.add = add;, which can sometimes lead to confusion if you accidentally overwrite module.exports</>.

Using this pattern, the module explicitly states its contract. Each function is a named property of the exported object, so when imported elsewhere, it’s clear what capabilities the module provides.

It’s also common to see the functions declared inline within the exported object, especially if they’re small or utility-like:

module.exports = {
  multiply: function (a, b) {
    return a * b;
  },
  divide: function (a, b) {
    if (b === 0) {
      throw new Error("Division by zero");
    }
    return a / b;
  }
};

This inline style reduces the noise of additional function declarations, but if the functions are complex or reused internally, declaring them separately and then exporting improves readability and testability.

One subtlety worth noting is the difference between assigning to module.exports</> and mutating exports. The former replaces the entire export object, the latter modifies the existing one. This distinction matters because exports is just a reference to module.exports initially. Assigning directly to exports won’t affect the actual exported object:

// This will NOT export anything because it reassigns exports, not module.exports
exports = {
  foo: function() { return 'foo'; }
};

// Correct way:
module.exports = {
  foo: function() { return 'foo'; }
};

It is good practice to stick with module.exports</> for clarity and to avoid subtle bugs, especially when exporting multiple functions.

Another pattern that sometimes comes up is exporting classes along with functions. Since all exports are properties of an object, there’s no issue mixing them:

class Logger {
  log(message) {
    console.log(message);
  }
}

function createLogger() {
  return new Logger();
}

module.exports = {
  Logger,
  createLogger
};

This keeps related functionality bundled together without forcing a “one thing per module” constraint, which can sometimes feel artificial in JavaScript’s flexible module system.

When the module starts to grow, grouping functions logically inside the exported object can also improve discoverability. For example, you might namespace like this:

module.exports = {
  math: {
    add,
    subtract,
    multiply
  },
  string: {
    capitalize(str) {
      return str.charAt(0).toUpperCase() + str.slice(1);
    },
    trim(str) {
      return str.trim();
    }
  }
};

This approach is especially useful when you have a utility library that covers multiple domains. Consumers can then do utils.math.add() or utils.string.capitalize(), which immediately conveys the intent and context of the function.

Remember, though, nested namespaces add a small overhead in usage and testing. If your consumers prefer flat imports or tree-shaking benefits, you might want to keep exports flat and provide multiple entry points or use ES modules instead.

Ultimately, exporting multiple functions via module.exports</> is about clarity and maintainability. Group related functions, avoid mixing export styles, and keep your module’s public API concise and intention-revealing. This makes consuming the module a predictable experience, reducing cognitive load on developers.

Now, when you switch gears to importing these functions, you’ll see how this structure influences the way you pull them into your consuming modules. The next step is to organize imports so that only the required functions get loaded, which can help with both performance and code readability.

Organizing and importing functions in consuming modules

In the consuming module, importing multiple functions from a CommonJS module involves requiring the exported object and then destructuring or accessing the functions you need. This explicitness helps keep the code clear about which parts of the API are being used.

The simplest way is to require the entire module and then extract the functions:

const utils = require('./utils');

const sum = utils.add(2, 3);
const difference = utils.subtract(5, 2);

While this works fine, it can be verbose if you only need a few functions. Destructuring assignment is a more concise approach that pulls out only the functions you want:

const { add, subtract } = require('./utils');

const sum = add(10, 20);
const difference = subtract(30, 15);

This pattern is common and mirrors the way ES module imports work. It also signals clearly which functions the consuming module depends on, improving readability and maintainability.

If you are dealing with namespaced exports like the math and string example, you can destructure nested objects accordingly:

const { math, string } = require('./utils');

const product = math.multiply(4, 5);
const capitalized = string.capitalize('hello');

When importing nested namespaces, it’s worth considering the trade-offs. Accessing deep namespaces can add verbosity, but it groups related functionality logically.

Sometimes, you might want to alias imports for clarity or to avoid naming conflicts:

const { add: sum, subtract: diff } = require('./utils');

console.log(sum(7, 3));  // 10
console.log(diff(10, 6)); // 4

That’s especially useful in larger codebases where multiple modules might export similar function names.

Another aspect to consider is lazy loading or conditional imports. Since require is synchronous and cached, you can require modules inside functions or conditionally to optimize startup time or resource usage:

function calculate(operation, a, b) {
  if (operation === 'add') {
    const { add } = require('./utils');
    return add(a, b);
  } else if (operation === 'subtract') {
    const { subtract } = require('./utils');
    return subtract(a, b);
  }
  throw new Error('Unknown operation');
}

This technique can be useful for large modules or rarely used functionality, though it should be applied judiciously to avoid complicating dependency graphs.

When consuming modules that export classes alongside functions, importing and using them follows the same pattern:

const { Logger, createLogger } = require('./logger');

const logger = createLogger();
logger.log('Starting application');

const directLogger = new Logger();
directLogger.log('Direct instance');

Because the exports are just properties on an object, the consuming code doesn’t need to treat classes differently from functions. This uniformity simplifies the mental model.

If you want to re-export imported functions from another module, you can do so by aggregating them in your module’s module.exports. This pattern is common in index files that consolidate multiple modules:

const { add, subtract } = require('./math');
const { capitalize, trim } = require('./string');

module.exports = {
  add,
  subtract,
  capitalize,
  trim
};

This allows consumers to import from a single entry point, improving discoverability and simplifying import paths.

Finally, while CommonJS doesn’t have native support for named imports like ES modules, destructuring the required object is the idiomatic way to simulate that behavior. Understanding this pattern is key to writing modular, maintainable Node.js code.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *