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:
And src/index.js wires it all together:
// 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.
| Folder | Exported function | Called with |
|---|---|---|
commands | content | (message) |
slash | execute | (interaction, client) |
slash | autocomplete | (interaction, client) |
events | content | whatever discord.js passes for that event, e.g. (message) for messageCreate |
interactions | content | (interaction, dynamicValues, separator) |
modals | run | (interaction, dynamicValues, client) |
clientis your fullERXClientinstance; the same one you built withnew ERXClient(...). You getclient.bot,client.database, anything you'd normally reach through your mainclientvariable, without importing your entry file from inside the command file.dynamicValuesis an object with one key per{placeholder}in the button/menu/modalidyou 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().
// 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.
// 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:
// 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.
// 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:
// 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:
// 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.
// 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:
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(), andmodal()reference. - SlashCommand; building the
dataobject used inslash/files. - Buttons and SelectMenus; creating the components whose
idyourinteractions/files match against. - Modal; creating the form whose
idyourmodals/files match against.