@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
containerinstance 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 ofnew-ing collaborators by hand. - Scoped lifetimes. Choose
Singleton,Transient, orRequestper 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 manualcontainer.addis the exception, not the rule. - Type-safe resolution.
get<T>andgetConstant<T>return typed instances and values, and failures throw a typedContainerException.
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.
| Stage | What happens |
|---|---|
| Registration | container.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. |
| Resolution | container.get(MyClass) asks Inversify for an instance; it recursively resolves and injects every @inject dependency. |
| Failure | If a binding is missing or construction throws, resolution raises a ContainerException with a descriptive message and code. |
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.
| Method | Signature | Description |
|---|---|---|
add | add(target: Constructor, scope?: EContainerScope): void | Registers a class. Defaults to EContainerScope.Singleton. Re-registering rebinds. |
get | get<T>(target: Constructor): T | Resolves an instance, injecting its dependencies. Throws ContainerException on failure. |
has | has(target: Constructor): boolean | Returns true if the class is registered. |
remove | remove(target: Constructor): void | Unbinds a registered class (no-op if not bound). |
addConstant | addConstant<T>(identifier: string | symbol, value: T): void | Stores a constant value under a string or symbol identifier. |
getConstant | getConstant<T>(identifier: string | symbol): T | Retrieves a constant. Throws ContainerException if not found. |
hasConstant | hasConstant(identifier: string | symbol): boolean | Returns true if a constant is registered. |
removeConstant | removeConstant(identifier: string | symbol): void | Unbinds a registered constant (no-op if not bound). |
Scopes
A scope decides how long a resolved instance lives. Pass anEContainerScope value as the second argument to add (or as the argument to a framework decorator). The default is Singleton.
| Scope | Enum member | Lifetime | Use it for |
|---|---|---|---|
| Singleton | EContainerScope.Singleton | One instance for the app’s lifetime. | Stateless services, config, env, loggers, repositories — the common case. |
| Transient | EContainerScope.Transient | A new instance on every resolution. | Lightweight, per-use helpers where sharing state would be wrong. |
| Request | EContainerScope.Request | A new instance per request context, reused within that request. | Per-request state such as a request-scoped logger or unit of work. |
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:
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:
addConstant / getConstant. Constants hold any value — strings, objects, or resolved instances — and get<T> / getConstant<T> return them typed:
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 callcontainer.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.
| Decorator | Registers | Resolved by |
|---|---|---|
@decorator.service() | A service class | Injected into controllers, middleware, other services |
@decorator.middleware() | An HTTP or socket middleware | The request pipeline before the controller |
@decorator.repository() | A repository class | Injected into services |
| Controller routing | A controller class | The router when a route matches |
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 aContainerException 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:
container.has(MyClass) to confirm registration while debugging.
Best practices
- Inject, don’t
new. Pull collaborators through the constructor with@injectso 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 manualcontainer.add; reach foraddonly for values the framework does not register for you. - Default to singleton. Most services are stateless and belong as singletons; choose
TransientorRequestonly 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>andgetConstant<T>with the expected type so the compiler checks how you use the result. - Handle
ContainerException. Wrap resolution that may fail and checkinstanceof ContainerExceptionto separate DI errors from application errors.
Related
- 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.