Skip to main content
The @talosjs/container package is the dependency injection (DI) engine of the framework. It wraps Inversify behind a small, typed 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.

What the container gives you

One global container instance backs the whole app. A service resolved in a controller and the same service resolved in middleware share their registration, and, for singletons, their instance. You declare what a class needs with @inject(Token) in its constructor, and the container constructs the dependency tree instead of you new-ing collaborators by hand. Each registration picks a lifetime. Singleton, Transient, or Request decide whether an instance is shared, recreated on every resolution, or recreated per request. Most of the time you never reach for container.add at all: the framework decorators (@decorator.service(), @decorator.middleware(), @decorator.repository(), and controller routing) register the class as a side effect of being applied. Resolution returns typed instances through get<T> and typed values through getConstant<T>, and failures throw a 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. The shared instance is exported for direct import:

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.

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.

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 @talosjs/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 resolved:
Injecting the application environment into middleware, the same pattern the framework uses throughout:
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. Repositories, for example, inject the active database via the "database" alias rather than a concrete class:
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:
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, needs nothing more from you.
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:
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.

Working with the container effectively

Pull collaborators through the constructor with @inject so they are resolved, scoped, and testable. Don’t instantiate them by hand inside methods. In nearly every case the decorators do the registering for you, so prefer @decorator.service(), @decorator.middleware(), and @decorator.repository() and reach for container.add only for values the framework does not register. Most services are stateless, so leave them as singletons. Choose Transient or Request only when a fresh or per-request instance is genuinely required. Depend on a class or a stable alias such as "database" rather than reaching into another module’s globals; that keeps the dependency graph explicit. Call get<T> and getConstant<T> with the expected type so the compiler checks how you use the result. And when resolution can fail, wrap it and check instanceof ContainerException to tell DI errors apart 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.