What is Spraxium
Spraxium is a TypeScript framework that sits on top of Discord.js and provides the architecture layer that Discord.js intentionally leaves out. Discord.js handles the low-level gateway communication, covering WebSocket connections, REST calls, raw events, and data models. Spraxium takes over from there and gives you modules, dependency injection, a guard pipeline, lifecycle hooks, and a deterministic boot sequence that ensures your bot starts the same way every time.
Each feature lives in its own module with clear boundaries and explicit dependencies. The DI container resolves and injects everything automatically; you declare what each class needs and the framework handles the wiring.
Prerequisites
You need three things before starting:
- Node.js 20 or higher. Spraxium targets the current LTS releases. You can verify your installed version by running
node -vin your terminal, and both LTS 20 and LTS 22 are fully supported. - A Discord bot token. Create a new application at Discord Developer Portal, add a Bot to it, and copy the token. This token is what allows your bot to authenticate with the Discord gateway.
- TypeScript project. The CLI scaffold automatically sets up tsconfig.json with experimentalDecorators and emitDecoratorMetadata enabled, which are required for the decorator-based dependency injection system that Spraxium relies on.
Never expose your bot token
The token acts as your bot's password, anyone who has it can take full control of the bot. Always keep it in .env and ensure .env is listed in .gitignore. The Spraxium scaffold adds that rule to the generated .gitignore automatically.
Using a test server
During development, always point your bot at a private Discord server you own. That way you can register slash commands instantly with guild-scoped registration and test interactions without affecting any real users. Keep it separate from any production server to avoid accidental command conflicts or rate limit issues.
Scaffold the project
Run the create command to scaffold a project with the correct folder structure, TypeScript configuration, and all dependencies pre-installed. The CLI asks for a template, package manager, and whether to initialize a Git repository.
npx @spraxium/cli@latest new my-botStarter structure
Each file has a specific architectural role:
- main.ts: bootstrap only; validates env and starts the framework
- app.env.ts: typed env schema validated before Discord connects
- app.module.ts: composition root, imports feature modules, no business logic
- modules/: one folder per feature domain
Bootstrap
main.ts is the only file that touches the framework factory. It validates environment variables, creates the application, loads the module graph, and opens the Discord connection.
import { IntentPreset, SpraxiumFactory } from '@spraxium/core';
import { EnvValidator } from '@spraxium/env';
import { AppEnv } from './app.env';
import { AppModule } from './app.module';
async function main(): Promise<void> {
const env = EnvValidator.validate(AppEnv);
const app = await SpraxiumFactory.create({ token: env.token });
app.useModule(AppModule);
app.intents(IntentPreset.Standard);
await app.listen();
}
main();EnvValidator.validate reads .env and validates every field against AppEnv; missing or malformed values throw immediately, before Discord connects. app.listen() resolves the DI graph, instantiates providers, binds listeners, and logs in.
Root module
AppModule is the composition root: it imports feature modules but contains no providers or listeners. Adding features means adding module imports here.
import { Module } from '@spraxium/common';
import { DefaultModule } from './modules/default/default.module';
@Module({
imports: [DefaultModule],
})
export class AppModule {}Core architectural rules
Three rules that underpin how the framework operates:
- One module per feature domain. Each domain in your bot, whether it handles moderation, commands, events, or external integrations, gets its own dedicated module folder with its own service, listener, and module files. Cross-feature dependencies go through explicit imports and exports rather than shared global state, which makes it easy to understand exactly what each module depends on.
- Dependency injection for everything. You should never instantiate services manually with the new keyword. Instead, declare dependencies as constructor parameters and let the DI container resolve them automatically. This keeps coupling explicit, makes testing straightforward with mock injection, and ensures the framework controls the lifecycle of every service.
- Startup validation before Discord. The entire DI graph is resolved and all providers are instantiated before the Discord login happens. If any service has invalid configuration or a missing dependency, the error surfaces at boot time with a clear message rather than appearing after deployment when a user triggers the affected command.