File-based loading
Instead of importing and registering every handler by hand, you can keep one handler per file and let spearkit discover them. The loader imports a directory, inspects each…
client.load
load(dir: string, options?: LoadOptions): Promise<number>client.load imports dir and registers every spearkit-registrable export it finds,
resolving to the number of items registered.
import { fileURLToPath } from "node:url";
import { SpearClient, Intents } from "spearkit";
const here = fileURLToPath(new URL(".", import.meta.url));
const client = new SpearClient({ intents: Intents.default });
const loaded =
(await client.load(`${here}commands`)) +
(await client.load(`${here}events`)) +
(await client.load(`${here}components`));
console.log(`Loaded ${loaded} modules.`);
await client.start(process.env.DISCORD_TOKEN);
await client.deployCommands({ guildId: process.env.GUILD_ID });What gets registered
For every imported file, spearkit walks all of its exports — default and
named — and registers each value that is a command (command, commandGroup),
an event (event), a component (button, stringSelect, modal, …), a
scheduled task (task), a prefix command (prefixCommand), a hybrid command
(hybridCommand), or a context-menu command (userCommand / messageCommand).
Other exports (helpers, constants, types) are ignored. So both of these are picked up:
// default export
export default command({ name: "ping", description: "…", run: (ctx) => ctx.reply("pong") });// named export
export const vote = button({ id: "vote:{choice}", label: "Vote", run: (ctx) => ctx.update(ctx.params.choice) });Options
interface LoadOptions {
extensions?: readonly string[]; // default: [".js", ".mjs", ".cjs"]
typescript?: boolean; // default: false — also import .ts / .mts
recursive?: boolean; // default: true
}extensions— which file extensions to import. By default the loader reads.js,.mjsand.cjs— i.e. compiled JavaScript.typescript— include.tsand.mts. Requires a runtime that can load TypeScript (tsx,bun, or Node with type stripping). Otherwise you get a clear error instead of a rawERR_UNKNOWN_FILE_EXTENSION.recursive— by default the loader descends into subdirectories. Passrecursive: falseto load only the top level.
await client.load(`${here}features`, { recursive: false });
await client.load(`${here}src`, { typescript: true });Default extensions are compiled JavaScript. Build first, then load the emitted output —
npx tsc && node dist/index.js. For a TypeScript source tree undertsxorbun, pass{ typescript: true }.
Standalone helpers
client.load is the method form of loadInto. Both helpers are exported if you
want to collect or register modules separately.
collectModules(dir: string, options?: LoadOptions): Promise<Registerable[]>
loadInto(client: SpearClient, dir: string, options?: LoadOptions): Promise<number>collectModulesimports a directory and returns the registrable exports it found, without touching any client. Use it to inspect, filter, or combine modules before registering.loadIntocallscollectModulesand thenclient.register(...)for you, returning the count.
import { collectModules, loadInto } from "spearkit";
// Inspect before registering:
const items = await collectModules(`${here}commands`);
console.log(`Found ${items.length} modules`);
client.register(...items);
// Or do both in one step:
const count = await loadInto(client, `${here}events`);Example layout
The examples/file-based-loading project keeps each handler in its own file:
file-based-loading/
index.ts # construct client, load each folder, start, deploy
commands/
ping.ts # export default command({ ... })
echo.ts # export default command({ ... })
events/
ready.ts # export default event("clientReady", ...)
components/
vote.ts # export const vote = button({ ... })A command file looks like this:
// commands/echo.ts
import { command, option } from "spearkit";
export default command({
name: "echo",
description: "Repeat a message",
options: {
text: option.string({ description: "What to say", required: true }),
loud: option.boolean({ description: "Shout it" }),
},
// text: string, loud: boolean | undefined
run: (ctx) => ctx.reply(ctx.options.loud ? ctx.options.text.toUpperCase() : ctx.options.text),
});Build the project, then run the compiled index.js; client.load will import the
emitted .js files and register ping, echo, ready and vote for you.
See also
Graceful shutdown
A Ctrl-C or a container stop sends your process a signal. If you don't handle it, the process dies mid-flight — the gateway connection, scheduler timers, and any open database…
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…