A production-ready Discord bot template built with TypeScript, featuring dynamic loading, persistent storage, and a developer-friendly generator CLI.
- Dynamic command & event loading — Drop files in the right folder and they're automatically registered. No manual imports.
- Interactive generators — Scaffold new commands and events from the CLI with searchable prompts, type inference, and generated boilerplate.
- Drizzle ORM — Type-safe database access with migrations and a schema-first workflow.
- Redis — Built-in caching and ephemeral data storage out of the box.
- Docker support — Fully containerised with
docker-composefor bot, database, and Redis together. - Pre-defined commands — A set of ready-to-use commands to get you started immediately.
- Bun >= 1.0
- Docker & Docker Compose (optional, for containerised setup)
- A Discord application & bot token from the Discord Developer Portal
git clone https://github.com/yourusername/djs-template.git
cd djs-templatebun installcp .env.example .envThen fill in your .env:
# Bot
DISCORD_TOKEN=your_bot_token_here
CLIENT_ID=your_client_id_here
GUILD_ID=your_dev_guild_id_here # Optional: for guild-scoped command registration
# Database
DATABASE_URL=postgresql://user:password@localhost:5432/djs_template
# Redis
REDIS_URL=redis://localhost:6379bun run db:migrate# Development (with hot reload)
bun run dev
# Production
bun run startThe easiest way to run the full stack (bot + PostgreSQL + Redis) is with Docker Compose:
docker-compose up -dThis will spin up:
| Service | Port |
|---|---|
| Bot | — |
| PostgreSQL | 5432 |
| Redis | 6379 |
To view logs:
docker-compose logs -f botTo stop:
docker-compose down.
├── Dockerfile
├── docker-compose.yml
├── drizzle.config.ts
├── env.example
├── package.json
├── tsconfig.json
│
├── scripts/
│ ├── generate-command.ts # Interactive command scaffolder
│ └── generate-event.ts # Interactive event scaffolder
│
└── src/
├── index.ts # Entry point
├── config.ts # Bot configuration
├── env.ts # Environment variable parsing & validation
│
├── commands/ # Command files, grouped by category
│ └── misc/
│ ├── EchoCommand.ts
│ └── HelpCommand.ts
│
├── events/ # Top-level Discord event listeners
│ ├── InteractionCreateEvent.ts
│ └── ReadyEvent.ts
│
├── listeners/ # Additional event listener logic
│ └── index.ts
│
├── core/
│ ├── Core.ts # Client setup & dynamic loader
│ ├── Embed.ts # Reusable embed builder
│ └── typings.ts # Shared types (Command, Event, etc.)
│
├── db/
│ ├── index.ts # Drizzle database client
│ ├── redis.ts # Redis client
│ └── schema.ts # Drizzle schema definitions
│
├── modules/ # Self-contained feature modules
│ ├── index.ts
│ └── management/
│ └── logging.ts
│
└── utils/
├── index.ts
├── logger.ts # Logging utility
├── misc.ts # General helpers
├── pagination.ts # Paginated embed utility
└── permissions.ts # Permission helpers
Instead of writing boilerplate by hand, use the built-in generator scripts to scaffold new commands and events.
bun run generate:commandThe CLI will walk you through:
- Command name, description, type, category, and usage
- Adding typed options (String, Integer, Boolean, User, Channel, etc.)
- Confirming the output path before writing
bun run generate:eventThe CLI will:
- Let you search across all ~90 Discord.js client events
- Show the correct argument types for the selected event
- Generate a fully typed event file, including the correct
discord.jsimports
Both generators are self-contained and do not import from src/, so they will never accidentally start the bot.
Create a file in src/commands/<category>/:
// src/commands/general/HelloCommand.ts
import { Command } from "@/core/typings";
import { ApplicationCommandType } from "discord.js";
export default {
name: "hello",
description: "Says hello!",
type: ApplicationCommandType.ChatInput,
usage: ["/hello"],
options: [],
run: async ({ ctx }) => {
await ctx.reply("Hello, world!");
},
} as Command;The loader will pick it up automatically on next start — no registration needed.
Create a file in src/events/:
// src/events/GuildMemberAddEvent.ts
import { Event } from "@/core/typings";
import { ClientEvents, GuildMember } from "discord.js";
export default {
name: "guildMemberAdd",
run: async (member: GuildMember) => {
console.log(`${member.user.tag} joined ${member.guild.name}`);
},
} as Event<keyof ClientEvents>;This template uses Drizzle ORM for database access.
# Generate a migration after schema changes
bun run db:generate
# Apply migrations
bun run db:migrate
# Open Drizzle Studio (visual DB browser)
bun run db:studioDefine your schema in src/db/schema.ts:
import { pgTable, serial, text, timestamp } from "drizzle-orm/pg-core";
export const users = pgTable("users", {
id: serial("id").primaryKey(),
discordId: text("discord_id").notNull().unique(),
createdAt: timestamp("created_at").defaultNow(),
});| Script | Description |
|---|---|
bun run dev |
Start in development mode with hot reload |
bun run start |
Start in production mode |
bun run generate:command |
Scaffold a new command |
bun run generate:event |
Scaffold a new event |
bun run db:generate |
Generate a Drizzle migration |
bun run db:push |
Run database push |
bun run db:studio |
Open Drizzle Studio |