Skip to main content
Middlewares in CommandKit allow you to execute code before and after command execution. They’re perfect for implementing permissions checks, logging, rate limiting, and other cross-cutting concerns.

Middleware pattern

Middlewares are defined by exporting beforeExecute and/or afterExecute functions from files with specific naming patterns:
import { MiddlewareContext, stopMiddlewares } from 'commandkit';

export function beforeExecute(ctx: MiddlewareContext) {
  console.log(`${ctx.commandName} will be executed`);
}

export function afterExecute(ctx: MiddlewareContext) {
  console.log(`${ctx.commandName} has been executed`);
}

Middleware types

CommandKit supports three levels of middleware scope:
Applies to all commands. Create a file named +global-middleware.ts in your commands directory:
src/app/commands/+global-middleware.ts
import { Logger, MiddlewareContext } from 'commandkit';

export function beforeExecute(ctx: MiddlewareContext) {
  Logger.info(`Global middleware: ${ctx.commandName} will be executed`);
}

export function afterExecute(ctx: MiddlewareContext) {
  Logger.info(`Global middleware: ${ctx.commandName} has been executed`);
}

Middleware execution order

Middlewares execute in a specific order from outermost to innermost:
1

Global beforeExecute

Runs first for all commands
2

Directory beforeExecute

Runs for commands in the directory
3

Command beforeExecute

Runs for the specific command
4

Command execution

The actual command handler runs
5

Command afterExecute

Runs after the specific command
6

Directory afterExecute

Runs after commands in the directory
7

Global afterExecute

Runs last for all commands
Global beforeExecute
  → Directory beforeExecute
    → Command beforeExecute
      → COMMAND EXECUTION
    → Command afterExecute
  → Directory afterExecute
→ Global afterExecute

Stopping middleware chain

Use stopMiddlewares() to prevent further middleware execution and optionally skip command execution:
import { MiddlewareContext, stopMiddlewares } from 'commandkit';

export function beforeExecute(ctx: MiddlewareContext) {
  const user = ctx.isInteraction() ? ctx.interaction.user : ctx.message.author;
  
  if (!hasPermission(user)) {
    if (ctx.isChatInputCommand()) {
      ctx.interaction.reply('You do not have permission!');
    }
    
    // Stop all remaining middlewares and skip command execution
    stopMiddlewares();
  }
}
When stopMiddlewares() is called in beforeExecute, the command will not execute and no afterExecute middlewares will run.

Middleware context

The MiddlewareContext extends the command context with additional methods:
ctx.commandName
string
The name of the command being executed
ctx.invokedCommandName
string
The invoked command name (could be an alias for message commands)
ctx.store
Collection<string, any>
Shared key-value store accessible across all middlewares and the command
ctx.isInteraction()
boolean
Check if the command was triggered by an interaction
ctx.isChatInputCommand()
boolean
Check if the command was triggered by a slash command
ctx.isMessage()
boolean
Check if the command was triggered by a message
ctx.setCommandRunner()
(fn: RunCommand) => void
Wrap the command execution with custom logic:
ctx.setCommandRunner(async (execute) => {
  console.log('Before command');
  await execute();
  console.log('After command');
});

Common use cases

Permission checks

Check if users have the required permissions:
src/app/commands/(admin)/+middleware.ts
import { Logger, MiddlewareContext, stopMiddlewares } from 'commandkit';
import { MessageFlags } from 'discord.js';

export function beforeExecute(ctx: MiddlewareContext) {
  const user = ctx.isInteraction() ? ctx.interaction.user : ctx.message.author;
  
  // Check if user is an admin
  if (user.id !== 'ADMIN_USER_ID') {
    if (ctx.isChatInputCommand()) {
      ctx.interaction.reply({
        content: 'You are not allowed to use this command.',
        flags: MessageFlags.Ephemeral,
      });
    } else {
      ctx.message.reply('You are not allowed to use this command.');
    }
    
    stopMiddlewares();
  }
}

Command logging

Log all command executions:
src/app/commands/+global-middleware.ts
import { Logger, MiddlewareContext } from 'commandkit';

export function beforeExecute(ctx: MiddlewareContext) {
  const user = ctx.isInteraction() ? ctx.interaction.user : ctx.message.author;
  const guild = ctx.guild?.name ?? 'DM';
  
  Logger.info(`[${guild}] ${user.tag} executed /${ctx.commandName}`);
}

export function afterExecute(ctx: MiddlewareContext) {
  Logger.info(`Command ${ctx.commandName} completed successfully`);
}

Rate limiting

Implement cooldowns for commands:
src/app/commands/+global-middleware.ts
import { MiddlewareContext, stopMiddlewares } from 'commandkit';
import { Collection } from 'discord.js';

const cooldowns = new Collection<string, number>();
const COOLDOWN_DURATION = 5000; // 5 seconds

export function beforeExecute(ctx: MiddlewareContext) {
  const user = ctx.isInteraction() ? ctx.interaction.user : ctx.message.author;
  const key = `${user.id}-${ctx.commandName}`;
  
  const lastUsed = cooldowns.get(key);
  const now = Date.now();
  
  if (lastUsed && now - lastUsed < COOLDOWN_DURATION) {
    const remaining = Math.ceil((COOLDOWN_DURATION - (now - lastUsed)) / 1000);
    
    if (ctx.isChatInputCommand()) {
      ctx.interaction.reply({
        content: `Please wait ${remaining}s before using this command again.`,
        ephemeral: true,
      });
    } else {
      ctx.message.reply(`Please wait ${remaining}s before using this command again.`);
    }
    
    stopMiddlewares();
    return;
  }
  
  cooldowns.set(key, now);
}

Performance monitoring

Track command execution time:
src/app/commands/+global-middleware.ts
import { Logger, MiddlewareContext } from 'commandkit';

export function beforeExecute(ctx: MiddlewareContext) {
  ctx.store.set('startTime', Date.now());
}

export function afterExecute(ctx: MiddlewareContext) {
  const startTime = ctx.store.get<number>('startTime');
  if (!startTime) return;
  
  const duration = Date.now() - startTime;
  Logger.info(`Command ${ctx.commandName} took ${duration}ms to execute`);
  
  // Log slow commands
  if (duration > 1000) {
    Logger.warn(`Slow command detected: ${ctx.commandName} took ${duration}ms`);
  }
}

Data validation

Validate command inputs before execution:
src/app/commands/(premium)/+middleware.ts
import { MiddlewareContext, stopMiddlewares } from 'commandkit';

export function beforeExecute(ctx: MiddlewareContext) {
  const user = ctx.isInteraction() ? ctx.interaction.user : ctx.message.author;
  
  // Check if user has premium
  const hasPremium = checkPremiumStatus(user.id);
  
  if (!hasPremium) {
    if (ctx.isChatInputCommand()) {
      ctx.interaction.reply({
        content: 'This command requires premium membership!',
        ephemeral: true,
      });
    }
    
    stopMiddlewares();
  }
}

function checkPremiumStatus(userId: string): boolean {
  // Your premium check logic
  return false;
}

Maintenance mode

Disable commands during maintenance:
src/app/commands/+global-middleware.ts
import { MiddlewareContext, stopMiddlewares } from 'commandkit';

const MAINTENANCE_MODE = process.env.MAINTENANCE_MODE === 'true';
const ADMIN_IDS = ['123456789', '987654321'];

export function beforeExecute(ctx: MiddlewareContext) {
  if (!MAINTENANCE_MODE) return;
  
  const user = ctx.isInteraction() ? ctx.interaction.user : ctx.message.author;
  
  // Allow admins to use commands during maintenance
  if (ADMIN_IDS.includes(user.id)) return;
  
  if (ctx.isChatInputCommand()) {
    ctx.interaction.reply({
      content: '🔧 Bot is currently under maintenance. Please try again later.',
      ephemeral: true,
    });
  }
  
  stopMiddlewares();
}

Wrapping command execution

Use setCommandRunner to wrap the command execution with custom logic:
import { MiddlewareContext } from 'commandkit';

export function beforeExecute(ctx: MiddlewareContext) {
  ctx.setCommandRunner(async (execute) => {
    console.log('About to execute command');
    
    try {
      await execute(); // Run the command
      console.log('Command executed successfully');
    } catch (error) {
      console.error('Command execution failed:', error);
      throw error;
    }
  });
}
setCommandRunner allows you to control the exact execution flow, including error handling and async operations around the command.

Shared data with store

Share data between middlewares and commands using the context store:
// In middleware
export function beforeExecute(ctx: MiddlewareContext) {
  ctx.store.set('userId', ctx.interaction.user.id);
  ctx.store.set('startTime', Date.now());
}

export function afterExecute(ctx: MiddlewareContext) {
  const userId = ctx.store.get('userId');
  const duration = Date.now() - ctx.store.get('startTime');
  console.log(`User ${userId} command took ${duration}ms`);
}
// In command
export const chatInput: ChatInputCommand = async (ctx) => {
  const userId = ctx.store.get('userId');
  // Use the data set by middleware
};

Best practices

1

Keep middlewares focused

Each middleware should handle one concern (permissions, logging, etc.)
2

Use appropriate scope

  • Global: Cross-cutting concerns (logging, rate limiting)
  • Directory: Related commands (admin permissions)
  • Command: Specific command needs
3

Handle both interaction and message contexts

const user = ctx.isInteraction() ? ctx.interaction.user : ctx.message.author;
4

Use the store for shared data

Pass data between middlewares and commands using ctx.store
5

Stop execution when needed

Call stopMiddlewares() to prevent command execution when validation fails

Built-in middlewares

CommandKit includes built-in permission middleware that automatically handles the userPermissions and botPermissions from command metadata:
import type { CommandMetadata } from 'commandkit';

export const metadata: CommandMetadata = {
  userPermissions: 'Administrator',
  botPermissions: ['KickMembers', 'BanMembers'],
};
This middleware runs automatically unless disabled in your configuration:
commandkit.config.ts
export default defineConfig({
  disablePermissionsMiddleware: true, // Disable built-in permissions
});

Next steps

Plugins

Extend CommandKit with plugins

Configuration

Configure your CommandKit setup

Build docs developers (and LLMs) love