Skip to main content
Prefix commands are message-based commands triggered by a prefix (like !ping or ?help). While Discord encourages slash commands, prefix commands remain useful for certain use cases.
Prefix commands require the MessageContent intent, which is a privileged intent for verified bots. Consider using slash commands instead for better user experience and fewer restrictions.

Basic Prefix Command

Create a command file that exports the message handler:
import type { CommandData, MessageCommand } from 'commandkit';

export const command: CommandData = {
  name: 'ping',
};

export const message: MessageCommand = async (ctx) => {
  const latency = (ctx.client.ws.ping ?? -1).toString();
  await ctx.message.reply(`Pong! Latency: ${latency}ms`);
};
With the default prefix !, users can run !ping to trigger this command.

Setting a Custom Prefix

Use setPrefixResolver to configure your bot’s prefix:
src/app.ts
import { Client } from 'discord.js';
import { commandkit } from 'commandkit';

const client = new Client({
  intents: ['Guilds', 'GuildMessages', 'MessageContent'],
});

// Set a global prefix
commandkit.setPrefixResolver(async (message) => {
  return '?';
});

export default client;
Now your bot responds to ?ping instead of !ping.

Multiple Prefixes

Support multiple prefixes by returning an array:
src/app.ts
import { commandkit } from 'commandkit';

commandkit.setPrefixResolver(async (message) => {
  return ['!', '?', '>'];
});
Users can now use !ping, ?ping, or >ping.

Guild-Specific Prefixes

Implement per-server prefixes with a database:
1

Create prefix resolver

Use setPrefixResolver with database lookups:
src/app.ts
import { commandkit } from 'commandkit';
import { prisma } from './database';

commandkit.setPrefixResolver(async (message) => {
  // Default prefix for DMs
  if (!message.guildId) return '!';

  // Fetch guild prefix from database
  const guild = await prisma.guild.findUnique({
    where: { id: message.guildId },
    select: { prefix: true },
  });

  return guild?.prefix ?? '!';
});
2

Create set-prefix command

Let server admins change the prefix:
src/app/commands/set-prefix.ts
import type { CommandData, ChatInputCommand, MessageCommand } from 'commandkit';
import { ApplicationCommandOptionType } from 'discord.js';
import { prisma } from '../database';

export const command: CommandData = {
  name: 'set-prefix',
  description: 'Change the server prefix',
  options: [
    {
      name: 'prefix',
      description: 'The new prefix',
      type: ApplicationCommandOptionType.String,
      required: true,
      maxLength: 5,
    },
  ],
};

async function updatePrefix(guildId: string, prefix: string) {
  return await prisma.guild.upsert({
    where: { id: guildId },
    update: { prefix },
    create: { id: guildId, prefix },
  });
}

export const chatInput: ChatInputCommand = async (ctx) => {
  if (!ctx.interaction.memberPermissions?.has('ManageGuild')) {
    await ctx.interaction.reply({
      content: 'You need Manage Server permission to change the prefix.',
      ephemeral: true,
    });
    return;
  }

  const prefix = ctx.options.getString('prefix', true);
  await updatePrefix(ctx.interaction.guildId!, prefix);

  await ctx.interaction.reply(`Prefix changed to \`${prefix}\``);
};

export const message: MessageCommand = async (ctx) => {
  if (!ctx.message.member?.permissions.has('ManageGuild')) {
    await ctx.message.reply('You need Manage Server permission to change the prefix.');
    return;
  }

  const prefix = ctx.args()[0];
  if (!prefix) {
    await ctx.message.reply('Please provide a prefix.');
    return;
  }

  if (prefix.length > 5) {
    await ctx.message.reply('Prefix must be 5 characters or less.');
    return;
  }

  await updatePrefix(ctx.message.guildId!, prefix);
  await ctx.message.reply(`Prefix changed to \`${prefix}\``);
};
3

Add caching (optional)

Optimize performance with caching:
src/app.ts
import { commandkit } from 'commandkit';
import { cache } from '@commandkit/cache';
import { prisma } from './database';

const prefixCache = new Map<string, string>();

commandkit.setPrefixResolver(async (message) => {
  if (!message.guildId) return '!';

  // Check cache first
  const cached = prefixCache.get(message.guildId);
  if (cached) return cached;

  // Fetch from database
  const guild = await prisma.guild.findUnique({
    where: { id: message.guildId },
    select: { prefix: true },
  });

  const prefix = guild?.prefix ?? '!';
  
  // Cache for 5 minutes
  prefixCache.set(message.guildId, prefix);
  setTimeout(() => prefixCache.delete(message.guildId), 5 * 60 * 1000);

  return prefix;
});
Prefix resolvers run on every message. Keep them lightweight to avoid performance issues. Consider using the @commandkit/cache plugin for better caching.

Command Arguments

Access command arguments using ctx.args():
src/app/commands/say.ts
import type { CommandData, MessageCommand } from 'commandkit';

export const command: CommandData = {
  name: 'say',
};

export const message: MessageCommand = async (ctx) => {
  const args = ctx.args(); // Returns string[]

  if (args.length === 0) {
    await ctx.message.reply('Please provide a message to repeat.');
    return;
  }

  const text = args.join(' ');
  await ctx.message.channel.send(text);
};
When a user runs !say hello world, args will be ['hello', 'world'].

Parsing Arguments

Manually parse complex arguments:
src/app/commands/kick.ts
import type { CommandData, MessageCommand } from 'commandkit';

export const command: CommandData = {
  name: 'kick',
};

export const message: MessageCommand = async (ctx) => {
  const args = ctx.args();

  // Parse user mention or ID
  const userArg = args[0];
  if (!userArg) {
    await ctx.message.reply('Please mention a user or provide their ID.');
    return;
  }

  // Extract user ID from mention or use as-is
  const userId = userArg.replace(/[<@!>]/g, '');
  
  try {
    const member = await ctx.message.guild?.members.fetch(userId);
    if (!member) {
      await ctx.message.reply('User not found.');
      return;
    }

    // Parse reason (rest of arguments)
    const reason = args.slice(1).join(' ') || 'No reason provided';

    await member.kick(reason);
    await ctx.message.reply(`Kicked ${member.user.username} - Reason: ${reason}`);
  } catch (error) {
    await ctx.message.reply('Failed to kick user. Make sure I have permission.');
  }
};

Hybrid Commands

Support both slash and prefix commands in one file:
src/app/commands/userinfo.ts
import type { CommandData, ChatInputCommand, MessageCommand } from 'commandkit';
import { ApplicationCommandOptionType, EmbedBuilder } from 'discord.js';

export const command: CommandData = {
  name: 'userinfo',
  description: 'Get information about a user',
  options: [
    {
      name: 'user',
      description: 'The user to get info about',
      type: ApplicationCommandOptionType.User,
    },
  ],
};

async function createUserEmbed(user: any, member: any) {
  const embed = new EmbedBuilder()
    .setTitle(`${user.username}'s Info`)
    .setThumbnail(user.displayAvatarURL())
    .addFields(
      { name: 'ID', value: user.id, inline: true },
      { name: 'Created', value: `<t:${Math.floor(user.createdTimestamp / 1000)}:R>`, inline: true },
    );

  if (member) {
    embed.addFields({
      name: 'Joined Server',
      value: `<t:${Math.floor(member.joinedTimestamp / 1000)}:R>`,
      inline: true,
    });
  }

  return embed;
}

export const chatInput: ChatInputCommand = async (ctx) => {
  const user = ctx.options.getUser('user') ?? ctx.interaction.user;
  const member = await ctx.interaction.guild?.members.fetch(user.id).catch(() => null);

  const embed = await createUserEmbed(user, member);
  await ctx.interaction.reply({ embeds: [embed] });
};

export const message: MessageCommand = async (ctx) => {
  // Parse mentioned user or use command author
  const mentionedUser = ctx.message.mentions.users.first();
  const user = mentionedUser ?? ctx.message.author;
  const member = await ctx.message.guild?.members.fetch(user.id).catch(() => null);

  const embed = await createUserEmbed(user, member);
  await ctx.message.reply({ embeds: [embed] });
};

Permissions and Validation

Validate permissions and input:
src/app/commands/ban.ts
import type { CommandData, MessageCommand } from 'commandkit';

export const command: CommandData = {
  name: 'ban',
};

export const message: MessageCommand = async (ctx) => {
  // Check if used in a guild
  if (!ctx.message.guild) {
    await ctx.message.reply('This command can only be used in a server.');
    return;
  }

  // Check user permissions
  if (!ctx.message.member?.permissions.has('BanMembers')) {
    await ctx.message.reply('You need Ban Members permission to use this command.');
    return;
  }

  // Check bot permissions
  if (!ctx.message.guild.members.me?.permissions.has('BanMembers')) {
    await ctx.message.reply('I need Ban Members permission to execute this command.');
    return;
  }

  const args = ctx.args();
  const userArg = args[0];

  if (!userArg) {
    await ctx.message.reply('Usage: `!ban <user> [reason]`');
    return;
  }

  // Continue with ban logic...
};

Help Command Example

Create a help command that lists all prefix commands:
src/app/commands/help.ts
import type { CommandData, MessageCommand } from 'commandkit';
import { EmbedBuilder } from 'discord.js';

export const command: CommandData = {
  name: 'help',
};

export const message: MessageCommand = async (ctx) => {
  const embed = new EmbedBuilder()
    .setTitle('Bot Commands')
    .setDescription('Here are all available commands:')
    .addFields(
      { name: '!ping', value: 'Check bot latency' },
      { name: '!help', value: 'Show this help message' },
      { name: '!userinfo [user]', value: 'Get user information' },
      { name: '!say <message>', value: 'Make the bot say something' },
    )
    .setColor('#5865F2');

  await ctx.message.reply({ embeds: [embed] });
};

Migration Tips

If you’re migrating from prefix to slash commands:
1

Support both temporarily

Keep both message and chatInput handlers during the transition period.
2

Announce the change

Notify users about the switch to slash commands.
3

Add deprecation warnings

Display warnings when users use prefix commands:
export const message: MessageCommand = async (ctx) => {
  await ctx.message.reply(
    '⚠️ This command is deprecated. Please use `/ping` instead.'
  );
};
4

Remove prefix support

After a transition period, remove the message export and MessageContent intent.

Best Practices

  • Validate all input: Never trust user input from message content
  • Provide usage examples: Help users understand command syntax
  • Use middleware: Add permission checks and validation via middleware instead of repeating code
  • Consider slash commands: Slash commands provide better UX and type safety
  • Cache prefix lookups: Optimize database queries with caching

Build docs developers (and LLMs) love