Skip to content

02 ‐ Project Layout

Spatulox edited this page Aug 27, 2026 · 2 revisions

Overview

Every file in the template exists for a reason, and most of them you will edit. This page walks the tree top to bottom and says what to do with each entry.

handler/                  interaction manifests, generated + deployed by `dim`
  commands/               slash commands (prod)
  commands_dev/           slash commands (dev), different `name`
  context_menu/           context menus (prod)
  context_menu_dev/       context menus (dev)
src/
  index.ts                entry point : builds the Bot and registers everything
  client.ts               the discord.js Client and its intents
  constantes.ts           channel / guild ids, switched on DISCORD_BOT_DEV
  activities.ts           activities randomly displayed by the bot
  interactions/           the code behind each interaction
    commands/ context-menu/ modal/ selectmenu/ buttons/
  modules/                toggleable features listening to Discord events
  utils/
    HandlersPath.ts       loads handler json, dev/prod aware
    RegisterInteractions.ts  maps interaction names -> functions
    RegisterModules.ts    registers and enables the modules
    rateLimiter.ts        per-user rate limit helper

Only interactions/commands/ ships with a file. Create the other interaction folders when you actually need them.


src/index.ts : the entry point

Three things happen here, in this order.

1. dotenv runs before the framework is imported. This is not cosmetic : BotEnv reads process.env at import time, so a dotenv.config() placed after the import would come too late.

import dotenv from "dotenv";

// Loaded before importing the framework: BotEnv reads process.env at import time.
dotenv.config({path: "./.env"});

import {Bot, BotConfig, Time} from "@spatulox/simplediscordbot"

2. The BotConfig. Name of the bot and where each log level goes : console, a Discord channel, or both. Flip discord: true on the levels you want mirrored into Discord.

const config: BotConfig = {
    botName: "My Bot",
    log: {
        info: {channelId: CHANNELS.log, console: true, discord: false},
        error: {channelId: CHANNELS.error, console: true, discord: false},
        warn: {channelId: CHANNELS.error, console: true, discord: false},
        debug: {channelId: CHANNELS.error, console: true, discord: false}
    }
};

const bot = new Bot(client, config);

new Bot(...) performs the login on its own, there is no client.login() to call. The config also accepts defaultSimpleColor; see the SimpleDiscordBot wiki.

3. Registration, inside ClientReady.

bot.client.once(Events.ClientReady, async () => {
    try {
        // Modules first : an interaction may need a module instance
        await RegisterModules.create()
        await RegisterInteraction.create()
        new ModuleUI(client, CHANNELS.module_ui)

        Bot.setRandomActivity(activities, Time.hour.HOUR_01.toMilliseconds())
    } catch (error) {
        Bot.log.error(`Failed to start the bot : ${error}`)
    }
})

Modules are registered before interactions, on purpose. A slash command handler is allowed to reach for a module instance through the ModuleRegistry, and that only works if the module already exists.


src/client.ts : intents

A plain discord.js Client. The template enables a broad set of intents so the examples work; trim it to what your bot actually needs.

export const client = new Client({ intents: [
    GatewayIntentBits.Guilds,
    GatewayIntentBits.GuildMembers,
    GatewayIntentBits.GuildMessages,
    GatewayIntentBits.MessageContent,
    ...
    ],
    partials: [Partials.Channel]
});

MessageContent and GuildMembers are privileged intents : enable them in the Discord Dev Portal too, or the login fails. ExampleModule needs MessageContent.


src/constantes.ts : your ids

Two frozen objects, DEV_CHANNELS and PROD_CHANNELS, and BotEnv.dev picks one. Add your own ids here rather than scattering literals across the code.

Discord regexes and the zero-width SPACE constant already exist in DiscordRegex, exported by @spatulox/simplediscordbot. Import it instead of redefining them.


src/activities.ts : the presence

A RandomBotActivity array, rotated by Bot.setRandomActivity(activities, interval). The interval in index.ts uses Time.hour.HOUR_01.toMilliseconds().

export const activities: RandomBotActivity = [
    {type: ActivityType.Playing, message: "with the Discord API"},
    {type: ActivityType.Watching, message: "the server"}
];

src/utils/ : the plumbing

File Role
HandlersPath.ts Handlers.load(category, name) reads the interaction json, dev or prod, with an in-memory cache and a typed whitelist
RegisterInteractions.ts one private method per interaction type, all called by RegisterInteraction.create()
RegisterModules.ts register() every module, then enableAll()
rateLimiter.ts isUserRateLimited(interaction, limiter, ms), replies ephemerally and returns true so a command can early-return

RegisterInteractions and RegisterModules share the same shape : a private constructor plus a static async create(), so the async work is done before the object is handed back.


handler/ : the interaction manifests

Json files describing your slash commands and context menus to Discord. You do not write them by hand. The dim CLI generates them and writes the Discord-assigned id back into them after the first deploy. See 03 - Interactions.


Tooling

tsconfig.json is deliberately strict : strict, noUnusedLocals, noUnusedParameters, noUncheckedIndexedAccess, noFallthroughCasesInSwitch, module: nodenext. Expect the compiler to complain about an unused import; that is the point.

.github/workflows/ci.yml runs on main and feat/multi-bot, on Node 22 and 24, and :

  • npm ci, then npx tsc --noEmit, then npm run build
  • parses every handler/**/*.json and fails if one is invalid or has no name
  • checks that the entry point declared in package.json#main was actually produced

.github/dependabot.yml and .github/workflows/audit-fix.yml keep the dependencies and the audit clean on their own.

Clone this wiki locally