Medusa/developer documentation/Handlers

Handlers

Encapsulate business logic, orchestrate models, and share workflows across commands and routes.

Handlers encapsulate business logic and data workflows. Rather than embedding database mutations or Discord interactions directly into slash commands or HTTP routes, all core logic resides in handlers. Commands and routes act as thin dispatch layers that parse input and delegate execution to handlers.

Handlers reside in src/handlers/ and load from every *.js file located directly in that directory.

Base Class Definition

modules/example/src/handlers/Items.js
import { Handlers } from "#medusa/modules";

export class ItemsHandler extends Handlers {
    constructor(medusa) {
        super(medusa, {
            name: "items"
        });
    }

    async create(guildId, userId, identifier, name, options = {}) {
        if (!identifier) this.fail(400, "An item identifier is required.");
        if (!name) this.fail(400, "An item name is required.");

        const existing = await this.medusa.modules.models.get(this.module, "example_items").get({ identifier });
        if (existing) this.fail(409, `An item with identifier "${identifier}" already exists.`);

        return this.medusa.modules.models.get(this.module, "example_items").create({
            guildId,
            userId,
            identifier,
            name,
            ...options
        });
    }

    async getByIdentifier(identifier) {
        const item = await this.medusa.modules.models.get(this.module, "example_items").get({ identifier });
        if (!item) this.fail(404, `Item "${identifier}" was not found.`);
        return item;
    }

    async listForGuild(guildId) {
        return this.medusa.modules.models.get(this.module, "example_items").list({ guildId });
    }

    async remove(identifier) {
        const deleted = await this.medusa.modules.models.get(this.module, "example_items").delete({ identifier });
        if (!deleted) this.fail(404, `Item "${identifier}" was not found.`);
        return { success: true };
    }
}

Built-In Methods

The Handlers base class provides the following core methods:

MethodSignatureDescription
getName()() => stringReturns the handler name identifier.
this.fail()(status: number, message: string) => neverThrows an Error with an attached HTTP status property. When thrown during an HTTP route execution, Medusa catches the error and sends the corresponding HTTP response code with { error: message }.

Error Bubbling with this.fail()

The fail() method allows one-line error throwing that integrates cleanly with both HTTP routes and Discord interactions:

if (amount <= 0) {
    this.fail(400, "Amount must be greater than zero.");
}

When called from an HTTP route, Medusa automatically catches the thrown error and responds with the matching HTTP status code:

async GET(request) {
    const handler = this.medusa.modules.handlers.get(this.module, "items");
    return handler.getByIdentifier(request.query.identifier);
}

When called from a Discord slash command, wrap execution in a try-catch block to render an error embed:

try {
    await handler.create(interaction.guild.id, interaction.user.id, id, name);
    return interaction.editReply({
        embeds: [this.medusa.embeds.success("Item created successfully.")]
    });
} catch (error) {
    return interaction.editReply({
        embeds: [this.medusa.embeds.error(error.message)]
    });
}

Cross-Module Handler Access

Modules frequently orchestrate actions implemented by other modules. Handlers are queried dynamically through this.medusa.modules.handlers.get(moduleName, handlerName):

const economy = this.medusa.modules.handlers.get("economy", "balances");
if (economy) {
    await economy.add(guildId, userId, 100, "wallet");
}

If the requested module is disabled by server administrators or not installed, get() returns null. Always check for existence before invoking methods on external handlers.

Interfacing with Core Utilities

Handlers have access to all root Medusa utilities via this.medusa:

async completePurchase(guildId, user, item) {
    await this.medusa.audit.record({
        type: "module",
        actorId: user.id,
        actorTag: user.tag,
        command: "item:purchase",
        success: true,
        detail: { item: item.identifier, price: item.price },
        guildId
    });

    await this.medusa.logs.post(this.module, "itemPurchased", {
        actor: user,
        subject: item.name,
        fields: [
            { name: "Price", value: `$${item.price}`, inline: true },
            { name: "Identifier", value: item.identifier, inline: true }
        ]
    });
}