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

meocord

v1.8.2

Published

Decorator-based Discord bot framework built on discord.js. Brings NestJS-style controllers, dependency injection, guards, and testing utilities to bot development — with a full CLI and TypeScript-first design.

Downloads

3,289

Readme

MeoCord Framework

MeoCord is a decorator-based Discord bot framework built on top of discord.js. It brings a NestJS-style architecture — controllers, services, guards, and dependency injection — to bot development, with a full CLI, TypeScript-first design, and testing utilities included out of the box.


Table of Contents


Features

  • Decorator-based controllers — Handle slash commands, buttons, modals, select menus, context menus, messages, and reactions with @Command, @Controller, and @UseGuard decorators. No routing boilerplate.
  • Dependency injection — Built on Inversify. Services are wired into controllers automatically; no manual instantiation or service locators.
  • Guard system — Pre-execution hooks for auth, rate limiting, metrics, and anything else. Apply per-method or per-class with @UseGuard. Guards receive the full interaction context.
  • Full CLImeocord create, build, start, generate. Scaffolds controllers, services, and guards; handles Webpack builds for both development and production.
  • Testing utilitiesMeoCordTestingModule, createMockInteraction, createMockMessage, createMockUser, createMockClient, createMockGuild, createMockChannel, createChatInputOptions, and overrideGuard let you test controllers against real guard logic without a Discord connection. Type guards and reply state machines work out of the box.
  • TypeScript-first — Strict types throughout. Decorator metadata, DeepMocked<T> for test mocks, and typed config interfaces included.
  • Extensible build — Expose a Webpack config hook in meocord.config.ts to add rules, plugins, or loaders without ejecting.

Getting Started

Prerequisites

  • Runtime: Node.js (latest LTS) or Bun 1.x+
  • TypeScript: 5.0+
  • Package manager: npm, yarn, pnpm, or bun

MeoCord ships dual ESM/CJS builds. New projects generated by the CLI are preconfigured for ESM.

Create a New App

npx meocord create <your-app-name>

The CLI detects installed package managers and prompts you to choose, or you can pass a flag directly:

npx meocord create <your-app-name> --use-bun
npx meocord create <your-app-name> --use-npm
npx meocord create <your-app-name> --use-pnpm
npx meocord create <your-app-name> --use-yarn

Set your Discord bot token in meocord.config.ts, then start the bot:

npx meocord start --dev   # development with live-reload
npx meocord start --build --prod  # production build + start

Quick Example

A minimal slash command controller:

import { Controller, Command, UseGuard } from 'meocord/decorator'
import { CommandType } from 'meocord/enum'
import { type ChatInputCommandInteraction } from 'discord.js'
import { RateLimiterGuard } from '@src/guards/rate-limiter.guard.js'
import { GreetingService } from '@src/services/greeting.service.js'

@Controller()
export class GreetingSlashController {
  constructor(private readonly greetingService: GreetingService) {}

  @Command('greet', CommandType.SLASH)
  @UseGuard({ provide: RateLimiterGuard, params: { limit: 3, window: 10_000 } })
  async greet(interaction: ChatInputCommandInteraction) {
    const name = interaction.options.getString('name', true)
    const message = await this.greetingService.buildGreeting(name)
    await interaction.reply({ content: message })
  }
}

Register it in src/app.ts:

import { MeoCord } from 'meocord/decorator'
import { GatewayIntentBits, Partials } from 'discord.js'
import { GreetingSlashController } from '@src/controllers/slash/greeting.slash.controller.js'
import { GreetingService } from '@src/services/greeting.service.js'

@MeoCord({
  controllers: [GreetingSlashController],
  // `services` is for specialized, event-driven services (e.g. RabbitMQ consumers,
  // schedulers). Regular business-logic services are injected via controller
  // constructors — they don't belong here.
  services: [RabbitMQService],
  clientOptions: {
    intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages],
    partials: [Partials.Message, Partials.Channel],
  },
})
export class App {}

Project Structure

.
├── meocord.config.ts
├── eslint.config.ts
├── jest.config.ts
├── tsconfig.json
├── tsconfig.eslint.json
├── tsconfig.test.json
├── package.json
└── src
    ├── main.ts                         # Entry point — bootstraps the app
    ├── app.ts                          # Root module — registers controllers and services
    ├── controllers
    │   ├── slash
    │   │   ├── builders/               # Slash command option/subcommand builders
    │   │   ├── sample.slash.controller.ts
    │   │   └── sample.slash.controller.spec.ts
    │   ├── button
    │   │   ├── sample.button.controller.ts
    │   │   └── sample.button.controller.spec.ts
    │   ├── select-menu
    │   │   ├── sample.select-menu.controller.ts
    │   │   └── sample.select-menu.controller.spec.ts
    │   ├── modal-submit
    │   │   ├── sample.modal-submit.controller.ts
    │   │   └── sample.modal-submit.controller.spec.ts
    │   ├── context-menu
    │   │   ├── builders/               # Context menu command builders
    │   │   ├── sample.context-menu.controller.ts
    │   │   └── sample.context-menu.controller.spec.ts
    │   ├── message
    │   │   ├── sample.message.controller.ts
    │   │   └── sample.message.controller.spec.ts
    │   └── reaction
    │       ├── sample.reaction.controller.ts
    │       └── sample.reaction.controller.spec.ts
    ├── guards
    │   ├── rate-limit.guard.ts
    │   └── rate-limit.guard.spec.ts
    └── services
        ├── sample.service.ts
        └── sample.service.spec.ts

Configuration

meocord.config.ts

The top-level config file. At minimum it needs discordToken. The webpack hook lets you extend the build without ejecting.

import { type MeoCordConfig } from 'meocord/interface'

export default {
  appName: 'MyBot',
  discordToken: process.env.TOKEN!,
  webpack: config => {
    config.module.rules?.push({
      // add custom rules here
    })
    return config
  },
} satisfies MeoCordConfig

ESLint

MeoCord exports a base ESLint config from meocord/eslint. Extend it as needed:

import meocordEslint, { typescriptConfig } from 'meocord/eslint'
import unusedImports from 'eslint-plugin-unused-imports'

export default [
  ...meocordEslint,
  {
    ...typescriptConfig,
    plugins: {
      ...typescriptConfig.plugins,
      'unused-imports': unusedImports,
    },
    rules: {
      ...typescriptConfig.rules,
      'unused-imports/no-unused-imports': 'error',
    },
  },
]

CLI Reference

npx meocord --help

| Command | Alias | Description | |------------|-------|--------------------------------------| | create | — | Scaffold a new MeoCord application | | build | — | Compile the application via Webpack | | start | — | Start the application | | generate | g | Scaffold controllers, services, guards | | show | — | Display framework info |

Common flags:

npx meocord build --prod          # production build
npx meocord build --dev           # development build
npx meocord start --dev           # dev mode with live-reload
npx meocord start --build --prod  # production build + start
npx meocord g co slash "profile"  # generate a slash controller
npx meocord g --help              # list all generator sub-commands

Guards

Guards run before the handler method. Each guard implements canActivate — return true to allow, false to block.

import { Guard } from 'meocord/decorator'
import { type GuardInterface } from 'meocord/interface'
import { type ChatInputCommandInteraction } from 'discord.js'
import { RedisService } from '@src/services/redis.service.js'

@Guard()
export class RateLimiterGuard implements GuardInterface {
  constructor(private readonly redis: RedisService) {}

  // limit and window are injected via @UseGuard params
  limit = 5
  window = 60_000

  async canActivate(interaction: ChatInputCommandInteraction): Promise<boolean> {
    const key = `ratelimit:${interaction.user.id}`
    const count = await this.redis.increment(key, this.window)
    return count <= this.limit
  }
}

Apply to a single method or an entire controller:

// Per-method, with params
@Command('search', CommandType.SLASH)
@UseGuard({ provide: RateLimiterGuard, params: { limit: 5, window: 60_000 } })
async search(interaction: ChatInputCommandInteraction) { ... }

// Per-class (applies to every command in the controller)
@Controller()
@UseGuard(MetricsGuard, DefaultGuard)
export class ProfileController { ... }

Custom Decorators

MeoCord exports applyDecorators and SetMetadata from meocord/common for composing reusable decorators.

Composing guards into a reusable decorator

import { applyDecorators } from 'meocord/common'
import { UseGuard } from 'meocord/decorator'
import { DefaultGuard, RateLimiterGuard } from '@src/guards/index.js'

export const Protected = (limit = 5) =>
  applyDecorators(
    UseGuard(DefaultGuard, { provide: RateLimiterGuard, params: { limit } }),
  )
@Command('profile', CommandType.SLASH)
@Protected(3)
async profile(interaction: ChatInputCommandInteraction) { ... }

Attaching metadata for guards to read

// Define the metadata decorator
import { SetMetadata } from 'meocord/common'
export const Roles = (...roles: string[]) => SetMetadata('roles', roles)

// Read it inside a guard
@Guard()
export class RolesGuard implements GuardInterface {
  async canActivate(interaction: ChatInputCommandInteraction): Promise<boolean> {
    const required: string[] = Reflect.getMetadata('roles', interaction.constructor) ?? []
    if (!required.length) return true
    // ... validate member roles
    return true
  }
}

// Compose into a single decorator
export const RequireRoles = (...roles: string[]) =>
  applyDecorators(Roles(...roles), UseGuard(RolesGuard))

// Apply
@Command('ban', CommandType.SLASH)
@RequireRoles('admin', 'moderator')
async ban(interaction: ChatInputCommandInteraction) { ... }

Testing

MeoCord ships a meocord/testing entry point with utilities for testing controllers in isolation — no real Discord connection required.

MeoCordTestingModule

Builds an isolated DI container from your controllers and providers.

import { MeoCordTestingModule } from 'meocord/testing'
import { GreetingSlashController } from '@src/controllers/slash/greeting.slash.controller.js'
import { GreetingService } from '@src/services/greeting.service.js'

const module = MeoCordTestingModule.create({
  controllers: [GreetingSlashController],
  providers: [{ provide: GreetingService, useValue: mockGreetingService }],
}).compile()

const controller = module.get(GreetingSlashController)

createMockInteraction

Creates a smart mock instance of any discord.js class. The full prototype chain is preserved so instanceof checks pass at every level.

Type guards run real logicisButton(), isRepliable(), isChatInputCommand(), etc. are backed by the actual discord.js prototype methods. The right fields (type, componentType, commandType) are set based on the class you pass in, so no manual .mockReturnValue(true) setup is needed. All type guard methods are still jest.fn() and can be overridden per test.

Reply state machine — for repliable interactions, replied and deferred start as false. Calling reply() or deferReply() twice throws, just like a real interaction. followUp(), editReply(), and deleteReply() throw if called before any reply. The ephemeral flag is tracked on interaction.ephemeral. All reply methods are still jest.fn() so call assertions work normally.

import { createMockInteraction } from 'meocord/testing'
import { ChatInputCommandInteraction, ButtonInteraction, BaseInteraction } from 'discord.js'

const interaction = createMockInteraction(ChatInputCommandInteraction)

// instanceof works at every level
expect(interaction).toBeInstanceOf(ChatInputCommandInteraction) // true
expect(interaction).toBeInstanceOf(BaseInteraction)            // true

// type guards work — no manual setup needed
interaction.isChatInputCommand() // → true
interaction.isRepliable()        // → true
interaction.isButton()           // → false

// reply state machine
interaction.replied   // → false
await interaction.reply({ content: 'hi' })
interaction.replied   // → true
await interaction.reply({ content: 'again' }) // → throws (already replied)

// still jest.fn() — call assertions work normally
expect(interaction.reply).toHaveBeenCalledWith({ content: 'hi' })

// direct property writes work normally
interaction.guildId = 'guild-123'

Works for any discord.js class — interactions, Message, MessageReaction, and anything else. No per-type maintenance.

createChatInputOptions

Builds a typed options resolver from a plain record. Type routing mirrors the real CommandInteractionOptionResolver: wrong-type access returns null, required=true throws if the option is absent.

import { createMockInteraction, createChatInputOptions } from 'meocord/testing'
import { ChatInputCommandInteraction } from 'discord.js'

const interaction = createMockInteraction(ChatInputCommandInteraction)
interaction.options = createChatInputOptions({
  subcommandGroup: 'admin',
  subcommand: 'ban',
  user: { id: '123456789' },
  reason: 'spam',
  duration: 7,
})

interaction.options.getSubcommandGroup()       // → 'admin'
interaction.options.getSubcommand(true)        // → 'ban'
interaction.options.getUser('user')            // → { id: '123456789' }
interaction.options.getString('reason')        // → 'spam'
interaction.options.getNumber('duration')      // → 7
interaction.options.getString('duration')      // → null (wrong type)
interaction.options.getNumber('x', true)       // → throws (absent + required)

All methods are jest.fn() — override any per test with .mockReturnValue().

createMockUser / createMockClient / createMockGuild / createMockChannel

Convenience wrappers for common discord.js classes. All methods are auto-stubbed as jest.fn(). Nested managers (client.users, guild.members, etc.) are independent nested stubs.

import { createMockUser, createMockClient, createMockGuild, createMockChannel } from 'meocord/testing'
import { TextChannel } from 'discord.js'

const user    = createMockUser()
const client  = createMockClient()
const guild   = createMockGuild()
const channel = createMockChannel(TextChannel)

// override nested manager methods per test
;(client.users as any).fetch = jest.fn(() => Promise.resolve(user))
await (client.users as any).fetch('user-123')
expect((client.users as any).fetch).toHaveBeenCalledWith('user-123')

createMockMessage

Creates a smart mock Message. Tracks a deleted boolean — delete(), edit(), reply(), react(), pin(), and unpin() throw if the message has already been deleted. edit() and reply() resolve to a new mock Message instance. All methods are jest.fn().

import { createMockMessage } from 'meocord/testing'

const msg = createMockMessage()

msg.deleted  // → false
await msg.delete()
msg.deleted  // → true
await msg.delete()          // → throws (already deleted)
await msg.edit({ content: 'x' }) // → throws (already deleted)

// edit() and reply() resolve to a new Message mock
const edited = await createMockMessage().edit({ content: 'updated' })
edited.delete  // → jest.fn()

// still jest.fn() — assertions work
expect(msg.delete).toHaveBeenCalledTimes(1)

overrideGuard

Replaces a guard class in the DI container with a stub. No guard dependencies need to be provided.

const module = MeoCordTestingModule.create({
  controllers: [GreetingSlashController],
  providers: [{ provide: GreetingService, useValue: mockGreetingService }],
})
  .overrideGuard(MetricsGuard).useValue({ canActivate: () => true })
  .overrideGuard(RateLimiterGuard).useValue({ canActivate: () => true })
  .compile()

canActivate: () => true allows the method to run. () => false blocks it. Multiple guards chain fluently.

Full example

import { jest } from '@jest/globals'
import { MeoCordTestingModule, createMockInteraction, createChatInputOptions } from 'meocord/testing'
import { ChatInputCommandInteraction } from 'discord.js'
import { GreetingSlashController } from '@src/controllers/slash/greeting.slash.controller.js'
import { GreetingService } from '@src/services/greeting.service.js'
import { RateLimiterGuard } from '@src/guards/rate-limiter.guard.js'

describe('GreetingSlashController', () => {
  let controller: GreetingSlashController
  let greetingService: { buildGreeting: jest.MockedFunction<GreetingService['buildGreeting']> }

  beforeEach(() => {
    greetingService = { buildGreeting: jest.fn() }

    const module = MeoCordTestingModule.create({
      controllers: [GreetingSlashController],
      providers: [{ provide: GreetingService, useValue: greetingService }],
    })
      .overrideGuard(RateLimiterGuard).useValue({ canActivate: () => true })
      .compile()

    controller = module.get(GreetingSlashController)
  })

  it('replies with a greeting for the provided name', async () => {
    jest.mocked(greetingService.buildGreeting).mockResolvedValue('Hello, Alice!')

    const interaction = createMockInteraction(ChatInputCommandInteraction)
    interaction.options = createChatInputOptions({ name: 'Alice' })

    await controller.greet(interaction)

    expect(greetingService.buildGreeting).toHaveBeenCalledWith('Alice')
    expect(interaction.reply).toHaveBeenCalledWith({ content: 'Hello, Alice!' })
  })
})

Deployment

Install all dependencies and build for production:

npm ci && npx meocord build --prod

Strip dev dependencies:

npm ci --omit=dev      # npm
yarn install --production  # yarn
pnpm install --prod    # pnpm
bun install --production   # bun

Required files on the server:

dist/
node_modules/   (production only)
package.json
.env            (if used)
<lockfile>

Start in production:

npx meocord start --prod

Contributing

  1. Fork the repository
  2. Create a feature branch: git checkout -b feat/your-feature
  3. Commit with conventional commits: git commit -m "feat: add X"
  4. Push and open a pull request against main

Include a description of what changed and why, and add tests for any new behaviour.


Release Notes

Full changelog is available on the GitHub Releases page.


License

MeoCord Framework is licensed under the MIT License.