R
Rishtaara
MongoDB Fundamentals
Lesson 38 of 40Article22 min

Projects: Signup & Login with Node.js

Build a minimal signup and login API for a Rishtaara-style learning platform. Stack: Express.js, Mongoose, bcrypt for password hashing, and JSON Web Tokens or sessions for keeping users logged in.

Project overview — auth with Express + Mongoose

Build a minimal signup and login API for a Rishtaara-style learning platform. Stack: Express.js, Mongoose, bcrypt for password hashing, and JSON Web Tokens or sessions for keeping users logged in.

Real-life example: Signup is registering for a library card. Login is showing that card at the desk. bcrypt hashes the secret PIN so even the librarian cannot read your password.

  • Never store plain-text passwords — always hash with bcrypt (cost factor 10–12).
  • Validate email format and password length in the route before touching the database.
  • Return generic error messages on login failure — do not reveal whether email exists.
  • Use HTTP-only cookies for session tokens in browser apps; Bearer tokens for mobile/API clients.

User schema and signup route

User model with password hash
import mongoose from "mongoose";
import bcrypt from "bcrypt";

const SALT_ROUNDS = 12;

const userSchema = new mongoose.Schema(
  {
    name: { type: String, required: true, trim: true },
    email: {
      type: String,
      required: true,
      unique: true,
      lowercase: true,
      trim: true,
    },
    passwordHash: { type: String, required: true, select: false },
    role: { type: String, enum: ["student", "instructor"], default: "student" },
  },
  { timestamps: true }
);

userSchema.pre("save", async function hashPassword(next) {
  if (!this.isModified("passwordHash")) return next();
  // passwordHash field receives plain password temporarily in signup flow
  this.passwordHash = await bcrypt.hash(this.passwordHash, SALT_ROUNDS);
  next();
});

userSchema.methods.comparePassword = function (plain) {
  return bcrypt.compare(plain, this.passwordHash);
};

export const User = mongoose.model("User", userSchema);
POST /api/signup — Express route
import express from "express";
import { User } from "./models/User.js";

const router = express.Router();

router.post("/signup", async (req, res) => {
  try {
    const name = String(req.body.name ?? "").trim();
    const email = String(req.body.email ?? "").trim().toLowerCase();
    const password = String(req.body.password ?? "");

    if (!name || !email || password.length < 8) {
      return res.status(400).json({ error: "Invalid input" });
    }

    const existing = await User.findOne({ email });
    if (existing) {
      return res.status(409).json({ error: "Email already registered" });
    }

    const user = await User.create({
      name,
      email,
      passwordHash: password, // pre-save hook hashes this
    });

    res.status(201).json({
      id: user._id,
      name: user.name,
      email: user.email,
    });
  } catch (err) {
    res.status(500).json({ error: "Signup failed" });
  }
});

export default router;

Login route with bcrypt verification

POST /api/login
router.post("/login", async (req, res) => {
  try {
    const email = String(req.body.email ?? "").trim().toLowerCase();
    const password = String(req.body.password ?? "");

    if (!email || !password) {
      return res.status(400).json({ error: "Email and password required" });
    }

    const user = await User.findOne({ email }).select("+passwordHash");
    if (!user) {
      return res.status(401).json({ error: "Invalid email or password" });
    }

    const match = await user.comparePassword(password);
    if (!match) {
      return res.status(401).json({ error: "Invalid email or password" });
    }

    // Issue session token (JWT sketch)
    const token = createToken({ userId: user._id, role: user.role });

    res.json({
      token,
      user: { id: user._id, name: user.name, email: user.email, role: user.role },
    });
  } catch (err) {
    res.status(500).json({ error: "Login failed" });
  }
});
Auth middleware — protect routes
export function requireAuth(req, res, next) {
  const header = req.headers.authorization ?? "";
  const token = header.startsWith("Bearer ") ? header.slice(7) : null;

  if (!token) return res.status(401).json({ error: "Unauthorized" });

  try {
    req.user = verifyToken(token);
    next();
  } catch {
    res.status(401).json({ error: "Invalid token" });
  }
}

router.get("/profile", requireAuth, async (req, res) => {
  const user = await User.findById(req.user.userId).select("name email role");
  res.json(user);
});

Simple HTML form sketch (optional frontend)

Tip: Extend this project on Rishtaara — add course enrollment, progress tracking, and protect /api/enroll with requireAuth. Store only hashed passwords; never log req.body.password.
Signup and login forms
<!-- Signup -->
<form action="/api/signup" method="POST">
  <input name="name" placeholder="Full name" required />
  <input name="email" type="email" placeholder="Email" required />
  <input name="password" type="password" minlength="8" required />
  <button type="submit">Create account</button>
</form>

<!-- Login -->
<form action="/api/login" method="POST">
  <input name="email" type="email" placeholder="Email" required />
  <input name="password" type="password" required />
  <button type="submit">Log in</button>
</form>