Your first command
In this guide, you will create a /ping command and connect its declaration to a DiscordKit handler.
This introduces an important DiscordKit concept:
declaring a Discord command and routing its interaction are two separate operations.
Define the command
Section titled “Define the command”DiscordKit provides typed builders for Discord application commands.
Create a command:
pingCommand := discordkit.Command( "ping", "Check whether the bot is responding",)This creates a CommandBuilder.
At this point, nothing has been sent to Discord yet.
Build the command
Section titled “Build the command”A builder is converted into a standard discordgo application command:
commands, err := discordkit.BuildCommands( pingCommand,)if err != nil { log.Fatal(err)}BuildCommands validates the declarations and returns:
[]*discordgo.ApplicationCommandDiscordKit therefore does not introduce its own command representation at runtime. The final values are normal discordgo types.
Synchronize the command with Discord
Section titled “Synchronize the command with Discord”Once the session is connected, synchronize the desired commands:
_, err = discordkit.SyncCommands( session, session.State.User.ID, commands, discordkit.SyncOptions{},)if err != nil { log.Fatal(err)}SyncCommands compares the commands declared by your application with the commands currently registered on Discord.
It can:
- create missing commands;
- update changed commands;
- leave unchanged commands untouched;
- optionally delete obsolete commands.
By default, obsolete commands are not deleted.
To remove commands that are no longer declared:
discordkit.SyncOptions{ Delete: true,}Register the handler
Section titled “Register the handler”Command synchronization only tells Discord that /ping exists.
Your application still needs to decide what should happen when someone uses it.
Register a route:
err = router.Command( "ping", func(c *discordkit.Context) error { return c.ReplyText("Pong!") },)if err != nil { log.Fatal(err)}When Discord sends a /ping interaction, the router matches the command path and invokes the registered handler.
The complete application
Section titled “The complete application”Your program can now look like this:
package main
import ( "log" "os" "os/signal"
"github.com/bwmarrin/discordgo" "github.com/freitaseric/discordkit")
func main() { token := os.Getenv("DISCORD_TOKEN") if token == "" { log.Fatal("DISCORD_TOKEN is not set") }
session, err := discordgo.New("Bot " + token) if err != nil { log.Fatal(err) }
router := discordkit.NewRouter()
if err := router.Command( "ping", func(c *discordkit.Context) error { return c.ReplyText("Pong!") }, ); err != nil { log.Fatal(err) }
session.AddHandler(router.Handle)
if err := session.Open(); err != nil { log.Fatal(err) } defer session.Close()
commands, err := discordkit.BuildCommands( discordkit.Command( "ping", "Check whether the bot is responding", ), ) if err != nil { log.Fatal(err) }
if _, err := discordkit.SyncCommands( session, session.State.User.ID, commands, discordkit.SyncOptions{}, ); err != nil { log.Fatal(err) }
log.Println("bot connected")
stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt) <-stop}Declaration and routing are intentionally separate
Section titled “Declaration and routing are intentionally separate”These two calls look similar:
discordkit.Command("ping", "Check whether the bot is responding")and:
router.Command("ping", pingHandler)but they solve different problems.
The first declares the command that should exist on Discord.
The second registers the application code that handles its interaction.
Conceptually:
Command declaration ↓BuildCommands ↓SyncCommands ↓Discord knows /ping exists
User executes /ping ↓Discord interaction ↓Router ↓"ping" route ↓HandlerKeeping these responsibilities separate makes command declarations testable and allows synchronization to be handled independently from interaction dispatch.
Guild commands during development
Section titled “Guild commands during development”DiscordKit can synchronize commands to a specific guild:
discordkit.SyncOptions{ GuildID: guildID,}This is useful during development because guild-scoped application commands generally become available faster than global commands.
The same declaration can later be synchronized globally by removing GuildID.
Synchronization plans
Section titled “Synchronization plans”Command synchronization is built around a pure diff operation.
You can inspect what would change without making Discord API requests:
plan := discordkit.DiffCommands( remoteCommands, desiredCommands, true,)A SyncPlan contains:
plan.Createplan.Updateplan.Unchangedplan.DeleteThis is especially useful for tests, tooling, and deployment diagnostics.
What happens when /ping runs?
Section titled “What happens when /ping runs?”When a user executes the command:
- Discord sends an interaction to your application.
- discordgo receives the interaction.
router.Handlecreates a DiscordKitContext.- the router resolves the command path as
ping; - matching middleware is applied;
- your handler runs;
ReplyTextsends the initial interaction response.
You will learn more about this flow in The interaction model.
Next step
Section titled “Next step”Continue with The interaction model to understand how commands, components, modals, autocomplete, the router, and handlers fit together.