Back-EndSchemas
User Schema
A complete Mongoose User schema with password hashing, comparison method, and JWT-ready fields — used as the base for every project.
User Schema
Almost every backend needs a User model. This is the base version used throughout this section — with password hashing built in, a method to compare passwords on login, and fields ready for JWT authentication and role-based access.
Full Schema
// src/models/user.model.js
import mongoose from "mongoose";
import bcrypt from "bcryptjs";
const userSchema = new mongoose.Schema(
{
name: {
type: String,
required: [true, "Name is required"],
trim: true,
},
email: {
type: String,
required: [true, "Email is required"],
unique: true,
lowercase: true,
trim: true,
match: [/^\S+@\S+\.\S+$/, "Please enter a valid email"],
},
password: {
type: String,
required: [true, "Password is required"],
minlength: [8, "Password must be at least 8 characters"],
select: false, // never returned in queries unless explicitly requested
},
role: {
type: String,
enum: ["user", "admin"],
default: "user",
},
avatar: {
type: String,
default: "",
},
isEmailVerified: {
type: Boolean,
default: false,
},
refreshToken: {
type: String,
select: false, // used for JWT refresh token rotation — never exposed
},
passwordResetToken: {
type: String,
select: false,
},
passwordResetExpires: {
type: Date,
select: false,
},
},
{ timestamps: true }
);
// Step 1 — hash the password before saving, only if it was modified
// this runs automatically on .save() — not on .updateOne() or findOneAndUpdate()
userSchema.pre("save", async function (next) {
if (!this.isModified("password")) return next();
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
});
// Step 2 — instance method to compare a plain password against the hashed one
// used during login
userSchema.methods.comparePassword = async function (candidatePassword) {
return bcrypt.compare(candidatePassword, this.password);
};
// Step 3 — instance method to return a safe version of the user
// excludes sensitive fields even if they were accidentally selected
userSchema.methods.toSafeObject = function () {
return {
id: this._id,
name: this.name,
email: this.email,
role: this.role,
avatar: this.avatar,
isEmailVerified: this.isEmailVerified,
createdAt: this.createdAt,
};
};
const User = mongoose.model("User", userSchema);
export default User;How Each Part Is Used
Creating a user — password hashes automatically
import User from "../models/user.model.js";
const user = await User.create({
name: "Ali Hassan",
email: "ali@example.com",
password: "plaintext123", // gets hashed by the pre-save hook automatically
});Login — comparing passwords
// password has select: false, so it must be explicitly requested
const user = await User.findOne({ email }).select("+password");
if (!user) {
throw new Error("Invalid credentials");
}
const isMatch = await user.comparePassword(candidatePassword);
if (!isMatch) {
throw new Error("Invalid credentials");
}
// safe to send back to the client
const safeUser = user.toSafeObject();Why select: false matters
// without select: false — password leaks in every query
const users = await User.find(); // ❌ includes hashed passwords
// with select: false — password is hidden by default
const users = await User.find(); // ✅ no password field at all
// explicitly include it only when needed (like login)
const user = await User.findOne({ email }).select("+password"); // ✅pre("save") only runs on .save() and .create(). If you ever update a password using findOneAndUpdate(), the hashing hook will NOT run — the password will be saved in plain text. Always use .save() for password updates, or hash manually before using update methods.
Updating a Password Safely
// ✅ correct — triggers the pre-save hook
const user = await User.findById(userId);
user.password = "newpassword123";
await user.save(); // hashing hook runs here
// ❌ wrong — bypasses the hashing hook entirely
await User.findByIdAndUpdate(userId, { password: "newpassword123" });
// password is now stored in plain text — security riskSummary
passwordfield usesselect: falseso it is never returned by default — must be explicitly requested with.select("+password")pre("save")hook hashes the password automatically using bcrypt — only runs when the password field is modifiedcomparePassword()instance method handles login verification usingbcrypt.compare()toSafeObject()returns a clean version of the user with no sensitive fields — use this whenever sending user data to the frontend- Always use
.save()for password changes —findOneAndUpdate()skips the hashing hook refreshToken,passwordResetToken, andpasswordResetExpiresfields are ready for the authentication flows covered later in this section