Coming from Necord
Introduction
Section titled “Introduction”If you’ve been building bots with Necord, you’re already comfortable with decorators and dependency injection — great, because Warden is built on the same principles. The biggest difference is that Warden is standalone: no NestJS required. Everything you need is in the framework.
This means less infrastructure to manage, fewer abstractions between you and discord.js, and a much lower barrier to entry. If you love the Necord developer experience but wish you didn’t need all of NestJS to get it, Warden is for you.
NestJS vs standalone
Section titled “NestJS vs standalone”In Necord, your bot is a NestJS application. In Warden, the bot is the application. No modules, no decorators on modules, no NestJS bootstrapping:
@Module({ imports: [ NecordModule.forRoot({ token: process.env.DISCORD_TOKEN, intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages], }), ],})export class AppModule {}import {Bot} from '@warden/core';
new Bot() .discover(d => d .interactions('./src/{commands,buttons,modals,menus}/**/*.ts') .events('./src/events/**/*.ts')) .intents(i => i.defaults()) .partials(p => p.defaults()) .start();If you were using NestJS solely for the Discord bot, this is a significant simplification. If you had HTTP endpoints, microservices, or other NestJS modules alongside your bot, see the “Things You’ll Miss” section below.
Commands
Section titled “Commands”Necord commands are methods on injectable service classes, decorated with @SlashCommand(). In Warden, each command is its own class. This might feel like more files, but each file is self-contained and focused:
@Injectable()export class ModerationCommands { @SlashCommand({ name: 'warn', description: 'Warn a user', }) public async warn( @Context() [interaction]: SlashCommandContext, @Options() { user, reason }: WarnDto, ) { await interaction.reply(`Warned ${user.username}: ${reason}`); }}@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(), ];
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}`)); }}No DTOs, no @Context() parameter extraction, no service class to group commands into. Each command stands on its own.
Events
Section titled “Events”Necord uses @On() or @Once() decorators on methods. In Warden, each event is its own class:
@Injectable()export class BotEvents { @On('guildMemberAdd') public async onMemberJoin(member: GuildMember) { // ... }
@On('guildMemberRemove') public async onMemberLeave(member: GuildMember) { // ... }}@event({name: 'MemberJoinLog', event: 'guildMemberAdd'})export default class MemberJoinLogEvent implements Event<'guildMemberAdd'> { public async execute(member: GuildMember): Promise<void> { // ... }}Where Warden pulls ahead is event decomposition. Necord has this too — custom events like guildMemberBoost and guildMemberRoleAdd — and it was one of Necord’s best features. Warden ships 40+ decomposed events and also lets you define your own with @decompose(). If you loved Necord’s custom events, you’ll feel right at home.
Components
Section titled “Components”Necord handles buttons, modals, and select menus with decorators on methods. Necord’s standout feature here is path-to-regexp for dynamic custom IDs — @Button('ban/:userId') with @ComponentParam('userId'). Warden has this too, with a slightly different approach using Params:
@Injectable()export class ButtonHandlers { @Button('confirm-ban') public async confirmBan(@Context() [interaction]: ButtonContext) { await interaction.reply('Banned!'); }}@button({customId: 'ban/:userId'})export default class BanButton implements Button { public async execute(interaction: ButtonInteraction, params: Params): Promise<void> { const userId = params.get('userId')!; // ... }}Warden also adds something Necord doesn’t have: auto-encoding. When your custom ID would exceed Discord’s 100-character limit, the framework automatically compresses it into a short token and decompresses it when the handler runs. You never think about the limit.
On top of that, Warden ships component builders that eliminate the ActionRowBuilder boilerplate:
const row = Row( Btn.danger('Confirm Ban', `ban/${target.id}`), Btn.secondary('Cancel', 'cancel'),);Guards vs middleware
Section titled “Guards vs middleware”Necord inherits NestJS’s full execution pipeline — Guards, Interceptors, Pipes, and Exception Filters. This is powerful but comes with NestJS complexity.
Warden simplifies this to two layers:
- Typed middleware — per handler type, like NestJS Guards but with full type safety on the interaction
- Global middleware — runs on everything, like NestJS Interceptors
// Necord (Guard)@UseGuards(ModeratorGuard)@SlashCommand({ name: 'ban', description: 'Ban a user' })// Warden (Middleware)@command({name: 'ban', description: 'Ban a user', middleware: [IsModerator]})Warden also supports AND/OR composition that NestJS doesn’t have:
middleware: [EnsureGuildIsAvailable, [IsAdmin, IsModerator]]// EnsureGuildIsAvailable AND (IsAdmin OR IsModerator)For permission checks specifically, Warden provides a dedicated @permissions() decorator that’s more concise than writing a Guard:
@permissions({user: ['BanMembers'], bot: ['BanMembers']})Dependency injection
Section titled “Dependency injection”Both frameworks use decorator-based DI. In Necord, you use NestJS’s @Injectable() and constructor injection. In Warden, the @injectable() decorator is implicit. Any class with a Warden decorator (@command(), @event(), etc.) is automatically registered for DI:
@Injectable()export class ModerationService { constructor(private readonly users: UsersService) {}}// Warden — no @injectable() needed@command({name: 'stats', description: 'Stats'})export default class StatsCommand implements Command { constructor(private caseService: CaseService, private logger: Logger) {}}One less decorator on every file. Warden uses tsyringe under the hood, which is lighter than NestJS’s IoC container but covers all the common use cases.
Configuration
Section titled “Configuration”Necord uses NecordModule.forRoot() or forRootAsync(). Warden uses a fluent builder for bot configuration and typed config files for everything else:
NecordModule.forRoot({ token: process.env.DISCORD_TOKEN, intents: [GatewayIntentBits.Guilds], development: [process.env.DEV_GUILD_ID],})// Warden — bot confignew Bot() .discover(d => d .config('./src/config/**/*.ts') .interactions('./src/{commands,buttons,modals,menus}/**/*.ts') .events('./src/events/**/*.ts')) .intents(i => i.defaults()) .registerTo(env('DEV_GUILD_ID')) .start();
// Warden — app config in src/config/moderation.tsexport default { logChannel: env('MOD_LOG_CHANNEL_ID'), maxWarnings: env.number('MAX_WARNINGS', 3),};The env() helper with typed variants (env.number(), env.boolean(), env.required()) replaces NestJS’s ConfigService.
Things you’ll gain
Section titled “Things you’ll gain”- No NestJS dependency — your bot is a standalone application, not a NestJS module
- Component builders —
Row(Btn.danger(...))instead of ActionRowBuilder verbosity - Auto-encoding for custom IDs exceeding 100 characters
@cooldown()decorator on any handler type- Advanced permissions —
@warden/permissionswith roles, scopes, boundaries,explain() - Audit logging —
@audit()decorator - Typed configuration —
env()helper + config files + injectableConfigservice reply(),confirm(),Embed.*presets,collect(),dd(),every.*cron helpers- Implicit
@injectable()— one less decorator on every class @decompose()for custom event decompositions (extending the 40+ built-in ones)- AND/OR middleware composition
- Simpler mental model — no Modules, Providers, Guards, Interceptors, Pipes, Filters. Just commands, events, middleware, and plugins.
Things you’ll miss (and what to do instead)
Section titled “Things you’ll miss (and what to do instead)”- NestJS ecosystem — if you used TypeORM, Bull queues, or other NestJS modules alongside your bot, you’ll need to find Warden equivalents.
@warden/drizzlereplaces ORM modules,@warden/jobsreplaces Bull, and@warden/cachereplaces caching modules. - HTTP endpoints — if your bot served a REST API or dashboard via NestJS controllers, you’ll need
@warden/api(planned for v1.1) or a separate HTTP server. - Request-scoped DI — NestJS supports per-request service instances via
AsyncLocalStorage. Warden’s DI is singleton/transient only. Most bots don’t need request scoping, but if yours does, this is a limitation. - DTO validation — Necord’s
class-validator+class-transformerintegration for validating command options. In Warden, theOptionsbuilder handles validation declaratively (.required(),.min(),.maxLength()) but doesn’t have DTO-level validation. - Interceptors — NestJS interceptors can transform input/output, implement caching, or add timing. In Warden, global middleware covers most of these use cases, and lifecycle events handle observability.