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.3

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.
  • Per-feature isolated containers — every decorator type (@Service, @Guard, @SlashCommand, @Listener) is registered and managed through its own dedicated container class, keeping registration logic isolated while dependency resolution stays centralized.
  • 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.
  • Robust circular dependency resolution — a Lazy Proxy mechanism backed by a centralized container layer (DependencyContainer + ServiceContainer) that tracks resolution state and instantiation order explicitly.
  • Compile-time type checking for event handlers — @On() and @Once() validate your handler's parameter types against the actual discord.js event signature via DcEvents<T>, so a mistyped parameter fails to compile instead of failing at runtime.
  • 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.

New in 0.0.3 — per-feature container isolation: under the hood, every decorator type now owns its own dedicated container class rather than being managed through a single loosely-shared structure. A @Guard() class's registration, its route definitions, and its instantiation are entirely the responsibility of its own container — with no crossover into how @Service() or @SlashCommand() classes are tracked. Each container exposes its own add() / get() contract, but all of them ultimately hand off instance creation and dependency resolution to the same centralized ServiceContainer. In other words: registration and definition are now split per feature and fully isolated from one another, while instantiation and dependency resolution remain centralized. This means a change to one container's internal logic (say, an update to Guard route matching) can no longer leak into the behavior of unrelated containers, which was possible in earlier versions.

🔄 Circular Dependency Resolution

In complex architectures, you might encounter scenarios where two services depend on each other (e.g., AService needs BService, and BService needs AService). Typically, this leads to runtime crashes or undefined properties because one of the services hasn't been instantiated yet.

Nexcord solves this seamlessly using a Lazy Proxy mechanism combined with the @Inject() decorator and the InjectType<T> utility type. When a circular dependency is detected, Nexcord injects a dynamic JS Proxy instead of an uninitialized instance. The real instance is resolved dynamically from the container the moment a method is called.

New in 0.0.3: the Proxy mechanism itself hasn't gone anywhere — it's still what makes circular access safe at the method-call level. What's changed is what sits underneath it. Circular detection and instance bookkeeping are now handled by a dedicated container layer (DependencyContainer for dependency graphs, ServiceContainer for singleton instances and resolution state), instead of being handled ad hoc during resolution. This makes the cycle-breaking behavior explicit and traceable: when a cycle is detected, the already-registered service is re-flagged (circular: false) and its instance is rebuilt through the container rather than relying solely on the Proxy to paper over an incomplete instantiation.

Here is how you can handle circular dependencies:

// A.service.ts
import { Inject, Service, type InjectType } from '@nexcord/core'
import { BService } from './B.service'

@Service()
export class AService {
    // 1. Wrap the target service in a function closure inside @Inject
    // 2. Use InjectType<T> to ensure proper TypeScript typings and autocomplete
    constructor(@Inject(() => BService) private readonly bService: InjectType<BService>) { }

    a() {
        this.bService.a()
    }

    b() {
        console.log('Method b executed in AService')
    }
}
// B.service.ts
import { Inject, Service, type InjectType } from '@nexcord/core'
import { AService } from './A.service'

@Service()
export class BService {
    constructor(@Inject(() => AService) private readonly aService: InjectType<AService>) { }

    b() {
        this.aService.b()
    }

    a() {
        console.log('Method a executed in BService')
    }
}

Now, any other service can consume these seamlessly without worrying about initialization order:

// Logger.service.ts
import { Service } from '@nexcord/core';
import { AService } from './A.service';
import { BService } from './B.service';
import { TestService } from './Test.service';

@Service()
export class LoggerService {
    constructor(
        private readonly testService: TestService,
        private readonly aService: AService,
        private readonly bService: BService,
    ) { }

    log(...args: any[]) {
        this.aService.a() // Safely resolves BService behind the scenes!
        this.bService.b() // Safely resolves AService behind the scenes!
        console.log(this.testService.test(), ...args)
    }
}

How it works under the hood: The framework tracks instantiations through its container layer. If it detects a cycle during the resolution phase, it leaves a Proxy placeholder in the constructor arguments array while the container records the cycle and re-registers the affected service. Once the bootstrap process finishes and the bot is fully ready, any interaction with the proxy transparently routes calls to the fully built singleton inside the ServiceContainer.

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 implements BaseSlashCommand {
  constructor(private readonly quotes: QuoteService) {}

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

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(() => {});
  }
}

Type-Safe Event Parameters with DcEvents<T>

Re-enabled and updated in 0.0.3. @On() and @Once() now validate, at compile time, that your handler's parameters match the actual signature discord.js defines for the event name you passed in. This existed at the core level in an earlier form but had been disabled; 0.0.3 updates the implementation and turns it back on.

The DcEvents<'eventName'> utility type resolves to the exact parameter tuple discord.js emits for that event, so you can spread it directly into your handler's signature:

@On('clientReady')
async ready(...[client]: DcEvents<'clientReady'>) {
    this.loggerService.log(`${client.user?.username} active`)
}

If you mistype the parameter here — for example, annotating client as string instead of letting DcEvents<'clientReady'> infer it — TypeScript will fail to compile, because DcEvents<'clientReady'> is bound to discord.js's actual [Client<true>] signature for that event and the decorator enforces it.

Note: this check is only as precise as the event name you provide. Passing a literal string (as in the examples above) gives you full type narrowing. If you pass a dynamic or generic value (e.g. @On(someVariable)), DcEvents<T> won't be able to narrow to a specific signature.

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:

  • DcEvents<T> type narrowing only works with literal event-name strings passed to @On() / @Once() — dynamic or generic event names won't be narrowed.
  • 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