Skip to main content
The @ooneex/container package is the dependency injection (DI) engine of the framework. It wraps Inversify behind a small, type-safe API and exposes a single shared container singleton. Almost every artifact you build — controllers, services, middleware, and repositories — is registered into this container and resolved with its dependencies supplied through the constructor. You rarely call the container directly; the framework’s decorators register decorated classes for you, and @inject wires the graph together.

Why dependency injection

  • One shared graph. A single global container instance backs the whole app, so a service resolved in a controller and the same service resolved in middleware share their registration and (for singletons) their instance.
  • Constructor injection. Declare what a class needs with @inject(Token) in its constructor; the container constructs the dependency tree for you instead of new-ing collaborators by hand.
  • Scoped lifetimes. Choose Singleton, Transient, or Request per registration to control whether an instance is shared, recreated each time, or recreated per request.
  • Auto-registration. Framework decorators (@decorator.service(), @decorator.middleware(), @decorator.repository(), controller routing) register the class as a side effect, so manual container.add is the exception, not the rule.
  • Type-safe resolution. get<T> and getConstant<T> return typed instances and values, and failures throw a typed ContainerException.

How it works

The package keeps one internal Inversify container (sharedDI) that every Container instance delegates to. When a class is registered, the container marks it injectable, binds it to itself, and applies the chosen scope. When you resolve it, Inversify reads the @inject metadata on its constructor and builds the dependency graph.
StageWhat happens
Registrationcontainer.add(MyClass, scope) binds the class to the shared container and applies its lifetime scope. Decorators do this automatically.
Wiring@inject(Token) on a constructor parameter records which dependency that slot needs.
Resolutioncontainer.get(MyClass) asks Inversify for an instance; it recursively resolves and injects every @inject dependency.
FailureIf a binding is missing or construction throws, resolution raises a ContainerException with a descriptive message and code.
The shared instance is exported ready to use:
import { container } from "@ooneex/container";

The Container class

Container implements IContainer and forwards to the shared Inversify instance, so creating a new Container() and using the exported container operate on the same registrations. Use the exported singleton in application code.
MethodSignatureDescription
addadd(target: Constructor, scope?: EContainerScope): voidRegisters a class. Defaults to EContainerScope.Singleton. Re-registering rebinds.
getget<T>(target: Constructor): TResolves an instance, injecting its dependencies. Throws ContainerException on failure.
hashas(target: Constructor): booleanReturns true if the class is registered.
removeremove(target: Constructor): voidUnbinds a registered class (no-op if not bound).
addConstantaddConstant<T>(identifier: string | symbol, value: T): voidStores a constant value under a string or symbol identifier.
getConstantgetConstant<T>(identifier: string | symbol): TRetrieves a constant. Throws ContainerException if not found.
hasConstanthasConstant(identifier: string | symbol): booleanReturns true if a constant is registered.
removeConstantremoveConstant(identifier: string | symbol): voidUnbinds a registered constant (no-op if not bound).
import { container, EContainerScope } from "@ooneex/container";

class UserService {
  public getUsers() {
    return [{ id: 1, name: "John" }];
  }
}

container.add(UserService); // singleton by default
const users = container.get(UserService).getUsers();

Scopes

A scope decides how long a resolved instance lives. Pass an EContainerScope value as the second argument to add (or as the argument to a framework decorator). The default is Singleton.
ScopeEnum memberLifetimeUse it for
SingletonEContainerScope.SingletonOne instance for the app’s lifetime.Stateless services, config, env, loggers, repositories — the common case.
TransientEContainerScope.TransientA new instance on every resolution.Lightweight, per-use helpers where sharing state would be wrong.
RequestEContainerScope.RequestA new instance per request context, reused within that request.Per-request state such as a request-scoped logger or unit of work.
import { container, EContainerScope } from "@ooneex/container";

container.add(DatabaseConnection, EContainerScope.Singleton);
container.add(RequestLogger, EContainerScope.Request);
container.add(TemporaryWorker, EContainerScope.Transient);

const a = container.get(TemporaryWorker);
const b = container.get(TemporaryWorker);
console.log(a === b); // false — transient gives a fresh instance each time

Constructor injection with @inject

Declare dependencies as constructor parameters and annotate each with @inject(Token), where the token is the class (or a string alias). The container resolves and supplies them when it builds the class. inject is re-exported from @ooneex/container, so import it from there. A service injected into a controller — the controller is constructed by the container during routing, so the service arrives ready to use:
import { inject } from "@ooneex/container";
import type { ContextType, IController } from "@ooneex/controller";
import type { IResponse } from "@ooneex/http-response";
import { Route } from "@ooneex/routing";
import { UserService } from "../services/UserService";

@Route.get("/api/users", {
  name: "api.users.list",
  version: 1,
  description: "List all users",
})
export class UserController implements IController {
  constructor(@inject(UserService) private readonly users: UserService) {}

  public async index(context: ContextType): Promise<IResponse> {
    return context.response.json({ users: this.users.getUsers() });
  }
}
Injecting the application environment into middleware — the same pattern the framework uses throughout:
import { AppEnv } from "@ooneex/app-env";
import { inject } from "@ooneex/container";
import type { ContextType } from "@ooneex/controller";
import { decorator, type IMiddleware } from "@ooneex/middleware";

@decorator.middleware()
export class VersionMiddleware implements IMiddleware {
  constructor(@inject(AppEnv) private readonly env: AppEnv) {}

  public async handler(context: ContextType): Promise<ContextType> {
    context.response.header.set("X-App-Version", this.env.APP_VERSION ?? "unknown");
    return context;
  }
}
Dependencies nest: a repository injects a database, a service injects the repository, a controller injects the service, and resolving the controller builds the whole chain.

Aliases and constants

Beyond classes, the container can resolve values by a string or symbol identifier. This is how the framework exposes shared infrastructure under stable names — for example, repositories inject the active database via the "database" alias rather than a concrete class:
import { inject } from "@ooneex/container";
import type { ITypeormDatabase } from "@ooneex/database";
import { decorator } from "@ooneex/repository";

@decorator.repository()
export class UserRepository {
  constructor(@inject("database") private readonly database: ITypeormDatabase) {}
}
Register and read constant values with addConstant / getConstant. Constants hold any value — strings, objects, or resolved instances — and get<T> / getConstant<T> return them typed:
import { container } from "@ooneex/container";

container.addConstant("app.name", "My Application");
container.addConstant<{ debug: boolean }>("app.config", { debug: true, maxConnections: 100 });

const name = container.getConstant<string>("app.name"); // "My Application"
const config = container.getConstant<{ debug: boolean }>("app.config");
console.log(config.debug); // true
Use a symbol identifier when you need a collision-proof token; use a string alias when readability and cross-package convention (like "database" or "mailer") matter more.

Auto-registration by the framework

You almost never call container.add yourself. Each framework decorator registers its class into the shared container as a side effect of being applied, using the scope you pass (default singleton). After that, resolving the class — or injecting it elsewhere — just works.
DecoratorRegistersResolved by
@decorator.service()A service classInjected into controllers, middleware, other services
@decorator.middleware()An HTTP or socket middlewareThe request pipeline before the controller
@decorator.repository()A repository classInjected into services
Controller routingA controller classThe router when a route matches
import { decorator } from "@ooneex/service";

@decorator.service()
export class PaymentService {
  public process(amount: number) {
    // ...
  }
}

// No manual container.add — the decorator already registered it.
// Injecting PaymentService anywhere now resolves this class.
The decorator-driven base injectable helper accepts a scope, so @decorator.service(EContainerScope.Transient) and similar control lifetime without ever touching the container API directly.

Error handling

When a binding is missing or a dependency cannot be constructed, resolution throws a ContainerException carrying a message and an error code (SERVICE_RESOLVE_FAILED for get, CONSTANT_RESOLVE_FAILED for getConstant). Catch it to distinguish DI failures from other errors:
import { container, ContainerException } from "@ooneex/container";

try {
  const service = container.get(UnregisteredService);
} catch (error) {
  if (error instanceof ContainerException) {
    console.error("Resolution failed:", error.message);
  }
}
A common cause is a forgotten decorator: if a class is injected but never registered, the framework cannot resolve it. Use container.has(MyClass) to confirm registration while debugging.

Best practices

  • Inject, don’t new. Pull collaborators through the constructor with @inject so they are resolved, scoped, and testable — never instantiate them by hand inside methods.
  • Let decorators register. Prefer @decorator.service(), @decorator.middleware(), and @decorator.repository() over manual container.add; reach for add only for values the framework does not register for you.
  • Default to singleton. Most services are stateless and belong as singletons; choose Transient or Request only when a fresh or per-request instance is genuinely required.
  • Depend on tokens, not internals. Inject a class or a stable alias (like "database") rather than reaching into another module’s globals, so the dependency graph stays explicit.
  • Use typed generics. Call get<T> and getConstant<T> with the expected type so the compiler checks how you use the result.
  • Handle ContainerException. Wrap resolution that may fail and check instanceof ContainerException to separate DI errors from application errors.
  • Services — the unit of business logic resolved through the container.
  • Repositories — data-access classes that inject the database via DI.
  • Middleware — pipeline classes constructed with injected dependencies.
  • Controllers — route handlers the container builds with their services.