How to implement notification functionality in MERN like Facebook and Twitter (part 1 : back-end)

Adel Benyahia
7 min readNov 17, 2022

In this tutorial we will implement notification functionality using , mongodb, express, reactjs and node (MERN),

No third party services used.

What is MERN?

MERN is one of several variations of the MEAN stack (MongoDB Express Angular Node), where the traditional Angular.js front-end framework is replaced with React.js. Other variants include MEVN (MongoDB, Express, Vue, Node), and really any front-end JavaScript framework can work.

MERN stands for MongoDB, Express, React, Node, after the four key technologies that make up the stack.

. MongoDB — document database

. Express(.js) — Node.js web framework

. React(.js) — a client-side JavaScript framework

. Node(.js) — the premier JavaScript web server

The MERN architecture allows you to easily construct a three-tier architecture (front end, back end, database) entirely using JavaScript and JSON.

source: https://www.mongodb.com/mern-stack

1. The Back-end:

. Create directory “backend”

mkdir backend
cd backend

. initialize create the ‘/backend/package.json’ file

npm init -y

. Install backend dependencies

npm install express mongoose cors

Mongoose provides a straight-forward, schema-based solution to model your application data. It includes built-in type casting, validation, query building, business logic hooks and more, out of the box.

Express is a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications.

CORS is a node.js package for providing a Connect/Express middleware that can be used to enable CORS with various options.

we will first add model schema for our mongodb like this

. in the “/backend/models” folder create a file “user.js”

const mongoose = require('mongoose')

const userSchema = new mongoose.Schema({
username: { type: String, required: true },
password: { type: String, required: true },
email: { type: String, required: true },
roles: [{ type: String, default: 'Employee' }],
active: { type: Boolean, default: true },
profileImage: {
type: String,
},
})
module.exports = mongoose.model('User', userSchema)

. in the “/backend/models” folder create a file “notification.js”

const mongoose = require('mongoose')

const notificationSchema = new mongoose.Schema(
{
user: { type: mongoose.Schema.Types.ObjectId, require: true },
title: { type: String, require: true },
type: { type: Number, required: true },
text: { type: String, require: true },
read: { type: Boolean, default: false },
},
{
timestamps: true,
}
)
module.exports = mongoose.model('notification', notificationSchema)

. in the “/backend/config” folder create a file “dbConn.js”

this file contain the connection function to our mongo database

change the URL value to your mongodb connection url

const connectDB = async () => {
const URL = '__your mongodb connection url__'
try {
mongoose.connect(URL, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
} catch (err) {
console.log(err)
}
}
module.exports = connectDB

. in the “/backend” folder create a file “socketio.js”

const user = require('./models/user');
const notification = require('./models/notification');
let usersio = [];

module.exports = function (io) {
io.on('connection', (socket) => {
socket.on('setUserId', async (userId) => {
if (userId) {
const oneUser = await user.findById(userId).lean().exec();
if (oneUser) {
usersio[userId] = socket;
console.log(`⚡ Socket: User with id ${userId} connected`);
} else {
console.log(`🚩 Socket: No user with id ${userId}`);
}
}
});
socket.on('getNotificationsLength', async (userId) => {
const notifications = await notification
.find({ user: userId, read: false })
.lean();
usersio[userId]?.emit('notificationsLength', notifications.length || 0);
});

socket.on('disconnect', (userId) => {
console.log(`🔥 user with id ${userId} disconnected from socket`);
usersio[userId] = null;
});
});
};

I like these Unicode icons ⚡ 🔥 🚩 that you can use with your console.lo

In this file we will intercept any socketio and add it to userId array linked to userId.

in we are connected:
setUserId : for adding a new user to the array

getNotificationsLength: to send the thmber of notifications (filtrer by the userid)

if we diconnect, we will set the user socket to null using 'userId'
usersio[userId] = null;

. in the “/backend” folder create a file “server.js”

const express = require('express')
const cors = require('cors')
const mongoose = require('mongoose')
const connectDB = require('./config/dbConn')

const app = express()
const port = 3500
connectDB()
app.use(cors)
app.use(express.json())

// Socketio must be declared before api routes
const server = require('http').createServer(app)
const io = require('socket.io')(server, {
transports: ['polling'],
cors: { origin: allowedOrigins },
})
require('./socketio.js')(io)

// API routes
app.use('/users', require('./routes/userRoutes'))
app.use('/auth', require('./routes/authRoutes'))
app.use('/notifications', require('./routes/notificationRoutes'))
app.all('*', require('./routes/404'))

// Connecting to MongoDB using mongoose
mongoose.connection.once('open', () => {
server.listen(port, () => {
console.log('🔗 Successfully Connected to MongoDB')
console.log(`✅ Application running on port: ${port}`);
})
})
mongoose.connection.on('error', (err) => {
console.log(err)
})

As you can see in the API routes section

app.use('/auth', require('./routes/authRoutes'))
app.use('/users', require('./routes/userRoutes'))
app.use('/notifications', require('./routes/notificationRoutes'))

We have three routes: /auth, /users, /notification, we will create specific file to each route, and for each one a controllers file

. For ‘/auth’ route

Create a file ‘/backend/routes/authRoutes.js’

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

router.route('/').post(authController.login)

module.exports = router

We have a authController.login

Create a file ‘/backend/controllers/authControllers.js’

const User = require('../models/user')
const notification = require('../models/notification')

// @desc Login
// @Route POST /auth
// @Access Public
const login = async (req, res) => {
const { username, password } = req.body
if (!username || !password) {
return res.status(400).json({ message: 'All fields are required' })
}
const foundUser = await User.findOne({ username }).exec()
if (!foundUser || !foundUser.active) {
res.status(401).json({ message: 'Unauthorized' })
}


res.json({ foundUser })

// add notification for login
await notification.create({
user: foundUser._id,
title: 'login',
type: 1,
text: `New login at ${new Date()}`,
read: false,
})
}

The login request contain: { username, password }, we will check in our data base for the user (by id) and verify if the password is correct, then return the found user.

‘add notification for login’

In this section we will add a notification record to the mongo database.

. For ‘/users’ route

Create a file ‘/backend/routes/userRoutes.js’

const express = require('express')
const router = express.Router()
const userController = require('../controllers/userController')

// @desc // @desc Get one user by ID
// @Route POST /users
// @Private access
router
.route('/')
.post(userController.getUser)
module.exports = router

We have a simple route to ‘/’ for POST requests

Create a file ‘/backend/controllers/userControllers.js’

const getOneUser = async (req, res) => {
const { id } = req.body
if (!id) {
return res
.status(400)
.json({ message: 'Verify your data and proceed again' })
}
if (!id || !id.match(/^[0-9a-fA-F]{24}$/)) {
return res.status(400).json({ message: `You must give a valid id: ${id}` });
}
// Check if the note exist
const oneUser = await user.findById(id).lean().exec()
if (!oneUser) {
return res
.status(400)
.json({ message: `Can't find a user with this id: ${id}` })
}
res.json(oneUser)
}

We do a backend check for the id

The ‘id.match(/^[0–9a-fA-F]{24}$/)’ will test if the id is a valid mongodb _id

If all is ok, we will return a ‘200’ response containing the user.

. For ‘/notifications’ route

Create a file ‘/backend/routes/notificatinonRoutes.js’

const express = require('express');
const router = express.Router();
const notificationController = require('../controllers/notificationController');

router
.route('/')
.post(notificationController.getAllNotifications)
.delete(notificationController.deleteNotification)
.patch(notificationController.markOneNotificationasread);

router
.route('/all')
.delete(notificationController.deleteAllNotifications)
.patch(notificationController.markAllNotificationsAsRead);

module.exports = router;

We have routes for single notification or for All notifications

const notification = require('../models/notification');

// @desc Get all notifications
// @Route GET /notes
// @Access Private
const getAllNotifications = async (req, res) => {
const { id, page, limit } = req.body;
const filtredNotifications = notification.find({ user: id });
const total = await filtredNotifications.countDocuments();
const notifications = await notification
.find({ user: id })
.limit(limit)
.skip(limit * page)
.lean();
if (!notifications) {
return res.status(400).json({ message: 'No notifications found' });
}
console.log('total: ', total);

res.json({ totalpage: Math.ceil(total / limit), notifications });
};

// @desc delete a notification
// @Route DELETE /notifications
// @Private access
const deleteNotification = async (req, res) => {
const { id } = req.body;
if (!id || !id.match(/^[0-9a-fA-F]{24}$/)) {
return res.status(400).json({ message: `You must give a valid id: ${id}` });
}

const deleteNotification = await notification.findById(id).exec();
if (!deleteNotification) {
return res
.status(400)
.json({ message: `Can't find a notification with id: ${id}` });
}
const result = await deleteNotification.deleteOne();
if (!result) {
return res
.status(400)
.json({ message: `Can't delete the notification with id: ${id}` });
}
res.json({ message: `Notification with id: ${id} deleted with success` });
};

// @desc delete All notification
// @Route DELETE /notifications/all
// @Private access
const deleteAllNotifications = async (req, res) => {
const { id } = req.body;
if (!id || !id.match(/^[0-9a-fA-F]{24}$/)) {
return res.status(400).json({ message: `You must give a valid id: ${id}` });
}
const notificationsDeleteMany = await notification.deleteMany({ user: id });
if (!notificationsDeleteMany) {
return res
.status(400)
.json({ message: 'Error Deleting all notifications as read' });
}
res.json({ message: `All notifications for user ${id}marked was deleted` });
};
// @desc Mark One Notification As Read
// @Route Patch /notifications/
// @Access Private
const markOneNotificationasread = async (req, res) => {
const { id } = req.body;
if (!id || !id.match(/^[0-9a-fA-F]{24}$/)) {
return res.status(400).json({ message: `You must give a valid id: ${id}` });
}
const updateNotification = await notification.find({ id }).exec();
if (!updateNotification) {
return res.status(400).json({ message: 'No notifications found' });
}
updateNotification.read = false;
await updateNotification.save();
res.json(updateNotification);
};
// @desc Mark All Notifications As Read
// @Route Patch /notifications/All
// @Access Private
const markAllNotificationsAsRead = async (req, res) => {
const { id } = req.body;
if (!id || !id.match(/^[0-9a-fA-F]{24}$/)) {
return res.status(400).json({ message: `You must give a valid id: ${id}` });
}
const notificationsUpdateMany = await notification.updateMany(
{ user: id },
{ $set: { read: true } }
);
if (!notificationsUpdateMany) {
return res
.status(400)
.json({ message: 'Error Marking all notifications as read' });
}
res.json({ message: `All notifications for user ${id}marked as read` });
};
module.exports = {
getAllNotifications,
deleteNotification,
deleteAllNotifications,
markOneNotificationasread,
markAllNotificationsAsRead,
};

In getAllNotifications

We will use mongo limit to configure our api pagination

we will return all notifications with ‘totalpage’

the request contain { id, page, limit } : the id of the user, the current page to calculate the pagination, and the limit with present the number of result by page

The other functions are self explained ( i hoop )

Complete project video

Complete project source code

--

--

Adel Benyahia

Web application developer (HTML │ CSS │ JS | ReactJS | NextJS | NestJS | MERN)