Setting up a Discord bot MongoDB connection is one of the most practical ways to give your bot a persistent memory so data doesn't vanish every time the process restarts. Level systems, economy bots, warning logs, user settings — all of it has to be written somewhere. A plain JSON file works for tiny bots, but it falls apart once you need concurrent writes, scaling, or real querying. In this guide we connect MongoDB to a discord.js bot through Mongoose and store user data the right way, step by step.
Why MongoDB and Mongoose?
MongoDB is a NoSQL database that stores data as documents in a JSON-like structure. For Discord bots this is a natural fit: a user's data is already made of nested fields (balance, inventory, settings), and flexible documents are easier to work with than forcing everything into a rigid table schema. Mongoose is an ODM (Object Data Modeling) library for MongoDB; it sits on top of the raw driver and adds schema validation, type checking, and a readable API.
- SQLite is ideal for small, single-server, file-based bots.
- MongoDB scales more comfortably for bots that run across multiple servers, need sharding, or live in the cloud (MongoDB Atlas).
- Thanks to its schema, Mongoose catches mistakes like "a field was written with the wrong type" before they reach production.
Installation and the connection string
First, add Mongoose to your project:
npm install mongoose
Next you need a database. The fastest route is MongoDB Atlas, which has a free tier: create a cluster, define a database user, and copy the connection string from the "Connect" screen. The string looks like this:
mongodb+srv://user:password@cluster0.xxxxx.mongodb.net/botdb
Never hard-code this string in your source. Keep secrets like tokens and URIs in a .env file and add it to .gitignore:
# .env
DISCORD_TOKEN=...
MONGO_URI=mongodb+srv://user:password@cluster0.xxxxx.mongodb.net/botdb
Connecting MongoDB to the bot
Open the connection once at startup, before the bot is ready. Because connecting is asynchronous, it's important to await it and catch errors; otherwise the bot will silently crash if the database is unreachable:
require('dotenv').config();
const mongoose = require('mongoose');
const { Client, GatewayIntentBits } = require('discord.js');
const client = new Client({
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers],
});
async function start() {
try {
await mongoose.connect(process.env.MONGO_URI);
console.log('Connected to MongoDB');
} catch (err) {
console.error('MongoDB connection error:', err);
process.exit(1);
}
await client.login(process.env.DISCORD_TOKEN);
}
start();
From Mongoose 6 onward, legacy options like useNewUrlParser are the default, so no extra config is needed. Mongoose manages the connection pool itself; don't try to reconnect on every command.
Defining a schema and model
In Mongoose, data is described by a schema, and a model is created from that schema. Keeping a typical user model in its own file is the cleanest approach:
// models/User.js
const { Schema, model } = require('mongoose');
const userSchema = new Schema({
userId: { type: String, required: true, unique: true },
guildId: { type: String, required: true },
balance: { type: Number, default: 0 },
xp: { type: Number, default: 0 },
level: { type: Number, default: 1 },
}, { timestamps: true });
module.exports = model('User', userSchema);
unique: true ensures there is only one document per user and also creates an index, which speeds up queries. timestamps: true automatically adds createdAt and updatedAt fields.
Reading and writing data: the upsert pattern
Your most frequent operation is "fetch this user's document if it exists, create it if it doesn't." Solving this with a single upsert instead of two separate queries is both faster and safer against concurrent calls. findOneAndUpdate with the upsert: true option does exactly that:
const User = require('./models/User');
async function getUser(userId, guildId) {
return User.findOneAndUpdate(
{ userId, guildId },
{ $setOnInsert: { userId, guildId } },
{ upsert: true, new: true }
);
}
$setOnInsert only runs when the document is first created; it won't overwrite an existing document. new: true returns the updated (or newly created) document. When you want to increment a value, use the $inc operator instead of fetching the document and editing it by hand; this is atomic and prevents race conditions:
// Inside a slash command: give the user some balance
await User.updateOne(
{ userId: interaction.user.id, guildId: interaction.guildId },
{ $inc: { balance: 100 } },
{ upsert: true }
);
A complete balance command looks like this:
if (interaction.commandName === 'balance') {
const doc = await getUser(interaction.user.id, interaction.guildId);
await interaction.reply(`Your balance: ${doc.balance} 💰`);
}
Performance and production tips
As your bot grows, a few habits make a difference:
- Add indexes: Give fields you query often (such as
xpfor a leaderboard)index: truein the schema. Queries without an index scan the entire collection. - Use
.lean()for read-only queries:User.find().sort({ xp: -1 }).limit(10).lean()returns plain JavaScript objects instead of full Mongoose documents and is noticeably faster. - Listen for connection events: Log disconnects with
mongoose.connection.on('error', ...); silent failures are the hardest to track down. - Restrict IP access: In Atlas, allow only your bot server's IP under Network Access — don't use
0.0.0.0/0.
Frequently Asked Questions
Should I choose MongoDB or SQLite?
For a small/medium bot running on a single VPS, SQLite is usually enough and simpler to set up. As your data grows, if you're considering sharding, or if you want a managed cloud database, MongoDB scales more comfortably. Pick based on need; both are correct tools for persistent storage.
Do I have to reconnect on every command?
No. mongoose.connect is called once, when the bot starts. Mongoose manages the connection pool and your models share that single connection. Reconnecting on every command causes serious performance problems.
Can I change the schema later?
Yes. Because MongoDB is flexible, adding a new field won't break existing documents; for documents missing that field, the schema's default value kicks in. For larger transformations like type changes, however, it's best to write a migration script.
Want a Discord bot that stores its data safely? I build MongoDB-backed economy, level, and moderation systems. Let's bring your project to life — get in touch with me.