Plexcord LogoPlexcord
Best Practices

Code Quality

TypeScript patterns, naming conventions, code organization, and clean code guidelines for Plexcord plugin development.

Code Quality

Writing maintainable plugin code is as important as writing working code. These guidelines help you produce plugins that are easy to understand, test, and update.


Use TypeScript Fully

Plexcord is TypeScript-first. Lean on it completely.

Always type your data models

// ✅ Good: clear, self-documenting
interface SavedMessage {
    id: string;
    content: string;
    authorId: string;
    savedAt: number;
}

// ❌ Avoid: all information lost
const data: any = { id: "...", content: "..." };

Type-safe settings access

When you use definePluginSettings, the returned object is fully typed:

const settings = definePluginSettings({
    maxItems: { type: OptionType.NUMBER, description: "...", default: 10 }
});

// settings.store.maxItems is typed as number
const limit: number = settings.store.maxItems;

Avoid as any in patches

In patches' replacement strings you sometimes must use runtime code that bypasses type checking. Keep these sandboxed:

// Acceptable: isolated to the replacement string
replacement: {
    replace: "$self.handleRender($1.props)"
}

// Plugin method is properly typed
handleRender(props: MessageProps): string {
    return props.content.toUpperCase();
}

Naming Conventions

ItemConventionExample
Plugin filecamelCase foldersrc/plugins/myPlugin/index.ts
Plugin namePascalCase stringname: "MyPlugin"
Settings variablesettings (always)const settings = definePluginSettings(...)
Logger variablelogger (always)const logger = new Logger("MyPlugin")
DataStore keysPluginName_keyName"VoiceLogger_history"
Flux handler propsDestructure what you need({ message, channelId })

File Organization

For simple plugins, a single index.ts is enough:

src/plugins/myPlugin/
└── index.ts

For complex plugins:

src/plugins/myPlugin/
├── index.ts          ← Plugin entry, exports definePlugin
├── settings.ts       ← definePluginSettings
├── components/
│   └── SettingsPanel.tsx
├── utils.ts          ← Helper functions
└── types.ts          ← TypeScript interfaces

Logger Usage

Always use Logger instead of console.log:

import { Logger } from "@utils/Logger";
const logger = new Logger("MyPlugin");

// ✅ Good
logger.info("Processing", count, "messages");
logger.error("Failed to save:", error);
logger.debug("State:", JSON.stringify(state));

// ❌ Avoid: no context, hard to filter in DevTools
console.log("Processing messages");
console.error(error);

In DevTools, you can filter by your plugin's prefix: type MyPlugin in the console filter.


Clean Cleanup in stop()

Every resource your plugin claims in start() must be released in stop():

start() {
    // Register event listeners
    document.addEventListener("keydown", this.keyHandler);

    // Store interval reference
    this.updateInterval = setInterval(() => this.update(), 5000);
}

stop() {
    // ✅ Remove listener
    document.removeEventListener("keydown", this.keyHandler);

    // ✅ Clear interval
    clearInterval(this.updateInterval);
    this.updateInterval = null;
}

If you don't clean up:

  • Disabled plugins continue consuming memory and CPU
  • Event handlers fire after the plugin is off
  • Users must restart Discord to fully unload your plugin

Flux Error Handling

Flux event handlers run in Discord's event loop. An unhandled error can break all subsequent events for that type across all plugins. Always wrap your handlers:

flux: {
    MESSAGE_CREATE({ message }) {
        try {
            this.processMessage(message);
        } catch (e) {
            logger.error("Unhandled error in MESSAGE_CREATE:", e);
        }
    }
}

Idiomatic Plugin Structure

import definePlugin, { OptionType } from "@utils/types";
import { definePluginSettings } from "@api/Settings";
import { Logger } from "@utils/Logger";

// 1. Logger at the top
const logger = new Logger("MyPlugin");

// 2. Settings next
const settings = definePluginSettings({
    enabled: {
        type: OptionType.BOOLEAN,
        description: "Enable feature X",
        default: true
    }
});

// 3. Pure utility functions (not on the plugin object)
function computeSomething(input: string): string {
    return input.trim();
}

// 4. Main plugin export
export default definePlugin({
    name: "MyPlugin",
    description: "Does something useful",
    authors: [{ name: "YourName", id: 0n }],
    settings,

    start() {
        logger.info("Started");
    },

    stop() {
        logger.info("Stopped");
    },

    flux: {
        MESSAGE_CREATE({ message }) {
            if (!settings.store.enabled) return;
            try {
                const result = computeSomething(message.content);
                logger.debug("Result:", result);
            } catch (e) {
                logger.error("Error:", e);
            }
        }
    }
});

Localize User-Facing Strings

Any string that appears in the UI (settings labels, notification bodies, button text) should use t() instead of a hardcoded value. This makes your plugin work for users in any supported language without any extra effort.

import { t, plugin } from "@api/i18n";

// ✅ Good: automatic Turkish, English, and future language support
export default definePlugin({
    name: "MyPlugin",
    description: () => t(plugin.myPlugin.description),
    settings: definePluginSettings({
        feature: {
            label: () => t(plugin.myPlugin.settings.featureLabel),
            description: () => t(plugin.myPlugin.settings.featureDesc),
            type: OptionType.BOOLEAN,
            default: true
        }
    }),
    start() {
        showNotification({
            title: t(plugin.myPlugin.notif.title),
            body: t(plugin.myPlugin.notif.body)
        });
    }
});

// ❌ Avoid: hardcoded English, users in other locales get no translation
export default definePlugin({
    name: "MyPlugin",
    description: "Does something useful",
    settings: definePluginSettings({
        feature: {
            label: "Enable feature",
            description: "Toggles the main feature on or off",
            type: OptionType.BOOLEAN,
            default: true
        }
    })
});

Key rules:

  • Always wrap t() in a function (() => t(...)) inside definePlugin; strings are evaluated at render time, not at module load
  • Use proxy key access (plugin.myPlugin.label) instead of string literals, for IntelliSense and type safety
  • Add your strings to src/locales/en.ts under plugin.yourPluginName.*, then mirror them in src/locales/tr.ts

See the i18n API reference for the full guide.


On this page