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

district.js

v0.12.0

Published

A powerful Node.js module for building District bots — post, react, run slash commands, and receive events.

Readme

district.js

A powerful Node.js module for building District bots — post, edit, react, run slash commands, and receive events with a clean, object-oriented API.

npm install district.js

Requires Node.js 18+. Create an app and a bot in the Developer Portal to get your token (dbot_…) and signing secret (dsk_…).

Your first bot

import { Client } from "district.js";

const client = new Client({
  token: process.env.DISTRICT_TOKEN,          // dbot_…
  signingSecret: process.env.DISTRICT_SECRET,  // dsk_… (needed to receive events)
});

client.on("ready", (me) => console.log(`Logged in as ${me.displayName}`));

client.on("messageCreate", (message) => {
  if (message.authoredByMe) return;            // don't reply to yourself
  if (message.content === "!ping") message.reply("pong 🏓");
});

// Starts an HTTP server that receives District's signed webhook events.
client.listen(3000);

Point your app's Event Subscription (in the portal) at https://your-host:3000/ and tick the events you want. District signs every event; district.js verifies the signature for you before emitting it.

Sending & acting

// Send to a channel
const msg = await client.channels.send(channelId, "hello world");

// Act on a message you have
await msg.edit("hello, world!");
await msg.react("👍");
await msg.delete();

// Read history (returns a Collection)
const recent = await client.channel(channelId).fetchMessages(50);
const commands = recent.filter((m) => m.content?.startsWith("!"));

Users, spaces & permissions

const user = await message.fetchAuthor();       // User
const space = await client.spaces.fetch(spaceId); // Space (app must be installed)

const members = await space.members();           // Collection<SpaceMember>
const roles = await space.roles();               // Collection<Role>
const named = members.filter((m) => m.nickname !== null);
const canBan = roles.some((r) => r.permissions.has("BanMembers"));

// Permissions
import { PermissionsBitField } from "district.js";
const perms = new PermissionsBitField(["SendMessages", "AddReactions"]);
perms.has(PermissionsBitField.Flags.SendMessages); // true
perms.toString(); // "32770" — use as the install-link permissions integer

// Markdown
import { Formatters } from "district.js";
channel.send(Formatters.bold("Heads up!") + " " + Formatters.code("v0.2.0"));

Embeds

import { EmbedBuilder } from "district.js";

const embed = new EmbedBuilder()
  .setTitle("Deploy complete")
  .setUrl("https://example.com/builds/128")
  .setColor("#c6a86b")
  .setDescription("Build **#128** shipped to production.")
  .addFields(
    { name: "Env", value: "prod", inline: true },
    { name: "By", value: "ci-bot", inline: true },
  )
  .setFooter({ text: "district.js" })
  .setTimestamp();

await channel.send({ content: "New release 🎉", embeds: [embed] });

The District app renders it as an embed card (accent bar, title, fields, image).

Buttons

import { ActionRowBuilder, ButtonBuilder } from "district.js";

const row = new ActionRowBuilder().addComponents(
  new ButtonBuilder().setLabel("Approve").setStyle("success").setCustomId("approve"),
  new ButtonBuilder().setLabel("Deny").setStyle("danger").setCustomId("deny"),
  new ButtonBuilder().setLabel("Docs").setUrl("https://usedistrict.org/developers/docs"),
);

await channel.send({ content: "Review this?", components: [row] });

// A click delivers an interactionCreate (needs an Event Subscription endpoint)
client.on("interactionCreate", (i) => {
  if (i.isButton() && i.customId === "approve") i.reply("Approved ✅");
});

Select menus & modals

import { ActionRowBuilder, StringSelectMenuBuilder, ButtonBuilder, ModalBuilder, TextInputBuilder } from "district.js";

// Dropdown
const menu = new ActionRowBuilder().addComponents(
  new StringSelectMenuBuilder().setCustomId("role").setPlaceholder("Pick a role")
    .addOptions({ label: "Dev", value: "dev" }, { label: "Design", value: "design" }),
);
await channel.send({ content: "Choose:", components: [menu] });

// Modal (opens instantly on click; only the submit is delivered to the bot)
const modal = new ModalBuilder().setCustomId("feedback").setTitle("Feedback")
  .addComponents(new TextInputBuilder().setCustomId("msg").setLabel("Your message").setStyle("paragraph"));
const row = new ActionRowBuilder().addComponents(
  new ButtonBuilder().setLabel("Give feedback").setStyle("primary").setModal(modal),
);
await channel.send({ components: [row] });

client.on("interactionCreate", (i) => {
  if (i.isSelectMenu() && i.customId === "role") i.reply(`You picked ${i.values[0]}`);
  if (i.isModalSubmit() && i.customId === "feedback") i.reply(`Thanks: ${i.field("msg")}`);
});

Because District delivers interactions asynchronously, modals are attached to a button and shown instantly, client-side; only the submitted values are sent back to your bot.

Slash commands

Register the command in the portal (name + handler URL), then:

client.on("interactionCreate", (interaction) => {
  if (interaction.commandName === "weather") {
    interaction.reply(`It's sunny in ${interaction.args.text}`);
  }
});

File-based handlers (commands / events / components)

Prefer folders over one giant file? Drop handlers into commands/, events/, and components/ and load them — the Client auto-routes slash commands and button/select/modal interactions for you.

my-bot/
├─ index.js
├─ commands/
│  └─ ping.js
├─ events/
│  └─ ready.js
└─ components/
   └─ vote.js
// commands/ping.js
import { SlashCommandBuilder } from "district.js";
export default {
  data: new SlashCommandBuilder().setName("ping").setDescription("Pong!"),
  async execute(interaction, client) {
    await interaction.reply("🏓 Pong!");
  },
};
// events/ready.js
export default {
  name: "ready",
  once: true,
  execute(me, client) { console.log(`Logged in as ${me.displayName}`); },
};
// components/vote.js — routes any customId starting with "vote:"
export default {
  id: "vote:*",                       // exact string, "prefix*", or (id) => boolean
  async execute(interaction, client) {
    await interaction.reply({ content: `You picked ${interaction.customId.slice(5)}`, ephemeral: true });
  },
};
// index.js
import { Client } from "district.js";
const client = new Client({ token: "dbot_…", signingSecret: "dsk_…" });

await client.events.load("./events");         // bind event handlers
await client.commands.load("./commands");     // load + start routing slash commands
await client.commands.register();             // publish the slash defs to District
await client.components.load("./components");  // route buttons / selects / modals

await client.listen(3000);

Notes: files starting with _ and subfolders are supported (subfolders load recursively; _helpers.js is skipped). Every handler receives the client as a trailing argument. client.commands.cache is a Collection of the loaded commands, keyed by name.

Events

| Event | Argument | | --- | --- | | ready | ClientUser | | messageCreate | Message | | messageUpdate | Message | | messageDelete | { id, channelId, spaceId } | | messageReactionAdd | { messageId, channelId, userId, emoji, spaceId } | | messageReactionRemove | { messageId, channelId, userId, emoji, spaceId } | | interactionCreate | Interaction | | error | Error |

Using with Express

If you already run a server, mount the middleware (feed it the raw body so the signature verifies):

import express from "express";
const app = express();
app.post("/district", express.text({ type: "*/*" }), client.middleware());
await client.login();
app.listen(3000);

Scopes & permissions

A bot only acts where its app is installed, and each action needs a scope (send_messages, read_messages, manage_messages). See the full API docs.

License

MIT