Build Your Own IoC Container in JavaScript: A Step‑by‑Step Guide

This tutorial walks you through creating a simple Inversion of Control (IoC) container in JavaScript, covering basic dependency injection, adding caching for singleton instances, and extending the container to load arbitrary classes automatically, all with clear code examples.

Node Underground
Node Underground
Node Underground
Build Your Own IoC Container in JavaScript: A Step‑by‑Step Guide

In the Java ecosystem, Spring Framework has become the de‑facto standard, and large projects often rely on an IoC container. If you are unfamiliar with Dependency Injection (DI) or IoC, this guide will teach you from scratch.

We start with a minimal requirement: given several classes, we want a simple method to obtain an instance of each class.

class A {
  b: B;
}

class B {
  c: C;
}

class C {
  hello() {
    console.log('hello world');
  }
}

const container = new Container();
const a = container.get(A);
// a.b.c.hello() === 'hello world'

First Step

The Container class must analyze a class’s properties to discover its dependencies. Assuming all properties are standard classes, we can recursively create the required objects.

class Container {
  get(Module) {
    // create object
    const obj = new Module();
    // get property names
    const properties = Object.getOwnPropertyNames(obj);
    for (let p of properties) {
      if (!obj[p]) {
        if (p === 'b') {
          obj[p] = this.get(B);
        } else if (p === 'c') {
          obj[p] = this.get(C);
        } else {}
      }
    }
  }
}

This recursive get method creates missing dependencies on demand.

Second Step

To avoid creating multiple instances of the same class, we introduce a cache so each class is instantiated only once per run.

class Container {
  cache = {};

  getName(Module) {
    return Module.name.toLowerCase();
  }

  get(Module) {
    // cache lookup
    if (this.cache[this.getName(Module)]) {
      return this.cache[this.getName(Module)];
    }
    // create object
    const obj = new Module();
    // store in cache
    this.cache[this.getName(Module)] = obj;
    const properties = Object.getOwnPropertyNames(obj);
    for (let p of properties) {
      if (!obj[p]) {
        if (p === 'b') {
          obj[p] = this.get(B);
        } else if (p === 'c') {
          obj[p] = this.get(C);
        } else if (p === 'd') {
          obj[p] = this.get(D);
        } else {}
      }
    }
  }
}

With caching, each class has a single instance during execution.

Third Step

To support an arbitrary number of classes, we add a scanning mechanism that populates a classTable map with all exported classes found in the project.

init() {
  const fileResults = globby.sync(['**/**.ts', '**/**.js'], {
    cwd: process.cwd(),
    ignore: ['**/node_modules/**'],
  });

  for (const name of fileResults) {
    const exports = require(process.cwd() + '/' + name);
    // store class name and class reference
    this.classTable[this.getName(exports)] = exports;
  }
}

The container can now retrieve any class by name without hard‑coding dependencies.

class Container {
  cwd = process.cwd();
  cache = {};
  classTable = {};

  init() {
    const fileResults = globby.sync(['**/**.ts', '**/**.js'], {
      cwd: this.cwd,
      ignore: ['**/node_modules/**'],
    });
    for (const name of fileResults) {
      const exports = require(this.cwd + '/' + name);
      this.classTable[this.getName(exports)] = exports;
    }
  }

  getName(Module) {
    return Module.name.toLowerCase();
  }

  get(Module) {
    if (this.cache[this.getName(Module)]) {
      return this.cache[this.getName(Module)];
    }
    const obj = new Module();
    this.cache[this.getName(Module)] = obj;
    const properties = Object.getOwnPropertyNames(obj);
    for (let p of properties) {
      if (!obj[p]) {
        if (this.classTable[p]) {
          obj[p] = this.get(this.classTable[p]);
        }
      }
    }
    return obj;
  }
}

Final Step

Congratulations! You now have a working IoC container you can discuss in interviews. In real projects you would typically use a mature library such as injection or the midway framework, but building your own helps you understand dependency injection and decoupling.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

IoCContainer
Node Underground
Written by

Node Underground

No language is immortal—Node.js isn’t either—but thoughtful reflection is priceless. This underground community for Node.js enthusiasts was started by Taobao’s Front‑End Team (FED) to share our original insights and viewpoints from working with Node.js. Follow us. BTW, we’re hiring.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.