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.
-
Swap the client. Replace
new Client(...)withnew SpearClient(...). It is a discord.jsClient(it extends it), so your existingclient.on,client.once,client.ws,client.restcode is unchanged — but now it also routes interactions to spearkit's registries.import { SpearClient, Intents } from "spearkit"; const client = new SpearClient({ intents: Intents.default }); -
Move commands to
command(). Replace a hand-writtenSlashCommandBuilderplus its branch of theinteractionCreateswitch with a single co-located definition. Option values become fully typed. -
Move events to
event(). Replaceclient.on(Events.X, ...)listeners withevent("x", ...)definitions and register them. -
Move components to spearkit builders. Replace manual
ButtonBuilder+ custom-id parsing withbutton(),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.
File-based loading
Instead of importing and registering every handler by hand, you can keep one command, event or component per file and let spearkit discover them. The loader imports a directory…
API reference
Every symbol spearkit exports, in addition to the entire re-exported discord.js surface. Import any of these from "spearkit".