On this page

@zirion/ioc · Examples

Runnable examples

Complete @zirion/ioc v3 examples: a Node.js HTTP server, provider lifetime checks, and retries of asynchronous singleton factories.

Updated · ·

Run the examples

These are complete JavaScript modules for Node.js 20 or newer. No framework or TypeScript compiler is needed. In a separate example directory, install the package:

npm init -y
npm install @zirion/ioc@^3.0.0

Save each example with the suggested .mjs filename and run it with node. The main guide explains the registrations in detail.

Node.js HTTP server

Save as server.mjs:

import { createServer } from 'node:http';
import { randomUUID } from 'node:crypto';
import { Container, InjectScope } from '@zirion/ioc';

class UserStore {
  users = [{ id: 1, name: 'Ada' }];
}

class RequestInfo {
  constructor(context) {
    this.context = context;
  }
}

class UserHandler {
  id = randomUUID();

  constructor(store, config, info) {
    this.store = store;
    this.config = config;
    this.info = info;
  }

  body() {
    return {
      requestId: this.info.context.requestId,
      handlerId: this.id,
      region: this.config.region,
      users: this.store.users,
    };
  }
}

const container = new Container()
  .add(UserStore, { scope: InjectScope.SINGLETON })
  .add('config', {
    scope: InjectScope.SINGLETON,
    valueFactory: async () => ({ region: 'eu-central-1' }),
  })
  .add(RequestInfo, { scope: InjectScope.REQUEST })
  .add(UserHandler, { inject: [UserStore, 'config', RequestInfo] });

const server = createServer(async (request, response) => {
  const context = { requestId: randomUUID() };

  try {
    let body;
    let status = 200;

    if (request.method === 'GET' && request.url === '/health') {
      const store = container.getOrFailSync(UserStore);
      body = { ok: true, users: store.users.length };
    } else if (request.method === 'GET' && request.url === '/users') {
      const handler = await container.getOrFail(UserHandler, context);
      body = handler.body();
    } else {
      status = 404;
      body = { error: 'Not found' };
    }

    response.writeHead(status, { 'content-type': 'application/json' });
    response.end(JSON.stringify(body));
  } catch (error) {
    console.error(error);
    response.writeHead(500, { 'content-type': 'application/json' });
    response.end(JSON.stringify({ error: 'Internal server error' }));
  }
});

server.listen(3000, '127.0.0.1', () => {
  console.log('http://127.0.0.1:3000/users');
});

process.once('SIGTERM', () => {
  server.close();
});

Run the server and make two requests:

node server.mjs
# In another terminal:
curl http://127.0.0.1:3000/health
curl http://127.0.0.1:3000/users
curl http://127.0.0.1:3000/users

The store and config are singleton values. Each HTTP request gets a fresh context and request state. UserHandler is transient; even two resolutions within one request would produce two handlers while sharing that request’s RequestInfo.

/health uses synchronous resolution for the synchronous store graph. /users awaits the graph because config initialization is asynchronous. A real connection factory can replace the demonstration config factory. Close any external clients in the application’s shutdown handler; finalize() does not dispose them.

Verify lifetimes and repeated injection

Save as lifetimes.mjs, then run node lifetimes.mjs:

import assert from 'node:assert/strict';
import { Container, InjectScope } from '@zirion/ioc';

class Store {}
class RequestState {}
class Operation {}
class Pair {
  constructor(first, second) {
    this.first = first;
    this.second = second;
  }
}

const container = new Container()
  .add(Store, { scope: InjectScope.SINGLETON })
  .add(RequestState, { scope: InjectScope.REQUEST })
  .add(Operation)
  .add(Pair, { inject: [Operation, Operation] });

assert.equal(
  container.getOrFailSync(Store),
  container.getOrFailSync(Store),
);

const requestA = {};
const requestB = {};
assert.equal(
  container.getOrFailSync(RequestState, requestA),
  container.getOrFailSync(RequestState, requestA),
);
assert.notEqual(
  container.getOrFailSync(RequestState, requestA),
  container.getOrFailSync(RequestState, requestB),
);

const pair = container.getOrFailSync(Pair);
assert.notEqual(pair.first, pair.second);
assert.notEqual(pair, container.getOrFailSync(Pair));
console.log('All lifetime checks passed');

The same context reuses request state; a different object does not. Duplicate transient selectors in inject are preserved and create different instances. Change Operation to singleton to see the repeated injection share one instance instead.

Retry a failed async singleton

Save as retry.mjs, then run node retry.mjs:

import assert from 'node:assert/strict';
import { Container, InjectScope } from '@zirion/ioc';

let attempts = 0;
const container = new Container().add('enabled', {
  scope: InjectScope.SINGLETON,
  valueFactory: async () => {
    attempts += 1;

    if (attempts === 1) {
      throw new Error('Temporary failure');
    }

    return false;
  },
});

await assert.rejects(container.getOrFail('enabled'), /Temporary failure/);
const [first, second] = await Promise.all([
  container.getOrFail('enabled'),
  container.getOrFail('enabled'),
]);
assert.equal(first, false);
assert.equal(second, false);
assert.equal(await container.getOrFail('enabled'), false);
assert.equal(attempts, 2);
console.log('Retry, concurrent resolution, and falsy caching verified');

The failed factory is not cached. The next two callers share one successful attempt, and false is then cached normally. Apply the same checks to request providers with a shared context and to classes whose onInitialized() can fail.

Next: v2 to v3 migration and migration from other containers.

Back to top