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

@wentthefox-org/discord-bot-framework

v1.0.0

Published

Shared framework for building Discord bots with discord.js: logging, env validation, interaction dispatch, API client, and optional Postgres/i18next helpers

Readme

@wentthefox-org/discord-bot-framework

npm version

Shared building blocks for discord.js-based Discord bots: a nestable console logger, zod-based env validation, a generic HTTP API client, a slash-command interaction dispatcher, command registration helpers, and thin client/shard bootstrap wrappers — plus optional Postgres (Prisma) and i18next helpers for bots that want them.

Extracted from HammerTimeBot, Fantastick, and PennyCurve, which had each independently reimplemented the same architecture. See CLAUDE.md for the design rationale and module-to-source mapping.

Install

pnpm add @wentthefox-org/discord-bot-framework zod discord.js @discordjs/rest discord-api-types

zod is a real dependency of this package but must also be listed by consumers directly (peer resolution quirk of subpath-only usage) if you use defineEnv at your own top level. prisma/@prisma/client/@prisma/adapter-pg and i18next/i18next-fs-backend are optional peers — only install them if you import @wentthefox-org/discord-bot-framework/db or /i18n.

Subpaths

Everything is available from the package root except ./db and ./i18n, which are kept as separate subpaths so bots that don't use Postgres/Prisma or i18next never need to install those peer dependencies.

@wentthefox-org/discord-bot-framework/logger

Backed by pino. Plain new Logger(prefix) / Logger.fromShardInfo(...) stay simple, console-only, worker-thread-free constructors:

import { Logger, NestableLogger, DevNullLogger } from '@wentthefox-org/discord-bot-framework/logger';

const logger = new Logger('Bot');
const interactionLogger = logger.nest(`Interaction#${interaction.id}`);
const shardLogger = Logger.fromShardInfo(process.env.SHARDS);

To additionally fan logs out to a Discord webhook (in batches, respecting Discord's per-webhook rate limits), use createLogger instead — it builds one pino instance with the requested transport targets (console + optional webhook), and nest() on the result shares that same instance rather than spawning a new worker thread per call:

import { createLogger } from '@wentthefox-org/discord-bot-framework/logger';

const logger = createLogger({
  prefix: 'Bot',
  discordWebhook: {
    url: env.LOG_WEBHOOK_URL,
    level: 'warn', // only warn/error/fatal are sent to Discord; default 'warn'
  },
});

@wentthefox-org/discord-bot-framework/env

import { defineEnv, boolFromString } from '@wentthefox-org/discord-bot-framework/env';
import { z } from 'zod';

export const env = defineEnv({
  DISCORD_BOT_TOKEN: z.string().min(1),
  API_URL: z.string().url(),
  LOCAL: boolFromString().default(false),
  SUPPORT_SERVER_ID: z.string().optional().default(''),
});

Throws one formatted Error listing every failing key. Pass { dotenv: false } to skip loading a .env file, or { source } to validate a fixture object (useful in tests).

@wentthefox-org/discord-bot-framework/api-client

import { ApiClient, ApiAuthType } from '@wentthefox-org/discord-bot-framework/api-client';

const apiClient = new ApiClient(logger, {
  baseUrl: `${env.API_URL}/api`,
  authentication: { type: ApiAuthType.AUTHORIZATION_HEADER, getValue: () => env.API_TOKEN },
  userAgent: env.UA_STRING,
});

const { response } = await apiClient.request({
  path: '/things',
  validator: typia.createValidate<Thing[]>(), // optional; omit for `response: unknown`
});

@wentthefox-org/discord-bot-framework/interactions

Commands/components/modals are self-describing — put the name/id directly on the object (as name or id) and pass an array to a createXRegistry() helper instead of hand-writing a Record<Enum, Handler> map. The registry derives the literal name/id union from the array itself (TS 5 const type params), so there's no separate enum to keep in sync, and registry.byName is a drop-in commands/components/modals value for createInteractionRouter/the dispatch* functions below.

import { createChatInputCommandRegistry, createComponentRegistry, createInteractionRouter, handleInteractionError } from '@wentthefox-org/discord-bot-framework/interactions';

const pingCommand = { name: 'ping', getDefinition: () => ({ name: 'ping', description: 'Replies with pong' }), handle: (interaction) => interaction.reply('pong') };

const chatInputCommandRegistry = createChatInputCommandRegistry([pingCommand /* , ... */]);
const componentRegistry = createComponentRegistry([/* ... */]);

const router = createInteractionRouter({
  commands: chatInputCommandRegistry.byName,
  components: componentRegistry.byName,
  buildContext: async (interaction, baseContext) => ({ ...baseContext, t: await buildT(interaction) }),
  onError: (interaction, context, error) =>
    handleInteractionError(interaction, context, { buildMessage: () => context.t('errors.unexpected') }),
});

client.on(Events.InteractionCreate, (interaction) => router(interaction, baseContext));

Bots that need to run logic between a command handler and error handling (e.g. telemetry) can call dispatchChatInputCommand/dispatchAutocomplete/ dispatchComponent/dispatchModal/dispatchContextMenu directly instead of the combined router — both take the same registry.byName maps.

There's also createContextMenuCommandRegistry/createModalRegistry for the other two interaction kinds, and flattenCommandModals(chatInputRegistry) for bots that nest a .modal map directly on the owning chat-input command (rather than registering modals as a standalone top-level registry) — it synthesizes a flat Registry<string, BotModal<Ctx>> view so dispatchModal can consume it unchanged.

@wentthefox-org/discord-bot-framework/commands

import { buildApplicationCommandsBody, createCommandRegistrar, fixedReplyCommandFactory } from '@wentthefox-org/discord-bot-framework/commands';

const registrar = createCommandRegistrar({ rest, applicationId: env.DISCORD_CLIENT_ID, logger });

const commandBodies = buildApplicationCommandsBody(
  { chatInput: chatInputCommandRegistry, contextMenu: contextMenuCommandRegistry },
  { sharedMetadata: { integration_types: [...], contexts: [...] }, definitionArg: t },
);
await registrar.updateGlobalCommands(commandBodies);

const pingCommand = fixedReplyCommandFactory('ping', 'Replies with pong', 'pong');

buildApplicationCommandsBody flattens one or more command registries into the flat JSON body createCommandRegistrar expects: it applies each command's registerCondition filter, merges sharedMetadata into every getDefinition() result (the command's own fields win, except name, which always comes from the registry key — command authors never need to repeat name inside getDefinition's return), and stably sorts every options array (including nested subcommand/subcommand-group options) so required options precede optional ones, matching Discord's API requirement automatically.

@wentthefox-org/discord-bot-framework/client

Sharding is entirely opt-in. Most bots — anything single-guild or otherwise small enough not to need multiple discord.js shards — should just use createBotClient and never touch createShardManager or anything shard-related at all:

import { createBotClient } from '@wentthefox-org/discord-bot-framework/client';

const client = await createBotClient({ intents: [GatewayIntentBits.Guilds], token, onInteraction });

Only reach for createShardManager if your bot actually runs across multiple discord.js shards (large multi-guild bots). It's a separate, independent function — pulling it in doesn't require any sharding-specific config elsewhere in the framework:

import { createShardManager } from '@wentthefox-org/discord-bot-framework/client';

const manager = await createShardManager({
  token, botScriptPath, logger,
  beforeSpawn: () => startupCommandsUpdate(logger),
});

@wentthefox-org/discord-bot-framework/utils

runAttempts, getGitData, queueLazyPromises, condenseStringArray, sendMessageSlices, loadAllMessages, getUserIdentifier, stringifyChannelName, stringifyOptionsData, and generic guild/member/role/ channel lookups (getServer, findServerTextChannelByName, findServerRoleByName, findServerMember, getServerMemberRole, serverMemberHasRole, isSameObject).

@wentthefox-org/discord-bot-framework/db (optional)

Requires @prisma/client and @prisma/adapter-pg (Postgres only).

import { createPostgresPrismaDb } from '@wentthefox-org/discord-bot-framework/db';
import { PrismaClient } from './generated/prisma/client.js';

export const db = createPostgresPrismaDb(PrismaClient, { connectionString: env.DATABASE_URL });

Bots that only talk to an externally-managed database (or no database at all) never need to import this subpath or install its peer dependencies.

@wentthefox-org/discord-bot-framework/i18n (optional)

Requires i18next and i18next-fs-backend.

import { createI18nInitializer } from '@wentthefox-org/discord-bot-framework/i18n';

const initI18next = createI18nInitializer({
  localesDir: './src/locales',
  supportedLngs: SUPPORTED_LANGUAGES,
  fallbackLng: DEFAULT_LANGUAGE,
  debug: env.DEBUG_I18N,
});

const i18nextInstance = await initI18next(logger);

Locale file content, translation-credit generation, and any custom eslint i18n-key-validation rules stay entirely bot-side.

Development

pnpm install
pnpm test
pnpm run lint
pnpm run build