Skip to content

Coming from Necord

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.

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 {}

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.

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}`);
}
}

No DTOs, no @Context() parameter extraction, no service class to group commands into. Each command stands on its own.

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) {
// ...
}
}

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.

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!');
}
}

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'),
);

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 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']})

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) {}
}

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.

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],
})

The env() helper with typed variants (env.number(), env.boolean(), env.required()) replaces NestJS’s ConfigService.

  • No NestJS dependency — your bot is a standalone application, not a NestJS module
  • Component buildersRow(Btn.danger(...)) instead of ActionRowBuilder verbosity
  • Auto-encoding for custom IDs exceeding 100 characters
  • @cooldown() decorator on any handler type
  • Advanced permissions@warden/permissions with roles, scopes, boundaries, explain()
  • Audit logging@audit() decorator
  • Typed configurationenv() helper + config files + injectable Config service
  • 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/drizzle replaces ORM modules, @warden/jobs replaces Bull, and @warden/cache replaces 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-transformer integration for validating command options. In Warden, the Options builder 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.