JavaScript Module Pattern & Dependency Injection (DI) That Scales

basanta sapkota

Ever sit down to unit-test a “tiny” JavaScript module and then… surprise. It’s quietly grabbing half your app by the throat. One random import, a sneaky singleton, maybe a global logger, and now your “unit” test is basically an integration test in disguise. Fake mustache and all. This is where the JavaScript module pattern and Dependency Injection actually earn their keep. You get tight encapsulation and you stop playing hide-and-seek with dependencies because everything the module needs is out in the open. ---

Key Takeaways

  • The JavaScript module pattern usually means an IIFE plus a closure. Private state stays private, your public API stays clean, and you don’t spray variables into the global scope. - Dependency Injection is just receiving dependencies like services, configs, functions instead of building them inside the module. Result: easier tests, less coupling, fewer headaches. - If you pass dependencies as arguments, or you have an init() that receives them, congrats. You’re already doing DI. This shows up a lot in “revealing module” styles and yep, Stack Overflow calls it out. - ES Modules and CommonJS are module systems. The classic module pattern is a design pattern. DI plays nicely with all of them. - Containers like Angular’s injector or libraries like InversifyJS can help when things get big. For small to medium projects, manual DI is often plenty. ---

Why JavaScript Module Pattern & Dependency Injection matter

The module pattern tackles an old-school JavaScript pain. Scope sprawl. Wrap your code in a function, expose only what you mean to expose, and keep the rest safely tucked away in closures. Simple. Effective. DI solves a different mess. Relationship sprawl. The DEV Community article nails the feeling: you end up “following a trail of. require() calls like breadcrumbs” just to figure out how anything is connected. Nobody wants to debug like they’re tracking a raccoon through the woods. DI flips the whole dynamic. Instead of a module wandering around for what it needs, you hand it what it needs from the outside. Martin Fowler describes it as separating configuration from use. Your code shouldn’t care how dependencies are created. It should only care what it’s given so it can do its job. Put the module pattern together with DI and you end up with encapsulation, explicit wiring, and tests don’t feel like a hostage negotiation. ---

JavaScript Module Pattern basics

Here’s the classic version you’ve probably seen everywhere:

const counterModule = {
  let count = 0. // private

  function inc() { count += 1; }
  function get() { return count; }

  return { inc, get }; // public API
})();

Why it works

  • count lives inside a closure, so it’s private state
  • consumers only get { inc, get } and nothing else

JavaScript revealing module pattern

Same idea, just a bit more explicit about what you “reveal”:

const userStore = {
  const users = []. Function add { users.push; }
  function all() { return [...users]; }

  return { add, all };
})();

Clean. Nice. Feels responsible. But then reality shows up. What happens when add() needs a logger or an apiClient? If you create those inside the module, you’ve basically welded the module to those exact implementations. Testing gets weird fast. That’s the moment DI walks in. ---

Dependency Injection in JavaScript, the practical definition

The Medium DI primer describes DI as a design pattern for managing relationships between objects. In normal developer language, it’s this:

Don’t construct dependencies inside your module. Pass them in.

Fowler frames it under “Inversion of Control.” The module doesn’t control its own wiring. Angular says it in its own way too. A dependency is any object, value, or function a class needs to work but doesn’t create itself. Angular uses an injector system to provide those dependencies. Different phrasing. Same core idea. ---

JavaScript Module Pattern & Dependency Injection: pass dependencies as arguments

The simplest DI move is just passing dependencies into a factory function:

function createGreeter => new Date() }) {
  function greet {
    logger.info.toISOString()}`). }
  return { greet };
}

// production wiring
const greeter = createGreeter. // test wiring
const fakeLogger = { info. (msg) => msgs.push(msg) }. Const greeterTest = createGreeter({ logger: fakeLogger, now: () => new Date("2020-01-01") });

This is JavaScript Module Pattern & Dependency Injection in the most useful, non-fancy form. You still encapsulate behavior.And just stop hiding your dependencies. And yep, it matches the vibe of that Reddit thread about “module pattern with dependencies passed as arguments.” People do it because it’s boring. Boring is good.So works at 2 a.m. ---

DI via init() in a revealing module (already DI)

Stack Overflow has a blunt, practical point here. If your module has an init(someService) style setup, you’re already doing Dependency Injection. ```js
const analyticsModule = (function () {
let analytics. // private reference, provided later

function init({ analyticsClient }) {
analytics = analyticsClient. }

function trackSignup(userId) {
analytics.track("signup", { userId }). }

return { init, trackSignup }. })(). ```

This comes in handy when

  • wiring happens after startup
  • you want a singleton-ish module but still need test injection

Module systems vs module pattern: ES Modules and CommonJS still need DI

People mix these up all the time, so here’s the clean separation:

  • Module pattern is a design pattern. Closures, IIFEs, revealing APIs. - ES Modules are language-level modules with export and import. - CommonJS is Node’s older module system with require() and module.exports. MDN points out modern browsers support ES modules natively. Node’s docs explain CommonJS treats each file as its own module and Node wraps modules in a function, so locals are private by default. DI still matters in both worlds because import x from "./x.js" is still a dependency. It’s just not always easy to swap in tests unless you design for it. A pragmatic pattern i use in Node and ESM apps is to import constructors or factories, then inject instances. It keeps the codebase sane without turning everything into a ceremony. ---

When to use a DI container (Angular DI, InversifyJS, etc.)

Manual DI gets you surprisingly far. But once you’re dealing with dozens of services, lifetimes like singleton vs per-request vs per-job, or a plugin-style architecture, a container starts looking less like overkill and more like… relief. Angular is the flagship JavaScript DI example. Their docs talk about @Injectable() services and things like providedIn: 'root' to make a service application-wide as a singleton. The DEV Community article also mentions Angular’s hierarchical injector model, so different injectors can exist at root, module, or component scope. Outside Angular, you’ve got libraries too. InversifyJS pitches itself as a TypeScript-powered IoC container compiles to plain JS and runs in browsers or Node environments supporting ES2022+. My rule of thumb stays pretty simple:

Start manual. Move to a container when wiring becomes repetitive, inconsistent, or error-prone. ---

Best practices for JavaScript Module Pattern & Dependency Injection

A few habits that tend to pay off

  • Inject interfaces, not concretes. In JavaScript terms, inject a function or object that matches the contract you expect. - Keep module APIs small. If a module exports 25 methods… come on. That’s a junk drawer. - Pass dependencies in one object like { logger, http, config } so you can add more later without breaking call sites. - Avoid hidden singletons for Date, random IDs, HTTP clients. Inject them if you might ever want to swap them. - Write a “composition root.” One place where the app gets wired together. Fowler really emphasizes separating configuration from use, and honestly, he’s right. ---

Common mistakes (that make DI feel “bad”)

  • Over-engineering early. If you have 3 modules, you probably don’t need a container and 20 abstractions. - Service locator dressed up as DI. If modules do Registry.get("logger"), your dependencies vanish from the surface and tests get messy. Fowler contrasts DI with Service Locator for a reason. - Injecting everything. Some things are fine as direct imports, like pure utility functions or constants. Inject what you actually expect to swap. ---

Conclusion

JavaScript Module Pattern & Dependency Injection is basically adult supervision for a codebase that’s starting to grow up. The module pattern keeps state private and APIs tidy. DI keeps dependencies honest and testable. Put them together and you stop chasing require() and import trails like a detective who hasn’t had coffee yet. Want to try it without rewriting your whole world? Pick one module that’s annoying to test. Refactor it so dependencies get passed in. Run the tests again. The calm is real. And if you’re building a modern TS/React app, you might also like my internal write-up on architecture tradeoffs when shipping features: Adding AI features to my TanStack Start. Different topic, same “keep the wiring sane” energy. ---

Sources

  • Medium . Getting Started with Dependency Injection (DI) in JavaScript. Https.//medium.com/@qrizan/getting-started-with-dependency-injection-di-in-javascript-5a1efb7a4d29
  • Reddit . Module pattern with dependencies passed as arguments. Https.//www.reddit.com/r/learnjavascript/comments/qgh5go/module_pattern_with_dependencies_passed_as/
  • Stack Overflow . Dependency injection in a revealing module. Https.//stackoverflow.com/questions/30406642/dependency-injection-in-a-revealing-module
  • XenonStack . Module Pattern in Javascript | A Quick Guide. Https.//www.xenonstack.com/insights/module-pattern-in-javascript
  • DEV Community , Dependency Injection in the JavaScript Ecosystem. Challenges and Benefits. Https.//dev.to/wroud/dependency-injection-in-the-javascript-ecosystem-challenges-and-benefits-1b31
  • Martin Fowler — Inversion of Control Containers and the Dependency Injection pattern. Https.//martinfowler.com/articles/injection.html
  • MDN — JavaScript modules (import/export). Https.//developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules
  • Node.js Docs — Modules. CommonJS modules. Https.//nodejs.org/api/modules.html
  • Angular Docs — Dependency Injection overview. Https.//angular.dev/guide/di
  • InversifyJS — Official documentation: https://inversify.github.io/

Post a Comment