npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

devcodes-djs

v1.1.0

Published

Prefix command handler, embed presets, and button paginator for discord.js v14 bots. Built by Dev Codes.

Readme

devcodes-djs

The essential discord.js v14 toolkit — prefix commands, slash commands, context menus, modals, select menus, embed presets, button paginator, event handler, and full Components V2 container support. One install, everything you need.

Install

npm install devcodes-djs

Peer requirement: discord.js ^14.0.0
Container components require discord.js >=14.18.0


Event Handler

Organize all your client events in one clean, chainable place instead of scattered client.on() calls.

const { EventHandler } = require('devcodes-djs');

const events = new EventHandler()
  .once('ready', (client) => console.log(`Logged in as ${client.user.tag}`))
  .on('guildMemberAdd', (member) => {
    member.guild.systemChannel?.send(`👋 Welcome ${member}!`);
  })
  .on('messageDelete', (message) => {
    console.log(`Message deleted in #${message.channel.name}`);
  });

events.attach(client); // binds all listeners at once

Modals

Build and handle multi-field modal (popup form) interactions.

const { buildModal, ModalHandler, TextInputStyle } = require('devcodes-djs');

const feedbackModal = buildModal({
  id:    'feedback_form',
  title: 'Submit Feedback',
  fields: [
    {
      id:          'name',
      label:       'Your name',
      style:       TextInputStyle.Short,
      placeholder: 'e.g. John',
      required:    true,
    },
    {
      id:        'message',
      label:     'Your feedback',
      style:     TextInputStyle.Paragraph,
      minLength: 10,
      maxLength: 1000,
    },
  ],
});

// Show the modal from a slash command or button
slash.command({
  name:        'feedback',
  description: 'Submit feedback',
  execute: async (interaction) => {
    await interaction.showModal(feedbackModal);
  },
});

// Handle the submission
const modals = new ModalHandler()
  .on('feedback_form', async (interaction) => {
    const name    = interaction.fields.getTextInputValue('name');
    const message = interaction.fields.getTextInputValue('message');
    await interaction.reply({ content: `Thanks ${name}! Got: "${message}"`, ephemeral: true });
  });

modals.attach(client);

Dynamic modal IDs

Use ID prefixes to handle modals with dynamic IDs:

modals.on('report_', async (interaction) => {
  // matches "report_123", "report_456", etc.
  const userId = interaction.customId.replace('report_', '');
  await interaction.reply({ content: `Report filed for user ${userId}`, ephemeral: true });
});

Select Menus

Build all 5 Discord select menu types and handle their interactions.

String select

const { SelectMenus, SelectHandler } = require('devcodes-djs');

const colorMenu = SelectMenus.stringSelect({
  id:          'color_pick',
  placeholder: 'Choose a color',
  options: [
    { label: 'Red',   value: 'red',   emoji: '🔴' },
    { label: 'Green', value: 'green', emoji: '🟢', default: true },
    { label: 'Blue',  value: 'blue',  emoji: '🔵' },
  ],
  maxValues: 2,
});

await message.reply({ components: [colorMenu] });

// Handle selection
const selects = new SelectHandler()
  .on('color_pick', async (interaction) => {
    await interaction.reply(`You picked: **${interaction.values.join(', ')}**`);
  });

selects.attach(client);

Other select types

// User picker
const picker = SelectMenus.userSelect({ id: 'pick_user', placeholder: 'Pick a user', maxValues: 3 });

// Role picker
const roles  = SelectMenus.roleSelect({ id: 'pick_role', placeholder: 'Pick a role' });

// Channel picker (text channels only)
const chans  = SelectMenus.channelSelect({ id: 'pick_chan', channelTypes: [ChannelType.GuildText] });

// Mentionable (users + roles)
const ment   = SelectMenus.mentionableSelect({ id: 'pick_mention' });

Context Menus

Add right-click commands to users and messages.

const { ContextMenuHandler } = require('devcodes-djs');

const ctx = new ContextMenuHandler({
  clientId: process.env.CLIENT_ID,
  token:    process.env.BOT_TOKEN,
  guildId:  process.env.GUILD_ID,
});

ctx
  .command({
    type:    'user',
    name:    'Get Avatar',
    execute: async (interaction) => {
      const url = interaction.targetUser.displayAvatarURL({ size: 512 });
      await interaction.reply({ content: url, ephemeral: true });
    },
  })
  .command({
    type:    'user',
    name:    'User Info',
    execute: async (interaction) => {
      const user = interaction.targetUser;
      await interaction.reply({
        embeds: [Embeds.info(`👤 ${user.tag}`, `ID: \`${user.id}\`\nCreated: <t:${Math.floor(user.createdTimestamp / 1000)}:R>`)],
        ephemeral: true,
      });
    },
  })
  .command({
    type:    'message',
    name:    'Translate Message',
    execute: async (interaction) => {
      await interaction.reply({ content: `📝 Content: ${interaction.targetMessage.content}`, ephemeral: true });
    },
  });

ctx.attach(client);

client.once('ready', () => ctx.deploy());

Slash Commands

Register, deploy, and handle application (/) commands with full support for options, subcommands, permissions, and autocomplete.

Basic setup

const { Client, GatewayIntentBits } = require('discord.js');
const { SlashHandler, Embeds } = require('devcodes-djs');

const client = new Client({ intents: [GatewayIntentBits.Guilds] });

const slash = new SlashHandler({
  clientId: process.env.CLIENT_ID,
  token:    process.env.BOT_TOKEN,
  guildId:  process.env.GUILD_ID, // omit for global commands
});

slash
  .command({
    name:        'ping',
    description: 'Check bot latency',
    execute: async (interaction) => {
      await interaction.reply({
        embeds: [Embeds.success('Pong!', `Latency: ${client.ws.ping}ms`)],
      });
    },
  })
  .command({
    name:        'say',
    description: 'Make the bot say something',
    options: [
      { name: 'message', description: 'What to say',        type: 'string',  required: true },
      { name: 'ephemeral', description: 'Only visible to you', type: 'boolean' },
    ],
    execute: async (interaction) => {
      const text      = interaction.options.getString('message', true);
      const ephemeral = interaction.options.getBoolean('ephemeral') ?? false;
      await interaction.reply({ content: text, ephemeral });
    },
  });

// Attach to client — handles interactionCreate automatically
slash.attach(client);

client.once('ready', async () => {
  await slash.deploy(); // register commands with Discord
  console.log('Ready!');
});

client.login(process.env.BOT_TOKEN);

Subcommands

slash.command({
  name:        'settings',
  description: 'Manage bot settings',
  subcommands: [
    {
      name:        'set',
      description: 'Change a setting',
      options: [
        { name: 'key',   description: 'Setting name',  type: 'string', required: true },
        { name: 'value', description: 'Setting value', type: 'string', required: true },
      ],
      execute: async (interaction) => {
        const key   = interaction.options.getString('key', true);
        const value = interaction.options.getString('value', true);
        await interaction.reply({ embeds: [Embeds.success('Updated', `\`${key}\` → \`${value}\``)] });
      },
    },
    {
      name:        'reset',
      description: 'Reset all settings to defaults',
      execute: async (interaction) => {
        await interaction.reply({ embeds: [Embeds.warn('Reset', 'All settings restored.')] });
      },
    },
  ],
});

Autocomplete

const fruits = ['apple', 'banana', 'cherry', 'durian', 'elderberry'];

slash.command({
  name:        'fruit',
  description: 'Pick a fruit',
  options: [
    { name: 'name', description: 'Fruit name', type: 'string', required: true, autocomplete: true },
  ],
  autocomplete: async (interaction) => {
    const focused = interaction.options.getFocused().toLowerCase();
    await interaction.respond(
      fruits.filter(f => f.startsWith(focused)).map(f => ({ name: f, value: f }))
    );
  },
  execute: async (interaction) => {
    await interaction.reply(`You picked: **${interaction.options.getString('name', true)}**`);
  },
});

Permission guard

slash.command({
  name:        'ban',
  description: 'Ban a user',
  permissions: ['BanMembers'],
  options: [
    { name: 'user', description: 'User to ban', type: 'user', required: true },
  ],
  execute: async (interaction) => {
    const user = interaction.options.getUser('user', true);
    await interaction.reply({ embeds: [Embeds.success('Banned', `${user.tag} was banned.`)] });
  },
});

Option types

| Type | discord.js getter | |------|-------------------| | string | options.getString(name) | | integer | options.getInteger(name) | | number | options.getNumber(name) | | boolean | options.getBoolean(name) | | user | options.getUser(name) | | channel | options.getChannel(name) | | role | options.getRole(name) | | mentionable | options.getMentionable(name) | | attachment | options.getAttachment(name) |

SlashHandlerConfig

| Option | Type | Description | |--------|------|-------------| | clientId | string | Your bot's application ID | | token | string | Bot token for REST deployment | | guildId | string? | Deploy to a specific guild (instant). Omit for global. |


Command Handler

Replace giant if/else chains with a clean, chainable command registry. Supports aliases, per-user cooldowns, and permission guards.

const { CommandHandler } = require('devcodes-djs');

const handler = new CommandHandler({ prefix: '!' });

handler
  .command('ping', async (message) => {
    await message.reply('Pong!');
  })
  .command('hello', async (message, args) => {
    await message.reply(`Hey ${args[0] ?? 'there'}!`);
  }, {
    aliases: ['hi', 'hey'],
    description: 'Say hello',
    usage: '[name]',
  })
  .command('ban', async (message, args) => {
    await message.reply(`Banned ${args[0]}`);
  }, {
    permissions: ['BanMembers'],
    cooldown: 5000,
    description: 'Ban a user',
    usage: '<user>',
  });

// In your messageCreate listener:
client.on('messageCreate', (message) => handler.handle(message));

Auto help command

Use handler.getCommands() to build a dynamic help menu:

handler.command('help', async (message) => {
  const lines = handler.getCommands()
    .map(cmd => `**!${cmd.name}** ${cmd.options.usage ?? ''} — ${cmd.options.description ?? ''}`)
    .join('\n');
  await message.reply(lines);
});

HandlerConfig

| Option | Type | Default | Description | |--------|------|---------|-------------| | prefix | string | "!" | Command prefix | | ignoreBots | boolean | true | Ignore messages from bots |

CommandOptions

| Option | Type | Default | Description | |--------|------|---------|-------------| | aliases | string[] | [] | Alternative trigger names | | cooldown | number | 0 | Per-user cooldown in ms | | permissions | PermissionResolvable[] | [] | Required member permissions | | description | string | — | Short description | | usage | string | — | Usage hint e.g. <user> [reason] |


Embed Presets

One-liner embeds with consistent colors — no more chaining .setColor().setTitle().setFooter() every time.

const { Embeds } = require('devcodes-djs');

// ✅ Green
await message.reply({ embeds: [Embeds.success('Done!', 'Operation completed.')] });

// ❌ Red
await message.reply({ embeds: [Embeds.error('Failed', 'Something went wrong.')] });

// ℹ️ Blurple
await message.reply({ embeds: [Embeds.info('Info', 'Here is some information.')] });

// ⚠️ Yellow
await message.reply({ embeds: [Embeds.warn('Warning', 'Be careful.')] });

// 🎨 Any color
await message.reply({ embeds: [Embeds.custom(0xFF8800, { title: 'Custom', description: 'Orange embed' })] });

With extra options

await message.reply({
  embeds: [
    Embeds.success('Deployment Complete', 'Version **1.4.2** is live.', {
      fields: [
        { name: 'Environment', value: 'Production', inline: true },
        { name: 'Duration',    value: '42s',        inline: true },
      ],
      footer:    'Dev Codes CI',
      thumbnail: 'https://example.com/logo.png',
      timestamp: true,
    }),
  ],
});

Preset colors

| Method | Color | |--------|-------| | Embeds.success() | #57F287 (green) | | Embeds.error() | #ED4245 (red) | | Embeds.info() | #5865F2 (blurple) | | Embeds.warn() | #FEE75C (yellow) | | Embeds.custom(color, opts) | Any hex integer |


Button Paginator

Reply with a paginated embed controlled by ⏮ ◀ ✖ ▶ ⏭ buttons. Buttons disable automatically at the first/last page and are removed when the collector times out.

const { paginate, Embeds } = require('devcodes-djs');

handler.command('list', async (message) => {
  const pages = [
    Embeds.info('Page 1', 'First batch of results...'),
    Embeds.info('Page 2', 'Second batch of results...'),
    Embeds.info('Page 3', 'Third batch of results...'),
  ];

  await paginate(message, pages, { timeout: 60_000, authorOnly: true });
});

PaginatorOptions

| Option | Type | Default | Description | |--------|------|---------|-------------| | timeout | number | 30000 | Button collector timeout in ms | | authorOnly | boolean | true | Only the command author can interact |


Containers (Components V2)

Build rich Discord Components V2 messages with a clean, fluent API. Containers support text, sections with thumbnails, separators, media galleries, and button rows — all composable.

⚠️ Components V2 messages require the MessageFlags.IsComponentsV2 flag and cannot include embeds.

Quick compose

const { Containers } = require('devcodes-djs');
const { MessageFlags, ButtonBuilder, ButtonStyle } = require('discord.js');

const c = Containers.compose({
  color:     0x57F287,
  header:    '✅ Deployment complete',
  body:      'Version **1.4.2** is now live on production.',
  thumbnail: 'https://example.com/logo.png',
  images:    ['https://example.com/screenshot.png'],
  buttons: [
    new ButtonBuilder().setCustomId('view').setLabel('View Logs').setStyle(ButtonStyle.Primary),
    new ButtonBuilder().setCustomId('rollback').setLabel('Rollback').setStyle(ButtonStyle.Danger),
  ],
});

await message.reply({
  components: [c],
  flags: MessageFlags.IsComponentsV2,
});

Build manually

Use the individual factory functions for full control:

const {
  container, text, section, separator, mediaGallery, buttonRow,
  SeparatorSpacingSize,
} = require('devcodes-djs');
const { ButtonBuilder, ButtonStyle, MessageFlags } = require('discord.js');

const c = container(0x5865F2) // accent color
  .addTextDisplayComponents(
    text('**📦 New Release — v2.0.0**')
  )
  .addSeparatorComponents(
    separator(true, SeparatorSpacingSize.Large)
  )
  .addSectionComponents(
    section('A major update with breaking changes. Read the migration guide before upgrading.', 'https://example.com/icon.png')
  )
  .addTextDisplayComponents(
    text('**Changes:**\n- Removed deprecated API\n- New `compose()` API\n- Performance improvements')
  )
  .addSeparatorComponents(separator())
  .addMediaGalleryComponents(
    mediaGallery([
      { url: 'https://example.com/before.png', description: 'Before' },
      { url: 'https://example.com/after.png',  description: 'After'  },
    ])
  )
  .addActionRowComponents(
    buttonRow([
      new ButtonBuilder().setCustomId('changelog').setLabel('Changelog').setStyle(ButtonStyle.Primary),
      new ButtonBuilder().setURL('https://docs.example.com').setLabel('Docs').setStyle(ButtonStyle.Link),
    ])
  );

await message.reply({ components: [c], flags: MessageFlags.IsComponentsV2 });

Container components

| Function | What it creates | |----------|----------------| | container(color?, spoiler?) | Top-level container, wraps everything | | text(content) | Plain / markdown text block | | section(content, imageUrl?) | Text + optional right-hand thumbnail | | separator(divider?, spacing?) | Horizontal divider | | mediaGallery(items) | Image gallery (up to 10 images) | | buttonRow(buttons[]) | Action row of buttons | | compose(opts) | Build a complete container from options |

compose() options

| Option | Type | Description | |--------|------|-------------| | color | number | Accent color hex integer | | header | string | Bold header text | | body | string | Body text (shown in a section) | | thumbnail | string | Image URL next to body | | extra | string[] | Additional text blocks | | images | string[] | Media gallery image URLs | | imageSeparator | boolean | Separator before gallery (default: true) | | buttons | ButtonBuilder[] | Buttons row at the bottom | | spoiler | boolean | Mark container as spoiler |

Spoiler container

const c = Containers.compose({
  header:  '🔞 Spoiler content',
  body:    'This will be hidden behind a spoiler.',
  spoiler: true,
});

TypeScript

Full types for everything.

import {
  CommandHandler,
  Embeds,
  paginate,
  Containers,
  type HandlerConfig,
  type CommandOptions,
  type EmbedPresetOptions,
  type PaginatorOptions,
  type QuickContainerOptions,
} from 'devcodes-djs';

Full bot example

const { Client, GatewayIntentBits, MessageFlags, ButtonBuilder, ButtonStyle } = require('discord.js');
const { CommandHandler, Embeds, paginate, Containers } = require('devcodes-djs');

const client  = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent] });
const handler = new CommandHandler({ prefix: '!' });

handler
  .command('ping', async (msg) => {
    await msg.reply({ embeds: [Embeds.success('Pong!', `Latency: ${client.ws.ping}ms`)] });
  })
  .command('list', async (msg) => {
    await paginate(msg, [
      Embeds.info('Results — Page 1', 'Item A\nItem B\nItem C'),
      Embeds.info('Results — Page 2', 'Item D\nItem E\nItem F'),
    ]);
  })
  .command('card', async (msg) => {
    const c = Containers.compose({
      color:     0x5865F2,
      header:    '🤖 Bot Info',
      body:      `Servers: **${client.guilds.cache.size}**\nPing: **${client.ws.ping}ms**`,
      thumbnail: client.user.displayAvatarURL(),
    });
    await msg.reply({ components: [c], flags: MessageFlags.IsComponentsV2 });
  });

client.on('messageCreate', (msg) => handler.handle(msg));
client.login(process.env.BOT_TOKEN);

License

MIT © azaresw

Support

Join our Discord for help and updates: discord.gg/ESh2Dp2xX9