Skip to main content
CommandKit uses a file-based routing system for commands, where each file in your src/app/commands/ directory automatically becomes a command. This approach provides a clean, organized structure for your Discord bot’s commands.

Command file structure

Commands are defined by exporting specific functions and objects from TypeScript files in your commands directory:
import type { CommandData, ChatInputCommand } from 'commandkit';

export const command: CommandData = {
  name: 'ping',
  description: 'Pong!',
};

export const chatInput: ChatInputCommand = async (ctx) => {
  await ctx.interaction.reply('Pong!');
};

Command exports

Each command file can export the following:
command
CommandData
required
The command definition containing the name, description, and options. This export is required for all commands.
export const command: CommandData = {
  name: 'avatar',
  description: 'Get a user\'s avatar',
  options: [
    {
      name: 'user',
      description: 'The user to get the avatar for',
      type: ApplicationCommandOptionType.User,
    },
  ],
};
chatInput
ChatInputCommand
Handler for slash command interactions. Receives a context object with the interaction, client, and command data.
export const chatInput: ChatInputCommand = async (ctx) => {
  const user = ctx.options.getUser('user') ?? ctx.interaction.user;
  await ctx.interaction.reply(`Avatar: ${user.displayAvatarURL()}`);
};
message
MessageCommand
Handler for prefix-based message commands. Allows your bot to respond to traditional text commands.
export const message: MessageCommand = async (ctx) => {
  await ctx.message.reply('Pong!');
};
userContextMenu
UserContextMenuCommand
Handler for user context menu commands (right-click on a user).
export const userContextMenu: UserContextMenuCommand = async (ctx) => {
  const target = ctx.interaction.targetUser;
  await ctx.interaction.reply(`Selected user: ${target.username}`);
};
messageContextMenu
MessageContextMenuCommand
Handler for message context menu commands (right-click on a message).
export const messageContextMenu: MessageContextMenuCommand = async (ctx) => {
  const target = ctx.interaction.targetMessage;
  await ctx.interaction.reply(`Message author: ${target.author.username}`);
};
autocomplete
AutocompleteCommand
Handler for autocomplete interactions. Provides dynamic option suggestions as users type.
export async function autocomplete(ctx: AutocompleteCommandContext) {
  const arg = ctx.interaction.options.getString('test', false);
  const choices = [
    { name: 'Option 1', value: 'opt1' },
    { name: 'Option 2', value: 'opt2' },
  ];
  
  await ctx.interaction.respond(choices);
}
metadata
CommandMetadata
Additional command configuration for permissions, guilds, and aliases.
export const metadata: CommandMetadata = {
  userPermissions: 'Administrator',
  botPermissions: ['KickMembers', 'BanMembers'],
  guilds: ['1234567890'],
  aliases: ['p', 'pong'],
};
generateMetadata
CommandMetadataFunction
Function to dynamically generate command metadata at runtime.
export const generateMetadata: CommandMetadataFunction = async () => {
  return {
    userPermissions: 'Administrator',
    guilds: await fetchGuildIds(),
  };
};

Command types

Slash commands

Slash commands are Discord’s modern command interface. They provide autocomplete, validation, and a better user experience:
import type { CommandData, ChatInputCommand } from 'commandkit';
import { ApplicationCommandOptionType } from 'discord.js';

export const command: CommandData = {
  name: 'greet',
  description: 'Greet a user',
  options: [
    {
      name: 'user',
      description: 'User to greet',
      type: ApplicationCommandOptionType.User,
      required: true,
    },
  ],
};

export const chatInput: ChatInputCommand = async (ctx) => {
  const user = ctx.options.getUser('user', true);
  await ctx.interaction.reply(`Hello, ${user}!`);
};

Prefix commands

Prefix commands allow users to trigger commands with a text prefix (e.g., !ping):
import type { MessageCommand } from 'commandkit';

export const message: MessageCommand = async (ctx) => {
  const args = ctx.args();
  await ctx.message.reply(`You passed ${args.length} arguments`);
};
Prefix commands require configuration in your bot’s main file to set the command prefix.

Context menu commands

Context menus appear when right-clicking users or messages in Discord:
import type {
  CommandData,
  UserContextMenuCommand,
  MessageContextMenuCommand,
  CommandMetadata,
} from 'commandkit';

export const command: CommandData = {
  name: 'avatar',
  description: 'Get avatar',
};

export const metadata: CommandMetadata = {
  nameAliases: {
    user: 'View Avatar',
    message: "View Author's Avatar",
  },
};

export const userContextMenu: UserContextMenuCommand = async (ctx) => {
  const target = ctx.interaction.targetUser;
  await ctx.interaction.reply({
    embeds: [{
      title: `${target.username}'s Avatar`,
      image: { url: target.displayAvatarURL({ size: 2048 }) },
    }],
  });
};

export const messageContextMenu: MessageContextMenuCommand = async (ctx) => {
  const target = ctx.interaction.targetMessage.author;
  await ctx.interaction.reply({
    embeds: [{
      title: `${target.username}'s Avatar`,
      image: { url: target.displayAvatarURL({ size: 2048 }) },
    }],
  });
};

Directory-based grouping

Organize commands into directories using parentheses for grouping without affecting command names:
src/app/commands/
├── (general)/
│   ├── ping.ts        → /ping
│   ├── help.ts        → /help
│   └── (animal)/
│       ├── cat.ts     → /cat
│       └── dog.ts     → /dog
└── (admin)/
    └── ban.ts         → /ban
Directory names wrapped in parentheses (like-this) are ignored in the command path. This allows you to organize commands without affecting their names.

Command context

All command handlers receive a context object with useful properties and methods:
export const chatInput: ChatInputCommand = async (ctx) => {
  // Discord.js objects
  ctx.interaction  // The interaction object
  ctx.client      // The Discord.js client
  ctx.guild       // The guild (null in DMs)
  ctx.channel     // The channel
  
  // Command info
  ctx.commandName        // The command name
  ctx.invokedCommandName // The invoked name (could be alias)
  ctx.command            // The loaded command object
  
  // Utilities
  ctx.options     // Command options
  ctx.store       // Shared key-value store
  ctx.commandkit  // The CommandKit instance
};

Command metadata

Use metadata to configure permissions, guild restrictions, and aliases:
import type { CommandMetadata } from 'commandkit';

export const metadata: CommandMetadata = {
  // User must have these permissions
  userPermissions: 'Administrator',
  
  // Bot must have these permissions
  botPermissions: ['KickMembers', 'BanMembers'],
  
  // Only available in these guilds
  guilds: ['1234567890', '0987654321'],
  
  // Aliases for message commands
  aliases: ['p', 'pong'],
  
  // Custom names for context menus
  nameAliases: {
    user: 'View Avatar',
    message: 'View Author Avatar',
  },
};

Dynamic metadata

Generate metadata dynamically at runtime:
import type { CommandMetadataFunction } from 'commandkit';

export const generateMetadata: CommandMetadataFunction = async () => {
  const adminGuilds = await fetchAdminGuilds();
  
  return {
    userPermissions: 'Administrator',
    guilds: adminGuilds.map(g => g.id),
  };
};
Dynamic metadata is evaluated when commands are loaded. Changes won’t take effect until the bot restarts or commands are reloaded.

Real-world examples

Multi-handler command

A command that works with both slash commands and prefix commands:
src/app/commands/greet.ts
import type { CommandData, ChatInputCommand, MessageCommand } from 'commandkit';

export const command: CommandData = {
  name: 'greet',
  description: 'Greet command',
};

export const chatInput: ChatInputCommand = async (ctx) => {
  await ctx.interaction.reply(`Hello, ${ctx.interaction.user}!`);
};

export const message: MessageCommand = async (ctx) => {
  await ctx.message.reply(`Hello, ${ctx.message.author}!`);
};

Command with options

src/app/commands/avatar.ts
import type { CommandData, ChatInputCommand } from 'commandkit';
import { ApplicationCommandOptionType } from 'discord.js';

export const command: CommandData = {
  name: 'avatar',
  description: 'Get a user\'s avatar',
  options: [
    {
      name: 'user',
      description: 'The user to get the avatar for',
      type: ApplicationCommandOptionType.User,
    },
  ],
};

export const chatInput: ChatInputCommand = async (ctx) => {
  const user = ctx.options.getUser('user') ?? ctx.interaction.user;
  
  await ctx.interaction.reply({
    embeds: [{
      title: `${user.username}'s Avatar`,
      image: { url: user.displayAvatarURL({ size: 2048 }) },
      color: 0x7289da,
    }],
  });
};

Admin-only command

src/app/commands/(admin)/ban.ts
import type { CommandData, ChatInputCommand, CommandMetadata } from 'commandkit';
import { ApplicationCommandOptionType } from 'discord.js';

export const command: CommandData = {
  name: 'ban',
  description: 'Ban a user from the server',
  options: [
    {
      name: 'user',
      description: 'User to ban',
      type: ApplicationCommandOptionType.User,
      required: true,
    },
    {
      name: 'reason',
      description: 'Reason for ban',
      type: ApplicationCommandOptionType.String,
    },
  ],
};

export const metadata: CommandMetadata = {
  userPermissions: 'BanMembers',
  botPermissions: 'BanMembers',
};

export const chatInput: ChatInputCommand = async (ctx) => {
  const user = ctx.options.getUser('user', true);
  const reason = ctx.options.getString('reason') ?? 'No reason provided';
  
  await ctx.guild?.members.ban(user, { reason });
  await ctx.interaction.reply(`Banned ${user.username}: ${reason}`);
};

Next steps

Events

Learn how to handle Discord events

Middlewares

Add middleware to your commands

Build docs developers (and LLMs) love