AI Agents Reference
This page contains information specifically for AI coding agents. It clarifies patterns that are commonly misunderstood and are not explicitly covered in other docs.
Discord imports: always use zumito-framework/discord
Never import from discord.js directly. The framework re-exports everything from discord.js via its own subpath to ensure version consistency.
// CORRECTO
import { Client, GuildMember, TextChannel, EmbedBuilder, PermissionsBitField } from 'zumito-framework/discord';
// INCORRECTO — no hagas esto
import { Client } from 'discord.js';
All types, enums, and classes from discord.js are available through zumito-framework/discord. This includes but is not limited to: Client, Guild, GuildMember, TextChannel, VoiceChannel, Message, EmbedBuilder, ActionRowBuilder, ButtonBuilder, StringSelectMenuBuilder, PermissionsBitField, ChannelType, ApplicationCommandOptionType, Interaction, CommandInteraction, ButtonInteraction, StringSelectMenuInteraction, ModalSubmitInteraction, User, roleMention, userMention, channelMention, inlineCode, bold, italic, hyperlink, etc.
Database access
The framework uses zumito-db, a decorator-based multi-driver ORM. Define models with @Collection and @Field, access them via DatabaseManager/Repository.
Mongoose appears in package.json dependencies for legacy reasons but is not used and will be removed. Do not create Mongoose models or schemas.
How to get the database instance
import { DatabaseManager, ServiceContainer } from 'zumito-framework';
const db = ServiceContainer.get(DatabaseManager) as DatabaseManager;
Auto-loading models from modules
Models placed in a module's models/ folder are automatically imported and registered during module.initialize(). The framework scans for exported classes with @Collection metadata and registers them with DatabaseManager. Schemas are created/ensured immediately after all modules are loaded. No manual registration needed.
Example:
// src/modules/my-module/models/Guild.ts
import { Collection, Field } from 'zumito-framework';
@Collection({ name: 'guilds' })
export class Guild {
@Field({ type: 'string', primary: true, unique: true })
guild_id: string;
}
The Guild model is now available via db.getRepository(Guild) anywhere in the app.
Defining models
import { Collection, Field, HasMany } from 'zumito-framework';
@Collection({ name: 'guilds' })
export class Guild {
@Field({ type: 'string', primary: true, unique: true })
guild_id: string;
@Field({ type: 'string', default: 'en' })
lang: string;
}
Always use explicit type in @Field() options when writing code that might be tested with vitest/esbuild (which does not support emitDecoratorMetadata).
Repository pattern (preferred)
const guilds = db.getRepository(Guild);
// Find
const all = await guilds.find();
const guild = await guilds.findOne({ guild_id: '123' });
// Insert
await guilds.insert({ guild_id: '789', lang: 'es' });
// Update
await guilds.update({ guild_id: '123' }, { lang: 'fr' });
// Delete
await guilds.delete({ guild_id: '789' });
// Count
const total = await guilds.count({ lang: 'en' });
QueryBuilder
const results = await guilds
.query()
.where('lang', 'eq', 'es')
.sort('guild_id', 'asc')
.limit(10)
.select(['guild_id', 'prefix'])
.exec();
Available operators: eq, neq, gt, gte, lt, lte, in, nin, like, between.
Legacy MongoDB access (deprecated)
// @deprecated — use DatabaseManager instead. Only works with mongo driver.
import { Db } from 'mongodb';
const db = ServiceContainer.get(Db);
await db.collection('guilds').findOne({ guild_id: '123' });
The above still works but emits deprecation warnings. Use DatabaseManager + Repository for new code.
Configuration collection convention
Guild settings are stored in a guilds collection. Each document has a guild_id field. Use the GuildDataGetter service to fetch guild settings with defaults merged in:
import { GuildDataGetter, ServiceContainer } from 'zumito-framework';
const guildDataGetter = ServiceContainer.get(GuildDataGetter) as GuildDataGetter;
const settings = await guildDataGetter.getGuildSettings(guildId);
ServiceContainer
The ServiceContainer is a global dependency injection container. Key services are pre-registered by the framework:
| Service class | How to get | Notes |
|---|---|---|
ZumitoFramework |
ServiceContainer.get(ZumitoFramework) |
The framework instance |
Client (discord) |
ServiceContainer.get(Client) |
Discord.js client |
DatabaseManager |
ServiceContainer.get(DatabaseManager) |
zumito-db ORM instance |
TranslationManager |
ServiceContainer.get(TranslationManager) |
Translation service |
CommandManager |
ServiceContainer.get(CommandManager) |
All loaded commands |
EventManager |
ServiceContainer.get(EventManager) |
Event handling |
ErrorHandler |
ServiceContainer.get(ErrorHandler) |
Centralized error handling |
GuildDataGetter |
ServiceContainer.get(GuildDataGetter) |
Guild settings from DB |
MemberPermissionChecker |
ServiceContainer.get(MemberPermissionChecker) |
Permission checks |
TextFormatter |
ServiceContainer.get(TextFormatter) |
Text formatting utilities |
EmojiFallback |
ServiceContainer.get(EmojiFallback) |
Emoji fallback resolution |
InviteUrlGenerator |
ServiceContainer.get(InviteUrlGenerator) |
Bot invite URL generation |
PrefixResolver |
ServiceContainer.get(PrefixResolver) |
Guild-specific prefix |
When writing a command, the client and framework are available in execute parameters, so you don't need ServiceContainer for those. Use ServiceContainer for other services like GuildDataGetter or ErrorHandler.
import { Command, CommandParameters, ServiceContainer, GuildDataGetter } from 'zumito-framework';
import { Client } from 'zumito-framework/discord';
export class MyCommand extends Command {
constructor(
private guildDataGetter: GuildDataGetter = ServiceContainer.get(GuildDataGetter),
) {
super();
}
async execute({ message, interaction, client, framework }: CommandParameters): Promise<void> {
const target = message || interaction;
// client and framework are already available in params
}
}
Module structure convention
Each module lives in src/modules/<ModuleName>/ and follows this structure:
src/modules/<ModuleName>/
├── commands/ # Command classes
├── events/ # Event classes (subfolder = source, e.g., events/discord/)
│ └── discord/ # Discord.js events
│ └── framework/ # Framework events (e.g., 'ready')
├── models/ # Database model classes (auto-loaded, use @Collection/@Field)
├── routes/ # Express route handlers
├── translations/ # Translation JSON files
│ ├── en.json
│ └── es.json
├── config/
│ └── default.ts # Default guild config values
└── index.ts # Module entry (extends Module)
The module entry extends Module from zumito-framework:
import { Module } from 'zumito-framework';
export class MyModule extends Module {
async initialize() {
await super.initialize(); // registers commands, events, translations, routes
}
async onAllReady() {
// Called after all modules initialized, DB ready, slash commands refreshed
}
}
Translation keys
Translation keys are relative to the current context:
- In commands:
trans("yourKey")resolves tocommand.<commandName>.yourKey - Use
$prefix for absolute keys:trans("$category.otherCommand.someKey")
Translation files are flat JSON objects. The framework merges all module translations.
Slash vs Prefix commands
- If only
execute()is defined: works for both slash and prefix. - If
executeSlashCommand()is defined: used for slash commands instead ofexecute(). - If
executePrefixCommand()is defined: used for prefix commands instead ofexecute(). - Check which mode by seeing if
messageorinteractionis defined in params.
TypeScript constructor conventions
Use parameter properties (private readonly inline) when constructor parameters are assigned directly to instance properties:
// CORRECTO — parameter properties
export class MyService {
constructor(
private readonly framework: ZumitoFramework,
private readonly someOption: string,
) {}
}
// INCORRECTO — declaración + asignación en cuerpo (redundante)
export class MyService {
private framework: ZumitoFramework;
private someOption: string;
constructor(framework: ZumitoFramework, someOption: string) {
this.framework = framework;
this.someOption = someOption;
}
}
When a dependency comes from ServiceContainer.get(), use a default parameter value:
export class MyService {
constructor(
private readonly errorHandler: ErrorHandler = ServiceContainer.get(ErrorHandler),
) {}
}
Only fall back to the declaration + body pattern when initialization requires logic beyond simple assignment (e.g., calling methods, conditional logic, super() requirements).
File access (FileManager)
The framework provides two file storage modules that implement the same FileManager abstract contract (get, put, delete, list, getUrl, exists). Use the one that matches the deployment environment.
Local filesystem (@zumito-team/local-filesystem)
Stores files on the local disk.
import { LocalFileManager, AssetInfo, FileManager } from '@zumito-team/local-filesystem';
import { ServiceContainer } from 'zumito-framework';
const fm = ServiceContainer.get(LocalFileManager) as LocalFileManager;
// Upload — returns a URL string
const url = await fm.put('images/banner.png', buffer, 'image/png');
// Read
const data: Buffer | null = await fm.get('images/banner.png');
// Delete
await fm.delete('images/banner.png');
// List files under a prefix
const files: AssetInfo[] = await fm.list('images/');
// Check existence
const exists: boolean = await fm.exists('images/banner.png');
// Get public URL
const publicUrl: string = await fm.getUrl('images/banner.png');
Configuration in zumito.config.ts:
import { LocalFilesystemModule } from '@zumito-team/local-filesystem';
export const config = {
bundles: [
{
path: 'node_modules/@zumito-team/local-filesystem',
}
]
};
S3-compatible storage (@zumito-team/s3-assets)
Works with AWS S3, DigitalOcean Spaces, MinIO, Cloudflare R2, etc. Same API as local filesystem.
import { S3FileManager, AssetInfo, FileManager } from '@zumito-team/s3-assets';
import { ServiceContainer } from 'zumito-framework';
const fm = ServiceContainer.get(S3FileManager) as S3FileManager;
// Same API as LocalFileManager — get, put, delete, list, getUrl, exists
const url = await fm.put('uploads/doc.pdf', pdfBuffer, 'application/pdf');
const data = await fm.get('uploads/doc.pdf');
Configuration in zumito.config.ts (DigitalOcean Spaces example):
import { S3AssetsModule } from '@zumito-team/s3-assets';
export const config = {
bundles: [
{
path: 'node_modules/@zumito-team/s3-assets',
options: {
bucket: 'my-bucket',
region: 'nyc3',
endpoint: 'https://nyc3.digitaloceanspaces.com',
accessKeyId: 'DO00...',
secretAccessKey: '...',
publicUrlBase: 'https://my-bucket.nyc3.cdn.digitaloceanspaces.com',
}
}
]
};
Writing code against the abstract contract
To write storage-agnostic code, depend on the abstract FileManager from @zumito-team/file-manager:
import { FileManager, type AssetInfo } from '@zumito-team/file-manager';
import { ServiceContainer } from 'zumito-framework';
// Will return whichever implementation is registered (Local or S3)
const fm = ServiceContainer.get(FileManager) as FileManager;
const url = await fm.put('key', buffer, 'text/plain');
Common pitfalls
- Don't instantiate the Discord
Clientyourself — the framework creates and manages it. - Don't create Mongoose models. Use
zumito-dbdecorators (@Collection,@Field) andDatabaseManager/Repository. - Don't import from
discord.jsdirectly. Usezumito-framework/discord. - Don't call
client.login()— the framework handles authentication viaFrameworkSettings.discordClientOptions.token. - In DM channels,
guildandguildSettingswill be undefined. Always guard against this. - The framework uses ESM (
"type": "module"in package.json). Always use.jsextensions in relative imports even in TypeScript files. - Always specify
typein@Field({ type: 'string' })options —emitDecoratorMetadatainference is not available in vitest/esbuild test environments.