Using the Handler

client.handler() loads every .js file inside the folders you point it to and registers each one automatically; so you don't have to call command(), slash(), event(), interaction(), and modal() by hand for every single file. This page walks through exactly what each folder expects, with a full file for every case.

Warning

handler() only loads and registers your definitions; it's a shortcut for calling command()/slash()/event()/interaction()/modal() yourself. You still need to call registerCommands(), registerSlashCommands(), and/or registerInteractions() afterward, exactly like you would without the handler.

Project layout

A typical project using all five folders looks like this:

File structure
my-bot
src
commands
ping.js
slash
ping.js
search.js
events
log-messages.js
interactions
confirm.js
delete-message.js
modals
feedback.js
index.js
package.json

And src/index.js wires it all together:

JavaScript
// src/index.js
const path = require("path");
const { ERXClient, Intents } = require("syntx.js");

const client = new ERXClient({
  token: process.env.TOKEN,
  intents: Intents.Fast,
  prefix: "!",
  clientId: "YOUR_APPLICATION_ID",
});

client.handler(
  {
    commands: "./commands",
    slash: "./slash",
    events: "./events",
    interactions: "./interactions"
    modals: "./modals",
  },
  true, // showLoad; prints what loaded successfully (and what didn't)
);

client.registerCommands();
client.registerSlashCommands({ showLoad: true });
client.registerInteractions();
client.start();

What arguments your functions receive

This is the part that trips people up most: each folder calls a different syntx.js method behind the scenes, and each one hands your function different arguments.

FolderExported functionCalled with
commandscontent(message)
slashexecute(interaction, client)
slashautocomplete(interaction, client)
eventscontentwhatever discord.js passes for that event, e.g. (message) for messageCreate
interactionscontent(interaction, dynamicValues, separator)
modalsrun(interaction, dynamicValues, client)
  • client is your full ERXClient instance; the same one you built with new ERXClient(...). You get client.bot, client.database, anything you'd normally reach through your main client variable, without importing your entry file from inside the command file.
  • dynamicValues is an object with one key per {placeholder} in the button/menu/modal id you matched, e.g. { messageId: "123" } for an id of "delete-{messageId}".
  • separator (interactions only) is the separator character used for that id pattern; "-" unless you set a custom one.

Note

Notice that commands and interactions do not receive client; only slash and modals do. If a text command needs the discord.js client, use message.client (the same object as client.bot); it just won't have your database or other custom properties attached to client itself.

Text commands — commands/

Each file exports { name, alias?, content }; exactly what you'd pass to client.command().

JavaScript
// src/commands/ping.js
module.exports = {
  name: "ping",
  alias: ["p"],
  content: (message) => {
    message.reply("Pong!");
  },
};

Note

This needs a prefix set in the ERXClient constructor and client.registerCommands() called once, same as defining the command manually.

Slash commands — slash/

Each file exports { data, execute, autocomplete? }; exactly what you'd pass to client.slash(). data must be a SlashCommand instance.

JavaScript
// src/slash/ping.js
const { SlashCommand } = require("syntx.js");

module.exports = {
  data: new SlashCommand({
    name: "ping",
    description: "Replies with the bot latency",
  }),
  execute: async (interaction, client) => {
    await interaction.reply(`Pong! ${client.bot.ws.ping}ms`);
  },
};

A second file in the same folder, this time using autocomplete:

JavaScript
// src/slash/search.js
const { SlashCommand } = require("syntx.js");

const ITEMS = ["Sword", "Shield", "Potion", "Bow"];

module.exports = {
  data: new SlashCommand({
    name: "search",
    description: "Search the item list",
    options: [
      { name: "item", description: "Item to search for", type: "string", required: true, autocomplete: true },
    ],
  }),
  execute: async (interaction) => {
    await interaction.reply(`You searched for: ${interaction.options.getString("item")}`);
  },
  autocomplete: async (interaction) => {
    const focused = interaction.options.getFocused();
    const matches = ITEMS.filter((item) => item.toLowerCase().startsWith(focused.toLowerCase()));
    await interaction.respond(matches.map((item) => ({ name: item, value: item })));
  },
};

Both files in slash/ are picked up and registered automatically; you'll see ping (slash) and search (slash) printed to the console once you call client.handler(...) with showLoad: true.

Warning

A slash/ file that's missing data or execute fails to load with a clear error (and handler() keeps going; it doesn't stop loading the rest of your files). Run with showLoad: true while developing so load errors don't go unnoticed.

Events — events/

Each file exports { event, content }, matched against client.event(). event must be a valid discord.js event name.

JavaScript
// src/events/log-messages.js
module.exports = {
  event: "messageCreate",
  content: (message) => {
    if (message.author.bot) return;
    console.log(`[${message.guild?.name ?? "DM"}] ${message.author.tag}: ${message.content}`);
  },
};

Note

content receives whatever discord.js itself passes for that event; just (message) for messageCreate, (member) for guildMemberAdd, and so on. Check the discord.js documentation for the arguments a specific event provides.

Interactions (buttons & select menus) — interactions/

Each file exports { content, separator? }, matched against client.interaction(). If the file doesn't export an id (or name), the file name itself (without .js) becomes the id:

JavaScript
// src/interactions/confirm.js
// no "id" exported, so the id becomes "confirm"; matching a button created with { id: "confirm", ... }
module.exports = {
  content: (interaction) => {
    interaction.reply("Confirmed!");
  },
};

For a dynamic {placeholder} pattern, export an explicit id instead of relying on the file name:

JavaScript
// src/interactions/delete-message.js
module.exports = {
  id: "delete-{messageId}",
  content: (interaction, { messageId }) => {
    interaction.reply(`Deleting message ${messageId}...`);
  },
};

Tip

Letting the file name double as the id works well for static buttons (confirm.js, cancel.js). Once you need a {placeholder}, export id explicitly; it's clearer than naming a file delete-{messageId}.js.

Modals — modals/

Each file exports { run }, matched against client.modal(). Just like interactions/, the id falls back to the file name unless you export one explicitly.

JavaScript
// src/modals/feedback.js
// no "id" exported, so the id becomes "feedback"; matching a Modal created with { id: "feedback", ... }
module.exports = {
  run: (interaction, dynamicValues, client) => {
    const text = interaction.fields.getTextInputValue("message");
    interaction.reply(`Thanks for the feedback: ${text}`);
  },
};

Organizing into subfolders

handler() reads subfolders recursively, so you can group files by category without changing how they're registered:

File structure
src
commands
fun
joke.js
moderation
ban.js
kick.js
index.js

Both joke.js and ban.js are still loaded as plain text commands; the folder nesting is purely for your own organization; syntx.js doesn't care how deep a file is.

See also

  • Client; the full command(), slash(), event(), interaction(), and modal() reference.
  • SlashCommand; building the data object used in slash/ files.
  • Buttons and SelectMenus; creating the components whose id your interactions/ files match against.
  • Modal; creating the form whose id your modals/ files match against.