Skip to content

Coming from Discordx

Of all the existing Discord bot frameworks, Discordx is probably the closest to Warden in philosophy. You both use TypeScript decorators, both have standalone DI, and both build on discord.js without requiring NestJS or any other meta-framework.

If you’re coming from Discordx, the transition will feel natural. Many concepts map one-to-one — but Warden takes things further with batteries-included utilities, a plugin ecosystem, and a few features that don’t exist anywhere else in the Discord framework space.

In Discordx, commands are methods on a class decorated with @Discord(). The class groups related commands, and each method is a separate slash command. In Warden, each command is its own class. The Options builder replaces the parameter decorators:

import {Discord, Slash, SlashOption} from 'discordx';
import {ApplicationCommandOptionType, CommandInteraction} from 'discord.js';
@Discord()
class ModerationCommands {
@Slash({name: 'warn', description: 'Warn a user'})
async warn(
@SlashOption({name: 'user', description: 'User to warn', type: ApplicationCommandOptionType.User, required: true})
user: User,
@SlashOption({name: 'reason', description: 'Reason', type: ApplicationCommandOptionType.String, required: true})
reason: string,
interaction: CommandInteraction,
) {
await interaction.reply(`Warned ${user.username}: ${reason}`);
}
}

The Warden version has more lines, but each command is fully self-contained — its name, description, options, and handler in one focused file. No grouping classes, no parameter decorators with verbose type enums.

Discordx handles subcommands with @SlashGroup(). Warden uses the entrypoint pattern — a parent class defines the structure, child classes handle each subcommand:

@Discord()
@SlashGroup({name: 'mod', description: 'Moderation'})
@SlashGroup('mod')
class ModerationCommands {
@Slash({name: 'warn', description: 'Warn a user'})
async warn(interaction: CommandInteraction) { ... }
@Slash({name: 'ban', description: 'Ban a user'})
async ban(interaction: CommandInteraction) { ... }
}

More files, but each subcommand is isolated — with its own middleware, permissions, and dependencies.

Discordx uses @On() on methods within a @Discord() class. In Warden, each event gets its own class:

import {Discord, On} from 'discordx';
@Discord()
class Events {
@On({event: 'guildMemberAdd'})
async onMemberJoin([member]: ArgsOf<'guildMemberAdd'>) {
const channel = member.guild.systemChannel;
if (channel) await channel.send(`Welcome, ${member.user.username}!`);
}
}

Where Warden really pulls ahead is event decomposition. Discordx doesn’t have this. Instead of listening to guildMemberUpdate and writing diffing logic, you listen to specific events:

@event({name: 'RoleAdded', event: 'guildMemberRoleAdd'})
export default class RoleAddedEvent implements Event<'guildMemberRoleAdd'> {
public async execute(member: GuildMember, role: Role): Promise<void> {
// fires only when a role is added — you get which role
}
}
@event({name: 'MemberTimedOut', event: 'guildMemberTimeout'})
export default class MemberTimedOutEvent implements Event<'guildMemberTimeout'> {
public async execute(member: GuildMember): Promise<void> {
// fires only on timeout
}
}

Warden ships 40+ decomposed events, and you can define your own with @decompose().

Discordx handles buttons, modals, and select menus with decorators on methods. Warden follows a similar decorator approach, but each component is its own class — and you get some features Discordx doesn’t have:

import {Discord, ButtonComponent, ModalComponent} from 'discordx';
@Discord()
class Components {
@ButtonComponent({id: 'confirm-ban'})
async confirmBan(interaction: ButtonInteraction) {
await interaction.reply('Banned!');
}
@ModalComponent({id: 'warn-reason'})
async warnReason(interaction: ModalSubmitInteraction) {
const reason = interaction.fields.getTextInputValue('reason');
await interaction.reply(`Warning issued: ${reason}`);
}
}

Discordx supports regex matching for custom IDs. Warden uses path-to-regexp instead, which is more readable and gives you automatic parameter extraction:

// Discordx — regex matching
@ButtonComponent({id: /^ban-(\d+)$/})
async ban(interaction: ButtonInteraction) {
const userId = interaction.customId.match(/^ban-(\d+)$/)?.[1]; // manual extraction
}

And Warden goes a step further with auto-encoding. When a custom ID would exceed Discord’s 100-character limit, the framework automatically compresses it into a short token. Your handler doesn’t need to know — params.get() works either way.

Discordx uses the standard discord.js builders. Warden provides shorthand:

// Discordx / discord.js
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder().setCustomId(`ban/${userId}`).setLabel('Confirm').setStyle(ButtonStyle.Danger),
new ButtonBuilder().setCustomId('cancel').setLabel('Cancel').setStyle(ButtonStyle.Secondary),
);

Discordx has a guard system that runs before handlers. Warden’s middleware is similar in concept but more structured. Middleware is a class with DI support, and you can compose them with AND/OR logic:

import {Guard} from 'discordx';
async function IsAdmin(interaction: CommandInteraction, client: Client, next: () => Promise<void>) {
if (interaction.memberPermissions?.has('Administrator')) {
await next();
} else {
await interaction.reply('No permission.');
}
}
@Discord()
class Commands {
@Slash({name: 'ban', description: 'Ban'})
@Guard(IsAdmin)
async ban(interaction: CommandInteraction) { ... }
}

That AND/OR composition doesn’t exist in Discordx. Plus, Warden adds global middleware that runs on every handler:

new Bot()
.middleware([LogExecution, TrackUsage])
.start();

For simple permission checks, Warden also provides @permissions() so you don’t even need a middleware class:

@command({name: 'ban', description: 'Ban'})
@permissions({user: ['BanMembers'], bot: ['BanMembers']})

Discordx has its own DI container (@DIService) or can integrate with TSyringe or typedi. Warden also uses TSyringe, but the @injectable() is implicit — any class with a Warden decorator is automatically registered:

// Discordx with TSyringe
@Discord()
@injectable()
class Commands {
constructor(private caseService: CaseService) {}
}

One fewer decorator on every class.

Discordx doesn’t have a configuration system — you manage environment variables yourself. Warden provides a typed, file-based configuration layer:

src/config/moderation.ts
import {env} from '@warden/core';
export default {
logChannel: env('MOD_LOG_CHANNEL_ID'),
maxWarnings: env.number('MAX_WARNINGS', 3),
muteRole: env('MUTE_ROLE_ID'),
};
// Inject and use anywhere
constructor(private config: Config) {}
const max = this.config.get('moderation.maxWarnings');

With typed env helpers (env.number(), env.boolean(), env.required()), auto-discovery of config files, and an injectable Config service.

Moving from Discordx to Warden, you’ll pick up quite a lot:

  • Event decomposition — 40+ specific events + @decompose() for custom ones
  • Component buildersRow(Btn.danger(...)) instead of ActionRowBuilder verbosity
  • Auto-encoding — custom IDs that exceed 100 chars are handled transparently
  • AND/OR middleware composition[GuildOnly, [Admin, Mod]]
  • Global middleware — runs on every handler
  • @cooldown() — rate limiting on any handler type
  • @permissions() — declarative permission checks
  • Configuration systemenv(), config files, injectable Config
  • Error handler system@errorHandler() with typed + catch-all handlers
  • Plugin ecosystem@warden/drizzle, @warden/cache, @warden/jobs, @warden/permissions, @warden/audit, @warden/i18n, @warden/heartbeat
  • Advanced permissions — custom roles, scopes, boundaries, deny-wins evaluation
  • Audit logging@audit() decorator
  • Utilitiesreply(), confirm(), Embed.*, Paginator, Collector, collect(), dd(), every.*
  • HMR — hot module reloading in development
  • Command diffing — only syncs changes to Discord’s API
  • CLIpnpm create @warden + warden make:* code generation
  • TestingTestBot, FakeInteraction, Vitest
  • Comprehensive docs — warm, written for humans, with migration guides (like this one)

Things you’ll miss (and what to do instead)

Section titled “Things you’ll miss (and what to do instead)”
  • Method-based commands — in Discordx, multiple commands can live on one class. In Warden, each command is its own file. This is more files, but each is focused and self-contained. It’s a trade-off: less grouping flexibility, more clarity.
  • Regex custom ID matching — Discordx lets you use full regex for component matching. Warden uses path-to-regexp, which is more readable but less flexible for edge cases. For most scenarios, path-to-regexp covers everything you need.
  • @SimpleCommand() — Discordx supports prefix-based message commands. Warden is slash-command-first. A community plugin could add this.
  • Discordx plugins — if you used @discordx/pagination, @discordx/utilities, or other Discordx packages, Warden has equivalents: Paginator, Collector, and the various utilities are all built into core.