dismodule
v0.1.5
Published
A module framework for Discord.js bots
Downloads
884
Maintainers
Readme
dismodule
A plain Discord.js setup gives you nothing but a blank canvas. No structure, no separation of concerns, no conventions for where commands live, how events get registered, or how recurring tasks are managed. As your bot grows, everything ends up tangled in a single file or scattered across imports with no clear ownership.
dismodule fixes that by introducing a single concept: the module — a self-contained unit that owns its slice of the bot.
Each module declares exactly what it needs:
- Slash commands — define the builder and the handler in one place; dismodule registers and deploys them automatically
- Event listeners — subscribe to any Discord.js event with a plain object; no manual
client.on()calls, no risk of registering duplicate listeners across modules - Background tasks — run recurring work on an interval, scoped to your module, started and stopped with the bot lifecycle
- HTTP API routes — optionally expose Express endpoints per module, useful for webhooks or dashboards
- Initialization logic — run async setup code once the bot is ready, without coordinating timing yourself
Every field is optional. A module can be as small as a single slash command or as large as a full feature with commands, events, tasks, and an API surface — the structure stays the same either way.
Split your Discord.js bot into clean, self-contained modules — each with its own commands, events, tasks, and API routes.
modules/
ping/
commands.ts
events.ts
tasks.ts
api.ts
index.ts ← one file ties it all togetherInstall
npm install dismoduleQuick start
.env
DISCORD_TOKEN=your_bot_token
DISCORD_CLIENT_ID=your_application_id
DISCORD_GUILD_ID=your_guild_id # optional, omit for global commandsmain.ts
import { createBot, loadModulesFromDir } from 'dismodule';
import { GatewayIntentBits } from 'discord.js';
async function main() {
const bot = createBot({
token: process.env.DISCORD_TOKEN!,
clientOptions: {
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages],
},
modules: await loadModulesFromDir('./modules'),
deployCommands: { guildId: process.env.DISCORD_GUILD_ID },
});
await bot.start();
}
main().catch(console.error);Modules
Scaffold a module
npx dismodule make-module <name>This creates modules/<name>/ with all the files you need, ready to fill in.
Module structure
modules/ping/index.ts
import { defineModule } from 'dismodule';
import { commands } from './commands';
import { events } from './events';
import { tasks } from './tasks';
import registerApi from './api';
export default defineModule({
name: 'ping',
commands,
events,
tasks,
registerApi,
});Every field except name is optional — include only what your module needs.
Slash commands
modules/ping/commands.ts
import { CommandDefinition } from 'dismodule';
import { SlashCommandBuilder } from 'discord.js';
export const commands: CommandDefinition[] = [
{
builder: new SlashCommandBuilder()
.setName('ping')
.setDescription('Replies with pong'),
execute: async (interaction) => {
await interaction.reply('Pong!');
},
},
];Events
modules/ping/events.ts
import { EventHandlers } from 'dismodule';
export const events: EventHandlers = {
messageCreate: (message) => {
if (message.author.bot) return;
console.log(`${message.author.tag}: ${message.content}`);
},
};All standard Discord.js events (messageCreate, guildMemberAdd, voiceStateUpdate, …) are supported and fully typed.
Background tasks
modules/ping/tasks.ts
import { ModuleTask } from 'dismodule';
export const tasks: ModuleTask[] = [
{
name: 'heartbeat',
interval: 60_000, // ms
run: async (client) => {
console.log(`Guilds: ${client.guilds.cache.size}`);
},
},
];Tasks start automatically when the bot is ready and are stopped cleanly on bot.stop().
HTTP API routes (optional)
Requires express to be installed. Enable the API server in createBot:
const bot = createBot({
// ...
api: { port: 3000 },
});modules/ping/api.ts
import { HttpApp } from 'dismodule';
import { Client } from 'discord.js';
export default function registerApi(app: HttpApp, client: Client) {
app.get('/api/ping', (_req, res) => {
res.json({ ping: client.ws.ping, guilds: client.guilds.cache.size });
});
}Auto-loading modules
loadModulesFromDir scans a directory and automatically imports every subdirectory that has a valid index.js default export.
modules: await loadModulesFromDir('./modules'),Adding a new module is as simple as dropping a new folder in modules/ — no changes to main.ts required.
Note: The directory is loaded from compiled JS files. Make sure to build your project before running, or use a TypeScript runner like
ts-node.
CLI
make-module
Scaffolds a new module with all the boilerplate files:
npx dismodule make-module <name>Creates modules/<name>/ with:
| File | Purpose |
|---|---|
| index.ts | Module entry point |
| commands.ts | Slash command definitions |
| commandsListener.ts | Command handler functions |
| events.ts | Discord event listeners |
| tasks.ts | Background tasks |
| api.ts | Express API routes |
refresh-commands
Compares your local slash commands against what is currently registered on Discord, shows a diff, and pushes the changes.
npx dismodule refresh-commandsReads DISCORD_TOKEN, DISCORD_CLIENT_ID, and DISCORD_GUILD_ID from your .env. Flags are also accepted for overrides:
| Flag | Env var | Description |
|---|---|---|
| --token | DISCORD_TOKEN | Bot token |
| --client-id | DISCORD_CLIENT_ID | Application ID |
| --guild-id | DISCORD_GUILD_ID | Limit to a specific guild |
| --modules-dir | — | Modules directory (default: ./modules) |
Example output:
Loading modules from "./modules"...
Found 3 command(s) across 2 module(s).
Fetching registered guild commands from Discord...
── Command diff ──
+ /stats (new)
~ /ping (updated)
- /oldcmd (will be removed)
= /help (unchanged)
Summary: 1 added, 1 updated, 1 removed, 1 unchanged.
✅ Successfully refreshed 3 command(s).Configuration
Full createBot options:
createBot({
token: string, // Bot token
clientOptions: ClientOptions, // Passed directly to discord.js Client
modules: DisModule[], // Array of modules to load
deployCommands?: boolean | { guildId?: string },
// true → deploy commands globally on startup
// false → skip deployment
// { guildId } → deploy to a specific guild (faster propagation)
api?: { port: number }, // Enable the Express API server
config?: {
logging?: {
level?: 'debug' | 'info' | 'warn' | 'error' | 'none', // default: 'info'
commands?: boolean, // Log every slash command execution
taskErrors?: boolean, // Log task errors
},
commands?: {
errorReply?: string, // Reply when a command throws an unhandled error
},
},
});BotInstance
createBot returns:
| Property | Description |
|---|---|
| client | The raw discord.js Client instance |
| start() | Logs in and starts the bot |
| stop() | Stops all tasks and destroys the client |
How it works
No duplicate listeners
Each module declares event handlers in a plain object. Internally, EventBus registers a single client.on() per Discord event type and fans out to all module handlers. No matter how many modules listen to messageCreate, only one listener is ever attached to the client.
Centralized command handling
All slash commands from all modules are collected into a single interactionCreate handler. Errors in one command are caught and replied to without affecting other commands.
License
MIT
