spearkit

Migrating from discord.js

spearkit re-exports the entire discord.js surface, so adopting it is not a rewrite — it is a one-line import change followed by optional, incremental cleanup. You can move to…

The drop-in story

Change from "discord.js" to from "spearkit". Nothing else has to change: every discord.js export is available under the same name with the same types.

// before
import { Client, EmbedBuilder, GatewayIntentBits } from "discord.js";

// after — identical behaviour
import { Client, EmbedBuilder, GatewayIntentBits } from "spearkit";

The full classic surface is there — builders, enums, the REST client, route helpers, the Events map, and so on:

import {
  ActionRowBuilder,
  ButtonBuilder,
  ButtonStyle,
  Client,
  EmbedBuilder,
  Events,
  GatewayIntentBits,
  REST,
  Routes,
  SlashCommandBuilder,
} from "spearkit";

const client = new Client({ intents: [GatewayIntentBits.Guilds] });

const pingCommand = new SlashCommandBuilder()
  .setName("ping")
  .setDescription("Replies with an embed and a button");

client.once(Events.ClientReady, (c) => {
  console.log(`Ready as ${c.user.tag}`);
});

client.on(Events.InteractionCreate, async (interaction) => {
  if (!interaction.isChatInputCommand()) return;
  if (interaction.commandName !== "ping") return;

  const embed = new EmbedBuilder().setTitle("Pong!").setDescription(`Latency: ${client.ws.ping}ms`);
  const buttons = new ActionRowBuilder<ButtonBuilder>().addComponents(
    new ButtonBuilder().setCustomId("again").setLabel("Again").setStyle(ButtonStyle.Primary),
  );
  await interaction.reply({ embeds: [embed], components: [buttons] });
});

async function deploy(token: string, appId: string): Promise<void> {
  const rest = new REST().setToken(token);
  await rest.put(Routes.applicationCommands(appId), { body: [pingCommand.toJSON()] });
}

This file is 100% classic discord.js — only the import source changed. It keeps working exactly as before.

Incremental adoption

Once your imports point at spearkit, you can convert pieces one at a time. There is no big-bang migration; old and new styles coexist.

  1. Swap the client. Replace new Client(...) with new SpearClient(...). It is a discord.js Client (it extends it), so your existing client.on, client.once, client.ws, client.rest code is unchanged — but now it also routes interactions to spearkit's registries.

    import { SpearClient, Intents } from "spearkit";
    
    const client = new SpearClient({ intents: Intents.default });
  2. Move commands to command(). Replace a hand-written SlashCommandBuilder plus its branch of the interactionCreate switch with a single co-located definition. Option values become fully typed.

  3. Move events to event(). Replace client.on(Events.X, ...) listeners with event("x", ...) definitions and register them.

  4. Move components to spearkit builders. Replace manual ButtonBuilder + custom-id parsing with button(), stringSelect(), modal(), etc. — spearkit routes them by custom-id namespace and decodes {param}s for you.

Convert at whatever pace suits you; un-migrated handlers keep running through your existing interactionCreate listener.

Before and after

The classic approach hand-routes every interaction through one big switch and parses custom ids by hand:

// discord.js: one listener routes everything by hand
import { Client, Events, GatewayIntentBits } from "discord.js";

const client = new Client({ intents: [GatewayIntentBits.Guilds] });

client.on(Events.InteractionCreate, async (interaction) => {
  if (interaction.isChatInputCommand()) {
    if (interaction.commandName === "greet") {
      const who = interaction.options.getUser("who", true);
      await interaction.reply(`Hello ${who}!`);
    }
  } else if (interaction.isButton()) {
    const [name, choice] = interaction.customId.split(":"); // manual parsing
    if (name === "vote") {
      await interaction.update({ content: `You chose ${choice}` });
    }
  }
});

spearkit co-locates each command and component with its handler, and routes interactions for you — no switch, no manual id parsing:

// spearkit: each handler owns its definition; routing is automatic
import { SpearClient, Intents, command, option, button, row } from "spearkit";

const client = new SpearClient({ intents: Intents.default });

const greet = command({
  name: "greet",
  description: "Greet someone",
  options: { who: option.user({ description: "Who to greet", required: true }) },
  run: (ctx) => ctx.reply(`Hello ${ctx.options.who}!`), // who: User
});

const vote = button({
  id: "vote:{choice}",                              // {choice} is a typed param
  label: "Yes",
  style: "Success",
  run: (ctx) => ctx.update(`You chose ${ctx.params.choice}`), // choice: string
});

client.register(greet, vote);
await client.start(process.env.DISCORD_TOKEN);
await client.deployCommands({ guildId: process.env.GUILD_ID });

// build() requires exactly the params the id pattern declares:
await channel.send({ content: "Vote:", components: [row(vote.build({ choice: "yes" }))] });

The option value (who) and the custom-id param (choice) are inferred from the definitions — no casts, no getUser/split boilerplate, and no interactionCreate switch to maintain.

See also

  • Getting started — install spearkit and build a first bot.
  • Commands — define slash commands with typed options.
  • Components — buttons, selects, modals and custom-id routing.

On this page