Goal: Connect your bot to a MongoDB database using Mongoose and store persistent data — starting with a simple user counter model.
UserCounter.jsMongoDB is a NoSQL database — instead of rows and columns like a spreadsheet, it stores data as documents (think JavaScript objects / JSON). Each document lives inside a collection (like a folder). MongoDB Atlas is the free cloud-hosted version — no server setup needed, just create an account and get a connection string.
Mongoose is a Node.js library that sits on top of MongoDB. It lets you define schemas (the shape of your data) and models (the object you use to read/write that data). Without Mongoose you'd write raw MongoDB queries — Mongoose makes it cleaner and safer by enforcing structure.
You call mongoose.connect(uri) once when your bot starts — usually in your main index.js. The URI comes from MongoDB Atlas and looks like:
Store this in your config.json as MONGO_URI — never hardcode it.
A schema defines what fields a document has and their types. In UserCounter.js the schema has two fields: userId (a unique string — one record per user) and count (a number, defaulting to 0 if not set). The versionKey: false option removes the __v field Mongoose normally adds — keeps documents clean.
A model is the interface you use to talk to a collection. When you call mongoose.model('UserCounter', userCounterSchema), Mongoose creates a collection called usercounters in your database (it lowercases and pluralizes the name automatically). You then use the model to find, create, update, and delete documents.
The export line checks mongoose.models.UserCounter first before calling mongoose.model(). This is a hot-reload guard — if your bot framework re-requires the file without a full restart, Mongoose would throw an error saying the model is already registered. This pattern prevents that crash entirely.
findOne({ userId }) — finds a single document matching the query, or returns null if none exists.
findOneAndUpdate({ userId }, update, { upsert: true, new: true }) — finds and updates in one call. upsert: true means create the document if it doesn't exist yet. new: true returns the updated document instead of the old one.
{ $inc: { count: 1 } } is a MongoDB update operator that increments a field by a given amount. It's atomic — meaning if two requests hit the database at the same time, neither will overwrite the other. Always use $inc for counters instead of reading the value, adding 1, then saving it back.
Every Mongoose operation (findOne, findOneAndUpdate, save) returns a Promise. Always await them inside an async function and wrap in try/catch — database calls can fail if the connection drops, and an unhandled error will crash your bot.
Run this in your project folder to install the Mongoose package.
npm install mongoose
Go to mongodb.com → create a free account → create a free M0 cluster → click Connect → choose Drivers → copy the connection string. Replace <password> with your database user's password, then add it to your config.json.
{
"MONGO_URI": "mongodb+srv://youruser:yourpassword@cluster0.mongodb.net/mybot",
"DOCK_API": "YOUR_DOCK_API_KEY_HERE"
}
Call mongoose.connect() before logging the bot in. This ensures the database is ready before any commands run.
const mongoose = require('mongoose');
const config = require('./config.json');
mongoose.connect(config.MONGO_URI)
.then(() => console.log('✅ Connected to MongoDB'))
.catch((err) => console.error('❌ MongoDB connection error:', err));
// then log your bot in below
client.login(config.TOKEN);
Create a models/ folder in your project and add UserCounter.js inside it. This defines the shape of your data and exports the model.
Import the model and use findOneAndUpdate() with $inc to increment the counter each time the command runs.
const UserCounter = require('../models/UserCounter');
// In your command's execute():
const data = await UserCounter.findOneAndUpdate(
{ userId: interaction.user.id }, // find this user's document
{ $inc: { count: 1 } }, // increment count by 1
{ upsert: true, new: true } // create if not found, return updated doc
);
console.log(`User has run this command ${data.count} times.`);
const mongoose = require('mongoose');
const userCounterSchema = new mongoose.Schema(
{
userId: {
type: String,
required: true,
unique: true, // one document per user, no duplicates
},
count: {
type: Number,
default: 0, // starts at 0 if not set
},
},
{
versionKey: false, // removes the __v field from documents
}
);
// Guard: reuse existing model if already registered (prevents hot-reload crash)
module.exports = mongoose.models.UserCounter || mongoose.model('UserCounter', userCounterSchema);
const { SlashCommandBuilder } = require('discord.js');
const UserCounter = require('../models/UserCounter');
module.exports = {
data: new SlashCommandBuilder()
.setName('count')
.setDescription('See how many times you have used this command.'),
async execute(interaction) {
try {
// Find the user's document and increment count.
// upsert: true creates the document if it doesn't exist yet.
// new: true returns the updated document (with the new count).
const data = await UserCounter.findOneAndUpdate(
{ userId: interaction.user.id },
{ $inc: { count: 1 } },
{ upsert: true, new: true }
);
return interaction.reply({
content: `You have used this command **${data.count}** time(s).`,
ephemeral: true,
});
} catch (err) {
console.error('DB error in /count:', err);
return interaction.reply({
content: 'Something went wrong with the database.',
ephemeral: true,
});
}
},
};
After this lesson your project should look something like this:
/resetcount command that sets a user's count back to 0/leaderboard command using .find().sort({ count: -1 }).limit(10)lastUsed field to the schema that stores a timestamp