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

@nexcord/core

v0.0.2

Published

<div align="center">

Readme

Nexcord

A decorator-driven, dependency-injection-powered framework for building Discord bots with discord.js and TypeScript — built for Bun.

npm version npm downloads license discord.js status


Nexcord takes the ergonomics of decorator/DI frameworks like NestJS or Angular and applies them to Discord bot development. Instead of wiring up event listeners, command handlers, and services by hand, you describe what each class is with a decorator, and Nexcord handles discovery, instantiation, and dependency injection for you.

@Service()
export class GreetingService {
  greet(username: string) {
    return `Welcome aboard, ${username}! 🎉`;
  }
}

@Listener()
export class MemberEvents {
  constructor(private readonly greeting: GreetingService) {}

  @On('guildMemberAdd')
  async onJoin(member: GuildMember) {
    await member.send(this.greeting.greet(member.user.username));
  }
}

No manual new GreetingService(), no manual client.on('guildMemberAdd', ...) wiring, no manual file imports. Nexcord discovers both classes, resolves the dependency, and boots them for you.

Table of Contents

✨ Features

  • Decorator-based architecture@Service(), @Listener(), @SlashCommand(), and @Guard() describe your classes; Nexcord does the rest.
  • Recursive constructor injection — powered by reflect-metadata. Services can depend on other services, which can depend on more services, resolved automatically and instantiated as singletons.
  • Convention-based autoloading — drop a file in the right folder and it's registered. No manual import lists to maintain.
  • Route Guard Forwarding — a type-safe, pattern-matching middleware system that can gate any command or event, including wildcards, prefix/suffix matches, and regular expressions.
  • Built-in services for common bot needs: channel lookups, guild lookups, role caching, and shared configuration.
  • A real ecosystem, not just a core@nexcord/tsx lets you write components as JSX, and @nexcord/config gives you a DI-aware shared config store.
  • Built for Bun — no build step required to run your bot in development.

🧩 The Nexcord Ecosystem

Nexcord is split into small, focused packages. @nexcord/core is the only one you strictly need — the others are optional but designed to plug in seamlessly.

| Package | Description | | --- | --- | | @nexcord/core | The framework itself — bootstrapping, dependency injection, decorators, guards, and built-in services. You are here. | | @nexcord/tsx | Write buttons, modals, embeds, and Discord Components V2 layouts as JSX instead of chaining builder methods. | | @nexcord/config | A tiny, dependency-injection-aware shared config store that ConfigService reads from — with helpers to register config that depends on your services or on the bot being ready. |

You can add either companion package at any point — neither is required to get a bot running.

📦 Installation

Nexcord is developed for and with Bun, and its autoloading layer is built on standard CommonJS-style require() calls, so a Node.js + ts-node/tsx setup works as well.

bun add @nexcord/core discord.js reflect-metadata
npm install @nexcord/core discord.js reflect-metadata
pnpm add @nexcord/core discord.js reflect-metadata
yarn add @nexcord/core discord.js reflect-metadata

Nexcord's decorators rely on TypeScript's experimental decorator metadata, so make sure your tsconfig.json enables it:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "strict": true
  }
}

🚀 Quick Start

Create your bot's entry point and bootstrap it with NexCord:

// src/index.ts
import 'reflect-metadata';
import { GatewayIntentBits } from 'discord.js';
import { NexCord } from '@nexcord/core';

NexCord
  .setIntents(
    GatewayIntentBits.Guilds,
    GatewayIntentBits.GuildMessages,
    GatewayIntentBits.MessageContent,
    GatewayIntentBits.GuildMembers
  )
  .run(process.env.DISCORD_TOKEN!);
DISCORD_TOKEN=your-bot-token bun run src/index.ts

That's it — run() recursively loads your commands, events, configs, and common folders, registers everything it finds, and logs in.

Need to run logic once the bot is actually online — with services already resolved for you? Pass a factory:

import { NexCord } from '@nexcord/core';
import { GuildService } from '@nexcord/core';

NexCord.run(process.env.DISCORD_TOKEN!, {
  inject: [GuildService],
  factory: async (guilds: GuildService) => {
    console.log(`Logged in as ${NexCord.client.user?.tag}`);
  }
});

📁 Recommended Project Structure

Nexcord doesn't force a rigid structure on you, but its autoloader looks for a small set of conventional folders under src/. A typical project looks like this:

src/
├── commands/           # @SlashCommand() classes — autoloaded
│   ├── Ping.command.ts
│   └── Info.command.ts
├── events/              # @Listener() classes — autoloaded
│   ├── Ready.listener.ts
│   └── Member.listener.ts
├── configs/             # registerConfig() definitions (@nexcord/config) — autoloaded
│   └── general.config.ts
├── common/              # Shared building blocks — autoloaded
│   ├── services/         # @Service() classes
│   │   └── Greeting.service.ts
│   ├── guards/            # @Guard() classes
│   │   └── Cooldown.guard.ts
│   └── actions/           # @nexcord/tsx component action controllers
│       └── Confirm.action.ts
├── components/           # .tsx component definitions (@nexcord/tsx)
│   └── confirm.components.tsx
└── index.ts              # Bootstrap entry point

Four top-level folders are autoloaded automatically (see DirEnum below): commands, events, configs, and common. Anything else — services, guards, components, or any structure you prefer — works fine as long as it's imported, directly or indirectly, from a file that is autoloaded. Decorators run the moment a class's module is evaluated, so a @Service() class gets registered as soon as anything imports it, regardless of which folder it physically lives in.

🧠 Core Concepts

Bootstrapping — the NexCord class

NexCord is a static entry point that controls your bot's lifecycle, imported from @nexcord/core:

| Method | Description | | --- | --- | | NexCord.setIntents(...intents) | Overrides the default GatewayIntentBits (Nexcord enables all intents by default). | | NexCord.setClient(client) | Provide your own pre-configured discord.js Client instance instead of letting Nexcord create one. | | NexCord.setGuild(guildId) | Resolves and caches a target Guild globally, handy for single-guild bots. | | NexCord.get(targetClass) | Retrieves a registered @Service() / @Listener() / @SlashCommand() / @Guard() instance from the DI cache. | | NexCord.run(token, options?) | Recursively loads your project folders, registers everything, and logs in. Accepts an optional { factory, inject } pair — see below. | | NexCord.client | The underlying discord.js Client, available once the bot has bootstrapped. |

setIntents, setClient, and setGuild are chainable, so you configure the bot fluently before calling run().

Autoloading & Directory Conventions

When NexCord.run() is called, its loader walks four directories (relative to your project root) and require()s every TypeScript file it finds, which is what triggers your decorators to run and register themselves:

| Directory | Purpose | | --- | --- | | src/commands | Slash command classes | | src/events | Event listener classes | | src/configs | Configuration registration files | | src/common | Everything shared — services, guards, actions, and anything else |

Dependency Injection

Any class decorated with @Service(), @Listener(), @SlashCommand(), or @Guard() participates in Nexcord's DI container. Constructor parameters are inspected via reflect-metadata, and their dependencies are resolved recursively: if A depends on B, and B depends on C, Nexcord instantiates C first, injects it into B, then injects B into A — all as singletons, so every consumer shares the same instance.

import { Service } from '@nexcord/core';

@Service()
export class QuoteService {
  private readonly quotes = [
    'Ship it.',
    'Works on my machine.',
    'It is always a race condition.'
  ];

  random(): string {
    return this.quotes[Math.floor(Math.random() * this.quotes.length)]!;
  }
}

Any other @Service(), @Listener(), @SlashCommand(), or @Guard() class can now simply ask for a QuoteService in its constructor and receive a fully resolved instance.

Services

Services are where your business logic belongs — keep commands and listeners thin, and let services do the heavy lifting. Just decorate a class with @Service(); there's no interface to implement.

Slash Commands

A slash command is a class decorated with @SlashCommand(name, description) that extends BaseSlashCommand and implements two hooks:

  • build(builder) — add options, subcommands, or permissions. Return the builder (sync or async).
  • handler(interaction) — runs when the command is invoked.
// src/commands/Quote.command.ts
import { BaseSlashCommand, SlashCommand } from '@nexcord/core';
import {
  CommandInteraction,
  SlashCommandBuilder,
  type SlashCommandOptionsOnlyBuilder
} from 'discord.js';
import { QuoteService } from '../common/services/Quote.service';

@SlashCommand('quote', 'Sends a random developer quote.')
export class QuoteCommand extends BaseSlashCommand {
  constructor(private readonly quotes: QuoteService) {
    super();
  }

  override build(b: SlashCommandBuilder | SlashCommandOptionsOnlyBuilder) {
    return b;
  }

  override async handler(interaction: CommandInteraction): Promise<void> {
    await interaction.reply(this.quotes.random());
  }
}

Notice QuoteService is simply requested in the constructor — Nexcord resolves and injects it automatically.

Event Listeners

Event listeners are classes decorated with @Listener(). Inside, individual methods are decorated with @On(event) (fires every time) or @Once(event) (fires once).

// src/events/Member.listener.ts
import { Listener, On } from '@nexcord/core';
import { GuildMember } from 'discord.js';
import { GreetingService } from '../common/services/Greeting.service';

@Listener()
export class MemberEvents {
  constructor(private readonly greeting: GreetingService) {}

  @On('guildMemberAdd')
  async onJoin(member: GuildMember) {
    await member.send(this.greeting.greet(member.user.username)).catch(() => {});
  }
}

Beta note: Nexcord also ships parameter decorators — @Content(), @Author(), @Member(), @Guild(), @GetClient() — meant to let you pull specific values straight into your method's parameters instead of destructuring the raw event object yourself. In the current beta, these decorators register their metadata correctly, but the invocation loop that would actually extract and inject those values is still a work in progress. Write your handlers to accept the raw discord.js event arguments for now (as in the example above), and treat the parameter decorators as a forward-compatible preview of where the API is headed.

Guards & Route Forwarding

Guards are Nexcord's middleware system, and its most distinctive feature. A @Guard() class declares one or more forwarding routes that connect a guard method to a listener method. If the guard method returns (or resolves to) false, the target's execution is aborted before it ever runs.

@Guard(listeners, ...routes: Forward<T, Ls>[])
  • listeners — an object mapping a name to a listener (or other registered) class, e.g. { MemberEvents }.
  • routes — one or more strings of the form "guardMethod->ListenerName::listenerMethod". TypeScript validates this format at compile time via the Forward<T, Ls> type — if the target method doesn't exist, your build fails.

Route strings support pattern matching, so a single guard can protect many targets at once:

| Pattern | Matches | Example | | --- | --- | --- | | * | Anything, in any position | checkAuth->*::* — every method of every listener | | prefix% | Names starting with prefix | on% matches onMessage, onReady | | %suffix | Names ending with suffix | %Listener matches MemberEvents (if aliased accordingly) | | %contains% | Names containing contains anywhere | %check% matches checkSpam | | /regex/ | Names matching a regular expression | /^isAdmin/ matches isAdminUser |

// src/common/guards/Cooldown.guard.ts
import { Guard } from '@nexcord/core';
import { Message } from 'discord.js';
import { HypeListener } from '../../events/Hype.listener';

@Guard(
  { HypeListener },
  'throttle->HypeListener::onMessage'
)
export class CooldownGuard {
  private readonly lastTrigger = new Map<string, number>();

  async throttle(message: Message): Promise<boolean> {
    const last = this.lastTrigger.get(message.author.id) ?? 0;
    if (Date.now() - last < 10_000) return false; // still cooling down — abort
    this.lastTrigger.set(message.author.id, Date.now());
    return true;
  }
}

Guard forwarding fires both when a listener's event is dispatched and when a slash command is invoked, so the same mechanism can be used to gate permission checks on commands as well as events.

Built-in Services

Nexcord ships a handful of @Service() classes out of the box, ready to be injected wherever you need them:

| Service | Purpose | | --- | --- | | ChannelService | Fast lookup and validation helpers for text, voice, DM, and "sendable" channels. | | ConfigService | A dot-notation reader/writer (get<T>(path) / set(path, value)) over the shared store from @nexcord/config. | | GuildService | Simple guild lookup helper, complementing NexCord.setGuild(). | | RoleService | Loads a guild's roles and caches them into config as roles.<name> / roles.<id>. Exposes helpers like hasUser(userId, ...roles). |

⚠️ Beta Status & Known Limitations

Nexcord is beta software. The public API described above is stable enough to build on, but expect rough edges:

  • Parameter decorators (@Content(), @Author(), @Member(), @Guild(), @GetClient()) are registered but not yet wired into the event invocation loop — see the Event Listeners section above.
  • APIs may still shift between minor releases while the framework stabilizes toward a 1.0.

Found a bug or a rough edge? Please open an issue — feedback at this stage directly shapes the 1.0 API.

🤝 Contributing

Issues and pull requests are welcome. If you're proposing a larger change (a new decorator, a change to the DI resolution order, etc.), please open an issue first to discuss the approach.

📄 License

Released under the MIT License.


Built by SignorMassimo · Part of the Nexcord ecosystem alongside @nexcord/tsx and @nexcord/config