DISCORD SYSTEMS

Lesson — MongoDB & Mongoose

Goal: Connect your bot to a MongoDB database using Mongoose and store persistent data — starting with a simple user counter model.

Lesson Overview

Video

Notes — Understanding MongoDB & Mongoose

What is MongoDB?

MongoDB 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.

What is Mongoose?

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.

mongoose.connect()

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:

mongodb+srv://username:password@cluster.mongodb.net/myDatabase

Store this in your config.json as MONGO_URI — never hardcode it.

What is a Schema?

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.

What is a Model?

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 Guard: mongoose.models.UserCounter ||

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() vs findOneAndUpdate()

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 Operator

{ $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.

async/await with Database Calls

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.

Step-by-Step

Install Mongoose

Run this in your project folder to install the Mongoose package.

npm install mongoose

Get Your MongoDB URI

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"
}

Connect to the Database in index.js

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 the Model — models/UserCounter.js

Create a models/ folder in your project and add UserCounter.js inside it. This defines the shape of your data and exports the model.

Use the Model in a Command

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.`);

Full Code

Model — models/UserCounter.js

Show Full File
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);

Example Command — commands/count.js

Show Full File
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,
            });
        }
    },
};

Folder Structure

After this lesson your project should look something like this:

📁 your-bot/
├── 📁 commands/
│   └── count.js
├── 📁 models/
│   └── UserCounter.js
├── index.js
└── config.json

Challenge

← Last Lesson Next Lesson →