If you’ve ever built a Discord bot using Discord.js, you know the struggle. The library is incredibly powerful, but it leaves architecture entirely up to you.
Every time I started a new bot project, I found myself doing the exact same chores:
- Setting up TypeScript and a linter.
- Writing a custom command/event handler from scratch.
- Fighting with
customIdstrings to pass data between commands and buttons. - Setting up Prisma and a Redis queue when the bot finally needed to scale.
I realized I wanted the create-t3-app experience, but for Discord bots. I wanted an interactive CLI that gives me a production-ready, type-safe architecture with opt-in features like a database or background jobs.
So, I built djstoolkit.
What is djstoolkit? 🧩
It’s a blazing-fast, strictly typed scaffolding CLI for Discord.js bots, built ground-up for Bun.
Instead of a monolithic template, it uses an "À La Carte" system. Run one command:
bun create djstoolkit my-awesome-bot
You are greeted with a prompt asking exactly what infrastructure you need:
- Need a database? Hit spacebar, and it sets up Prisma (PostgreSQL) and a Docker Compose file.
- Need delayed tasks? Hit spacebar, and it sets up BullMQ (Redis) with a type-safe worker system.
- Don't need them? Leave them unchecked, and you get a clean, barebones project without a single line of bloat.
TL;DR:
- ✅ Bun-native (instant startup, no Node.js required)
- ✅ Type-safe stateful component routing (no more
customIdstring hacking) - ✅ Opt-in Prisma / BullMQ / Redis — nothing you don't ask for
- ✅ Zero-config TypeScript + Biome (formatting/linting in milliseconds)
How is this different from CommandKit or Sapphire? CommandKit is a great meta-framework but stays Node.js-first and leaves DB/queue setup entirely to you. Sapphire is powerful but heavily OOP (everything is a class), which means more boilerplate. djstoolkit is Bun-native from the ground up, ships opt-in infra (Prisma/BullMQ) out of the box, and keeps the file-based defineX handler style instead of classes — so the learning curve stays low while you still get strict type-safety.
The Features that make it different
I didn't just want to create a file copier. I wanted to solve the biggest DX (Developer Experience) issues in Discord.js.
1. Revolutionary Component Routing (Prefix:UUID)
Passing data from a command to a button in standard D.js is painful. You either use temporary Collectors (which expire) or you shove data into the customId like "BAN_USER_12345", forcing you to write messy .startsWith() checks everywhere.
In djstoolkit, we built a stateful prefix router.
You set your button ID to BAN_USER:12345. The framework intercepts the interaction, splits it by the colon, routes it to the BAN_USER handler, and passes 12345 securely as a session variable.
import { defineButton } from "@/lib/helpers/defineButton";
export const { config, run } = defineButton(
{
customId: "BAN_USER",
name: "Confirm Ban",
description: "Executes the ban",
},
async (interaction, sessionId) => {
// sessionId is automatically extracted! (e.g. "12345")
await interaction.guild.members.ban(sessionId);
await interaction.reply("User banned!");
}
);
2. Built-in Memory Store (With Auto-TTL)
Combined with the router above, we ship an elegant In-Memory Store. You can generate a UUID session, save a complex object to RAM with a 15-minute TTL, and pass that UUID to a Modal or a Button. When the user clicks the button, you retrieve the data using takeStore(sessionId), which automatically deletes it from RAM to prevent double-clicks or memory leaks.
3. 100% Type-Safety via Module Augmentation
If you opt into the internal Event Bus (Relay) or Background Jobs (BullMQ), you get absolute type safety without importing massive central registries.
Using TypeScript's Module Augmentation, you define your jobs anywhere in your code, and the enqueueJob function magically knows what payload to ask for:
// Define it once
declare module "@/lib/bullmq/jobs" {
interface JobRegistry {
"send-reminder": { channelId: string; reminder: string };
}
}
// Full autocomplete and validation anywhere in your app!
await enqueueJob("send-reminder", { channelId: "123", reminder: "Hello!" });
4. Blazing Fast (Bun + Biome)
The entire project runs on Bun. It starts instantly. We also ditched ESLint/Prettier in favor of Biome, meaning formatting and linting take milliseconds.
Try it out!
The project is currently in Beta (v0.3.x), and the core architecture is stable — the docs in /docs cover everything you need to get started. If you're planning to build a new Discord bot this weekend, give it a spin!
📦 NPM: create-djstoolkit
🐙 GitHub: fakejsdev/djstoolkit (Stars are greatly appreciated! ⭐)
I’m actively looking for feedback on the CLI flow and the routing patterns. If you have any ideas, drop a comment below or open an issue on GitHub. PRs are extremely welcome!
Let me know what you think! 👇
Tags: #discordjs #bun #typescript #showdev

Top comments (0)