Spraxium logoSpraxium

Modules

Build feature modules with clear boundaries, wire them into the root module, and understand DI resolution. Learn the recommended folder structure, module properties, global modules, and the rules that keep your bot maintainable.

Why modules matter

Every feature in your bot belongs to exactly one module. Each module declares what it provides, what it imports, and what it exposes. That boundary lets you grow the bot by adding new modules without touching existing ones.

The standard layout isolates each feature behind a module boundary with its own folder containing at minimum a module file and a service file. Modules evolve independently from each other, and the DI container handles all the wiring between them at boot time without any manual configuration or factory functions.

Module-first architecture
main.ts# Application bootstrap and root module binding
app.module.ts# Central module that composes features
module-name.module.ts# Feature boundary and module wiring
module-name.service.ts# Business logic and providers

Declare the root module

The bootstrap file main.ts is the entry point where you validate the environment, create the application instance, load the root module, and start listening for Discord events. The app.useModule(AppModule) call is the composition entry point, and from there Spraxium loads all imported modules recursively, resolves providers through dependency injection, and binds every registered listener to the Discord client automatically.

src/main.ts
import { IntentPreset, Logger, SpraxiumFactory } from '@spraxium/core';
import { EnvValidator } from '@spraxium/env';
import { AppEnv } from './app.env';
import { AppModule } from './app.module';

async function main() {
  const logger = new Logger('Main');
  const environment = EnvValidator.validate(AppEnv);
  const app = await SpraxiumFactory.create({ token: environment.token });

  app.useModule(AppModule);
  app.intents(IntentPreset.Standard);

  logger.info('Starting application...');
  await app.listen();
}

main();

Build the composition root

The root module AppModule composes features by importing other modules but should never contain feature logic itself. Add a new domain by creating a feature module and importing it here.

src/app.module.ts
import { Module } from '@spraxium/common';
import { AnalyticsModule } from '@spraxium/analytics';
import { ModuleNameModule } from './modules/module-name/module-name.module';

@Module({
  imports: [AnalyticsModule, ModuleNameModule],
})
export class AppModule {}

Module properties reference

Global modules

By default, a module's providers are only available to modules that explicitly import it, which is the desired behavior for most feature modules because it keeps dependencies visible and traceable. Marking a module with @Global() makes its exports available everywhere in the application without requiring explicit imports, but this should be reserved exclusively for cross-cutting concerns like logging, analytics, or shared configuration services that genuinely need to be accessed from every part of the codebase.

Use @Global() sparingly

Marking too many modules as global bypasses the encapsulation that makes the module system valuable and makes dependency tracking harder as the project grows. Reserve @Global() for infrastructure that every module genuinely needs, such as logging or analytics, and keep feature-specific services scoped to their own module with explicit exports.

src/modules/analytics/analytics.module.ts
import { Global, Module } from '@spraxium/common';
import { AnalyticsService } from './analytics.service';

@Global()
@Module({
  providers: [AnalyticsService],
  exports: [AnalyticsService],
})
export class AnalyticsModule {}

Use global modules sparingly because they bypass the encapsulation boundaries that make the module system valuable in the first place. Feature-specific services should always stay local to their own module with explicit exports, and only truly universal infrastructure should be marked as global. If you find yourself marking several modules as global, it usually indicates that module boundaries need to be redesigned rather than bypassed.

Rules to keep modules maintainable

One business boundary per module folder. Export only what other modules must consume. Separate command and listener declarations from service internals. Prefer many small focused modules composed by AppModule over a few large shared ones.

When a single domain grows beyond its original scope, split it into two focused modules. The module system makes this straightforward because each module already owns its own folder and dependencies.