Skip to main content

Dependency injection container

Open Mercato uses Awilix to assemble services at runtime. Each HTTP request, background job, or CLI command spins up a scoped container with core services and module contributions.

Container creation

  • packages/shared/src/lib/di/container.ts exports createContainer (application scope) and createRequestContainer (per-request).
  • Module registrars (src/modules/<module>/di.ts) receive the container instance and can register services, factories, and values.
  • Registration helpers: asClass, asFunction, and asValue from Awilix. Prefer scoped registrations so each request owns its service instances.
packages/core/src/modules/auth/di.ts
import { asClass } from 'awilix';
import type { AppContainer } from '@/lib/di/container';
import { AuthService } from './services/auth-service';

export function register(container: AppContainer) {
container.register({
authService: asClass(AuthService).scoped(),
});
}

Resolving services

  • In API handlers: const authService = container.resolve('authService');
  • In subscribers: the second argument is { container, logger, ... }—resolve services the same way.
  • Avoid importing services directly; resolving them keeps modules overridable.

Best practices

  • Naming – use descriptive keys (organizationInvitationService). Keep module-specific prefixes to avoid collisions.
  • Isolation – services should not import files from other modules. Communicate via events, APIs, or shared packages under packages/shared.
  • Testing – instantiate a container in tests, register fake services, and resolve handlers to unit test modules.

The DI container is the backbone of module composability—treat it as the single source of truth for service wiring.