In today’s fast-paced world, effective note-taking is an indispensable tool for both personal and professional productivity. From capturing fleeting thoughts to managing detailed to-do lists, a well-designed notes app can significantly enhance how we organize information. In this guide, we will walk through the process of building a robust notes application using Express, MongoDB, and Passport, adopting the MVC (Model-View-Controller) architectural pattern to ensure a scalable and maintainable codebase.
Our goal is to create a backend-driven notes app that offers a range of features tailored to meet users’ needs:
- Secure User Authentication: Users will be able to register and log in securely, ensuring that their notes are accessible only to them.
- Comprehensive Note Management: The app will allow users to create, update, copy, and delete notes on the fly, with a simple and intuitive interface.
- Support for Todos: Users can add checkboxes to their notes, turning them into actionable to-do lists with the ability to mark items as complete or incomplete.
- Real-Time Subscriptions: When notes are created or deleted in different browser tabs, updates will be pushed from the server to the client in real time.
- Advanced Features: We’ll include search, filtering, and pagination functionalities to help users manage their notes efficiently.
- Robust Error Handling and Validation: Comprehensive error handling and data validation will ensure a smooth and reliable user experience.
Project Overview and Modular Approach
To build this application, we will break down the development process into several key modules. Each module focuses on a specific aspect of the application, ensuring that our approach is systematic and manageable. Here’s an overview of the modules we will cover:
- Setup and Installation: We’ll set up the development environment, install necessary dependencies, and connect our project to a GitHub repository for version control.
- Project Structure and Configuration: Organizing the project into a clean and scalable structure, setting up configuration files for database connections and environment variables.
- User Authentication Module: Implementing secure user authentication using Passport, allowing users to register and log in to their accounts.
- Note Management Module: Creating, reading, updating, and deleting notes with additional features like copying notes and a simple text mode for quick updates.
- Todo Management Module: Adding functionality for to-do lists within notes, enabling users to manage tasks with checkboxes.
- Real-Time Subscriptions: Implementing real-time updates using WebSocket or a similar protocol to keep the client synchronized with the server.
- Search, Filtering, and Pagination: Enhancing user experience with advanced search, filtering, and pagination options.
- Error Handling and Validation: Ensuring robust error handling and data validation to maintain application reliability.
- Documentation and Diagrams: Documenting the application’s API endpoints, usage, and creating diagrams for architecture and database schema.
- Deployment and Production Setup: Preparing the application for deployment, setting up production configurations, and deploying to a cloud provider.
Getting Started
To kick off this project, we’ll begin with the initial setup and installation. This will involve initializing a new Node.js project, installing dependencies, and setting up our development environment. By laying a solid foundation, we ensure that subsequent modules can be developed smoothly and efficiently.
Module 1: Setup and Installation
In this module, we’ll set up our development environment, install necessary dependencies, and initialize our project. We’ll also connect the project to a GitHub repository for version control.
Step 1: Initialize a New Node.js Project
Create a project directory:
mkdir notes-app
cd notes-app
Initialize a new Node.js project:
npm init -y
This will create a
package.json
file with default settings.
Step 2: Install Necessary Dependencies
We’ll need several packages to build our notes app. Install them using npm:
npm install express mongoose passport passport-local bcryptjs express-session connect-mongo dotenv
express
: A web framework for Node.js.mongoose
: An ODM (Object Data Modeling) library for MongoDB.passport
: Middleware for authentication.passport-local
: Strategy for Passport to authenticate using a username and password.bcryptjs
: Library to hash passwords.express-session
: Middleware for session management.connect-mongo
: MongoDB session store for Express.dotenv
: Module to load environment variables from a.env
file.
Step 3: Set Up GitHub Repository
- Create a new repository on GitHub.
- Initialize a Git repository in your project directory:
git init
Add the remote repository:
git remote add origin <your-github-repo-url>
Create a .gitignore
file:
node_modules
.env
Commit and push initial changes
git add .
git commit -m "Initial commit"
git push -u origin master
Step 4: Set Up Project Structure
Create project directories:
mkdir models views controllers routes config
Set up a basic Express server: Create a file named server.js
in the root directory with the following content:
const express = require('express');
const mongoose = require('mongoose');
const session = require('express-session');
const MongoStore = require('connect-mongo');
const passport = require('passport');
const dotenv = require('dotenv');
// Load environment variables from .env file
dotenv.config();
const app = express();
// Middleware for parsing JSON and urlencoded form data
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Initialize session
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
store: MongoStore.create({ mongoUrl: process.env.MONGO_URI })
}));
// Initialize Passport
app.use(passport.initialize());
app.use(passport.session());
// Connect to MongoDB
mongoose.connect(process.env.MONGO_URI, { useNewUrlParser: true, useUnifiedTopology: true })
.then(() => console.log('MongoDB connected'))
.catch(err => console.log(err));
// Start server
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
Create a .env
file in the root directory:
MONGO_URI=mongodb://localhost:27017/notes-app
SESSION_SECRET=yourSecretKey
Step 5: Document Project Setup
Create a README.md
file to document the setup and usage instructions:
# Notes App
## Project Setup
1. **Clone the repository:**
```bash
git clone <your-github-repo-url>
cd notes-app
Install dependencies
npm install
Set up environment variables: Create a .env
file in the root directory with the following content:
MONGO_URI=mongodb://localhost:27017/notes-app
SESSION_SECRET=yourSecretKey
Run the server:
npm start
Features
- User authentication and authorization
- CRUD operations for notes
- Todo management with checkboxes
- Real-time subscriptions
- Search, filtering, and pagination
- Robust error handling and validation
Module 2: User Authentication
In this module, we will implement user authentication using Passport. This includes setting up user registration and login functionalities, and securing routes. We will create models, routes, and controllers for handling user data and authentication logic.
Step 1: Create the User Model
First, let’s create the user model using Mongoose. This model will represent the user data in our MongoDB database.
Create a
user.js
file in themodels
directory:
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
// Define the schema for the user
const UserSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true // Ensures username is unique
},
email: {
type: String,
required: true,
unique: true // Ensures email is unique
},
password: {
type: String,
required: true
},
date: {
type: Date,
default: Date.now // Sets the default value to the current date
}
});
// Middleware to encrypt the password before saving the user document
UserSchema.pre('save', async function(next) {
if (!this.isModified('password')) {
return next();
}
const salt = await bcrypt.genSalt(10); // Generate a sa
Explanation:
- We define a
UserSchema
with fields forusername
,email
,password
, anddate
. - The
pre('save')
middleware hashes the password before saving the user document. matchPassword
method is used to compare the entered password with the stored hashed password.- Finally, we export the model so it can be used in other parts of the application.
- We define a
Step 2: Configure Passport
Create a
passport.js
file in theconfig
directory:
const LocalStrategy = require('passport-local').Strategy;
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
// Load User model
const User = require('../models/user');
module.exports = function(passport) {
// Define the strategy for local authentication
passport.use(
new LocalStrategy({ usernameField: 'email' }, async (email, password, done) => {
// Match user by email
try {
const user = await User.findOne({ email: email });
if (!user) {
return done(null, false, { message: 'That email is not registered' });
}
// Match password
const isMatch = await user.matchPassword(password);
if (!isMatch) {
return done(null, false, { message: 'Password incorrect' });
}
return done(null, user);
} catch (err) {
console.error(err);
return done(err);
}
})
);
// Serialize user to store user ID in the session
passport.serializeUser((user, done) => {
done(null, user.id);
});
// Deserialize user by finding user by ID in the session
passport.deserializeUser((id, done) => {
User.findById(id, (err, user) => {
done(err, user);
});
});
};
Explanation:
- We define a local strategy for Passport to authenticate users using their email and password.
passport.use
sets up the strategy to find the user by email and then compare the entered password with the stored hashed password.serializeUser
anddeserializeUser
methods handle storing and retrieving the user ID in the session.
Update
server.js
to include Passport configuration:
const express = require('express');
const mongoose = require('mongoose');
const session = require('express-session');
const MongoStore = require('connect-mongo');
const passport = require('passport');
const dotenv = require('dotenv');
// Load environment variables from .env file
dotenv.config();
const app = express();
// Passport configuration
require('./config/passport')(passport);
// Middleware for parsing JSON and urlencoded form data
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Initialize session
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
store: MongoStore.create({ mongoUrl: process.env.MONGO_URI }) // Store session in MongoDB
}));
// Initialize Passport
app.use(passport.initialize());
app.use(passport.session());
// Connect to MongoDB
mongoose.connect(process.env.MONGO_URI, { useNewUrlParser: true, useUnifiedTopology: true })
.then(() => console.log('MongoDB connected'))
.catch(err => console.log(err));
// Routes
app.use('/users', require('./routes/users'));
// Start server
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
Explanation:
- We load environment variables from a
.env
file. - We configure and initialize Express, Mongoose, session management, and Passport.
- We connect to MongoDB and set up a route for user-related operations.
- We load environment variables from a
Step 3: Create User Routes
Create a
users.js
file in theroutes
directory:
const express = require('express');
const router = express.Router();
const { registerUser, loginUser, logoutUser } = require('../controllers/users');
const { ensureAuthenticated } = require('../middleware/auth');
// Route to register a new user
router.post('/register', registerUser);
// Route to log in a user
router.post('/login', loginUser);
// Route to log out a user
router.get('/logout', ensureAuthenticated, logoutUser);
module.exports = router;
Explanation:
- We define routes for user registration, login, and logout.
- The
ensureAuthenticated
middleware ensures that only authenticated users can access the logout route.
Create a
users.js
file in thecontrollers
directory:
const User = require('../models/user');
const passport = require('passport');
// Controller to handle user registration
exports.registerUser = async (req, res) => {
const { username, email, password } = req.body;
try {
let user = await User.findOne({ email });
if (user) {
return res.status(400).json({ message: 'Email already exists' });
}
user = new User({
username,
email,
password
});
await user.save();
res.status(201).json({ message: 'User registered successfully' });
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Server error' });
}
};
// Controller to handle user login
exports.loginUser = (req, res, next) => {
passport.authenticate('local', (err, user, info) => {
if (err) {
return next(err);
}
if (!user) {
return res.status(400).json({ message: info.message });
}
req.logIn(user, err => {
if (err) {
return next(err);
}
res.status(200).json({ message: 'User logged in successfully' });
});
})(req, res, next);
};
// Controller to handle user logout
exports.logoutUser = (req, res) => {
req.logout(err => {
if (err) {
return res.status(500).json({ message: 'Server error' });
}
res.status(200).json({ message: 'User logged out successfully' });
});
};
Explanation:
registerUser
checks if the email already exists and, if not, creates a new user and saves it to the database.loginUser
uses Passport to authenticate the user. If successful, it logs the user in.logoutUser
logs the user out and ends the session.
Create an
auth.js
file in themiddleware
directory for route protection:
exports.ensureAuthenticated = (req, res, next) => {
if (req.isAuthenticated()) {
return next();
}
res.status(401).json({ message: 'Please log in to view this resource' });
};
Explanation:
ensureAuthenticated
is middleware that checks if the user is authenticated. If not, it returns a 401 Unauthorized status.
Module 3: Note Management Module
In this module, we will implement CRUD (Create, Read, Update, Delete) operations for managing notes. We will create a note model, set up routes, and implement controllers for handling these operations.
Step 1: Create the Note Model
Create a
note.js
file in themodels
directory:
const mongoose = require('mongoose');
// Define the schema for the note
const NoteSchema = new mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId, // Reference to the User model
ref: 'User',
required: true
},
title: {
type: String,
required: true
},
content: {
type: String,
required: true
},
todos: [
{
text: {
type: String,
required: true
},
completed: {
type: Boolean,
default: false
}
}
],
date: {
type: Date,
default: Date.now
}
});
// Export the Note model
module.exports = mongoose.model('Note', NoteSchema);
Explanation:
- We define a
NoteSchema
with fields foruser
,title
,content
,todos
, anddate
. - The
user
field references theUser
model to associate notes with a specific user. - The
todos
field is an array of objects, each containing atext
field and acompleted
boolean field. - Finally, we export the model so it can be used in other parts of the application.
- We define a
Step 2: Create Note Routes
Create a
notes.js
file in theroutes
directory:
const express = require('express');
const router = express.Router();
const { createNote, getNotes, getNoteById, updateNote, deleteNote } = require('../controllers/notes');
const { ensureAuthenticated } = require('../middleware/auth');
// Route to create a new note
router.post('/', ensureAuthenticated, createNote);
// Route to get all notes for the authenticated user
router.get('/', ensureAuthenticated, getNotes);
// Route to get a specific note by ID
router.get('/:id', ensureAuthenticated, getNoteById);
// Route to update a specific note by ID
router.put('/:id', ensureAuthenticated, updateNote);
// Route to delete a specific note by ID
router.delete('/:id', ensureAuthenticated, deleteNote);
module.exports = router;
Explanation:
- We define routes for creating, reading, updating, and deleting notes.
- The
ensureAuthenticated
middleware ensures that only authenticated users can access these routes.
Update
server.js
to include Note routes:
// ... other requires and configurations
// Routes
app.use('/users', require('./routes/users'));
app.use('/notes', require('./routes/notes'));
// Start server
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
Explanation:
- We add the
/notes
route to the Express application, pointing to the note routes we just created.
- We add the
Step 3: Create Note Controllers
Create a
notes.js
file in thecontrollers
directory:
const Note = require('../models/note');
// Controller to create a new note
exports.createNote = async (req, res) => {
const { title, content, todos } = req.body;
try {
const newNote = new Note({
user: req.user.id, // Associate note with the authenticated user
title,
content,
todos
});
const note = await newNote.save();
res.status(201).json(note);
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Server error' });
}
};
// Controller to get all notes for the authenticated user
exports.getNotes = async (req, res) => {
try {
const notes = await Note.find({ user: req.user.id });
res.status(200).json(notes);
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Server error' });
}
};
// Controller to get a specific note by ID
exports.getNoteById = async (req, res) => {
try {
const note = await Note.findById(req.params.id);
if (!note) {
return res.status(404).json({ message: 'Note not found' });
}
if (note.user.toString() !== req.user.id) {
return res.status(401).json({ message: 'Unauthorized' });
}
res.status(200).json(note);
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Server error' });
}
};
// Controller to update a specific note by ID
exports.updateNote = async (req, res) => {
const { title, content, todos } = req.body;
try {
let note = await Note.findById(req.params.id);
if (!note) {
return res.status(404).json({ message: 'Note not found' });
}
if (note.user.toString() !== req.user.id) {
return res.status(401).json({ message: 'Unauthorized' });
}
note = await Note.findByIdAndUpdate(
req.params.id,
{ title, content, todos },
{ new: true, runValidators: true }
);
res.status(200).json(note);
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Server error' });
}
};
// Controller to delete a specific note by ID
exports.deleteNote = async (req, res) => {
try {
const note = await Note.findById(req.params.id);
if (!note) {
return res.status(404).json({ message: 'Note not found' });
}
if (note.user.toString() !== req.user.id) {
return res.status(401).json({ message: 'Unauthorized' });
}
await note.remove();
res.status(200).json({ message: 'Note deleted successfully' });
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Server error' });
}
};
Explanation:
createNote
: Creates a new note associated with the authenticated user and saves it to the database.getNotes
: Retrieves all notes for the authenticated user.getNoteById
: Retrieves a specific note by its ID, ensuring the note belongs to the authenticated user.updateNote
: Updates a specific note by its ID, ensuring the note belongs to the authenticated user.deleteNote
: Deletes a specific note by its ID, ensuring the note belongs to the authenticated user.
Module 4: Todo Management
In this module, we will extend the note functionality to include to-do lists with checkboxes. This involves updating the note model, routes, and controllers to handle todos within notes.
Step 1: Update the Note Model
Update the
note.js
file in themodels
directory to include todos:
const mongoose = require('mongoose');
// Define the schema for the note
const NoteSchema = new mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId, // Reference to the User model
ref: 'User',
required: true
},
title: {
type: String,
required: true
},
content: {
type: String,
required: true
},
// Add a todos field that is an array of objects
todos: [
{
text: {
type: String,
required: true
},
completed: {
type: Boolean,
default: false
}
}
],
date: {
type: Date,
default: Date.now
}
});
// Export the Note model
module.exports = mongoose.model('Note', NoteSchema);
Explanation:
- The
todos
field is added as an array of objects, each containing atext
field for the todo item and acompleted
boolean field to track its status.
- The
Step 2: Update Note Routes
Update the
notes.js
file in theroutes
directory to handle todos:
const express = require('express');
const router = express.Router();
const { createNote, getNotes, getNoteById, updateNote, deleteNote, addTodo, updateTodo, deleteTodo } = require('../controllers/notes');
const { ensureAuthenticated } = require('../middleware/auth');
// Route to create a new note
router.post('/', ensureAuthenticated, createNote);
// Route to get all notes for the authenticated user
router.get('/', ensureAuthenticated, getNotes);
// Route to get a specific note by ID
router.get('/:id', ensureAuthenticated, getNoteById);
// Route to update a specific note by ID
router.put('/:id', ensureAuthenticated, updateNote);
// Route to delete a specific note by ID
router.delete('/:id', ensureAuthenticated, deleteNote);
// Route to add a todo to a specific note
router.post('/:id/todos', ensureAuthenticated, addTodo);
// Route to update a todo in a specific note
router.put('/:id/todos/:todoId', ensureAuthenticated, updateTodo);
// Route to delete a todo from a specific note
router.delete('/:id/todos/:todoId', ensureAuthenticated, deleteTodo);
module.exports = router;
Explanation:
- We add routes for adding, updating, and deleting todos within a specific note. These routes are nested under the
/notes/:id/todos
path.
- We add routes for adding, updating, and deleting todos within a specific note. These routes are nested under the
Step 3: Update Note Controllers
Update the
notes.js
file in thecontrollers
directory to handle todos:
const Note = require('../models/note');
// Controller to create a new note
exports.createNote = async (req, res) => {
const { title, content, todos } = req.body;
try {
const newNote = new Note({
user: req.user.id, // Associate note with the authenticated user
title,
content,
todos
});
const note = await newNote.save();
res.status(201).json(note);
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Server error' });
}
};
// Controller to get all notes for the authenticated user
exports.getNotes = async (req, res) => {
try {
const notes = await Note.find({ user: req.user.id });
res.status(200).json(notes);
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Server error' });
}
};
// Controller to get a specific note by ID
exports.getNoteById = async (req, res) => {
try {
const note = await Note.findById(req.params.id);
if (!note) {
return res.status(404).json({ message: 'Note not found' });
}
if (note.user.toString() !== req.user.id) {
return res.status(401).json({ message: 'Unauthorized' });
}
res.status(200).json(note);
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Server error' });
}
};
// Controller to update a specific note by ID
exports.updateNote = async (req, res) => {
const { title, content, todos } = req.body;
try {
let note = await Note.findById(req.params.id);
if (!note) {
return res.status(404).json({ message: 'Note not found' });
}
if (note.user.toString() !== req.user.id) {
return res.status(401).json({ message: 'Unauthorized' });
}
note = await Note.findByIdAndUpdate(
req.params.id,
{ title, content, todos },
{ new: true, runValidators: true }
);
res.status(200).json(note);
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Server error' });
}
};
// Controller to delete a specific note by ID
exports.deleteNote = async (req, res) => {
try {
const note = await Note.findById(req.params.id);
if (!note) {
return res.status(404).json({ message: 'Note not found' });
}
if (note.user.toString() !== req.user.id) {
return res.status(401).json({ message: 'Unauthorized' });
}
await note.remove();
res.status(200).json({ message: 'Note deleted successfully' });
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Server error' });
}
};
// Controller to add a todo to a specific note
exports.addTodo = async (req, res) => {
const { text } = req.body;
try {
const note = await Note.findById(req.params.id);
if (!note) {
return res.status(404).json({ message: 'Note not found' });
}
if (note.user.toString() !== req.user.id) {
return res.status(401).json({ message: 'Unauthorized' });
}
note.todos.push({ text });
await note.save();
res.status(200).json(note);
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Server error' });
}
};
// Controller to update a todo in a specific note
exports.updateTodo = async (req, res) => {
const { text, completed } = req.body;
try {
const note = await Note.findById(req.params.id);
if (!note) {
return res.status(404).json({ message: 'Note not found' });
}
if (note.user.toString() !== req.user.id) {
return res.status(401).json({ message: 'Unauthorized' });
}
const todo = note.todos.id(req.params.todoId);
if (!todo) {
return res.status(404).json({ message: 'Todo not found' });
}
if (text !== undefined) todo.text = text;
if (completed !== undefined) todo.completed = completed;
await note.save();
res.status(200).json(note);
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Server error' });
}
};
// Controller to delete a todo from a specific note
exports.deleteTodo = async (req, res) => {
try {
const note = await Note.findById(req.params.id);
if (!note) {
return res.status(404).json({ message: 'Note not found' });
}
if (note.user.toString() !== req.user.id) {
return res.status(401).json({ message: 'Unauthorized' });
}
const todo = note.todos.id(req.params.todoId);
if (!todo) {
return res.status(404).json({ message: 'Todo not found' });
}
todo.remove();
await note.save();
res.status(200).json(note);
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Server error' });
}
};
Explanation:
addTodo
: Adds a new todo to a specific note.updateTodo
: Updates a specific todo within a note.deleteTodo
: Deletes a specific todo from a note.
Module 5: Real-Time Subscriptions Module
In this module, we will implement real-time updates for notes and todos using WebSocket. This will allow changes made in one browser tab to be reflected in other tabs in real-time.
Step 1: Set Up WebSocket
Install the
ws
package:
npm install ws
Update server.js
to include WebSocket setup:
const express = require('express');
const mongoose = require('mongoose');
const session = require('express-session');
const MongoStore = require('connect-mongo');
const passport = require('passport');
const dotenv = require('dotenv');
const http = require('http');
const WebSocket = require('ws');
// Load environment variables from .env file
dotenv.config();
const app = express();
// Passport configuration
require('./config/passport')(passport);
// Middleware for parsing JSON and urlencoded form data
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Initialize session
const sessionMiddleware = session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
store: MongoStore.create({ mongoUrl: process.env.MONGO_URI })
});
app.use(sessionMiddleware);
// Initialize Passport
app.use(passport.initialize());
app.use(passport.session());
// Connect to MongoDB
mongoose.connect(process.env.MONGO_URI, { useNewUrlParser: true, useUnifiedTopology: true })
.then(() => console.log('MongoDB connected'))
.catch(err => console.log(err));
// Routes
app.use('/users', require('./routes/users'));
app.use('/notes', require('./routes/notes'));
// Create HTTP server and WebSocket server
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });
// Middleware to handle WebSocket connections
wss.on('connection', (ws) => {
console.log('New WebSocket connection');
ws.on('message', (message) => {
const data = JSON.parse(message);
// Broadcast the message to all connected clients
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(data));
}
});
});
ws.on('close', () => {
console.log('WebSocket connection closed');
});
});
// Start server
const PORT = process.env.PORT || 5000;
server.listen(PORT, () => console.log(`Server running on port ${PORT}`));
Explanation:
- We set up an HTTP server and a WebSocket server using the
ws
package. - When a WebSocket connection is established, we listen for messages and broadcast them to all connected clients.
- This allows changes to be reflected in real-time across all connected clients.
- We set up an HTTP server and a WebSocket server using the
Step 2: Update Note Controllers to Broadcast Changes
Update the
notes.js
file in thecontrollers
directory to broadcast changes:
const Note = require('../models/note');
const WebSocket = require('ws');
// Broadcast function to send updates to all WebSocket clients
const broadcast = (wss, data) => {
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(data));
}
});
};
// Controller to create a new note
exports.createNote = (wss) => async (req, res) => {
const { title, content, todos } = req.body;
try {
const newNote = new Note({
user: req.user.id, // Associate note with the authenticated user
title,
content,
todos
});
const note = await newNote.save();
// Broadcast the new note
broadcast(wss, { action: 'create', note });
res.status(201).json(note);
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Server error' });
}
};
// Controller to get all notes for the authenticated user
exports.getNotes = async (req, res) => {
try {
const notes = await Note.find({ user: req.user.id });
res.status(200).json(notes);
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Server error' });
}
};
// Controller to get a specific note by ID
exports.getNoteById = async (req, res) => {
try {
const note = await Note.findById(req.params.id);
if (!note) {
return res.status(404).json({ message: 'Note not found' });
}
if (note.user.toString() !== req.user.id) {
return res.status(401).json({ message: 'Unauthorized' });
}
res.status(200).json(note);
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Server error' });
}
};
// Controller to update a specific note by ID
exports.updateNote = (wss) => async (req, res) => {
const { title, content, todos } = req.body;
try {
let note = await Note.findById(req.params.id);
if (!note) {
return res.status(404).json({ message: 'Note not found' });
}
if (note.user.toString() !== req.user.id) {
return res.status(401).json({ message: 'Unauthorized' });
}
note = await Note.findByIdAndUpdate(
req.params.id,
{ title, content, todos },
{ new: true, runValidators: true }
);
// Broadcast the updated note
broadcast(wss, { action: 'update', note });
res.status(200).json(note);
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Server error' });
}
};
// Controller to delete a specific note by ID
exports.deleteNote = (wss) => async (req, res) => {
try {
const note = await Note.findById(req.params.id);
if (!note) {
return res.status(404).json({ message: 'Note not found' });
}
if (note.user.toString() !== req.user.id) {
return res.status(401).json({ message: 'Unauthorized' });
}
await note.remove();
// Broadcast the deleted note
broadcast(wss, { action: 'delete', noteId: req.params.id });
res.status(200).json({ message: 'Note deleted successfully' });
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Server error' });
}
};
// Controller to add a todo to a specific note
exports.addTodo = (wss) => async (req, res) => {
const { text } = req.body;
try {
const note = await Note.findById(req.params.id);
if (!note) {
return res.status(404).json({ message: 'Note not found' });
}
if (note.user.toString() !== req.user.id) {
return res.status(401).json({ message: 'Unauthorized' });
}
note.todos.push({ text });
await note.save();
// Broadcast the updated note
broadcast(wss, { action: 'update', note });
res.status(200).json(note);
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Server error' });
}
};
// Controller to update a todo in a specific note
exports.updateTodo = (wss) => async (req, res) => {
const { text, completed } = req.body;
try {
const note = await Note.findById(req.params.id);
if (!note) {
return res.status(404).json({ message: 'Note not found' });
}
if (note.user.toString() !== req.user.id) {
return res.status(401).json({ message: 'Unauthorized' });
}
const todo = note.todos.id(req.params.todoId);
if (!todo) {
return res.status(404).json({ message: 'Todo not found' });
}
if (text !== undefined) todo.text = text;
if (completed !== undefined) todo.completed = completed;
await note.save();
// Broadcast the updated note
broadcast(wss, { action: 'update', note });
res.status(200).json(note);
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Server error' });
}
};
// Controller to delete a todo from a specific note
exports.deleteTodo = (wss) => async (req, res) => {
try {
const note = await Note.findById(req.params.id);
if (!note) {
return res.status(404).json({ message: 'Note not found' });
}
if (note.user.toString() !== req.user.id) {
return res.status(401).json({ message: 'Unauthorized' });
}
const todo = note.todos.id(req.params.todoId);
if (!todo) {
return res.status(404).json({ message: 'Todo not found' });
}
todo.remove();
await note.save();
// Broadcast the updated note
broadcast(wss, { action: 'update', note });
res.status(200).json(note);
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Server error' });
}
};
Explanation:
- We update each controller to broadcast the changes to all WebSocket clients.
- The
broadcast
function sends updates to all connected WebSocket clients, ensuring real-time synchronization.
Pass WebSocket server instance to the controllers:
Update
server.js
to pass the WebSocket server instance (wss
) to the controllers:
// Update route definitions to pass wss to the controllers
const noteRoutes = require('./routes/notes');
app.use('/notes', (req, res, next) => {
req.wss = wss;
next();
}, noteRoutes);
Explanation:
- We update the middleware to pass the WebSocket server instance (
wss
) to the routes, which then pass it to the controllers.
- We update the middleware to pass the WebSocket server instance (
Update Note Routes to Accept WebSocket Server Instance:
Update
notes.js
file in theroutes
directory:
const express = require('express');
const router = express.Router();
const { createNote, getNotes, getNoteById, updateNote, deleteNote, addTodo, updateTodo, deleteTodo } = require('../controllers/notes');
const { ensureAuthenticated } = require('../middleware/auth');
// Route to create a new note
router.post('/', ensureAuthenticated, (req, res) => createNote(req.wss)(req, res));
// Route to get all notes for the authenticated user
router.get('/', ensureAuthenticated, getNotes);
// Route to get a specific note by ID
router.get('/:id', ensureAuthenticated, getNoteById);
// Route to update a specific note by ID
router.put('/:id', ensureAuthenticated, (req, res) => updateNote(req.wss)(req, res));
// Route to delete a specific note by ID
router.delete('/:id', ensureAuthenticated, (req, res) => deleteNote(req.wss)(req, res));
// Route to add a todo to a specific note
router.post('/:id/todos', ensureAuthenticated, (req, res) => addTodo(req.wss)(req, res));
// Route to update a todo in a specific note
router.put('/:id/todos/:todoId', ensureAuthenticated, (req, res) => updateTodo(req.wss)(req, res));
// Route to delete a todo from a specific note
router.delete('/:id/todos/:todoId', ensureAuthenticated, (req, res) => deleteTodo(req.wss)(req, res));
module.exports = router;
Explanation:
- We update the routes to pass the WebSocket server instance (
wss
) to the controllers. This ensures that the controllers can broadcast changes to all connected clients.
Module 6: Search, Filtering, and Pagination Amazing
In this module, we will implement search, filtering, and pagination functionalities for notes. These features will enhance the user experience by allowing users to efficiently manage and find their notes.
Step 1: Update Note Routes
Update the
notes.js
file in theroutes
directory:
const express = require('express');
const router = express.Router();
const { ensureAuthenticated } = require('../middleware/auth');
const {
createNote,
getNotes,
getNoteById,
updateNote,
deleteNote,
addTodo,
updateTodo,
deleteTodo,
searchNotes
} = require('../controllers/notes');
// Route to create a new note
router.post('/', ensureAuthenticated, createNote);
// Route to get all notes for the authenticated user
router.get('/', ensureAuthenticated, getNotes);
// Route to search notes
router.get('/search', ensureAuthenticated, searchNotes);
// Route to get a specific note by ID
router.get('/:id', ensureAuthenticated, getNoteById);
// Route to update a specific note by ID
router.put('/:id', ensureAuthenticated, updateNote);
// Route to delete a specific note by ID
router.delete('/:id', ensureAuthenticated, deleteNote);
// Route to add a todo to a specific note
router.post('/:id/todos', ensureAuthenticated, addTodo);
// Route to update a todo in a specific note
router.put('/:id/todos/:todoId', ensureAuthenticated, updateTodo);
// Route to delete a todo from a specific note
router.delete('/:id/todos/:todoId', ensureAuthenticated, deleteTodo);
module.exports = router;
Explanation:
- We add a route for searching notes, which is a GET request to
/notes/search
.
- We add a route for searching notes, which is a GET request to
Step 2: Update Note Controllers
Update the
notes.js
file in thecontrollers
directory to include search, filtering, and pagination:
const Note = require('../models/note');
const WebSocket = require('ws');
// Broadcast function to send updates to all WebSocket clients
const broadcast = (wss, data) => {
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(data));
}
});
};
// Controller to create a new note
exports.createNote = async (req, res) => {
const { title, content, todos } = req.body;
try {
const newNote = new Note({
user: req.user.id, // Associate note with the authenticated user
title,
content,
todos
});
const note = await newNote.save();
// Broadcast the new note
broadcast(req.wss, { action: 'create', note });
res.status(201).json(note);
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Server error' });
}
};
// Controller to get all notes for the authenticated user
exports.getNotes = async (req, res) => {
const { page = 1, limit = 10 } = req.query;
try {
const notes = await Note.find({ user: req.user.id })
.limit(limit * 1)
.skip((page - 1) * limit)
.exec();
const count = await Note.countDocuments({ user: req.user.id });
res.status(200).json({
notes,
totalPages: Math.ceil(count / limit),
currentPage: page
});
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Server error' });
}
};
// Controller to search notes for the authenticated user
exports.searchNotes = async (req, res) => {
const { query } = req.query;
try {
const notes = await Note.find({
user: req.user.id,
$or: [
{ title: { $regex: query, $options: 'i' } },
{ content: { $regex: query, $options: 'i' } }
]
});
res.status(200).json(notes);
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Server error' });
}
};
// Controller to get a specific note by ID
exports.getNoteById = async (req, res) => {
try {
const note = await Note.findById(req.params.id);
if (!note) {
return res.status(404).json({ message: 'Note not found' });
}
if (note.user.toString() !== req.user.id) {
return res.status(401).json({ message: 'Unauthorized' });
}
res.status(200).json(note);
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Server error' });
}
};
// Controller to update a specific note by ID
exports.updateNote = async (req, res) => {
const { title, content, todos } = req.body;
try {
let note = await Note.findById(req.params.id);
if (!note) {
return res.status(404).json({ message: 'Note not found' });
}
if (note.user.toString() !== req.user.id) {
return res.status(401).json({ message: 'Unauthorized' });
}
note = await Note.findByIdAndUpdate(
req.params.id,
{ title, content, todos },
{ new: true, runValidators: true }
);
// Broadcast the updated note
broadcast(req.wss, { action: 'update', note });
res.status(200).json(note);
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Server error' });
}
};
// Controller to delete a specific note by ID
exports.deleteNote = async (req, res) => {
try {
const note = await Note.findById(req.params.id);
if (!note) {
return res.status(404).json({ message: 'Note not found' });
}
if (note.user.toString() !== req.user.id) {
return res.status(401).json({ message: 'Unauthorized' });
}
await note.remove();
// Broadcast the deleted note
broadcast(req.wss, { action: 'delete', noteId: req.params.id });
res.status(200).json({ message: 'Note deleted successfully' });
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Server error' });
}
};
// Controller to add a todo to a specific note
exports.addTodo = async (req, res) => {
const { text } = req.body;
try {
const note = await Note.findById(req.params.id);
if (!note) {
return res.status(404).json({ message: 'Note not found' });
}
if (note.user.toString() !== req.user.id) {
return res.status(401).json({ message: 'Unauthorized' });
}
note.todos.push({ text });
await note.save();
// Broadcast the updated note
broadcast(req.wss, { action: 'update', note });
res.status(200).json(note);
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Server error' });
}
};
// Controller to update a todo in a specific note
exports.updateTodo = async (req, res) => {
const { text, completed } = req.body;
try {
const note = await Note.findById(req.params.id);
if (!note) {
return res.status(404).json({ message: 'Note not found' });
}
if (note.user.toString() !== req.user.id) {
return res.status(401).json({ message: 'Unauthorized' });
}
const todo = note.todos.id(req.params.todoId);
if (!todo) {
return res.status(404).json({ message: 'Todo not found' });
}
if (text !== undefined) todo.text = text;
if (completed !== undefined) todo.completed = completed;
await note.save();
// Broadcast the updated note
broadcast(req.wss, { action: 'update', note });
res.status(200).json(note);
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Server error' });
}
};
// Controller to delete a todo from a specific note
exports.deleteTodo = async (req, res) => {
try {
const note = await Note.findById(req.params.id);
if (!note) {
return res.status(404).json({ message: 'Note not found' });
}
if (note.user.toString() !== req.user.id) {
return res.status(401).json({ message: 'Unauthorized' });
}
const todo = note.todos.id(req.params.todoId);
if (!todo) {
return res.status(404).json({ message: 'Todo not found' });
}
todo.remove();
await note.save();
// Broadcast the updated note
broadcast(req.wss, { action: 'update', note });
res.status(200).json(note);
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Server error' });
}
};
Explanation:
getNotes
: Retrieves notes for the authenticated user with pagination. Thepage
andlimit
query parameters are used to control pagination.searchNotes
: Searches notes based on thequery
parameter, matching the title or content fields using a case-insensitive regular expression.
Module 7: Error Handling and Validation
In this module, we will implement robust error handling and validation to ensure our application is reliable and secure. We will create middleware for handling errors and validating user input.
Step 1: Create Error Handling Middleware
Create a
errorHandler.js
file in themiddleware
directory:
// Custom error handling middleware
const errorHandler = (err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ message: err.message });
};
module.exports = errorHandler;
Explanation:
- This middleware catches any errors that occur in the application and sends a response with a 500 status code and the error message.
- The
console.error(err.stack)
logs the error stack trace to the console for debugging purposes.
Update
server.js
to use the error handling middleware:
// ... other requires and configurations
const errorHandler = require('./middleware/errorHandler');
// ... routes
// Use the error handling middleware
app.use(errorHandler);
// Start server
const PORT = process.env.PORT || 5000;
server.listen(PORT, () => console.log(`Server running on port ${PORT}`));
Explanation:
- We add the error handling middleware at the end of the middleware stack to catch any errors that occur during request processing.
Step 2: Create Validation Middleware
Install the
express-validator
package:
npm install express-validator
Create a validators
directory with validation files:
Create userValidator.js
in the validators
directory:
const { check, validationResult } = require('express-validator');
// Validation rules for user registration and login
const validateUser = [
check('username')
.notEmpty()
.withMessage('Username is required')
.isLength({ min: 3 })
.withMessage('Username must be at least 3 characters long'),
check('email')
.isEmail()
.withMessage('Please enter a valid email address'),
check('password')
.isLength({ min: 6 })
.withMessage('Password must be at least 6 characters long')
];
// Middleware to check validation results
const userValidationHandler = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
};
module.exports = {
validateUser,
userValidationHandler
};
Explanation:
validateUser
: Defines validation rules forusername
,email
, andpassword
.userValidationHandler
: Checks validation results and sends a 400 response with validation errors if any are found.
Create noteValidator.js
in the validators
directory:
const { check, validationResult } = require('express-validator');
// Validation rules for creating and updating notes
const validateNote = [
check('title')
.notEmpty()
.withMessage('Title is required')
.isLength({ min: 3 })
.withMessage('Title must be at least 3 characters long'),
check('content')
.notEmpty()
.withMessage('Content is required')
];
// Middleware to check validation results
const noteValidationHandler = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
};
module.exports = {
validateNote,
noteValidationHandler
};
Explanation:
validateNote
: Defines validation rules fortitle
andcontent
.noteValidationHandler
: Checks validation results and sends a 400 response with validation errors if any are found.
Step 3: Apply Validation Middleware to Routes
Update
users.js
file in theroutes
directory to include validation:
const express = require('express');
const router = express.Router();
const { registerUser, loginUser, logoutUser } = require('../controllers/users');
const { ensureAuthenticated } = require('../middleware/auth');
const { validateUser, userValidationHandler } = require('../validators/userValidator');
// Register route with validation
router.post('/register', validateUser, userValidationHandler, registerUser);
// Login route
router.post('/login', validateUser, userValidationHandler, loginUser);
// Logout route
router.get('/logout', ensureAuthenticated, logoutUser);
module.exports = router;
Explanation:
- We apply the
validateUser
anduserValidationHandler
middleware to theregister
andlogin
routes to validate user input.
- We apply the
Update
notes.js
file in theroutes
directory to include validation:
const express = require('express');
const router = express.Router();
const {
createNote,
getNotes,
getNoteById,
updateNote,
deleteNote,
addTodo,
updateTodo,
deleteTodo,
searchNotes
} = require('../controllers/notes');
const { ensureAuthenticated } = require('../middleware/auth');
const { validateNote, noteValidationHandler } = require('../validators/noteValidator');
// Route to create a new note with validation
router.post('/', ensureAuthenticated, validateNote, noteValidationHandler, createNote);
// Route to get all notes for the authenticated user
router.get('/', ensureAuthenticated, getNotes);
// Route to search notes
router.get('/search', ensureAuthenticated, searchNotes);
// Route to get a specific note by ID
router.get('/:id', ensureAuthenticated, getNoteById);
// Route to update a specific note by ID with validation
router.put('/:id', ensureAuthenticated, validateNote, noteValidationHandler, updateNote);
// Route to delete a specific note by ID
router.delete('/:id', ensureAuthenticated, deleteNote);
// Route to add a todo to a specific note
router.post('/:id/todos', ensureAuthenticated, addTodo);
// Route to update a todo in a specific note
router.put('/:id/todos/:todoId', ensureAuthenticated, updateTodo);
// Route to delete a todo from a specific note
router.delete('/:id/todos/:todoId', ensureAuthenticated, deleteTodo);
module.exports = router;
Explanation:
- We apply the
validateNote
andnoteValidationHandler
middleware to thecreate
andupdate
note routes to validate note input.
Module 8: Documentation
In this module, we will document the application and create necessary diagrams to help understand the application architecture and API endpoints. Proper documentation ensures that the code is maintainable and comprehensible for future developers and users.
Step 1: Create a README.md File
Create a
README.md
file in the root directory:
# Notes App
## Introduction
This is a feature-rich notes application built with Express, MongoDB, and Passport. It follows the MVC (Model-View-Controller) architectural pattern and includes functionalities like user authentication, note management, todos with checkboxes, real-time updates, search, filtering, pagination, error handling, and validation.
## Features
- User Authentication: Register and login securely.
- Note Management: Create, read, update, delete notes.
- Todos: Add, update, and delete todos within notes.
- Real-Time Updates: Reflect changes across browser tabs in real-time.
- Search and Filtering: Search and filter notes efficiently.
- Pagination: Paginate notes for better management.
- Error Handling: Robust error handling and validation.
## Installation
1. **Clone the repository:**
```bash
git clone <your-github-repo-url>
cd notes-app
2. **Install dependencies:**
```bash
Copy code
npm install
3. **Set up environment variables:**
Create a .env file in the root directory with the following content:
MONGO_URI=mongodb://localhost:27017/notes-app
SESSION_SECRET=yourSecretKey
4. Run the server:
npm start
API Endpoints
User Routes
POST /users/register: Register a new user.
Request Body: { "username": "string", "email": "string", "password": "string" }
POST /users/login: Login a user.
Request Body: { "email": "string", "password": "string" }
GET /users/logout: Logout the current user.
Note Routes
POST /notes: Create a new note.
Request Body: { "title": "string", "content": "string", "todos": [{ "text": "string" }] }
GET /notes: Get all notes for the authenticated user.
GET /notes/search: Search notes.
Query Params: ?query=searchTerm
GET /notes/:id: Get a specific note by ID.
PUT /notes/:id: Update a specific note by ID.
Request Body: { "title": "string", "content": "string", "todos": [{ "text": "string", "completed": "boolean" }] }
DELETE /notes/:id: Delete a specific note by ID.
Todo Routes
POST /notes/:id/todos: Add a todo to a specific note.
Request Body: { "text": "string" }
PUT /notes/:id/todos/:todoId: Update a todo in a specific note.
Request Body: { "text": "string", "completed": "boolean" }
DELETE /notes/:id/todos/:todoId: Delete a todo from a specific note
Module 9: Deployment and Production Setup
In this final module, we will prepare the application for deployment. This involves setting up production configurations and deploying the application to a cloud provider, such as Heroku.
Step 1: Prepare for Deployment
Set Up Production Environment Variables:
Ensure your
.env
file contains production values or configure environment variables directly in your deployment environment.
MONGO_URI=your_production_mongo_uri
SESSION_SECRET=yourProductionSecretKey
Update package.json
:
Add a start script for production in your package.json
file.
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
}
Set Up Node.js Version:
Specify the Node.js version in your package.json
to ensure consistency in different environments.
"engines": {
"node": "14.x"
}
Update CORS Settings:
If you have a frontend hosted on a different domain, make sure to set up CORS (Cross-Origin Resource Sharing) properly. You can use the cors
package to allow requests from your frontend domain.
npm install cors
const cors = require('cors');
const app = express();
app.use(cors({
origin: 'your-frontend-domain', // e.g., http://example.com
credentials: true
}));
heroku login
Create a New Heroku Application:
Create a new application on Heroku.
heroku create your-app-name
Set Environment Variables on Heroku:
Set your environment variables on Heroku.
heroku config:set MONGO_URI=your_production_mongo_uri
heroku config:set SESSION_SECRET=yourProductionSecretKey
Deploy Your Application:
Initialize a git repository if you haven’t already, add your files, commit, and push to Heroku.
git init
git add .
git commit -m "Initial commit"
git push heroku master
Scale the Dynos:
Ensure your app has at least one web dyno running.
heroku ps:scale web=1
Open Your Application:
Open your deployed application in the browser.
heroku open
Step 3: Set Up Logging and Monitoring
Enable Heroku Logs:
View logs to monitor your application.
heroku logs --tail
Set Up Monitoring Tools:
Use Heroku add-ons for monitoring, such as Papertrail for logging and New Relic for performance monitoring.
heroku addons:create papertrail
heroku addons:create newrelic
Configure Alerting:
Set up alerts for any issues such as downtime or high error rates.
In this module, we prepared the application for deployment and deployed it to Heroku. We set up production environment variables, configured the application for production, and used the Heroku CLI to manage the deployment. Additionally, we set up logging and monitoring to ensure the application runs smoothly in production.
With the application now deployed, it is ready for users to register, create notes, add todos, and enjoy real-time updates and other features. This concludes the development and deployment of our feature-rich notes application.
Welcome to DevTechTutor.com, your ultimate resource for mastering web development and technology! Whether you're a beginner eager to dive into coding or an experienced developer looking to sharpen your skills, DevTechTutor.com is here to guide you every step of the way. Our mission is to make learning web development accessible, engaging, and effective.