Skip to main content
Slash commands (chat input commands) are Discord’s modern way for users to interact with bots. This guide covers everything you need to create powerful slash commands.

Basic Slash Command

Create a new file in src/app/commands/ with your command name:
import type { CommandData, ChatInputCommand } from 'commandkit';

export const command: CommandData = {
  name: 'ping',
  description: "Ping the bot to check if it's online.",
};

export const chatInput: ChatInputCommand = async (ctx) => {
  const latency = (ctx.client.ws.ping ?? -1).toString();
  const response = `Pong! Latency: ${latency}ms`;

  await ctx.interaction.reply(response);
};
1

Export command data

The command export defines your command’s name, description, and options. This data is sent to Discord’s API.
2

Export chatInput handler

The chatInput function executes when a user runs your slash command.
3

Test your command

Run npm run dev and use /ping in Discord to test your command.

Adding Command Options

Command options let users provide input to your commands:
src/app/commands/userinfo.ts
import type { CommandData, ChatInputCommand } from 'commandkit';
import { ApplicationCommandOptionType, EmbedBuilder } from 'discord.js';

export const command: CommandData = {
  name: 'userinfo',
  description: 'Get information about a user',
  options: [
    {
      name: 'user',
      description: 'The user to get info about',
      type: ApplicationCommandOptionType.User,
      required: true,
    },
  ],
};

export const chatInput: ChatInputCommand = async (ctx) => {
  const user = ctx.options.getUser('user', true);
  const member = await ctx.interaction.guild?.members.fetch(user.id);

  const embed = new EmbedBuilder()
    .setTitle(`${user.username}'s Info`)
    .setThumbnail(user.displayAvatarURL())
    .addFields(
      { name: 'ID', value: user.id, inline: true },
      { name: 'Joined Discord', value: `<t:${Math.floor(user.createdTimestamp / 1000)}:R>`, inline: true },
    );

  if (member) {
    embed.addFields({
      name: 'Joined Server',
      value: `<t:${Math.floor(member.joinedTimestamp! / 1000)}:R>`,
      inline: true,
    });
  }

  await ctx.interaction.reply({ embeds: [embed] });
};

Available Option Types

  • String - Text input
  • Integer - Whole numbers
  • Number - Decimal numbers
  • Boolean - True/false
  • User - User selection
  • Channel - Channel selection
  • Role - Role selection
  • Mentionable - User or role selection
  • Attachment - File upload

Command with Choices

Provide predefined choices for string or number options:
src/app/commands/weather.ts
import type { CommandData, ChatInputCommand } from 'commandkit';
import { ApplicationCommandOptionType } from 'discord.js';

export const command: CommandData = {
  name: 'weather',
  description: 'Get the current weather for a location',
  options: [
    {
      name: 'location',
      description: 'The location to get weather for',
      type: ApplicationCommandOptionType.String,
      required: true,
    },
    {
      name: 'unit',
      description: 'The temperature unit',
      type: ApplicationCommandOptionType.String,
      choices: [
        { name: 'Celsius', value: 'C' },
        { name: 'Fahrenheit', value: 'F' },
      ],
    },
  ],
};

export const chatInput: ChatInputCommand = async (ctx) => {
  const location = ctx.options.getString('location', true);
  const unit = (ctx.options.getString('unit') as 'C' | 'F') || 'C';

  // Fetch and display weather data
  await ctx.interaction.reply(`Fetching weather for ${location} in °${unit}...`);
};

Autocomplete

Provide dynamic suggestions as users type:
src/app/commands/country.ts
import type { CommandData, ChatInputCommand, AutocompleteHandler } from 'commandkit';
import { ApplicationCommandOptionType } from 'discord.js';

const COUNTRIES = [
  { name: 'United States', code: 'US' },
  { name: 'United Kingdom', code: 'GB' },
  { name: 'Canada', code: 'CA' },
  { name: 'Australia', code: 'AU' },
  { name: 'Germany', code: 'DE' },
];

export const command: CommandData = {
  name: 'country',
  description: 'Get information about a country',
  options: [
    {
      name: 'name',
      description: 'The country name',
      type: ApplicationCommandOptionType.String,
      required: true,
      autocomplete: true,
    },
  ],
};

export const autocomplete: AutocompleteHandler = async (ctx) => {
  const focusedValue = ctx.interaction.options.getFocused();
  
  const filtered = COUNTRIES.filter(country =>
    country.name.toLowerCase().includes(focusedValue.toLowerCase())
  );

  await ctx.interaction.respond(
    filtered.slice(0, 25).map(country => ({
      name: country.name,
      value: country.code,
    }))
  );
};

export const chatInput: ChatInputCommand = async (ctx) => {
  const countryCode = ctx.options.getString('name', true);
  const country = COUNTRIES.find(c => c.code === countryCode);

  await ctx.interaction.reply(`You selected: ${country?.name ?? 'Unknown'}`);
};

Deferred Replies

For commands that take time to process:
src/app/commands/analyze.ts
import type { CommandData, ChatInputCommand } from 'commandkit';

export const command: CommandData = {
  name: 'analyze',
  description: 'Analyze data (takes a few seconds)',
};

export const chatInput: ChatInputCommand = async (ctx) => {
  // Defer the reply immediately
  await ctx.interaction.deferReply();

  // Perform time-consuming task
  await someHeavyOperation();

  // Send the final response
  await ctx.interaction.editReply('Analysis complete!');
};

async function someHeavyOperation() {
  // Simulate processing
  await new Promise(resolve => setTimeout(resolve, 3000));
}
Use deferReply({ ephemeral: true }) to make the response visible only to the user who ran the command.

Ephemeral Responses

Send responses only visible to the command user:
src/app/commands/secret.ts
import type { CommandData, ChatInputCommand } from 'commandkit';

export const command: CommandData = {
  name: 'secret',
  description: 'Get a secret message',
};

export const chatInput: ChatInputCommand = async (ctx) => {
  await ctx.interaction.reply({
    content: 'This message is only visible to you!',
    ephemeral: true,
  });
};

Subcommands

Group related commands together:
src/app/commands/admin.ts
import type { CommandData, ChatInputCommand } from 'commandkit';
import { ApplicationCommandOptionType } from 'discord.js';

export const command: CommandData = {
  name: 'admin',
  description: 'Admin commands',
  options: [
    {
      name: 'ban',
      description: 'Ban a user',
      type: ApplicationCommandOptionType.Subcommand,
      options: [
        {
          name: 'user',
          description: 'User to ban',
          type: ApplicationCommandOptionType.User,
          required: true,
        },
      ],
    },
    {
      name: 'kick',
      description: 'Kick a user',
      type: ApplicationCommandOptionType.Subcommand,
      options: [
        {
          name: 'user',
          description: 'User to kick',
          type: ApplicationCommandOptionType.User,
          required: true,
        },
      ],
    },
  ],
};

export const chatInput: ChatInputCommand = async (ctx) => {
  const subcommand = ctx.interaction.options.getSubcommand();

  if (subcommand === 'ban') {
    const user = ctx.options.getUser('user', true);
    // Handle ban logic
    await ctx.interaction.reply(`Banned ${user.username}`);
  } else if (subcommand === 'kick') {
    const user = ctx.options.getUser('user', true);
    // Handle kick logic
    await ctx.interaction.reply(`Kicked ${user.username}`);
  }
};

Error Handling

Always handle errors gracefully:
src/app/commands/risky.ts
import type { CommandData, ChatInputCommand } from 'commandkit';

export const command: CommandData = {
  name: 'risky',
  description: 'A command that might fail',
};

export const chatInput: ChatInputCommand = async (ctx) => {
  try {
    await ctx.interaction.deferReply();

    // Risky operation
    const result = await performRiskyOperation();

    await ctx.interaction.editReply(`Success: ${result}`);
  } catch (error) {
    console.error('Command failed:', error);
    
    const errorMessage = 'An error occurred while executing this command.';
    
    if (ctx.interaction.deferred) {
      await ctx.interaction.editReply(errorMessage);
    } else {
      await ctx.interaction.reply({ content: errorMessage, ephemeral: true });
    }
  }
};

async function performRiskyOperation(): Promise<string> {
  // Simulate an operation that might fail
  if (Math.random() > 0.5) {
    throw new Error('Operation failed');
  }
  return 'Operation successful';
}

Testing Commands

1

Start development server

Run npm run dev to start your bot with hot reload enabled.
2

Wait for registration

New commands take a few seconds to register with Discord. Check the console for confirmation.
3

Test in Discord

Type / in any channel where your bot has permissions to see your commands.
4

Check logs

Monitor your console for errors or debugging information.

Best Practices

  • Use descriptive names: Command and option names should clearly indicate their purpose
  • Validate input: Always validate user input before processing
  • Defer long operations: Use deferReply() for any operation taking more than 2 seconds
  • Handle errors: Never let errors crash your bot or leave interactions unresponsed
  • Use ephemeral for sensitive data: Make responses ephemeral when they contain sensitive or private information

Build docs developers (and LLMs) love