Project structure
Organize plugin files so commands, events, config, and presentation stay easy to review.
Recommended layout
Use this structure after the first prototype:
my-plugin/
package.json
src/
index.ts
metadata.ts
config.ts
commands/
greet.ts
events/
mention-reply.ts
messages/
render-greeting.ts
README.md
icon.png
screenshots/
overview.pngsrc/index.ts stays the only required entrypoint. Imported files are still bundled, so splitting code does not create extra runtime setup.
File responsibilities
src/metadata.ts owns plugin identity:
import { PluginCapability, PluginCategory } from "itsmybot";
export const metadata = {
id: "welcome-helper",
version: "0.1.0",
name: "Welcome Helper",
summary: "Send configurable welcome messages.",
category: PluginCategory.Engagements,
compatibilityDate: "2026-04-20",
capabilities: [
PluginCapability.DiscordInteractionReply,
PluginCapability.DiscordMessageWrite,
] as const,
};src/config.ts owns dashboard config types and defaults:
import { defineConfig } from "itsmybot";
export interface Config extends Record<string, unknown> {
/**
* @title Greeting
* @description Text used in slash command replies.
* @minLength 1
* @maxLength 80
*/
greeting: string;
}
export const defaultConfig = defineConfig<Config>({
greeting: "Welcome",
});src/commands/* owns slash command classes and command option definitions.
src/events/* owns Discord event handlers.
src/messages/*, src/lib/*, or src/rules/* can hold pure helper logic when commands and events share behavior.
README.md, icon.png, and screenshots/* are marketplace presentation assets.
Why split files
Split files when each piece has different ownership:
- metadata changes during release work.
- config changes affect dashboard forms.
- commands change command routing and options.
- events change Discord trigger behavior.
- message rendering can be tested without plugin runtime context.
Do not split only to make tiny wrappers. A one-command plugin can start in src/index.ts, then move pieces out when it grows.
Comment policy
Write comments where TypeScript cannot express intent:
- JSDoc on exported config fields, because dashboard schema uses it.
- capability comments when a Discord write is not obvious from nearby code.
- storage index comments when query shape matters.
- validation comments when Discord or platform limits explain a number.
Skip comments that restate code:
// Bad: creates command class.
class PreviewCommand {}Use comments for intent:
stringOption({
description: "Who should receive the greeting",
// Discord slash command string option limits are enforced by publish validation too.
minLength: 1,
maxLength: 32,
});Entrypoint stays small
Keep src/index.ts as composition glue:
import { } from "itsmybot";
import { } from "./commands/greet";
import type { Config } from "./config";
export { } from "./config";
import { } from "./metadata";
class extends <Config> {
readonly = .;
readonly = .;
readonly = .;
readonly = .;
readonly = .;
readonly = .;
readonly = .;
readonly = [new ()];
}
export default new ();