Skip to content

Deep Dive: Components

Technical reference for BaseComponent—the foundation for creating reusable, pluggable features in Ignis. Components are powerful containers that can group together multiple providers, services, controllers, repositories, and even entire mini-applications into a single, redistributable module.

File: packages/core/src/base/components/base.ts

Quick Reference

FeatureBenefit
EncapsulationBundle feature bindings (services, controllers) into single class
Lifecycle ManagementAuto-called binding() method during startup
Default BindingsSelf-contained with automatic DI registration
Idempotent Configureconfigure() is safe to call multiple times — runs binding() only once
Controller TransportsRestComponent and GrpcComponent handle controller discovery per transport

Component Directory Structure

A well-organized component follows a consistent directory structure that separates concerns and makes the codebase maintainable.

Simple Component

src/components/health-check/
├── index.ts              # Barrel exports (re-exports everything)
├── component.ts          # Component class with binding logic
├── controller.ts         # Controller class(es)
└── common/
    ├── index.ts          # Barrel exports for common/
    ├── keys.ts           # Binding key constants
    ├── types.ts          # Interfaces and type definitions
    ├── constants.ts      # Static class constants (optional)
    └── rest-paths.ts     # Route path constants (optional)

Complex Component (with services, models, strategies)

src/components/auth/
├── index.ts
├── authenticate/
│   ├── index.ts
│   ├── component.ts
│   ├── common/
│   │   ├── index.ts
│   │   ├── keys.ts
│   │   ├── types.ts
│   │   └── constants.ts
│   ├── controllers/
│   │   ├── index.ts
│   │   └── auth.controller.ts
│   ├── services/
│   │   ├── index.ts
│   │   └── jwt-token.service.ts
│   └── strategies/
│       ├── index.ts
│       ├── jwt.strategy.ts
│       └── basic.strategy.ts
└── models/
    ├── index.ts
    ├── entities/
    │   └── user-token.model.ts
    └── requests/
        ├── sign-in.schema.ts
        └── sign-up.schema.ts

Controller Transport Component

Transport components live under src/components/controller/ and are instantiated directly by the application during registerControllers() — they are not registered via this.component().

src/components/controller/
├── index.ts              # Barrel re-exports (REST only; gRPC excluded from barrel)
├── rest/
│   ├── rest.component.ts
│   └── common/
│       └── types.ts      # IRestComponentConfig, RestBindingKeys
└── grpc/
    ├── grpc.component.ts
    └── common/
        └── types.ts      # IGrpcComponentConfig, GrpcBindingKeys

The common/ Directory

The common/ directory contains shared definitions that are used throughout the component. Every component should have this directory with at least keys.ts and types.ts.

1. Binding Keys (keys.ts)

Binding keys are string constants used to register and retrieve values from the DI container. They follow the pattern @app/[component]/[feature].

typescript
// src/components/health-check/common/keys.ts
export class HealthCheckBindingKeys {
  static readonly HEALTH_CHECK_OPTIONS = '@app/health-check/options';
}

For components with multiple features:

typescript
// src/components/auth/authenticate/common/keys.ts
export class AuthenticateBindingKeys {
  static readonly AUTHENTICATE_OPTIONS = '@app/authenticate/options';
  static readonly JWT_OPTIONS = '@app/authenticate/jwt/options';
}

Naming Convention:

  • Class name: [Feature]BindingKeys
  • Key format: @app/[component]/[feature] or @app/[component]/[sub-feature]/[name]

2. Types (types.ts)

Define all interfaces and type aliases that the component exposes or uses internally.

typescript
// src/components/health-check/common/types.ts
export interface IHealthCheckOptions {
  restOptions: { path: string };
}

For complex components with service interfaces:

typescript
// src/components/auth/authenticate/common/types.ts
import { Context } from 'hono';
import { AnyObject, ValueOrPromise } from '@venizia/ignis-helpers';

// Options interface for the component
export interface IAuthenticateOptions {
  jwtOptions?: IJWTTokenServiceOptions;
  basicOptions?: IBasicTokenServiceOptions;
  restOptions?: {
    useAuthController?: boolean;
    controllerOpts?: TDefineAuthControllerOpts;
  };
}

// Service options interface
export interface IJWTTokenServiceOptions {
  jwtSecret: string;
  applicationSecret: string;
  getTokenExpiresFn: () => ValueOrPromise<number>;
}

// Service contract interface
export interface IAuthService<
  SIRQ = AnyObject,
  SIRS = AnyObject,
> {
  signIn(context: Context, opts: SIRQ): Promise<SIRS>;
  signUp(context: Context, opts: SIRQ): Promise<SIRS>;
}

// Auth user type
export interface IAuthUser {
  userId: string;
  [extra: string | symbol]: any;
}

Naming Conventions:

  • Interfaces: I prefix (e.g., IHealthCheckOptions, IAuthService)
  • Type aliases: T prefix (e.g., TDefineAuthControllerOpts)

3. Constants (constants.ts)

Use static classes (not enums) for constants that need type extraction and validation.

typescript
// src/components/auth/authenticate/common/constants.ts
export class Authentication {
  // Strategy identifiers
  static readonly STRATEGY_BASIC = 'basic';
  static readonly STRATEGY_JWT = 'jwt';

  // Token types
  static readonly TYPE_BASIC = 'Basic';
  static readonly TYPE_BEARER = 'Bearer';

  // Context keys
  static readonly CURRENT_USER = 'auth.current.user';
  static readonly SKIP_AUTHENTICATION = 'authentication.skip';
}

With validation (for user-configurable values):

typescript
// src/components/swagger/common/constants.ts
import { TConstValue } from '@venizia/ignis-helpers';

export class DocumentUITypes {
  static readonly SWAGGER = 'swagger';
  static readonly SCALAR = 'scalar';

  // Set for O(1) validation
  static readonly SCHEME_SET = new Set([this.SWAGGER, this.SCALAR]);

  // Validation helper
  static isValid(value: string): boolean {
    return this.SCHEME_SET.has(value);
  }
}

// Extract union type: 'swagger' | 'scalar'
export type TDocumentUIType = TConstValue<typeof DocumentUITypes>;

4. REST Paths (rest-paths.ts)

Define route path constants for controllers.

typescript
// src/components/health-check/common/rest-paths.ts
export class HealthCheckRestPaths {
  static readonly ROOT = '/';
  static readonly PING = '/ping';
  static readonly METRICS = '/metrics';
}

5. Barrel Exports (index.ts)

Every folder should have an index.ts that re-exports its contents:

typescript
// src/components/health-check/common/index.ts
export * from './keys';
export * from './rest-paths';
export * from './types';

// src/components/health-check/index.ts
export * from './common';
export * from './component';
export * from './controller';

BaseComponent Class

Abstract class for all components - structures resource binding and lifecycle management.

Class Signature

typescript
abstract class BaseComponent<ConfigurableOptions extends object = {}>
  extends BaseHelper
  implements IConfigurable<ConfigurableOptions>

BaseComponent extends BaseHelper (scoped logging via this.logger) and implements IConfigurable (the configure() method).

Constructor Options

The super() constructor in your component can take the following options:

OptionTypeDescription
scopestringRequired. A unique name for the component, typically MyComponent.name. Used for logging.
initDefault{ enable: false } | { enable: true; container: Container }If enable is true, the bindings defined below will be automatically registered with the provided container (usually the application instance) when configure() is called — but only if they are not already bound. Defaults to { enable: false }.
bindingsRecord<string | symbol, Binding>An object where keys are binding keys and values are Binding instances. These are the default services, values, or providers that your component offers. Defaults to {}.

Properties

PropertyTypeAccessDescription
bindingsRecord<string | symbol, Binding>protectedDefault bindings the component provides. Can be set in constructor or assigned directly in the constructor body.
initDefaultTInitDefaultprotectedControls whether default bindings are auto-registered to a container.
isConfiguredbooleanprotectedGuard flag — prevents configure() from running more than once.

Methods

MethodSignatureDescription
binding()abstract binding(): ValueOrPromise<void>Abstract. Override this to register services, controllers, and other resources. Called by configure().
configure(opts?)async configure(opts?: ConfigurableOptions): Promise<void>Entry point. Calls initDefaultBindings() (if enabled), then binding(). Idempotent — skips if already configured.
initDefaultBindings(opts)protected initDefaultBindings(opts: { container: Container }): voidIterates this.bindings and registers each into the container if not already bound.

Lifecycle Flow

  1. Application Instantiates Component: When you call this.component(MyComponent) in your application, the DI container creates an instance of your component.
  2. Constructor Runs: Your component's constructor calls super(), setting up its scope and defining its default bindings.
  3. Application Calls configure(): During the registerComponents phase, the application resolves each component and calls configure().
  4. configure() Executes: Checks isConfigured guard (idempotent). If initDefault.enable is true, registers default bindings into the container. Then calls binding().
  5. binding() Runs: Your override registers controllers, services, reads options from the container, and performs any additional setup.

Controller Transport Components

Controller transport components are a special category. Unlike regular components registered via this.component(), transport components are instantiated and configured directly by BaseApplication.registerControllers(). They are responsible for discovering controller bindings and mounting them onto the application router.

Transport Configuration

The application config transports controls which transports are enabled. Defaults to ['rest'].

typescript
// In your application constructor
super({
  scope: MyApplication.name,
  config: {
    // ...
    transports: [ControllerTransports.REST, ControllerTransports.GRPC],
  },
});

The ControllerTransports constants:

typescript
export class ControllerTransports {
  static readonly REST = 'rest';
  static readonly GRPC = 'grpc';
}

How registerControllers() Works

During the application lifecycle, registerControllers() iterates the configured transports and creates the corresponding component:

typescript
// Simplified from BaseApplication.registerControllers()
const transports = this.configs.transports ?? [ControllerTransports.REST];

for (const transport of transports) {
  switch (transport) {
    case ControllerTransports.REST: {
      const restComponent = new RestComponent(this);
      await restComponent.configure();
      break;
    }
    case ControllerTransports.GRPC: {
      const grpcComponent = new GrpcComponent(this);
      await grpcComponent.configure();
      break;
    }
  }
}

If gRPC controllers are discovered but the 'grpc' transport is not in the transports config, the application logs an error warning for each one.

RestComponent

File: packages/core/src/components/controller/rest/rest.component.ts

Discovers all controller bindings tagged with BindingNamespaces.CONTROLLER, skips any whose metadata has transport === ControllerTransports.GRPC, and configures the rest as REST controllers.

typescript
export class RestComponent extends BaseComponent {
  constructor(private application: BaseApplication) {
    super({
      scope: RestComponent.name,
      initDefault: { enable: true, container: application },
      bindings: {
        [RestBindingKeys.REST_COMPONENT_OPTIONS]: Binding.bind<IRestComponentConfig>({
          key: RestBindingKeys.REST_COMPONENT_OPTIONS,
        }).toValue(DEFAULT_OPTIONS),
      },
    });
  }

  override async binding(): Promise<void> { /* ... */ }
}

Binding loop:

  1. Fetches all controller bindings not yet configured (tracked via a Set<string>).
  2. For each binding, reads controller metadata from MetadataRegistry.
  3. Skips bindings with transport === ControllerTransports.GRPC.
  4. Validates that metadata.path is present (throws if missing).
  5. Resolves the controller instance from the DI container, calls instance.configure(), and mounts it: router.route(metadata.path, instance.getRouter()).
  6. Re-fetches bindings after each configure to pick up dynamically added controllers.

Config types:

typescript
export interface IRestComponentConfig {}

export class RestBindingKeys {
  static readonly REST_COMPONENT_OPTIONS = '@app/rest/options';
}

GrpcComponent

File: packages/core/src/components/controller/grpc/grpc.component.ts

Discovers all controller bindings tagged with BindingNamespaces.CONTROLLER, skips any whose metadata does not have transport === ControllerTransports.GRPC, and configures gRPC controllers.

typescript
export class GrpcComponent extends BaseComponent {
  constructor(private application: BaseApplication) {
    super({
      scope: GrpcComponent.name,
      initDefault: { enable: true, container: application },
      bindings: {
        [GrpcBindingKeys.GRPC_COMPONENT_OPTIONS]: Binding.bind<IGrpcComponentConfig>({
          key: GrpcBindingKeys.GRPC_COMPONENT_OPTIONS,
        }).toValue(DEFAULT_OPTIONS),
      },
    });
  }

  override async binding(): Promise<void> { /* ... */ }
}

Binding loop (same dynamic re-fetch pattern as RestComponent):

  1. Fetches all controller bindings not yet configured.
  2. Skips bindings with transport !== ControllerTransports.GRPC.
  3. Validates metadata.path is present.
  4. Resolves the gRPC controller instance. Skips if the instance has no service definition (logs a warning).
  5. Sets instance.basePath from the application's project configs.
  6. Calls instance.configure() and mounts: router.route(metadata.path, instance.getRouter()).
  7. Re-fetches bindings to pick up dynamically added controllers.

Config types:

typescript
export interface IGrpcComponentConfig {
  interceptors?: unknown[];
}

export class GrpcBindingKeys {
  static readonly GRPC_COMPONENT_OPTIONS = '@app/grpc/options';
}

Note: GrpcComponent is excluded from the barrel export at src/components/controller/index.ts. Import it directly from @venizia/ignis/components/controller/grpc if needed.

Component Implementation Patterns

Basic Component

typescript
// src/components/health-check/component.ts
import { BaseApplication, BaseComponent, inject, CoreBindings, Binding, ValueOrPromise } from '@venizia/ignis';
import { HealthCheckBindingKeys, IHealthCheckOptions } from './common';
import { HealthCheckController } from './controller';

// 1. Define default options
const DEFAULT_OPTIONS: IHealthCheckOptions = {
  restOptions: { path: '/health' },
};

export class HealthCheckComponent extends BaseComponent {
  constructor(
    // 2. Inject the application instance
    @inject({ key: CoreBindings.APPLICATION_INSTANCE })
    private application: BaseApplication,
  ) {
    super({
      scope: HealthCheckComponent.name,
      // 3. Enable automatic binding registration
      initDefault: { enable: true, container: application },
      // 4. Define default bindings
      bindings: {
        [HealthCheckBindingKeys.HEALTH_CHECK_OPTIONS]: Binding.bind<IHealthCheckOptions>({
          key: HealthCheckBindingKeys.HEALTH_CHECK_OPTIONS,
        }).toValue(DEFAULT_OPTIONS),
      },
    });
  }

  // 5. Configure resources in binding()
  override binding(): ValueOrPromise<void> {
    // Read options (may have been overridden by user)
    const healthOptions = this.application.get<IHealthCheckOptions>({
      key: HealthCheckBindingKeys.HEALTH_CHECK_OPTIONS,
      isOptional: true,
    }) ?? DEFAULT_OPTIONS;

    // Register controller with dynamic path
    Reflect.decorate(
      [controller({ path: healthOptions.restOptions.path })],
      HealthCheckController,
    );
    this.application.controller(HealthCheckController);
  }
}

Component with Services

typescript
// src/components/auth/authenticate/component.ts
import { BaseApplication, BaseComponent, inject, CoreBindings, Binding, ValueOrPromise } from '@venizia/ignis';
import { getError } from '@venizia/ignis-helpers';
import { AuthenticateBindingKeys, IAuthenticateOptions, IBasicTokenServiceOptions, IJWTTokenServiceOptions } from './common';
import { BasicTokenService, JWTTokenService } from './services';
import { defineAuthController } from './controllers';

const DEFAULT_OPTIONS: IAuthenticateOptions = {
  restOptions: {
    useAuthController: false,
  },
};

export class AuthenticateComponent extends BaseComponent {
  constructor(
    @inject({ key: CoreBindings.APPLICATION_INSTANCE })
    private application: BaseApplication,
  ) {
    super({
      scope: AuthenticateComponent.name,
      initDefault: { enable: true, container: application },
      bindings: {
        [AuthenticateBindingKeys.AUTHENTICATE_OPTIONS]: Binding.bind<IAuthenticateOptions>({
          key: AuthenticateBindingKeys.AUTHENTICATE_OPTIONS,
        }).toValue(DEFAULT_OPTIONS),
      },
    });
  }

  // Validate at least one auth option is provided
  private validateOptions(opts: IAuthenticateOptions): void {
    if (!opts.jwtOptions && !opts.basicOptions) {
      throw getError({
        message: '[AuthenticateComponent] At least one of jwtOptions or basicOptions must be provided',
      });
    }
  }

  // Configure JWT authentication if jwtOptions is provided
  private defineJWTAuth(opts: IAuthenticateOptions): void {
    if (!opts.jwtOptions) return;

    this.application
      .bind<IJWTTokenServiceOptions>({ key: AuthenticateBindingKeys.JWT_OPTIONS })
      .toValue(opts.jwtOptions);
    this.application.service(JWTTokenService);
  }

  // Configure Basic authentication if basicOptions is provided
  private defineBasicAuth(opts: IAuthenticateOptions): void {
    if (!opts.basicOptions) return;

    this.application
      .bind<IBasicTokenServiceOptions>({ key: AuthenticateBindingKeys.BASIC_OPTIONS })
      .toValue(opts.basicOptions);
    this.application.service(BasicTokenService);
  }

  // Configure auth controllers if enabled
  private defineControllers(opts: IAuthenticateOptions): void {
    if (!opts.restOptions?.useAuthController) return;

    // Auth controller requires JWT for token generation
    if (!opts.jwtOptions) {
      throw getError({
        message: '[defineControllers] Auth controller requires jwtOptions to be configured',
      });
    }

    this.application.controller(defineAuthController(opts.restOptions.controllerOpts));
  }

  override binding(): ValueOrPromise<void> {
    const options = this.application.get<IAuthenticateOptions>({
      key: AuthenticateBindingKeys.AUTHENTICATE_OPTIONS,
    });

    this.validateOptions(options);
    this.defineJWTAuth(options);
    this.defineBasicAuth(options);
    this.defineControllers(options);
  }
}

Component with Factory Controllers

When controllers need to be dynamically configured:

typescript
// src/components/static-asset/component.ts
override binding(): ValueOrPromise<void> {
  const componentOptions = this.application.get<TStaticAssetsComponentOptions>({
    key: StaticAssetComponentBindingKeys.STATIC_ASSET_COMPONENT_OPTIONS,
  });

  // Create multiple controllers from configuration
  for (const [key, opt] of Object.entries(componentOptions)) {
    this.application.controller(
      AssetControllerFactory.defineAssetController({
        controller: opt.controller,
        storage: opt.storage,
        helper: opt.helper,
      }),
    );

    this.application.logger.info(
      '[binding] Asset storage bound | Key: %s | Type: %s',
      key,
      opt.storage,
    );
  }
}

Component without initDefault (Manual Binding Assignment)

Some components skip initDefault and assign this.bindings directly in the constructor body:

typescript
// src/components/swagger/component.ts
export class SwaggerComponent extends BaseComponent {
  constructor(
    @inject({ key: CoreBindings.APPLICATION_INSTANCE }) private application: BaseApplication,
  ) {
    super({ scope: SwaggerComponent.name });

    this.bindings = {
      [SwaggerBindingKeys.SWAGGER_OPTIONS]: Binding.bind<ISwaggerOptions>({
        key: SwaggerBindingKeys.SWAGGER_OPTIONS,
      }).toValue(DEFAULT_SWAGGER_OPTIONS),
    };
  }

  override async binding() {
    // Read options, configure OpenAPI doc endpoint, UI endpoint, etc.
  }
}

In this pattern, initDefault defaults to { enable: false }, so the bindings are defined but not auto-registered. The component manages its own option reading and setup in binding().

Built-in Components

Registered via this.component() (Standard Components)

ComponentKey Features
HealthCheckComponentGET /health (default path, configurable). Registers HealthCheckController with GET / and POST /ping endpoints.
SwaggerComponentOpenAPI doc at /doc/openapi.json, UI at /doc/explorer (Scalar by default, Swagger UI also supported). Auto-populates app info and server URL. Registers JWT and Basic security schemes.
AuthenticateComponentJWT and/or Basic auth strategies. Optional auth controller (signIn/signUp). Token services (JWTTokenService, BasicTokenService).
AuthorizationComponentCasbin-based RBAC, permission mapping, authorize() middleware.
RequestTrackerComponentRegisters Hono requestId() middleware and a RequestSpyMiddleware for x-request-id header tracking and request body parsing.

Excluded from Barrel (Import Directly)

ComponentImport PathKey Features
StaticAssetComponent@venizia/ignis/components/static-assetFile upload/download CRUD, MinIO/Disk storage.
MailComponent@venizia/ignis/components/mailNodemailer/Mailgun transporters, Direct/BullMQ/InternalQueue executors.
SocketIOComponent@venizia/ignis/components/socket-ioSocket.IO server with Redis adapter for horizontal scaling.
WebSocketComponent@venizia/ignis/components/websocketWebSocket support.

Controller Transport Components (Not Registered via this.component())

ComponentWhen Used
RestComponentInstantiated by registerControllers() when transports includes 'rest' (the default). Discovers and mounts REST controllers.
GrpcComponentInstantiated by registerControllers() when transports includes 'grpc'. Discovers and mounts gRPC controllers (ConnectRPC over HTTP).

Exposing and Consuming Component Options

Pattern 1: Override Before Registration

The most common pattern - override options before registering the component:

typescript
// src/application.ts
import { HealthCheckComponent, HealthCheckBindingKeys, IHealthCheckOptions } from '@venizia/ignis';

export class Application extends BaseApplication {
  preConfigure(): ValueOrPromise<void> {
    // 1. Override options BEFORE registering component
    this.bind<IHealthCheckOptions>({ key: HealthCheckBindingKeys.HEALTH_CHECK_OPTIONS })
      .toValue({
        restOptions: { path: '/api/health' }, // Custom path
      });

    // 2. Register component (will use overridden options)
    this.component(HealthCheckComponent);
  }
}

Pattern 2: Merge with Defaults

For partial overrides, merge with defaults in the component:

typescript
// In your component's binding() method
override binding(): ValueOrPromise<void> {
  const extraOptions = this.application.get<Partial<IMyOptions>>({
    key: MyBindingKeys.OPTIONS,
    isOptional: true,
  }) ?? {};

  // Merge with defaults
  const options = { ...DEFAULT_OPTIONS, ...extraOptions };

  // Use merged options...
}

Pattern 3: Deep Merge for Nested Options

For complex nested configurations:

typescript
override binding(): ValueOrPromise<void> {
  const extraOptions = this.application.get<Partial<ISwaggerOptions>>({
    key: SwaggerBindingKeys.SWAGGER_OPTIONS,
    isOptional: true,
  }) ?? {};

  // Deep merge nested objects
  const options: ISwaggerOptions = {
    ...DEFAULT_OPTIONS,
    ...extraOptions,
    restOptions: {
      ...DEFAULT_OPTIONS.restOptions,
      ...extraOptions.restOptions,
    },
    explorer: {
      ...DEFAULT_OPTIONS.explorer,
      ...extraOptions.explorer,
    },
  };
}

Best Practices Summary

AspectRecommendation
DirectoryUse common/ for shared keys, types, constants
KeysUse @app/[component]/[feature] format
TypesI prefix for interfaces, T prefix for type aliases
ConstantsUse static classes with SCHEME_SET for validation
DefaultsDefine DEFAULT_OPTIONS constant at file top
ExportsUse barrel exports (index.ts) at every level
ValidationValidate required options in binding()
LoggingLog binding activity with structured messages
ScopeAlways set scope: ComponentName.name
Idempotencyconfigure() is already idempotent via isConfigured guard — no need to add your own

Quick Reference Template

typescript
// common/keys.ts
export class MyComponentBindingKeys {
  static readonly OPTIONS = '@app/my-component/options';
}

// common/types.ts
export interface IMyComponentOptions {
  restOptions: { path: string };
  // ... other options
}

// common/constants.ts (optional)
export class MyConstants {
  static readonly VALUE_A = 'a';
  static readonly VALUE_B = 'b';
}

// common/rest-paths.ts (optional)
export class MyRestPaths {
  static readonly ROOT = '/';
  static readonly BY_ID = '/:id';
}

// common/index.ts
export * from './keys';
export * from './types';
export * from './constants';
export * from './rest-paths';

// component.ts
import { BaseApplication, BaseComponent, inject, CoreBindings, Binding, ValueOrPromise } from '@venizia/ignis';
import { MyComponentBindingKeys, IMyComponentOptions } from './common';
import { MyController } from './controller';

const DEFAULT_OPTIONS: IMyComponentOptions = {
  restOptions: { path: '/my-feature' },
};

export class MyComponent extends BaseComponent {
  constructor(
    @inject({ key: CoreBindings.APPLICATION_INSTANCE })
    private application: BaseApplication,
  ) {
    super({
      scope: MyComponent.name,
      initDefault: { enable: true, container: application },
      bindings: {
        [MyComponentBindingKeys.OPTIONS]: Binding.bind<IMyComponentOptions>({
          key: MyComponentBindingKeys.OPTIONS,
        }).toValue(DEFAULT_OPTIONS),
      },
    });
  }

  override binding(): ValueOrPromise<void> {
    const options = this.application.get<IMyComponentOptions>({
      key: MyComponentBindingKeys.OPTIONS,
      isOptional: true,
    }) ?? DEFAULT_OPTIONS;

    // Register controllers, services, etc.
    this.application.controller(MyController);
  }
}

// index.ts
export * from './common';
export * from './component';
export * from './controller';

See Also