Skip to main content

Command Palette

Search for a command to run...

MongoDB Tutorial Guide: A Beginner-to-Builder Backend Journey with Node.js, Express, Mongoose, JWT, Swagger, Comments, Wallets, and Admin

Updated
20 min read
MongoDB Tutorial Guide: A Beginner-to-Builder Backend Journey with Node.js, Express, Mongoose, JWT, Swagger, Comments, Wallets, and Admin
O

I'm Ogunuyo Ogheneruemu Brown, a senior software developer. I specialize in DApp apps, fintech solutions, nursing web apps, fitness platforms, and e-commerce systems. Throughout my career, I've delivered successful projects, showcasing strong technical skills and problem-solving abilities. I create secure and user-friendly fintech innovations. Outside work, I enjoy coding, swimming, and playing football. I'm an avid reader and fitness enthusiast. Music inspires me. I'm committed to continuous growth and creating impactful software solutions. Let's connect and collaborate to make a lasting impact in software development.

Introduction

Backend development can feel mysterious at first. You write some routes, connect to a database, test in Postman, and suddenly your app starts behaving like a real product. This guide is a complete walkthrough of that journey using Node.js, Express, MongoDB, and Mongoose.

It starts with simple CRUD, then grows into a real backend project with:

  • user registration and login

  • password hashing

  • forgot password with OTP

  • protected dashboard and profile routes

  • Swagger/OpenAPI documentation

  • environment variables with .env

  • comments linked to users

  • wallet balance and transfers

  • admin role and admin-only actions

  • MongoDB Compass for seeing your data visually

This is written as a practical blog/tutorial you can keep and revisit.


Table of Contents

  1. What backend development means

  2. The stack we used

  3. MongoDB basics

  4. Express basics

  5. Mongoose basics

  6. Your first CRUD API

  7. How Postman fits in

  8. Better schema design

  9. Moving from one file to structure

  10. Building authentication

  11. Why JWT token is best for protected routes

  12. Forgot password, OTP, verify OTP, reset password

  13. Profile and dashboard

  14. Using .env and dotenv

  15. Adding Swagger / OpenAPI

  16. Moving MongoDB from local to cloud with Atlas

  17. Building a comment system

  18. Building a wallet system

  19. Building an admin system

  20. MongoDB Compass and where your data is stored

  21. Common mistakes beginners make

  22. Best practices you are already learning

  23. Final mental model


1. What Backend Development Means

A backend is the part of your application that handles:

  • data storage

  • business logic

  • authentication

  • validation

  • communication between frontend and database

If the frontend is what users see, the backend is what makes the app actually work.

Examples:

  • when a user registers, backend saves the user

  • when a user logs in, backend checks password and returns token

  • when a user sends money, backend updates two balances and records a transaction

  • when admin edits a user, backend verifies admin role before allowing it


2. The Stack We Used

Node.js

Runs JavaScript on the server.

Express

A lightweight framework for building APIs and routes.

MongoDB

A NoSQL database that stores data as documents.

Mongoose

A tool that helps Node.js talk to MongoDB in a structured way.

Postman

Used to test API endpoints.

Swagger / OpenAPI

Used to document and test APIs in the browser.

MongoDB Compass

A GUI tool for seeing databases, collections, and documents visually.

bcryptjs

Used to hash passwords.

jsonwebtoken

Used to create and verify tokens for authenticated routes.

dotenv

Used to load secrets like database URLs and JWT secret from .env.


3. MongoDB Basics

MongoDB stores data in documents, not tables.

A user document looks like this:

{
  "fullName": "Ruemu Brown",
  "email": "brown@email.com",
  "age": 25
}

Important terms

  • Database: a container for collections

  • Collection: a group of similar documents

  • Document: one record

  • Field: one piece of data inside a document

SQL vs MongoDB

SQL MongoDB
Database Database
Table Collection
Row Document
Column Field

Local MongoDB connection

mongodb://127.0.0.1:27017/myapp

Breakdown:

  • mongodb:// = MongoDB protocol

  • 127.0.0.1 = your local machine

  • 27017 = default MongoDB port

  • myapp = database name


4. Express Basics

Express helps you create endpoints like:

app.get("/users", ...)
app.post("/users", ...)
app.put("/users/:id", ...)
app.delete("/users/:id", ...)

Think of Express like a receptionist:

  • request comes in

  • route matches

  • logic runs

  • response goes out

Basic setup

const express = require("express");
const app = express();
app.use(express.json());

Why express.json() matters

It allows Express to understand JSON from Postman or frontend.

Without it, req.body may be empty.


5. Mongoose Basics

Mongoose helps structure MongoDB usage through schemas and models.

Schema

A schema describes what a document should look like.

const userSchema = new mongoose.Schema({
  name: String,
  age: Number,
  email: String,
});

Better schema

const userSchema = new mongoose.Schema(
  {
    name: {
      type: String,
      required: true,
      trim: true,
    },
    age: {
      type: Number,
      required: true,
    },
    email: {
      type: String,
      required: true,
      unique: true,
      trim: true,
    },
  },
  { timestamps: true }
);

What these mean

  • required: true → field must exist

  • trim: true → remove surrounding spaces

  • unique: true → avoid duplicate values

  • timestamps: true → adds createdAt and updatedAt

Model

const User = mongoose.model("User", userSchema);

A model is the tool you use to work with the collection.

Examples:

  • User.create()

  • User.find()

  • User.findById()

  • User.findByIdAndUpdate()

  • User.findByIdAndDelete()


6. Your First CRUD API

CRUD means:

  • Create

  • Read

  • Update

  • Delete

Full simple CRUD example

const express = require("express");
const mongoose = require("mongoose");

const app = express();
app.use(express.json());

mongoose
  .connect("mongodb://127.0.0.1:27017/myapp")
  .then(() => console.log("MongoDB connected"))
  .catch((err) => console.log("Connection error:", err));

const userSchema = new mongoose.Schema(
  {
    name: {
      type: String,
      required: true,
      trim: true,
    },
    age: {
      type: Number,
      required: true,
    },
    email: {
      type: String,
      required: true,
      unique: true,
      trim: true,
    },
  },
  { timestamps: true }
);

const User = mongoose.model("User", userSchema);

app.post("/users", async (req, res) => {
  try {
    const newUser = await User.create(req.body);
    res.status(201).json(newUser);
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
});

app.get("/users", async (req, res) => {
  try {
    const users = await User.find();
    res.json(users);
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
});

app.get("/users/:id", async (req, res) => {
  try {
    const user = await User.findById(req.params.id);

    if (!user) {
      return res.status(404).json({ message: "User not found" });
    }

    res.json(user);
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
});

app.put("/users/:id", async (req, res) => {
  try {
    const updatedUser = await User.findByIdAndUpdate(
      req.params.id,
      req.body,
      { new: true, runValidators: true }
    );

    if (!updatedUser) {
      return res.status(404).json({ message: "User not found" });
    }

    res.json(updatedUser);
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
});

app.delete("/users/:id", async (req, res) => {
  try {
    const deletedUser = await User.findByIdAndDelete(req.params.id);

    if (!deletedUser) {
      return res.status(404).json({ message: "User not found" });
    }

    res.json({ message: "User deleted successfully" });
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
});

app.listen(3000, () => {
  console.log("Server is running on port 3000");
});

What each route means

  • POST /users → create user

  • GET /users → get all users

  • GET /users/:id → get one user

  • PUT /users/:id → update one user

  • DELETE /users/:id → delete one user

What req and res mean

  • req = request coming in

  • res = response going out

Important request pieces

  • req.body = JSON sent by client

  • req.params.id = ID from route path


7. How Postman Fits In

Postman is your API testing tool.

Example requests

Create user

POST http://localhost:3000/users
{
  "name": "Brown",
  "age": 25,
  "email": "brown@email.com"
}

Get all users

GET http://localhost:3000/users

Get one user

GET http://localhost:3000/users/PUT_USER_ID_HERE

Update user

PUT http://localhost:3000/users/PUT_USER_ID_HERE
{
  "name": "Ruemu",
  "age": 30,
  "email": "ruemu@email.com"
}

Delete user

DELETE http://localhost:3000/users/PUT_USER_ID_HERE

Common beginner issue

Testing http://localhost:3000 instead of a real route like /users will often give:

Cannot GET /

That only means you did not define a / route.


8. Better Schema Design

Once you understood CRUD, you improved schema quality.

Why better schema matters

It helps with:

  • validation

  • cleaner data

  • fewer duplicates

  • timestamps for history

Example

const userSchema = new mongoose.Schema(
  {
    fullName: {
      type: String,
      required: true,
      trim: true,
    },
    age: {
      type: Number,
      required: true,
    },
    email: {
      type: String,
      required: true,
      unique: true,
      trim: true,
      lowercase: true,
    },
  },
  { timestamps: true }
);

Why runValidators: true on update matters

{ new: true, runValidators: true }

Without runValidators: true, updates can skip some schema checks.


9. Moving from One File to Structure

For learning, one file is okay.

For real projects, structure matters.

Example structure

auth-system/
  models/
    User.js
  middleware/
    authMiddleware.js
  routes/
    authRoutes.js
    userRoutes.js
  server.js

Later it can become:

src/
  config/
  controllers/
  middleware/
  models/
  routes/
  services/
  utils/
  server.js

10. Building Authentication

After CRUD, you moved to real authentication.

Main features

  • register

  • login

  • forgot password

  • send OTP

  • verify OTP

  • reset password

  • dashboard

  • profile

Packages used

npm install express mongoose bcryptjs jsonwebtoken dotenv
npm install --save-dev nodemon

Why these packages

  • bcryptjs hashes passwords

  • jsonwebtoken creates login tokens

  • dotenv loads secrets from .env

  • nodemon restarts server automatically while coding

User model for auth

const mongoose = require("mongoose");

const userSchema = new mongoose.Schema(
  {
    fullName: {
      type: String,
      required: true,
      trim: true,
    },
    age: {
      type: Number,
      required: true,
    },
    email: {
      type: String,
      required: true,
      unique: true,
      trim: true,
      lowercase: true,
    },
    password: {
      type: String,
      required: true,
    },
    otpCode: {
      type: String,
      default: null,
    },
    otpExpires: {
      type: Date,
      default: null,
    },
    isOtpVerified: {
      type: Boolean,
      default: false,
    },
  },
  { timestamps: true }
);

module.exports = mongoose.model("User", userSchema);

11. Why JWT Token Is Best for Protected Routes

Instead of trusting email sent by frontend every time, the backend should trust a signed token.

Login flow

User sends:

{
  "email": "brown@email.com",
  "password": "12345678"
}

Backend:

  • finds user

  • compares password with hashed password

  • creates JWT token

  • returns token

Example:

const token = jwt.sign(
  { id: user._id, email: user.email },
  process.env.JWT_SECRET,
  { expiresIn: "1d" }
);

Using the token

Frontend sends:

Authorization: Bearer your_token_here

Middleware

const jwt = require("jsonwebtoken");

const authMiddleware = (req, res, next) => {
  try {
    const authHeader = req.headers.authorization;

    if (!authHeader || !authHeader.startsWith("Bearer ")) {
      return res.status(401).json({ message: "No token provided" });
    }

    const token = authHeader.split(" ")[1];
    const decoded = jwt.verify(token, process.env.JWT_SECRET);

    req.user = decoded;
    next();
  } catch (error) {
    res.status(401).json({ message: "Invalid or expired token" });
  }
};

module.exports = authMiddleware;

This lets protected routes use:

req.user.id
req.user.email

12. Forgot Password, OTP, Verify OTP, Reset Password

This was your first multi-step auth flow.

Register

  • check if email exists

  • hash password

  • create user

Login

  • check email

  • compare password

  • return JWT token

Forgot Password

  • find user by email

  • generate OTP

  • save OTP + expiry time

Verify OTP

  • check user

  • compare OTP

  • check if expired

  • mark as verified

Reset Password

  • confirm OTP was verified

  • hash new password

  • update password

  • clear OTP fields

Example OTP generator

const generateOtp = () => {
  return Math.floor(100000 + Math.random() * 900000).toString();
};

Example forgot-password logic

router.post("/forgot-password", async (req, res) => {
  try {
    const { email } = req.body;

    const user = await User.findOne({ email });
    if (!user) {
      return res.status(404).json({ message: "User not found" });
    }

    const otp = generateOtp();

    user.otpCode = otp;
    user.otpExpires = new Date(Date.now() + 5 * 60 * 1000);
    user.isOtpVerified = false;

    await user.save();

    res.json({
      message: "OTP sent successfully",
      otp,
    });
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
});

For learning, returning OTP in response is okay. In real apps, OTP should be sent by email or SMS.


13. Profile and Dashboard

These routes are protected by token.

Dashboard

router.get("/dashboard", authMiddleware, async (req, res) => {
  try {
    const user = await User.findById(req.user.id).select("-password");

    res.json({
      message: "Dashboard data fetched successfully",
      user,
    });
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
});

Profile

router.get("/profile", authMiddleware, async (req, res) => {
  try {
    const user = await User.findById(req.user.id).select("-password");

    res.json({
      profile: user,
    });
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
});

Why token is better here

Because the server uses trusted token identity to fetch data, instead of trusting raw email from the client.


14. Using .env and dotenv

Once your project became more real, hardcoded secrets were no longer a good idea.

.env example

PORT=3000
MONGO_URI=mongodb://127.0.0.1:27017/authsystem
JWT_SECRET=supersecretkey123

Load it at top of server

require("dotenv").config();

Why .env matters

It keeps secrets out of code:

  • database URLs

  • JWT secrets

  • API keys

  • SMTP credentials

.gitignore

node_modules
.env

That prevents accidental uploads of secrets.


15. Adding Swagger / OpenAPI

Swagger made the API self-documented.

Install

npm install swagger-ui-express swagger-jsdoc

Basic server setup

const swaggerUi = require("swagger-ui-express");
const swaggerJsdoc = require("swagger-jsdoc");

const swaggerOptions = {
  definition: {
    openapi: "3.0.0",
    info: {
      title: "Auth System API",
      version: "1.0.0",
      description: "Authentication API with register, login, OTP, reset password, dashboard and profile",
    },
    servers: [
      {
        url: `http://localhost:${process.env.PORT || 3000}`,
      },
    ],
    components: {
      securitySchemes: {
        bearerAuth: {
          type: "http",
          scheme: "bearer",
          bearerFormat: "JWT",
        },
      },
    },
  },
  apis: ["./routes/*.js"],
};

const swaggerSpec = swaggerJsdoc(swaggerOptions);
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec));

Why Swagger is useful

  • documents all routes

  • shows request body shape

  • shows response descriptions

  • supports protected route testing

  • helps frontend and QA teams

Important Swagger lesson you learned

For routes like:

/admin/users/:id

Swagger needs explicit parameter docs:

parameters:
  - in: path
    name: id
    required: true
    schema:
      type: string

Otherwise Swagger shows “No parameters.”


16. Moving MongoDB from Local to Cloud with Atlas

Local MongoDB is fine for learning.
Cloud MongoDB is better for real apps.

Local connection

mongodb://127.0.0.1:27017/authsystem

Atlas connection

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

Atlas setup idea

  1. create Atlas account

  2. create cluster

  3. create DB user

  4. allow IP access

  5. copy connection string

  6. store in .env

Why Atlas is useful

  • online database

  • easier deployment

  • shared access for team

  • backups and monitoring

  • good for real products


17. Building a Comment System

Next you learned relationships in MongoDB.

Goal

Each logged-in user can:

  • view comments

  • create comment

  • edit own comment

  • delete own comment

Comment model

const mongoose = require("mongoose");

const commentSchema = new mongoose.Schema(
  {
    text: {
      type: String,
      required: true,
      trim: true,
    },
    user: {
      type: mongoose.Schema.Types.ObjectId,
      ref: "User",
      required: true,
    },
  },
  { timestamps: true }
);

module.exports = mongoose.model("Comment", commentSchema);

Why ref: "User" matters

It links a comment to a user.

Why populate() matters

Without populate:

{
  "text": "Hello",
  "user": "67d27f2f8f1c2f3a4b5c6d7e"
}

With populate:

{
  "text": "Hello",
  "user": {
    "_id": "67d27f2f8f1c2f3a4b5c6d7e",
    "fullName": "Ruemu Brown",
    "email": "brown@email.com"
  }
}

Why ownership check matters

if (comment.user.toString() !== req.user.id) {
  return res.status(403).json({ message: "You can only edit your own comment" });
}

This prevents users from editing or deleting comments that are not theirs.


18. Building a Wallet System

This taught you business logic beyond basic CRUD.

Goal

Each user:

  • starts with demo balance of 5000

  • can view balance

  • can send money by email

  • can receive money automatically

  • can view transaction history

User balance field

balance: {
  type: Number,
  default: 5000,
  min: 0,
}

Transaction model idea

const transactionSchema = new mongoose.Schema(
  {
    sender: {
      type: mongoose.Schema.Types.ObjectId,
      ref: "User",
      required: true,
    },
    receiver: {
      type: mongoose.Schema.Types.ObjectId,
      ref: "User",
      required: true,
    },
    senderEmail: {
      type: String,
      required: true,
    },
    receiverEmail: {
      type: String,
      required: true,
    },
    amount: {
      type: Number,
      required: true,
      min: 1,
    },
    status: {
      type: String,
      enum: ["success"],
      default: "success",
    },
    type: {
      type: String,
      enum: ["transfer"],
      default: "transfer",
    },
  },
  { timestamps: true }
);

Wallet routes

  • GET /wallet/balance

  • POST /wallet/send

  • GET /wallet/transactions

Transfer logic summary

  1. get sender from token

  2. find receiver by email

  3. validate amount

  4. stop self-transfer

  5. stop insufficient balance

  6. subtract from sender

  7. add to receiver

  8. create transaction record

Important production note

In real finance apps, database transactions/sessions should be used so debit and credit happen safely together.


19. Building an Admin System

This introduced role-based access control.

New idea: roles

User model gained:

role: {
  type: String,
  enum: ["user", "admin"],
  default: "user",
}

Token now includes role

const token = jwt.sign(
  { id: user._id, email: user.email, role: user.role },
  process.env.JWT_SECRET,
  { expiresIn: "1d" }
);

Admin middleware

const adminMiddleware = (req, res, next) => {
  try {
    if (req.user.role !== "admin") {
      return res.status(403).json({ message: "Access denied. Admin only" });
    }

    next();
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
};

Admin can

  • view all users

  • view one user

  • edit user account

  • add balance

  • deduct balance

  • delete user account

Admin routes idea

  • GET /admin/users

  • GET /admin/users/:id

  • PUT /admin/users/:id

  • POST /admin/users/:id/credit

  • POST /admin/users/:id/debit

  • DELETE /admin/users/:id

Important lesson from Swagger

Routes with {id} must document path parameters or Swagger will not show a place to type them.


20. MongoDB Compass and Where Your Data Is Stored

MongoDB Compass is the GUI that lets you inspect your actual database.

What Compass shows

  • databases

  • collections

  • documents

  • field values

  • edit tools

Connection string

mongodb://127.0.0.1:27017

Once connected, Compass shows databases on that MongoDB server.

Example:

admin
config
local
authsystem
toystore

Inside authsystem

You may see:

users
comments
transactions

Why Compass matters

It lets you see exactly what your API has written.

For example:

  • register user → user appears in users

  • create comment → comment appears in comments

  • send money → transaction appears in transactions

Creating an admin user with Compass

  1. open users collection

  2. find your user

  3. edit role from user to admin

  4. save

  5. login again to get new admin token

Where data is physically stored

Local MongoDB data is stored on disk by the MongoDB server, usually under local MongoDB data directories. You normally do not edit those files directly. Compass is the safe human-friendly interface.


21. Common Mistakes Beginners Make

MongoDB not running

If MongoDB is not active, connection fails.

Forgetting express.json()

Then req.body is empty.

Wrong route

Testing / instead of the actual defined route.

Wrong HTTP method

Using GET instead of POST, or DELETE instead of PUT.

Invalid MongoDB ID

This can cause errors in findById routes.

Forgetting to restart server

Unless using nodemon, changes will not apply automatically.

Hardcoding secrets

Database URLs and JWT secrets should not stay in source code.

Returning only 500 for everything

Later you should return clearer status codes such as 400, 401, 403, 404, 409.


22. Best Practices You Are Already Learning

You are already moving in the right direction by learning:

  • schema validation

  • runValidators: true

  • timestamps: true

  • password hashing

  • JWT auth

  • .env

  • Swagger docs

  • database visualization with Compass

  • populate() for references

  • ownership checks

  • role-based authorization

  • transaction history for wallet actions

These are real backend engineering concepts.


23. Final Mental Model

Think of the whole system like this:

  • Express = office front desk

  • Routes = service counters

  • Mongoose = office worker talking to storage

  • MongoDB = storage room

  • Schema = form rules

  • Model = tool for working with records

  • JWT = access pass

  • Swagger = office manual and testing screen

  • MongoDB Compass = x-ray window into storage room

  • Admin middleware = restricted access security guard

And the flow looks like this:

Frontend / Postman / Swagger
        ↓
Express Routes
        ↓
Middleware (auth, admin)
        ↓
Mongoose Models
        ↓
MongoDB Database
        ↓
Collections and Documents

Conclusion

What started as a simple MongoDB CRUD tutorial became a full backend learning journey.

You learned how to:

  • create and structure an API with Express

  • connect Node.js to MongoDB with Mongoose

  • perform CRUD operations

  • test endpoints in Postman

  • build authentication with hashed passwords and JWT

  • create OTP-based password reset flow

  • protect dashboard and profile routes

  • document APIs with Swagger

  • manage config using .env

  • connect to local MongoDB and understand Atlas

  • inspect real data with MongoDB Compass

  • build comment relationships with ref and populate()

  • build a wallet system with balances and transfers

  • add admin-only features using role-based authorization

That is no longer just beginner CRUD. It is the foundation of a real backend system.

As next steps, the strongest upgrades would be:

  • input validation with Joi or Zod

  • real OTP email sending with Nodemailer

  • MongoDB Atlas production connection

  • pagination and search

  • reply-to-comments system

  • wallet transaction references

  • MongoDB session transactions for wallet safety

  • soft delete and account suspension for admin

  • cleaner controllers/, services/, and utils/ architecture

If you keep building small systems like this, your backend understanding will grow very fast.


Quick Reference Checklist

Core setup

  • install Node.js

  • npm init -y

  • install Express and Mongoose

  • connect MongoDB

  • create schema

  • create model

  • create routes

  • test with Postman

Auth setup

  • register

  • login

  • hash password

  • create JWT

  • verify JWT in middleware

  • protect dashboard/profile

Swagger setup

  • install swagger packages

  • define OpenAPI config

  • add JSDoc route comments

  • open /api-docs

Comment setup

  • create comment model

  • link to user with ObjectId

  • use populate()

  • enforce owner-only edit/delete

Wallet setup

  • add balance to user

  • create transaction model

  • send by email

  • debit sender

  • credit receiver

  • show transaction history

Admin setup

  • add role field

  • add admin middleware

  • include role in token

  • protect admin routes

  • manage users and balances

Environment setup

  • create .env

  • add PORT

  • add MONGO_URI

  • add JWT_SECRET

  • ignore .env in .gitignore

Tools

  • Postman for testing

  • Swagger for docs and browser testing

  • Compass for visual database inspection

  • Nodemon for auto-restart