A Discord bot built with Effect and discord.js, running on Bun. A learning project for exploring Effect's approach to dependency injection, layered architecture, and typed error handling.
- Bun (latest)
- A Discord application with a bot token
-
Install dependencies:
bun install
-
Copy the example env file and fill in your values:
cp .env.example .env.local
Variable Description NODE_ENVdevelopmentorproductionBOT_TOKENYour Discord bot token CLIENT_IDYour Discord application's client ID GUILD_IDThe server to register commands in -
Deploy slash commands to your guild:
bun run deploy:dev
-
Start the bot:
bun run dev
src/
├── index.ts # Entry point — creates the client and wires up layers
├── config/
│ ├── index.ts # Exports AppConfig (dev/prod) and Config namespace
│ └── bot.ts # Bot service tag + config layers (token, clientId, guildId)
├── structures/
│ ├── command.ts # Command interface and factory
│ └── event.ts # Event interface and factory
├── layers/
│ ├── index.ts # Merges AppConfig + registries into AppLayer
│ ├── command-registry.ts # Auto-discovers commands from src/commands/
│ └── event-registry.ts # Auto-discovers events from src/events/
├── commands/
│ └── ping.ts # Example slash command
├── events/
│ ├── ready.ts # Logs when the bot connects
│ └── interactionCreate.ts # Routes interactions to commands
scripts/
└── deploy-commands.ts # Registers slash commands with the Discord API
Create a new file in src/commands/ — it will be auto-discovered at startup.
// src/commands/hello.ts
import { MessageFlags } from "discord.js";
import { Effect } from "effect";
import { Command } from "@/structures/command";
export const hello = Command.make({
name: "hello",
description: "Say hello",
execute: (interaction) =>
Effect.promise(() =>
interaction.reply({ content: "Hello!", flags: MessageFlags.Ephemeral })
),
});Then redeploy commands so Discord knows about it:
bun run deploy:devCreate a new file in src/events/ — also auto-discovered.
// src/events/messageCreate.ts
import { Effect } from "effect";
import { Event } from "@/structures/event";
export const messageCreate = Event.make({
name: "messageCreate",
handle: (message) => Effect.log(`Message from ${message.author.username}`),
});| Script | Description |
|---|---|
bun run dev |
Start the bot |
bun run deploy:dev |
Deploy slash commands (guild, development) |
bun run deploy:prod |
Deploy slash commands (guild, production) |
bun run check |
Lint/format check via ultracite |
bun run fix |
Auto-fix lint/format issues |
The bot uses Effect's Layer system for dependency injection:
Config.Bot— provides bot credentials from environment variablesCommandRegistry— scanssrc/commands/and builds aMap<string, Command>EventRegistry— scanssrc/events/and builds aMap<string, Event>AppLayer— merges all the above into a single layer provided to the main program
At startup, the bot iterates over registered events and binds them to the Discord client. Commands are resolved from the registry when an interactionCreate event fires.