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.
nsfwbooleanMarks the command age-restricted.
defaultMemberPermissionsPermissionResolvable | nullDefault permission gate (members without it don't see the command).
nameLocalizations / descriptionLocalizationsLocalizationMapPer-locale name/description.

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 handler throws, spearkit catches it. By default it emits the client's error event and replies with an ephemeral "something went wrong" message. Override that:

client.commands.onError((error, interaction) => {
  console.error(`/${interaction.commandName} failed`, error);
  if (!interaction.replied && !interaction.deferred) {
    return interaction.reply({ content: "Command failed.", ephemeral: true });
  }
});

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.

On this page