Skip to content

AMNavinKumar2701/Note-Application

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

1 Commit
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ“ NoteApp β€” Complete Beginner's Build Guide

Who is this for? You are a junior backend developer who has just received this project. You have never seen this code before. This guide will teach you how to build this exact same project from scratch, step by step β€” just like a YouTube tutorial. Every command, every file, every reason is explained.


πŸ“Œ What You Will Build

A full-stack Note Taking Web Application using Node.js with:

  • User Authentication β€” Register β†’ Verify OTP β†’ Login β†’ Logout
  • Session-based access control β€” only logged-in users can see notes
  • Full CRUD β€” Create, Read, Update, Delete notes
  • Winston Logger β€” professional logging on every API action
  • Handlebars Templating β€” server-side rendered UI
  • MongoDB β€” database to store users, OTPs and notes

🧰 Tech Stack (What & Why)

Package What it does Why we use it
express Web framework Handles HTTP requests and routes
mongoose MongoDB ODM Makes DB queries easy with models
express-handlebars Template engine Renders HTML pages on the server
express-session Session management Remembers who is logged in
bcryptjs Password hashing Never store plain passwords in DB
winston Logger Professional logging to console + files
dotenv Environment variables Hides secrets like DB URLs and keys
connect-mongo Session store Saves sessions in MongoDB (optional upgrade)

βœ… Prerequisites β€” Install These First

Before you write a single line of code, install these on your computer:

1. Node.js

Download from: https://nodejs.org (choose LTS version)

Check it's installed:

node -v    # should show v18.x or higher
npm -v     # should show 9.x or higher

2. MongoDB

Download from: https://www.mongodb.com/try/download/community

After installing, start MongoDB service:

# Windows (run in Command Prompt as Admin)
net start MongoDB

# Mac
brew services start mongodb-community

# Linux
sudo systemctl start mongod

Check it's running:

mongosh     # should open a MongoDB shell β€” type exit to close

3. VS Code (Code Editor)

Download from: https://code.visualstudio.com

4. Postman (for testing APIs)

Download from: https://www.postman.com/downloads


πŸ“ Final Folder Structure

This is how your project will look when it is 100% complete. Read this carefully β€” every file has a purpose.

noteapp/
β”‚
β”œβ”€β”€ πŸ“„ server.js              ← Entry point. Starts the whole app
β”œβ”€β”€ πŸ“„ package.json           ← Project info + list of all packages
β”œβ”€β”€ πŸ“„ .env                   ← Secret values (NEVER share this file)
β”‚
β”œβ”€β”€ πŸ“‚ config/
β”‚   └── config.js             ← Reads .env and exports the values
β”‚
β”œβ”€β”€ πŸ“‚ utils/
β”‚   └── logger.js             ← Winston logger setup
β”‚
β”œβ”€β”€ πŸ“‚ middleware/
β”‚   └── authMiddleware.js     ← Guards routes from non-logged-in users
β”‚
β”œβ”€β”€ πŸ“‚ model/
β”‚   β”œβ”€β”€ userSchema.js         ← MongoDB schema for User accounts
β”‚   β”œβ”€β”€ otpSchema.js          ← MongoDB schema for OTP codes
β”‚   └── noteSchema.js         ← MongoDB schema for Notes
β”‚
β”œβ”€β”€ πŸ“‚ controllers/
β”‚   β”œβ”€β”€ authController.js     ← Register, Verify OTP, Login, Logout logic
β”‚   └── noteController.js     ← Add, View, Edit, Delete note logic
β”‚
β”œβ”€β”€ πŸ“‚ router/
β”‚   β”œβ”€β”€ authRoutes.js         ← URL paths for auth (login, register, etc.)
β”‚   └── noteRoutes.js         ← URL paths for notes (all protected)
β”‚
β”œβ”€β”€ πŸ“‚ public/
β”‚   └── style.css             ← All CSS styling for the whole app
β”‚
β”œβ”€β”€ πŸ“‚ logs/
β”‚   β”œβ”€β”€ error.log             ← Auto-created by Winston
β”‚   └── combined.log          ← Auto-created by Winston
β”‚
└── πŸ“‚ views/                 ← All HTML pages (Handlebars templates)
    β”œβ”€β”€ πŸ“‚ layouts/
    β”‚   └── main.handlebars   ← The master layout (navbar + footer wrapper)
    β”œβ”€β”€ πŸ“‚ partials/
    β”‚   β”œβ”€β”€ _navbar.handlebars
    β”‚   └── _footer.handlebars
    β”œβ”€β”€ πŸ“‚ auth/
    β”‚   β”œβ”€β”€ register.handlebars
    β”‚   β”œβ”€β”€ verifyOtp.handlebars
    β”‚   └── login.handlebars
    β”œβ”€β”€ πŸ“‚ noteApp/
    β”‚   β”œβ”€β”€ addNote.handlebars
    β”‚   β”œβ”€β”€ allNotes.handlebars
    β”‚   β”œβ”€β”€ editNote.handlebars
    β”‚   └── singleNote.handlebars
    └── home.handlebars

πŸš€ STEP-BY-STEP BUILD GUIDE


STEP 1 β€” Create the Project Folder and Initialize

Open your terminal, go to wherever you keep your projects, and run:

mkdir noteapp
cd noteapp
npm init -y

What this does:

  • mkdir noteapp β€” creates a new folder called noteapp
  • cd noteapp β€” enter that folder
  • npm init -y β€” creates package.json automatically with default values

You will now see a package.json file. This file tracks your project name, version, and all the packages you install.


STEP 2 β€” Install All Required Packages

Run this single command to install every package the project needs:

npm install express mongoose express-handlebars express-session bcryptjs winston dotenv connect-mongo

What each package does (explained simply):

Package Simple explanation
express The main web server. Think of it as the engine of your car
mongoose Lets you talk to MongoDB easily using JavaScript objects
express-handlebars Lets you create HTML pages with dynamic data using {{variable}} syntax
express-session After a user logs in, the server "remembers" them using a session
bcryptjs Scrambles (hashes) passwords so even if your DB is hacked, passwords are safe
winston A professional logging tool β€” better than console.log because it saves to files
dotenv Reads your .env file and makes those values available as process.env.SOMETHING
connect-mongo Optional: stores sessions in MongoDB instead of memory

After installing, you will see a node_modules/ folder and a package-lock.json file appear. Never edit these manually.

Add this to .gitignore to never accidentally push them:

node_modules/
.env
logs/

STEP 3 β€” Create the .env File

Create a file called .env in the root of your project (same level as package.json):

PORT=3000
MONGODB_URL=mongodb://localhost:27017/Note_App
SESSION_SECRET=noteapp_super_secret_key_2025

Why do we need this?

Imagine you have a database password. If you write it directly in your code and push to GitHub, everyone in the world can see it. The .env file keeps secrets out of your code. The dotenv package reads this file and makes the values available in your app.

⚠️ IMPORTANT: Always add .env to .gitignore. Never commit it to Git.


STEP 4 β€” Create config/config.js

Create the config/ folder, then create config/config.js:

require('dotenv').config()

module.exports = {
    PORT: process.env.PORT || 3000,
    MONGODB_URL: process.env.MONGODB_URL,
    SESSION_SECRET: process.env.SESSION_SECRET || 'fallback_secret'
}

Why do we need this separate file?

Instead of calling require('dotenv').config() in every file, we do it once here. Then everywhere else in the project, we just do const { PORT } = require('./config/config') and get the value cleanly.

The || 3000 part means: "use PORT from .env, but if it's missing, use 3000 as a fallback."


STEP 5 β€” Create the Winston Logger (utils/logger.js)

Create the utils/ folder, then create utils/logger.js:

const { createLogger, format, transports } = require('winston')
const { combine, timestamp, printf, colorize } = format

const logFormat = printf(({ level, message, timestamp }) => {
    return `[${timestamp}] ${level.toUpperCase()}: ${message}`
})

const logger = createLogger({
    level: 'info',
    format: combine(
        timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
        logFormat
    ),
    transports: [
        new transports.Console({
            format: combine(
                colorize(),
                timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
                logFormat
            )
        }),
        new transports.File({ filename: 'logs/error.log', level: 'error' }),
        new transports.File({ filename: 'logs/combined.log' })
    ]
})

module.exports = logger

Why Winston instead of console.log?

console.log is fine for learning, but in real projects:

  • You need logs saved to files (so you can check them later)
  • You need timestamps on every message
  • You need different levels β€” info, warn, error
  • You need color coding in the terminal

Winston does all of this.

How to use it in any file:

const logger = require('../utils/logger')

logger.info('User logged in successfully')      // normal info
logger.warn('Failed login attempt')             // warning
logger.error('Database connection failed')      // error (also goes to error.log)

Create the logs/ folder now so Winston can write to it:

mkdir logs

STEP 6 β€” Create MongoDB Models

Think of a model as a blueprint. It tells MongoDB: "Every document in this collection must look exactly like this."

6a β€” User Model (model/userSchema.js)

Create the model/ folder, then create model/userSchema.js:

const { Schema, model } = require('mongoose')

const UserSchema = new Schema({
    name: { type: String, required: true },
    email: { type: String, required: true, unique: true, lowercase: true },
    password: { type: String, required: true },
    isVerified: { type: Boolean, default: false }
}, { timestamps: true })

module.exports = model('User', UserSchema)

Field explanations:

Field Type Why
name String The user's display name
email String, unique Login identifier. unique: true prevents two accounts with same email
password String Stores the hashed password (never plain text)
isVerified Boolean Starts as false. Only becomes true after OTP verification

timestamps: true automatically adds createdAt and updatedAt fields to every document.

6b β€” OTP Model (model/otpSchema.js)

Create model/otpSchema.js:

const { Schema, model } = require('mongoose')

const OtpSchema = new Schema({
    email: { type: String, required: true },
    otp: { type: String, required: true },
    expiresAt: { type: Date, required: true }
})

// This line tells MongoDB to automatically delete the document when expiresAt passes
OtpSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 })

module.exports = model('Otp', OtpSchema)

Why a separate OTP model?

When a user registers, we generate a 6-digit code and save it here temporarily. Once they verify, we delete this record. The expireAfterSeconds: 0 on the expiresAt field tells MongoDB's built-in TTL (Time To Live) feature to automatically delete the document when the expiry time passes. This is clean and automatic.

6c β€” Note Model (model/noteSchema.js)

Create model/noteSchema.js:

const { Schema, model } = require('mongoose')

const NoteSchema = new Schema({
    userId: { type: Schema.Types.ObjectId, ref: 'User', required: true },
    noteId: { type: String, required: true },
    noteName: { type: String, required: true },
    description: { type: String, required: true },
    status: { type: String, required: true, enum: ['active', 'inactive'] }
}, { timestamps: true })

module.exports = model('Note', NoteSchema)

The key field here is userId.

Schema.Types.ObjectId is MongoDB's unique ID type. ref: 'User' creates a relationship β€” every Note belongs to a User. When we query notes, we always filter by userId: req.session.userId so users can only ever see their own notes.

enum: ['active', 'inactive'] means if you try to save any other value for status, MongoDB will reject it.


STEP 7 β€” Create the Auth Middleware (middleware/authMiddleware.js)

Create the middleware/ folder, then create middleware/authMiddleware.js:

const logger = require('../utils/logger')

const authMiddleware = (req, res, next) => {
    if (!req.session || !req.session.userId) {
        logger.warn(`Unauthorized access attempt β†’ ${req.originalUrl}`)
        return res.redirect('/auth/login?msg=login_required')
    }
    next()
}

module.exports = authMiddleware

What is middleware?

Middleware is a function that runs between the request arriving and your controller handling it. Think of it as a security guard at the door.

When a user tries to visit /api/allNotes:

  1. The request arrives at Express
  2. Express runs authMiddleware first
  3. If req.session.userId does not exist β†’ user is not logged in β†’ redirect to login with a message
  4. If it does exist β†’ call next() β†’ continue to the controller

Why ?msg=login_required?

We pass this as a URL query parameter so the login page can read it and show the user a message: "Please login first to access that page."


STEP 8 β€” Create the Auth Controller (controllers/authController.js)

Create the controllers/ folder, then create controllers/authController.js.

The controller holds all the business logic for each route. Here is the full file with explanations inline:

const bcrypt = require('bcryptjs')
const User = require('../model/userSchema')
const Otp = require('../model/otpSchema')
const logger = require('../utils/logger')

// Helper: generates a random 6-digit number as a string
const generateOtp = () => Math.floor(100000 + Math.random() * 900000).toString()

// ─── REGISTER PAGE (GET) ─────────────────────────────────
// Just renders the register HTML page
exports.registerPage = (req, res) => {
    res.render('auth/register', { title: 'Register' })
}

// ─── REGISTER (POST) ─────────────────────────────────────
exports.register = async (req, res) => {
    try {
        const { name, email, password } = req.body
        // req.body holds what the user typed in the form

        // Check if this email already exists in DB
        const existing = await User.findOne({ email })
        if (existing) {
            logger.warn(`Register failed - email already exists: ${email}`)
            return res.render('auth/register', {
                title: 'Register',
                error: 'Email already registered. Please login.'
            })
        }

        // Hash the password β€” NEVER store plain text passwords
        // bcrypt.hash(password, saltRounds) β€” 10 is the industry standard
        const hashed = await bcrypt.hash(password, 10)

        // Save new user to MongoDB β€” isVerified defaults to false
        await User.create({ name, email, password: hashed })

        // Generate OTP and save to DB
        const otp = generateOtp()
        const expiresAt = new Date(Date.now() + 10 * 60 * 1000) // expires in 10 minutes
        await Otp.deleteMany({ email }) // delete any old OTPs for this email first
        await Otp.create({ email, otp, expiresAt })

        // Log the OTP via Winston (in production this would be emailed)
        logger.info(`OTP generated for ${email} β†’ ${otp}`)

        // Redirect to verify page, passing the OTP in devOtp for dev/testing
        res.render('auth/verifyOtp', {
            title: 'Verify OTP',
            email,
            devOtp: otp
        })
    } catch (err) {
        logger.error(`Register error: ${err.message}`)
        res.render('auth/register', { title: 'Register', error: 'Something went wrong.' })
    }
}

// ─── VERIFY OTP PAGE (GET) ───────────────────────────────
exports.verifyOtpPage = (req, res) => {
    res.render('auth/verifyOtp', { title: 'Verify OTP', email: req.query.email })
}

// ─── VERIFY OTP (POST) ───────────────────────────────────
exports.verifyOtp = async (req, res) => {
    try {
        const { email, otp } = req.body

        // Look for an OTP record matching this email
        const record = await Otp.findOne({ email })

        if (!record) {
            logger.warn(`OTP verify failed - no OTP found for: ${email}`)
            return res.render('auth/verifyOtp', {
                title: 'Verify OTP',
                email,
                error: 'OTP expired or not found. Register again.'
            })
        }

        // Check if the code the user entered matches what we saved
        if (record.otp !== otp.trim()) {
            logger.warn(`OTP verify failed - wrong OTP for: ${email}`)
            return res.render('auth/verifyOtp', {
                title: 'Verify OTP',
                email,
                error: 'Invalid OTP. Try again.'
            })
        }

        // Check if OTP has expired (extra safety check alongside MongoDB TTL)
        if (new Date() > record.expiresAt) {
            await Otp.deleteMany({ email })
            logger.warn(`OTP expired for: ${email}`)
            return res.render('auth/verifyOtp', {
                title: 'Verify OTP',
                email,
                error: 'OTP has expired. Register again.'
            })
        }

        // OTP is valid β€” mark user as verified in DB
        await User.findOneAndUpdate({ email }, { isVerified: true })

        // Delete the OTP record β€” it's been used, we don't need it anymore
        await Otp.deleteMany({ email })

        logger.info(`Email verified successfully: ${email}`)

        // Send user to login page with a success message
        res.redirect('/auth/login?verified=true')
    } catch (err) {
        logger.error(`OTP verify error: ${err.message}`)
        res.render('auth/verifyOtp', {
            title: 'Verify OTP',
            email: req.body.email,
            error: 'Something went wrong.'
        })
    }
}

// ─── LOGIN PAGE (GET) ────────────────────────────────────
exports.loginPage = (req, res) => {
    const verified = req.query.verified === 'true'
    const loginRequired = req.query.msg === 'login_required'
    res.render('auth/login', { title: 'Login', verified, loginRequired })
}

// ─── LOGIN (POST) ────────────────────────────────────────
exports.login = async (req, res) => {
    try {
        const { email, password } = req.body

        // Find user by email
        const user = await User.findOne({ email })
        if (!user) {
            logger.warn(`Login failed - user not found: ${email}`)
            return res.render('auth/login', {
                title: 'Login',
                error: 'Invalid email or password.'
            })
        }

        // Block login if email is not verified yet
        if (!user.isVerified) {
            logger.warn(`Login blocked - email not verified: ${email}`)
            return res.render('auth/login', {
                title: 'Login',
                error: 'Please verify your email first.'
            })
        }

        // Compare typed password with the hashed one in DB
        // bcrypt.compare returns true if they match
        const match = await bcrypt.compare(password, user.password)
        if (!match) {
            logger.warn(`Login failed - wrong password for: ${email}`)
            return res.render('auth/login', {
                title: 'Login',
                error: 'Invalid email or password.'
            })
        }

        // Save user info in the session β€” this is how the server "remembers" them
        req.session.userId = user._id
        req.session.userName = user.name

        logger.info(`User logged in: ${email}`)
        res.redirect('/')   // send to home page
    } catch (err) {
        logger.error(`Login error: ${err.message}`)
        res.render('auth/login', { title: 'Login', error: 'Something went wrong.' })
    }
}

// ─── LOGOUT (GET) ────────────────────────────────────────
exports.logout = (req, res) => {
    const name = req.session.userName
    req.session.destroy()   // destroys the session on the server
    logger.info(`User logged out: ${name}`)
    res.redirect('/auth/login')
}

STEP 9 β€” Create the Auth Routes (router/authRoutes.js)

Create the router/ folder, then create router/authRoutes.js:

const { Router } = require('express')
const router = Router()
const auth = require('../controllers/authController')

router.get('/register', auth.registerPage)   // shows the register form
router.post('/register', auth.register)      // handles form submission

router.get('/verify-otp', auth.verifyOtpPage)  // shows the OTP form
router.post('/verify-otp', auth.verifyOtp)     // handles OTP check

router.get('/login', auth.loginPage)   // shows the login form
router.post('/login', auth.login)      // handles login form

router.get('/logout', auth.logout)     // logs the user out

module.exports = router

Why GET and POST for the same path?

  • GET /auth/login β†’ User visits the URL β†’ show the login page
  • POST /auth/login β†’ User submits the form β†’ check the credentials

HTML forms can only do GET or POST. The browser sends a POST when the user clicks the submit button.


STEP 10 β€” Create the Note Controller (controllers/noteController.js)

Create controllers/noteController.js:

const mongoose = require('mongoose')
const Note = require('../model/noteSchema')
const logger = require('../utils/logger')

// Helper: checks if a MongoDB ID is valid format
const isValidId = (id) => mongoose.Types.ObjectId.isValid(id)

// Home page β€” only reached after login because of authMiddleware
exports.home = (req, res) => {
    res.render('home', {
        title: 'Home',
        userName: req.session.userName  // pass name to show "Welcome back, John!"
    })
}

// Show the Add Note form page
exports.addNotePage = (req, res) => {
    res.render('noteApp/addNote', { title: 'Add Note' })
}

// Handle the Add Note form submission
exports.createNote = async (req, res) => {
    try {
        // req.body has the form data. We add userId from session so the note is linked to this user
        await Note.create({ ...req.body, userId: req.session.userId })
        logger.info(`Note created by user: ${req.session.userId}`)
        res.redirect('/api/allNotes')
    } catch (err) {
        logger.error(`Create note error: ${err.message}`)
        res.render('noteApp/addNote', { title: 'Add Note', error: 'Failed to create note.' })
    }
}

// Show all notes belonging to the logged-in user only
exports.allNotes = async (req, res) => {
    // IMPORTANT: filter by userId so users never see each other's notes
    const payload = await Note.find({ userId: req.session.userId }).lean()
    // .lean() converts Mongoose documents to plain JS objects β€” needed for Handlebars
    res.render('noteApp/allNotes', { title: 'All Notes', payload })
}

// Show one specific note
exports.singleNote = async (req, res) => {
    if (!isValidId(req.params.id)) return res.redirect('/api/allNotes')
    // Also filter by userId β€” prevents User A from viewing User B's note by guessing the ID
    const payload = await Note.findOne({
        _id: req.params.id,
        userId: req.session.userId
    }).lean()
    if (!payload) return res.redirect('/api/allNotes')
    res.render('noteApp/singleNote', { title: payload.noteName, payload })
}

// Show the Edit form pre-filled with existing note data
exports.editPage = async (req, res) => {
    if (!isValidId(req.params.id)) return res.redirect('/api/allNotes')
    const payload = await Note.findOne({
        _id: req.params.id,
        userId: req.session.userId
    }).lean()
    if (!payload) return res.redirect('/api/allNotes')
    res.render('noteApp/editNote', { title: 'Edit Note', payload })
}

// Handle the Edit form submission β€” update the note in DB
exports.updateNote = async (req, res) => {
    if (!isValidId(req.params.id)) return res.redirect('/api/allNotes')
    await Note.findOneAndUpdate(
        { _id: req.params.id, userId: req.session.userId },
        req.body
    )
    logger.info(`Note updated: ${req.params.id}`)
    res.redirect('/api/allNotes')
}

// Delete a note
exports.deleteNote = async (req, res) => {
    await Note.findOneAndDelete({
        _id: req.params.id,
        userId: req.session.userId
    })
    logger.info(`Note deleted: ${req.params.id}`)
    res.redirect('/api/allNotes')
}

STEP 11 β€” Create the Note Routes (router/noteRoutes.js)

Create router/noteRoutes.js:

const { Router } = require('express')
const router = Router()
const controller = require('../controllers/noteController')
const authMiddleware = require('../middleware/authMiddleware')

// Apply authMiddleware to EVERY route in this file
// If you're not logged in, ALL of these will redirect you to login
router.use(authMiddleware)

router.get('/addNote', controller.addNotePage)      // show add form
router.get('/allNotes', controller.allNotes)         // show all notes
router.get('/edit/:id', controller.editPage)         // show edit form

router.post('/addNote', controller.createNote)       // submit add form
router.post('/edit/:id', controller.updateNote)      // submit edit form
router.get('/delete/:id', controller.deleteNote)     // delete a note

// ⚠️ IMPORTANT: this route MUST be last
// Because /:id would match /addNote and /allNotes too if placed first
router.get('/:id', controller.singleNote)

module.exports = router

Why must /:id be last?

Express matches routes in the order you define them. If /:id comes first, then /allNotes would be treated as an ID (the string "allNotes"). By placing it last, Express only reaches it after checking all the specific routes above.


STEP 12 β€” Create server.js (The Main Entry Point)

This is the file that ties everything together. Create server.js in the root:

const express = require('express')
const mongoose = require('mongoose')
const session = require('express-session')
const { engine } = require('express-handlebars')
const { PORT, MONGODB_URL, SESSION_SECRET } = require('./config/config')
const logger = require('./utils/logger')
const authMiddleware = require('./middleware/authMiddleware')
const noteRoutes = require('./router/noteRoutes')
const authRoutes = require('./router/authRoutes')
const noteController = require('./controllers/noteController')

const app = express()

// ─── BODY PARSERS ─────────────────────────────────────────
// These two lines allow Express to read form data (req.body)
app.use(express.urlencoded({ extended: true }))  // for HTML forms
app.use(express.json())                           // for JSON body (API calls)

// ─── STATIC FILES ─────────────────────────────────────────
// Tells Express: "serve files from the public/ folder directly"
// So /style.css in the browser actually serves public/style.css
app.use(express.static('public'))

// ─── SESSION SETUP ────────────────────────────────────────
app.use(session({
    secret: SESSION_SECRET,       // used to sign/encrypt the session cookie
    resave: false,                // don't save session if nothing changed
    saveUninitialized: false,     // don't create a session until something is stored
    cookie: { maxAge: 1000 * 60 * 60 * 24 }  // session lasts 1 day (in milliseconds)
}))

// ─── INJECT SESSION INTO ALL VIEWS ───────────────────────
// This middleware runs on EVERY request.
// It puts isLoggedIn and userName into res.locals so EVERY Handlebars
// template can use {{isLoggedIn}} and {{userName}} without us passing
// them manually in every controller.
app.use((req, res, next) => {
    res.locals.isLoggedIn = !!req.session.userId   // !! converts to true/false
    res.locals.userName = req.session.userName || null
    next()
})

// ─── HANDLEBARS ENGINE SETUP ──────────────────────────────
app.engine('handlebars', engine({
    helpers: {
        // Custom helper: allows {{#if (eq a b)}} comparisons in templates
        eq: (a, b) => a === b
    }
}))
app.set('view engine', 'handlebars')  // tells Express to use handlebars by default

// ─── ROUTES ───────────────────────────────────────────────
// Home route β€” protected by authMiddleware (must be logged in)
app.get('/', authMiddleware, noteController.home)

// Auth routes β€” /auth/register, /auth/login, etc.
app.use('/auth', authRoutes)

// Note routes β€” /api/allNotes, /api/addNote, etc. (all protected inside)
app.use('/api', noteRoutes)

// ─── CONNECT DB AND START SERVER ──────────────────────────
const connectDb = async () => {
    await mongoose.connect(MONGODB_URL)
    logger.info('MongoDB connected successfully')
    app.listen(PORT, () => {
        logger.info(`Server running at http://localhost:${PORT}`)
    })
}

connectDb().catch(err => logger.error(`DB connection failed: ${err.message}`))

STEP 13 β€” Create the Handlebars Views

Handlebars is a template engine. It lets you write HTML with dynamic placeholders like {{name}} which get replaced with real data when the page is rendered.

The Layout File (views/layouts/main.handlebars)

Create views/layouts/ folder, then create main.handlebars:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{title}} | NoteApp</title>
    <link rel="stylesheet" href="/style.css">
</head>
<body>
<div class="page">
    {{> _navbar}}
    <main class="content">
        {{{body}}}
    </main>
    {{> _footer}}
</div>
</body>
</html>

Key concepts here:

  • {{{body}}} β€” this is where the content of each page gets injected (triple braces = unescaped HTML)
  • {{> _navbar}} β€” this inserts the _navbar partial (a reusable piece of HTML)
  • Every page automatically gets the navbar and footer without you repeating the code

Partials (views/partials/)

Create views/partials/_navbar.handlebars:

<nav id="navbar">
    <div class="nav-logo">
        <a href="/">πŸ“ NoteApp</a>
    </div>

    <ul id="navLinks">
        {{#if isLoggedIn}}
        <li><a href="/">🏠 Home</a></li>
        <li><a href="/api/allNotes">πŸ“‹ My Notes</a></li>
        <li><a href="/api/addNote">βž• Add Note</a></li>
        {{else}}
        <li><a href="/auth/login">πŸ”‘ Login</a></li>
        <li><a href="/auth/register">πŸ“ Register</a></li>
        {{/if}}
    </ul>

    <div class="nav-right">
        {{#if isLoggedIn}}
        <span class="nav-user">πŸ‘€ {{userName}}</span>
        <a href="/auth/logout" class="nav-logout">πŸšͺ Logout</a>
        {{/if}}
    </div>
</nav>

Why {{#if isLoggedIn}}?

This comes from res.locals.isLoggedIn that we set in server.js. If the user is logged in, show Home + My Notes + Add Note + Logout. If not, only show Login + Register.

Create views/partials/_footer.handlebars:

<footer id="footer">
    <p>Β© 2025 NoteApp β€” Built with ❀️</p>
</footer>

Auth Pages

Create views/auth/register.handlebars:

<div class="auth-page">
    <div class="auth-card">
        <div class="auth-logo">πŸ“</div>
        <h2>Create Account</h2>
        <p class="auth-sub">Join NoteApp and start capturing ideas</p>

        {{#if error}}
        <div class="alert alert-error">{{error}}</div>
        {{/if}}

        <form action="/auth/register" method="POST" class="auth-form">
            <div class="form-group">
                <label>Full Name</label>
                <input type="text" name="name" placeholder="John Doe" required autofocus>
            </div>
            <div class="form-group">
                <label>Email Address</label>
                <input type="email" name="email" placeholder="[email protected]" required>
            </div>
            <div class="form-group">
                <label>Password</label>
                <input type="password" name="password" placeholder="Min. 6 characters" minlength="6" required>
            </div>
            <button type="submit" class="auth-btn">Register β†’</button>
        </form>

        <p class="auth-switch">Already have an account? <a href="/auth/login">Login</a></p>
    </div>
</div>

Create views/auth/verifyOtp.handlebars:

<div class="auth-page">
    <div class="auth-card">
        <div class="auth-logo">πŸ”</div>
        <h2>Verify Your Email</h2>
        <p class="auth-sub">Enter the 6-digit OTP for <strong>{{email}}</strong></p>

        {{#if error}}
        <div class="alert alert-error">{{error}}</div>
        {{/if}}

        {{#if devOtp}}
        <div class="alert alert-info">
            <strong>Dev Mode OTP:</strong> {{devOtp}}<br>
            <small>In production, this is sent via email.</small>
        </div>
        {{/if}}

        <form action="/auth/verify-otp" method="POST" class="auth-form">
            <input type="hidden" name="email" value="{{email}}">
            <div class="form-group">
                <label>OTP Code</label>
                <input type="text" name="otp" placeholder="Enter 6-digit OTP"
                       maxlength="6" pattern="[0-9]{6}" required autofocus
                       style="letter-spacing:8px;font-size:22px;text-align:center;">
            </div>
            <button type="submit" class="auth-btn">Verify OTP β†’</button>
        </form>

        <p class="auth-switch">Wrong email? <a href="/auth/register">Register again</a></p>
    </div>
</div>

Create views/auth/login.handlebars:

<div class="auth-page">
    <div class="auth-card">
        <div class="auth-logo">πŸ“</div>
        <h2>Welcome Back</h2>
        <p class="auth-sub">Login to access your notes</p>

        {{#if loginRequired}}
        <div class="alert alert-warning">πŸ”’ Please login first to access that page.</div>
        {{/if}}

        {{#if verified}}
        <div class="alert alert-success">βœ… Email verified! You can now login.</div>
        {{/if}}

        {{#if error}}
        <div class="alert alert-error">{{error}}</div>
        {{/if}}

        <form action="/auth/login" method="POST" class="auth-form">
            <div class="form-group">
                <label>Email Address</label>
                <input type="email" name="email" placeholder="[email protected]" required autofocus>
            </div>
            <div class="form-group">
                <label>Password</label>
                <input type="password" name="password" placeholder="Your password" required>
            </div>
            <button type="submit" class="auth-btn">Login β†’</button>
        </form>

        <p class="auth-switch">No account? <a href="/auth/register">Register here</a></p>
    </div>
</div>

Home Page (views/home.handlebars)

<div id="home">
    <div class="home-hero">
        <div class="home-icon">πŸ“</div>
        <h1>Welcome back, <span class="accent">{{userName}}</span>!</h1>
        <p class="home-text">Your personal space to capture ideas, thoughts, goals and everything in between.</p>
        <div class="home-actions">
            <a href="/api/allNotes" class="hero-btn primary">View My Notes</a>
            <a href="/api/addNote" class="hero-btn secondary">+ New Note</a>
        </div>
    </div>
</div>

Note Pages

Create views/noteApp/addNote.handlebars:

<div class="form-page">
    <div class="form-card">
        <div class="form-header">
            <span class="form-icon">πŸ“</span>
            <h2>Add New Note</h2>
        </div>

        {{#if error}}
        <div class="alert alert-error">{{error}}</div>
        {{/if}}

        <form action="/api/addNote" method="POST" class="note-form">
            <div class="form-group">
                <label>Note ID</label>
                <input type="text" name="noteId" placeholder="e.g. NOTE-001" required>
            </div>
            <div class="form-group">
                <label>Note Name</label>
                <input type="text" name="noteName" placeholder="Enter note title" required>
            </div>
            <div class="form-group">
                <label>Description</label>
                <textarea name="description" placeholder="Write your note here..." required></textarea>
            </div>
            <div class="form-group">
                <label>Status</label>
                <select name="status">
                    <option value="active">🟒 Active</option>
                    <option value="inactive">πŸ”΄ Inactive</option>
                </select>
            </div>
            <div class="form-actions">
                <a href="/api/allNotes" class="btn-outline">Cancel</a>
                <button type="submit" class="btn-primary">Save Note</button>
            </div>
        </form>
    </div>
</div>

Create views/noteApp/allNotes.handlebars:

<div class="notes-page">
    <div class="notes-header">
        <h1>πŸ“‹ My Notes</h1>
        <a href="/api/addNote" class="btn-primary">+ Add Note</a>
    </div>

    <div class="notes-grid">
        {{#each payload}}
        <div class="note-card hover-card">
            <div class="note-top">
                <span class="note-id">{{noteId}}</span>
                <span class="status {{status}}">{{status}}</span>
            </div>
            <h3>{{noteName}}</h3>
            <p class="note-desc">{{description}}</p>
            <div class="card-actions">
                <a href="/api/{{_id}}" class="btn btn-view">πŸ‘ View</a>
                <a href="/api/edit/{{_id}}" class="btn btn-edit">✏️ Edit</a>
                <a href="/api/delete/{{_id}}" class="btn btn-delete"
                   onclick="return confirm('Delete this note?')">πŸ—‘ Delete</a>
            </div>
        </div>
        {{else}}
        <div class="empty-state">
            <div class="empty-icon">πŸ—’οΈ</div>
            <h3>No notes yet</h3>
            <p>Start capturing your thoughts!</p>
            <a href="/api/addNote" class="btn-primary">Create First Note</a>
        </div>
        {{/each}}
    </div>
</div>

Create views/noteApp/editNote.handlebars:

<div class="form-page">
    <div class="form-card">
        <div class="form-header">
            <span class="form-icon">✏️</span>
            <h2>Edit Note</h2>
        </div>

        <form action="/api/edit/{{payload._id}}" method="POST" class="note-form">
            <div class="form-group">
                <label>Note ID</label>
                <input type="text" name="noteId" value="{{payload.noteId}}" required>
            </div>
            <div class="form-group">
                <label>Note Name</label>
                <input type="text" name="noteName" value="{{payload.noteName}}" required>
            </div>
            <div class="form-group">
                <label>Description</label>
                <textarea name="description" required>{{payload.description}}</textarea>
            </div>
            <div class="form-group">
                <label>Status</label>
                <select name="status">
                    <option value="active" {{#if (eq payload.status "active")}}selected{{/if}}>🟒 Active</option>
                    <option value="inactive" {{#if (eq payload.status "inactive")}}selected{{/if}}>πŸ”΄ Inactive</option>
                </select>
            </div>
            <div class="form-actions">
                <a href="/api/allNotes" class="btn-outline">Cancel</a>
                <button type="submit" class="btn-primary">Update Note</button>
            </div>
        </form>
    </div>
</div>

Create views/noteApp/singleNote.handlebars:

<div class="single-page">
    <div class="single-card">
        <div class="single-top">
            <span class="note-id">{{payload.noteId}}</span>
            <span class="status {{payload.status}}">{{payload.status}}</span>
        </div>
        <h2>{{payload.noteName}}</h2>
        <p class="note-desc">{{payload.description}}</p>
        <div class="single-actions">
            <a href="/api/allNotes" class="btn btn-view">← Back</a>
            <a href="/api/edit/{{payload._id}}" class="btn btn-edit">✏️ Edit</a>
            <a href="/api/delete/{{payload._id}}" class="btn btn-delete"
               onclick="return confirm('Delete this note?')">πŸ—‘ Delete</a>
        </div>
    </div>
</div>

STEP 14 β€” Add the CSS (public/style.css)

The full CSS is included in the project ZIP file. Copy it as-is into public/style.css.

Key design decisions in the CSS:

  • CSS variables (--primary, --danger, etc.) make color changes easy β€” change in one place, updates everywhere
  • .form-page uses min-height: calc(100vh - 60px - 56px) to fill the full screen height minus navbar and footer
  • The navbar uses display: flex with justify-content: space-between for logo-left, links-center, logout-right layout
  • .notes-grid uses grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)) β€” this automatically adjusts columns based on screen width

STEP 15 β€” Create the logs Folder

Winston needs this folder to exist before it can write log files:

mkdir logs

πŸ”„ Complete User Flow (How Everything Connects)

USER REGISTERS
    β”‚
    β–Ό
POST /auth/register
    β”œβ”€β”€ Validate email not taken
    β”œβ”€β”€ Hash password with bcrypt
    β”œβ”€β”€ Save user to DB (isVerified: false)
    β”œβ”€β”€ Generate 6-digit OTP
    β”œβ”€β”€ Save OTP to DB (expires in 10 min)
    β”œβ”€β”€ Log OTP via Winston
    └── Render verifyOtp page

USER ENTERS OTP
    β”‚
    β–Ό
POST /auth/verify-otp
    β”œβ”€β”€ Find OTP record by email
    β”œβ”€β”€ Check OTP matches
    β”œβ”€β”€ Check OTP not expired
    β”œβ”€β”€ Update user isVerified = true
    β”œβ”€β”€ Delete OTP record from DB
    └── Redirect to /auth/login?verified=true

USER LOGS IN
    β”‚
    β–Ό
POST /auth/login
    β”œβ”€β”€ Find user by email
    β”œβ”€β”€ Check isVerified is true
    β”œβ”€β”€ bcrypt.compare(typed password, hashed DB password)
    β”œβ”€β”€ Save userId + userName in session
    └── Redirect to / (home page)

USER VISITS ANY NOTES PAGE
    β”‚
    β–Ό
authMiddleware runs first
    β”œβ”€β”€ req.session.userId exists? β†’ next() β†’ controller runs
    └── req.session.userId missing? β†’ redirect to /auth/login?msg=login_required

USER LOGS OUT
    β”‚
    β–Ό
GET /auth/logout
    β”œβ”€β”€ req.session.destroy()
    └── Redirect to /auth/login

▢️ How to Run the Project

1. Clone or unzip the project

cd noteapp

2. Install dependencies

npm install

3. Set up your .env file

Open .env and confirm your MongoDB URL is correct:

PORT=3000
MONGODB_URL=mongodb://localhost:27017/Note_App
SESSION_SECRET=noteapp_super_secret_key_2025

4. Make sure MongoDB is running

# Check if MongoDB is running
mongosh
# If it opens a shell, MongoDB is running. Type 'exit' to close.

5. Start the server

node server.js

You should see in your terminal:

[2025-04-09 10:00:00] INFO: MongoDB connected successfully
[2025-04-09 10:00:00] INFO: Server running at http://localhost:3000

6. Open in browser

Go to: http://localhost:3000

You will be redirected to the login page. Register a new account to get started.


🌐 Full API Route Reference

Method URL Protected? What it does
GET / βœ… Yes Home page (welcome screen)
GET /auth/register ❌ No Show register form
POST /auth/register ❌ No Process registration, send OTP
GET /auth/verify-otp ❌ No Show OTP form
POST /auth/verify-otp ❌ No Verify OTP code
GET /auth/login ❌ No Show login form
POST /auth/login ❌ No Process login, create session
GET /auth/logout ❌ No Destroy session, redirect
GET /api/allNotes βœ… Yes Show all notes for logged-in user
GET /api/addNote βœ… Yes Show add note form
POST /api/addNote βœ… Yes Save new note to DB
GET /api/:id βœ… Yes Show single note by ID
GET /api/edit/:id βœ… Yes Show edit form pre-filled
POST /api/edit/:id βœ… Yes Update note in DB
GET /api/delete/:id βœ… Yes Delete note from DB

🐞 Common Errors and Fixes

Error: Cannot find module 'express'

Cause: You forgot to run npm install Fix:

npm install

Error: MongoServerError: connect ECONNREFUSED 127.0.0.1:27017

Cause: MongoDB is not running Fix: Start MongoDB service (see Prerequisites above)

Error: Error: ENOENT: no such file or directory 'logs/error.log'

Cause: The logs/ folder doesn't exist Fix:

mkdir logs

Error: TypeError: Cannot read properties of undefined (reading 'userId')

Cause: express-session is not set up before your routes Fix: Make sure app.use(session(...)) comes before app.use('/api', noteRoutes) in server.js

Error: Handlebars template shows {{userName}} as literal text

Cause: You used {{ for HTML content β€” use {{{ for unescaped content, or make sure the variable is being passed correctly Fix: Check that res.locals.userName is set in the app.use middleware in server.js

Logging in gives no error but redirects back to login

Cause: The user's isVerified field is false β€” they haven't verified their OTP yet Fix: Check MongoDB β€” find the user and confirm isVerified: true. Or re-register.

Notes from another user are visible

Cause: You forgot userId: req.session.userId filter in your query Fix: All Note.find() calls must include { userId: req.session.userId }


πŸ” Security Notes (For Your Learning)

What we did Why it matters
bcrypt.hash(password, 10) Even if DB is stolen, passwords are unreadable
isVerified flag on User Prevents fake email registrations
OTP expires in 10 minutes Time-limited codes are safer
Session-based auth Server controls access, not the browser
userId filter on all note queries Users can never access each other's data
.env for secrets Credentials never in source code

πŸ“š What to Learn Next

Once you are comfortable with this project, explore:

  1. Email integration β€” use nodemailer to actually send OTP via email instead of showing it on screen
  2. Input validation β€” use express-validator to validate form fields on the server
  3. JWT tokens β€” an alternative to sessions (used in REST APIs)
  4. Rate limiting β€” use express-rate-limit to block brute force login attempts
  5. Pagination β€” show 10 notes per page when the list grows large
  6. Search β€” add a search bar that filters notes by name or description

Built with Node.js, Express, MongoDB, Handlebars, and Winston. This README was written as a teaching guide for beginner backend developers.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors