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:
src/app/commands/ping.ts
src/app/commands/ping.js
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 );
};
Export command data
The command export defines your command’s name, description, and options. This data is sent to Discord’s API.
Export chatInput handler
The chatInput function executes when a user runs your slash command.
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
Start development server
Run npm run dev to start your bot with hot reload enabled.
Wait for registration
New commands take a few seconds to register with Discord. Check the console for confirmation.
Test in Discord
Type / in any channel where your bot has permissions to see your commands.
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