Part 2: Meet Cozy Café (and Give Your Bot a Backbone)
Part 2 of a beginner-friendly series on building a real Discord bot with JavaScript, discord.js, and Prisma. Part 1 got a bot online and replying to /ping. Today we reveal what we're actually building — and lay the foundation for it.
Welcome back! In Part 1 you did something genuinely cool: you built a program that logs into Discord and answers a /ping command with Pong!. That's the entire skeleton of every Discord bot ever made — listen for an event, react to it.
But /ping is a bit lonely. So in this part, two things happen. First, the big reveal: I'll show you the actual project we're building across this series. Second, we'll give your bot a backbone — a tidy structure that lets it grow from one command to dozens without turning into an unreadable mess — and write the first two real commands of our game.
Let's meet the café.
The reveal: we're building Cozy Café ☕
Here's the project: Cozy Café — an idle café game you run entirely from inside Discord.
You open a little café with a slash command. From then on, your café quietly serves customers and earns coins in real time — even while you're offline. You drop back in whenever you feel like it to collect your earnings, spend them on upgrades and new recipes, and slowly master your menu. Over time a humble coffee cart grows into a cozy corner the whole server knows.
That's it. No twitch reflexes, no grinding, no pressure. Just a warm little thing that's yours and is always gently ticking along in the background.
What a moment of play feels like
You open Discord in the morning, type /collect, and your bot replies:
☕ While you were away (9h 12m) your café served 74 customers and earned 184 coins.
You also discovered a new recipe: Cinnamon Roll — add it to your menu with/menu.
You spend thirty seconds deciding what to do — upgrade your espresso machine so you earn faster, or save up for that Matcha Latte you've been eyeing — then close Discord and get on with your day. The whole visit takes a minute, and it's completely optional.
Why this game? (the design philosophy)
This is the part I really want you to understand, because it's a deliberate design choice, not an accident. You might know the feeling of a game or app that punishes you for not showing up — a streak you'll lose, a pet that starves, daily quests that pile up into guilt. That's the opposite of cozy.
Cozy Café is built on one rule:
Reward presence, never punish absence.
Everything follows from that sentence:
- Your café earns while you're away, so checking in once a day feels rewarding — there's always something waiting to collect.
- But earnings cap out at about a day's worth, so you never need to check more than once a day. No compulsive refreshing.
- And nothing ever decays. Skip three days, skip a week — your coins, recipes, and progress are all exactly where you left them. You just collect a full batch when you're back.
The thing that keeps pulling you back isn't fear of losing progress — it's the quiet satisfaction of collecting and mastering: filling out a recipe book, leveling up your favourite drinks, watching your café grow. A carrot, never a stick.
That's the game. Now let's build the foundation it needs.
Why we can't just keep piling code into one file
In Part 1, everything lived in a single index.js. That was perfect for one command. But look at where we're heading: /open, /cafe, /collect, /daily, /shop, /recipes, /menu… If we cram all of those into index.js, we'll end up with a 600-line monster where everything is tangled together and finding anything is a nightmare.
So before we add features, we're going to reorganize. This is a habit real developers live by, and the "why" behind it has a name: separation of concerns. The idea is simple — each piece of your program should have one clear job and live in one obvious place. When every command is its own little file, you always know exactly where to look, and adding a new command never risks breaking an old one.
Here's the structure we're building toward:
cozy-cafe-bot/
├─ commands/
│ ├─ open.js ← one file per command
│ └─ cafe.js
├─ data/
│ └─ store.js ← where café data lives (for now)
├─ .env ← your secrets (from Part 1)
├─ deploy-commands.js
└─ index.js ← the "brain" that ties it together
(That top folder is simply your project from Part 1. I've named it cozy-cafe-bot/ here, but the name honestly doesn't matter — keep my-discord-bot from Part 1 if you like. Only the files and folders **inside* it matter.)*
The trick that makes this work is a command handler: a small bit of code in index.js that automatically finds every file in the commands/ folder and loads it. Add a new command file, and the bot just picks it up — you never have to manually wire each one in. Let's build it.
Step 1 — Give every command a consistent shape
The handler can only auto-load our commands if they all look the same on the outside. So we'll agree on a simple contract: every command file exports an object with exactly two things —
-
data— the command's name and description (built withSlashCommandBuilder, same as Part 1). -
execute— the function that runs when someone uses the command.
That's the whole contract. { data, execute }. Once every command follows it, the handler can treat them all identically, without caring what any individual command actually does. That consistency is what makes the magic possible.
Step 2 — A place to keep café data (just for today)
Our commands need somewhere to store each player's café. For this part, we'll use the simplest storage that exists: a Map held in the bot's memory.
A
Mapis a built-in JavaScript container that stores key → value pairs. We'll use each player's Discord user ID as the key, and their café as the value — so we can look up "the café belonging to this person."
Create a folder called data, and inside it a file store.js:
// data/store.js
// A simple in-memory store: Discord user ID -> that user's café.
// ⚠️ Heads up: this lives in the bot's memory (RAM), which means it is
// wiped clean every time the bot restarts. We'll fix that in Part 3.
const cafes = new Map();
module.exports = { cafes };
Why put this in its own file? Because both of our commands (/open and /cafe) need to read and write the same café data. By keeping the Map in one shared module, every file that imports it gets the exact same Map — when /open adds a café, /cafe can immediately see it. (Node.js loads a module once and reuses it everywhere, which is exactly the behaviour we want here.)
That warning in the comment is the whole point of this part, by the way. Keep it in mind — we'll come back to it.
Step 3 — The first command: /open
Create commands/open.js:
// commands/open.js
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
const { cafes } = require('../data/store.js');
module.exports = {
// The "data" half of our contract — name + description
data: new SlashCommandBuilder()
.setName('open')
.setDescription('Open your very own café!'),
// The "execute" half — what actually happens
async execute(interaction) {
const userId = interaction.user.id;
// Do they already have a café?
if (cafes.has(userId)) {
await interaction.reply('You already run a café! Check on it with `/cafe`.');
return;
}
// Create a brand-new café for this player
const cafe = {
name: `${interaction.user.username}'s Café`,
coins: 50, // a little starting cash
level: 1,
openedAt: Date.now(), // we'll use this timestamp properly in Part 3
};
cafes.set(userId, cafe);
// Reply with a nice-looking embed instead of plain text
const embed = new EmbedBuilder()
.setColor(0xc5774a)
.setTitle('☕ Your café is now open!')
.setDescription(`Welcome to **${cafe.name}**. Grab an apron — let's get brewing.`)
.addFields(
{ name: 'Coins', value: `${cafe.coins}`, inline: true },
{ name: 'Level', value: `${cafe.level}`, inline: true },
)
.setFooter({ text: 'Check on your café anytime with /cafe' });
await interaction.reply({ embeds: [embed] });
},
};
A few things worth understanding here:
-
interaction.user.idis Discord's unique ID for whoever ran the command. Using it as our key means each person gets their own café — your café and your friend's café never get mixed up. - The "already have one?" check is a small but important habit: think about what happens if someone runs a command twice. Here, we politely stop instead of wiping out their existing café.
-
EmbedBuildercreates those tidy, bordered message cards you've seen fancy bots use. It's purely about presentation —setTitle,setDescription,addFields— but it instantly makes your bot feel polished instead of plain. (0xc5774ais a warm coffee-brown colour, written in hex.)
Step 4 — The second command: /cafe
Create commands/cafe.js:
// commands/cafe.js
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
const { cafes } = require('../data/store.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('cafe')
.setDescription('Check in on your café.'),
async execute(interaction) {
const cafe = cafes.get(interaction.user.id);
// No café yet? Point them to /open.
if (!cafe) {
await interaction.reply("You don't have a café yet! Open one with `/open`.");
return;
}
const embed = new EmbedBuilder()
.setColor(0xc5774a)
.setTitle(`☕ ${cafe.name}`)
.addFields(
{ name: 'Coins', value: `${cafe.coins}`, inline: true },
{ name: 'Level', value: `${cafe.level}`, inline: true },
)
.setFooter({ text: 'Coming soon: collect earnings, unlock recipes…' });
await interaction.reply({ embeds: [embed] });
},
};
Notice how this command only reads the café — it looks one up with cafes.get(...) and shows it. The matching pattern to /open's cafes.set(...). Read and write, sharing the same Map. That symmetry is the payoff for putting our store in its own file.
Step 5 — The command handler that ties it all together
Now we upgrade index.js so it automatically loads everything in commands/ and routes each slash command to the right file. Replace your index.js with this:
// index.js
require('dotenv').config();
const fs = require('node:fs');
const path = require('node:path');
const { Client, Collection, Events, GatewayIntentBits } = require('discord.js');
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
// --- 1. Load every command file into a collection ---
client.commands = new Collection();
const commandsPath = path.join(__dirname, 'commands');
const commandFiles = fs.readdirSync(commandsPath).filter((file) => file.endsWith('.js'));
for (const file of commandFiles) {
const command = require(path.join(commandsPath, file));
if ('data' in command && 'execute' in command) {
client.commands.set(command.data.name, command);
} else {
console.log(`⚠️ Skipping ${file}: missing "data" or "execute".`);
}
}
// --- 2. Say hello when the bot connects ---
client.once(Events.ClientReady, (c) => {
console.log(`✅ Logged in as ${c.user.tag}`);
});
// --- 3. Run the matching command whenever someone uses a slash command ---
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand()) return;
const command = client.commands.get(interaction.commandName);
if (!command) return;
try {
await command.execute(interaction);
} catch (error) {
console.error(error);
await interaction.reply('Something went wrong running that command. 😬');
}
});
client.login(process.env.DISCORD_TOKEN);
This is the heart of Part 2, so let's unpack the three numbered sections:
Loading the commands.
fs.readdirSyncreads thecommands/folder and gives us a list of filenames; we keep only the.jsones. For each, werequireit (that runs the file and hands us its{ data, execute }object) and store it in aCollection— discord.js's souped-up version of aMap— keyed by the command's name. The little check makes sure we only load files that honour our contract. The beautiful part: drop a new file intocommands/and it's automatically loaded. You never edit this section again.The ready event is unchanged from Part 1 — still using the future-proof
Events.ClientReady.Dispatching. When a slash command comes in, we look up the matching command by name (
client.commands.get(...)) and call itsexecute. Thetry/catchis a safety net: if a command crashes, we log the real error for ourselves and show the user a friendly message instead of the bot silently dying.
See how the handler doesn't know or care that /open makes a café and /cafe shows one? It just finds commands and runs them. That's separation of concerns paying off.
Step 6 — Tell Discord about the new commands
Slash commands have to be registered with Discord before they appear (remember this from Part 1?). We'll make our deploy-commands.js read the commands/ folder too, so it always stays in sync. Replace it with:
// deploy-commands.js
require('dotenv').config();
const fs = require('node:fs');
const path = require('node:path');
const { REST, Routes } = require('discord.js');
const commands = [];
const commandsPath = path.join(__dirname, 'commands');
const commandFiles = fs.readdirSync(commandsPath).filter((file) => file.endsWith('.js'));
for (const file of commandFiles) {
const command = require(path.join(commandsPath, file));
if ('data' in command && 'execute' in command) {
commands.push(command.data.toJSON()); // convert to the format Discord wants
}
}
const rest = new REST().setToken(process.env.DISCORD_TOKEN);
(async () => {
try {
console.log(`Registering ${commands.length} command(s)…`);
await rest.put(
Routes.applicationGuildCommands(process.env.CLIENT_ID, process.env.GUILD_ID),
{ body: commands },
);
console.log('✅ Commands registered!');
} catch (error) {
console.error(error);
}
})();
It's the same idea as the handler: walk the folder, collect each command's data, and send the batch to Discord. Run it now:
node deploy-commands.js
You should see Registering 2 command(s)… then ✅ Commands registered!. (Re-run this script whenever you add or rename a command — not every time you start the bot.)
Step 7 — Run it and open your café
Start the bot:
node index.js
Over in your test server, type /open. Your bot welcomes you with a warm embed and your café springs to life. Now type /cafe — there it is, coins and all. Run /open again and it politely tells you that you already have one.
You just played the first thirty seconds of Cozy Café. 🎉
The catch (and it's on purpose)
Now do this little experiment. With the bot running and your café open, go back to your terminal, press Ctrl + C to stop the bot, then start it again with node index.js. Hop back to Discord and type /cafe.
You don't have a café yet! Open one with
/open.
Gone. Your café vanished. 😟
This isn't a bug — it's the lesson. Remember that warning we wrote in store.js? Our Map lives in the bot's memory (RAM), and memory is temporary — the moment the program stops, everything in it evaporates. This is exactly the cliffhanger Part 1 ended on: a bot, on its own, has no lasting memory.
And here's why it matters so much for this game specifically. Cozy Café's whole magic trick — "while you were away for 9 hours you earned 184 coins" — depends on the bot reliably remembering two things between visits: your café's state, and when you last collected. An idle game that forgets everything on restart isn't an idle game at all. So our game doesn't just want permanent memory; it literally cannot exist without it.
That's our cue.
What you built, and what's next
Look at how far you came in this part:
- You learned what we're building and why it's designed the way it is — reward presence, never punish absence.
- You gave your bot a real backbone: a
commands/folder, a consistent{ data, execute }contract, and a handler that auto-loads everything. Your bot can now grow to dozens of commands without becoming a mess. - You wrote
/openand/cafe, your first real game commands, complete with polished embeds. - And you felt, first-hand, the problem that defines the next chapter.
In Part 3, we give Cozy Café a permanent memory. We'll introduce a real database and meet Prisma — the friendly tool that lets us save and load café data with clean, readable JavaScript. That store.js Map will become a real Cafe table, your cafés will survive restarts, and we'll finally write /collect with genuine "earn while you're away" math powered by that openedAt timestamp we quietly planted today.
Try it yourself before Part 3
The best way to make this stick is to tinker. A few gentle challenges:
- Change the welcome message or the café's starting coins in
open.js. - Add a fun fact to the
/cafeembed — maybe show how long ago the café opened (hint: compareDate.now()to theopenedAtyou stored). - Bolder: create a brand-new command file,
commands/rename.js, that lets a player rename their café. If you follow the{ data, execute }contract, the handler will pick it up automatically — no wiring required. (That's the whole point!)
Break things, fix them, make the café yours. See you in Part 3, where it finally remembers. ☕
Enjoying the series? Part 3 builds directly on the project you just structured, so keep this folder handy.
Top comments (0)