Coming from Discordx
Introduction
Section titled “Introduction”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.
Commands
Section titled “Commands”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}`); }}import {command, Command, Options, CommandInteraction, Params, reply, Embed} from '@warden/core';
@command({name: 'warn', description: 'Warn a user'})export default class WarnCommand implements Command { public options = [ Options.user('user', 'User to warn').required(), Options.string('reason', 'Reason').required().maxLength(500), ];
public async execute(interaction: CommandInteraction, params: Params): Promise<void> { const target = interaction.options.getUser('user')!; const reason = interaction.options.getString('reason')!; await reply(interaction, Embed.success(`Warned ${target.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.
Subcommands
Section titled “Subcommands”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) { ... }}@command({name: 'mod', description: 'Moderation commands'})export default class ModCommand extends CommandGroup { public options = [ Options.subcommand('warn', 'Warn a user', [ Options.user('user', 'User to warn').required(), Options.string('reason', 'Reason').required(), ]), Options.subcommand('ban', 'Ban a user', [ Options.user('user', 'User to ban').required(), Options.string('reason', 'Reason').required(), ]), ];}
@command({parent: 'mod', name: 'warn', description: 'Warn a user'})export default class ModWarnCommand extends Subcommand { public async execute(interaction: CommandInteraction, params: Params): Promise<void> { // ... }}More files, but each subcommand is isolated — with its own middleware, permissions, and dependencies.
Events
Section titled “Events”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}!`); }}@event({name: 'MemberJoinLog', event: 'guildMemberAdd'})export default class MemberJoinLogEvent implements Event<'guildMemberAdd'> { constructor(private config: Config) {}
public async execute(member: GuildMember): Promise<void> { const channelId = this.config.get('moderation.logChannel') as string; const channel = member.guild.channels.cache.get(channelId) as TextChannel; 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().
Components
Section titled “Components”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}`); }}@button({customId: 'confirm-ban'})export default class ConfirmBanButton implements Button { public async execute(interaction: ButtonInteraction, params: Params): Promise<void> { await reply(interaction, Embed.success('Banned!')); }}Dynamic custom IDs
Section titled “Dynamic custom IDs”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}// Warden — path-to-regexp + Params@button({customId: 'ban/:userId'})export default class BanButton implements Button { public async execute(interaction: ButtonInteraction, params: Params): Promise<void> { const userId = params.get('userId')!; // automatic 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.
Component builders
Section titled “Component builders”Discordx uses the standard discord.js builders. Warden provides shorthand:
// Discordx / discord.jsconst row = new ActionRowBuilder<ButtonBuilder>().addComponents( new ButtonBuilder().setCustomId(`ban/${userId}`).setLabel('Confirm').setStyle(ButtonStyle.Danger), new ButtonBuilder().setCustomId('cancel').setLabel('Cancel').setStyle(ButtonStyle.Secondary),);// Wardenconst row = Row( Btn.danger('Confirm', `ban/${userId}`), Btn.secondary('Cancel', 'cancel'),);Guards vs middleware
Section titled “Guards vs middleware”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) { ... }}export default class IsAdmin extends Middleware<CommandInteraction> { public async handle(ctx: CommandInteraction, next: NextFn<CommandInteraction>): Promise<void> { if (!ctx.memberPermissions?.has('Administrator')) { throw new AuthorizationError(); } await next(ctx); }}
@command({name: 'ban', description: 'Ban', middleware: [EnsureGuildIsAvailable, [IsAdmin, IsModerator]]})// EnsureGuildIsAvailable AND (IsAdmin OR IsModerator)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']})Dependency injection
Section titled “Dependency injection”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) {}}// Warden — no @injectable() needed@command({name: 'warn', description: 'Warn'})export default class WarnCommand implements Command { constructor(private caseService: CaseService, private logger: Logger) {}}One fewer decorator on every class.
Configuration
Section titled “Configuration”Discordx doesn’t have a configuration system — you manage environment variables yourself. Warden provides a typed, file-based configuration layer:
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 anywhereconstructor(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.
Things you’ll gain
Section titled “Things you’ll gain”Moving from Discordx to Warden, you’ll pick up quite a lot:
- Event decomposition — 40+ specific events +
@decompose()for custom ones - Component builders —
Row(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 system —
env(), config files, injectableConfig - 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 - Utilities —
reply(),confirm(),Embed.*,Paginator,Collector,collect(),dd(),every.* - HMR — hot module reloading in development
- Command diffing — only syncs changes to Discord’s API
- CLI —
pnpm create @warden+warden make:*code generation - Testing —
TestBot,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.