Skip to content

The interaction model


Most of DiscordKit is built around one idea:

Discord sends interactions, and your application routes them to handlers.

Commands are only one kind of interaction.

DiscordKit uses the same application model for commands, components, modal submissions, and autocomplete.

At a high level:

Discord
│ Interaction
discordgo.Session
Router.Handle
Context
Route resolution
Global middleware
Route middleware
Handler
Response
Discord

Understanding each stage explains most of the framework.

Interactions are generated by actions such as:

  • executing a slash command;
  • using a user context command;
  • using a message context command;
  • clicking a button;
  • choosing an item from a select menu;
  • submitting a modal;
  • typing into an autocomplete-enabled command option.

discordgo receives those interactions from Discord.

DiscordKit does not replace that connection.

A DiscordKit Router is connected to discordgo like this:

session.AddHandler(router.Handle)

Router.Handle is responsible for adapting the incoming discordgo interaction into the DiscordKit application model.

It creates a Context and dispatches it through the Router.

Every DiscordKit handler receives:

*discordkit.Context

A Context exists for exactly one interaction.

It gives the handler access to:

  • the underlying *discordgo.Session;
  • the original interaction;
  • command options;
  • resolved users, roles, channels, members, and attachments;
  • route parameters;
  • form submissions;
  • response helpers;
  • a standard Go context.Context.

For example:

func greet(c *discordkit.Context) error {
user, err := c.RequireUserOption("user")
if err != nil {
return err
}
return c.ReplyText("Hello, " + user.Username)
}

Route resolution depends on the interaction type

Section titled “Route resolution depends on the interaction type”

DiscordKit supports four route categories.

router.Command("ping", pingHandler)

A command with subcommands uses its full command path:

router.Command("admin ban", banHandler)

Components are routed through their custom_id:

router.Component(
"/jobs/:jobID/save",
saveJobHandler,
)

Named route parameters become available through Context:

jobID := c.MustParam("jobID")

Modal submissions use the same pattern-based routing model:

router.Modal(
"/jobs/:jobID/edit",
editJobHandler,
)

Autocomplete routes combine the command path with the focused option:

router.Autocomplete(
"search",
"query",
searchAutocomplete,
)

Middleware can run behavior before or after application handlers.

DiscordKit middleware follows the standard Go wrapping model:

type Middleware func(Handler) Handler

For example:

router := discordkit.NewRouter(
discordkit.Logging(nil),
discordkit.RequireGuild(),
)

Global middleware is applied to all matched routes.

A route can also have additional middleware:

router.Command(
"admin",
adminHandler,
discordkit.RequirePermissions(
discordgo.PermissionAdministrator,
),
)

The Router automatically applies DiscordKit’s recovery middleware around dispatch.

If a handler panics, the panic becomes a *discordkit.PanicError instead of escaping through the interaction handler.

Recovery does not automatically send an error message to the user.

Applications remain responsible for deciding how user-facing errors should be presented.

A Handler is:

type Handler func(*Context) error

Returning errors instead of handling every failure locally allows errors to propagate through middleware and eventually reach the Router error hook.

For example:

router.OnError(func(c *discordkit.Context, err error) {
slog.Error("interaction failed", "error", err)
})

If no custom hook is installed, DiscordKit logs the error using slog.

Discord interactions must be acknowledged according to Discord’s interaction rules.

A handler may send an initial response:

return c.ReplyText("Done")

or defer work:

if err := c.Defer(false); err != nil {
return err
}
// perform work...
_, err := c.Edit(
discordkit.MessageSpec{
Content: "Done",
},
)
return err

DiscordKit tracks this lifecycle to prevent invalid sequences such as sending two initial responses.

A larger application can register different interaction types in the same Router:

router := discordkit.NewRouter(
discordkit.Logging(nil),
)
router.Command(
"jobs search",
searchJobs,
)
router.Component(
"/jobs/:jobID/save",
saveJob,
)
router.Modal(
"/jobs/:jobID/edit",
editJob,
)
router.Autocomplete(
"jobs search",
"query",
autocompleteJobs,
)

These routes use the same Context, Handler, middleware, error handling, and response model.

That consistency is one of DiscordKit’s central design goals.

A useful way to think about DiscordKit is:

Discord interaction
+
routing information
+
application middleware
=
handler execution

The framework does not attempt to hide Discord.

Instead, it provides a structured path from an incoming Discord interaction to application code.

Continue with Router to learn how route registration, groups, parameters, conflicts, and middleware composition work.