spearkit
Guides

Commands

Slash commands in spearkit are defined as a single object: the metadata, the typed options, and the handler all live together. spearkit serialises them for discord and routes…

A first command

import { command } from "spearkit";

export const ping = command({
  name: "ping",
  description: "Check latency",
  run: (ctx) => ctx.reply(`Pong! ${ctx.client.ws.ping}ms`),
});

Register it on a client (client.register(ping)) and deploy it (see Deployment). That's the whole loop.

The command context

The handler receives a CommandContext. It wraps the discord.js ChatInputCommandInteraction and adds ergonomic accessors and reply helpers.

MemberDescription
ctx.optionsResolved, fully-typed option values (see Options).
ctx.commandNameThe invoked command name.
ctx.subcommandThe invoked subcommand name, or null.
ctx.showModal(modal)Present a modal in response.
ctx.user / ctx.member / ctx.guild / ctx.guildId / ctx.channel / ctx.channelId / ctx.localeActor and location accessors.
ctx.reply / ctx.replyEphemeral / ctx.defer / ctx.editReply / ctx.followUp / ctx.send / ctx.errorReply helpers (see Contexts).
ctx.interactionThe raw discord.js interaction, for anything not wrapped.
import { command, option } from "spearkit";

export const echo = command({
  name: "echo",
  description: "Repeat a message",
  options: {
    text: option.string({ description: "What to say", required: true }),
    times: option.integer({ description: "Repeat count", minValue: 1, maxValue: 5 }),
  },
  run: (ctx) => {
    ctx.options.text;  // string
    ctx.options.times; // number | undefined
    return ctx.reply({
      content: ctx.options.text.repeat(ctx.options.times ?? 1),
      ephemeral: true,
    });
  },
});

Options are covered in depth in Options.

Command metadata

import { command, PermissionFlagsBits } from "spearkit";

export const purge = command({
  name: "purge",
  description: "Delete recent messages",
  guildOnly: true,                                  // only usable in guilds
  nsfw: false,                                      // age-restricted command
  defaultMemberPermissions: PermissionFlagsBits.ManageMessages, // who sees it by default
  nameLocalizations: { tr: "temizle" },             // localized name
  descriptionLocalizations: { tr: "Mesajları sil" },
  run: (ctx) => ctx.reply("…"),
});
FieldTypeEffect
guildOnlybooleanRestricts the command to guild contexts (alias for contexts: ["guild"]).
install("guild" | "user")[]App installation targets → integration_types. ["user"] makes the command user-installable.
contexts("guild" | "botDm" | "privateChannel")[]Where the command may run → contexts.
nsfwbooleanMarks the command age-restricted.
defaultMemberPermissionsPermissionResolvable | nullDefault permission gate (members without it don't see the command).
nameLocalizations / descriptionLocalizationsLocalizationMapPer-locale name/description.
cooldownnumber | CooldownConfigRate-limit the command (a number is milliseconds). See Cooldowns.
guardsreadonly Guard[]Preconditions run before the handler. See Guards.
autoDeferboolean | { ephemeral?, delayMs? }Auto-deferReply() if the handler is slow (>~2s), preventing Unknown interaction. Respond via ctx.send/ctx.editReply.

User-installable apps and contexts

Commands expose two orthogonal Discord axes:

  • install — where your app is installed: guild install ("guild") or per-user install ("user"), serialised to integration_types.
  • contexts — where a command may run: "guild", "botDm" or "privateChannel" (private channels are only reachable via user install).
import { command } from "spearkit";

export const remind = command({
  name: "remind",
  description: "Set a reminder from anywhere",
  install: ["user"],                            // integration_types: [UserInstall]
  contexts: ["guild", "botDm", "privateChannel"],
  run: (ctx) => ctx.reply(`Reminder! ${ctx.interaction.context ?? ""}`),
});

guildOnly: true keeps working as an alias for contexts: ["guild"]; passing both it and an explicit contexts list throws at definition time. The app itself must allow user installs in the Developer Portal for integration_types: [UserInstall] to be accepted.

Subcommands and groups

For commands with subcommands, use commandGroup together with subcommand and (optionally) subcommandGroup. Each subcommand has its own typed options and handler; spearkit routes to the right one automatically.

import { commandGroup, subcommand, subcommandGroup, option } from "spearkit";

export const admin = commandGroup({
  name: "admin",
  description: "Administration",
  guildOnly: true,
  // Direct subcommands: /admin say
  subcommands: {
    say: subcommand({
      description: "Make the bot say something",
      options: { message: option.string({ description: "Message", required: true }) },
      run: (ctx) => ctx.reply(ctx.options.message),
    }),
  },
  // Grouped subcommands: /admin users ban
  groups: {
    users: subcommandGroup({
      description: "Manage users",
      subcommands: {
        ban: subcommand({
          description: "Ban a member",
          options: {
            target: option.user({ description: "Member", required: true }),
            reason: option.string({ description: "Reason" }),
          },
          run: (ctx) =>
            ctx.reply(`Banned ${ctx.options.target.tag}: ${ctx.options.reason ?? "no reason"}`),
        }),
      },
    }),
  },
});

Inside a subcommand handler, ctx.options is typed from that subcommand's options. There is no switch (subcommand) to write — spearkit dispatches by the invoked subcommand group/name.

The command registry

client.commands is a CommandRegistry. You usually feed it through client.register(...), but you can use it directly:

import { CommandRegistry } from "spearkit";

const registry = new CommandRegistry();
registry.add(ping, echo, admin);

registry.get("ping");   // SlashCommand | undefined
registry.names;         // string[]
registry.size;          // number
registry.remove("ping"); // boolean
registry.toJSON();      // REST payloads for all commands

SpearClient calls registry.handle(interaction) and registry.handleAutocomplete(interaction) for you on every interaction.

Error handling

If a routed handler throws, spearkit logs it and sends a safe response. Configure one policy for slash commands, components, context menus, and prefix commands:

const client = new SpearClient({
  onHandlerError: ({ source, name, error }) => {
    report(error, { source, name });
    return "That action failed. Try again later."; // override safe reply
    // return false; // suppress automatic reply
  },
});

Individual registries still expose .onError(...) for advanced overrides.

Automatic help

helpCommand() reads the live slash, prefix, and context-menu registries and renders a user-scoped paginator:

import { helpCommand } from "spearkit";

client.register(
  ping,
  settings,
  helpCommand({
    title: "Bot commands",
    pageSize: 8,
    // transform: (entries) => entries.filter(canShow),
  }),
);

The help command excludes itself. Set includePrefix: false or includeContextMenus: false to limit surfaces. transform can filter or reorder entries per invocation.

Deployment

Commands must be registered with discord before they appear. spearkit gives you two ways.

From the client (uses the client's authenticated REST; call after ready):

await client.start(process.env.DISCORD_TOKEN);
await client.deployCommands({ guildId: process.env.GUILD_ID }); // omit guildId for global

Standalone (a separate deploy script, no running client needed):

import { CommandRegistry } from "spearkit";

const registry = new CommandRegistry().add(ping, echo, admin);
await registry.deploy({
  token: process.env.DISCORD_TOKEN,
  applicationId: process.env.DISCORD_APP_ID,
  guildId: process.env.GUILD_ID, // optional
});

Guild deploys apply instantly and are ideal during development. Global deploys (no guildId) can take up to an hour to propagate.

See also

  • Options — typed option builders, choices, autocomplete.
  • Components — buttons, selects, modals.
  • Client — registering and deploying from the client.
  • Contexts — the reply helpers every handler shares.
  • Cooldowns — rate-limit a command with cooldown.
  • Guards — gate a command with guards.

On this page