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>