Web Push Notification Full Stack Application With Node Js Restful API
Let me tell you first, why Web Push Notification is essential for any web application.
It's helps in broadcasting short messages to the subscriber of the web application and as well as help to support in user engagement with your application to notify the every subscribers, It is really hard to any organisation send email notification for a short messaging event loop or broadcast messages like offers and updates of short messages and it also increases the cost of server operations so the solution is Web Push Notification .it help your the maximum post reachability and getting more consumer engagement on web application.
let me tell you how it's works.
service worker is main key ingrediant and knight of this feature which install in client broswer and run indenpendently as application after intalling in browser as service worker which regularly a send a query to the provider server and ask for any new event happening and than respond to the client if any new event is happening in the server it popup a message like
click on allow button service worker start to install in client browser and send a promise request for subscriber with Public VAPID key and check on server for whether user is already subscribe notification or not if it's already subscribe it sent back request with false statement otherwise server sent a true request.
and done that's it.
Now let's Comes to the Coding section and how to implement this feature in your application without using third party paid services and use as long as you.
Step 1
Need Prequest which are listed below if you don't have in your System.
Prequiests:
Now Let's Beigin with next Step
Step 2
open your IDE Visual Studio Code
and than run command in integrated terminal with your IDE
git init
than add the all fields or skip as you want
and than run command again to intall all the dependacy with
npm install express web-push body-parser mongoose q --save
and hit the Enter and wait for install all the dependency will install correclty in your project than run again run command for creat new application running file
in same project folder by
touch server.js
and again need to create three folders in same project directory by commands as below
mkdir config
cd config
touch keys.js
touch keys_prod.js
touch keys_dev.js
mkdir model
cd model
touch subscribers_model.js
mkdir router
cd router
touch push.js
touch subscriber.js
touch index.js
now all the essential folders and file are created and in this project we move to next coding parts in next step.
Step 3
The file structure of this project is as below
|
|
|________________________./config
| |
| |____keys_prod.js
| |____keys_dev.js
| |____keys.js
|
|________________________./public
| |
| |____index.html
| |____sw.js
| |____app.js
|
|________________________./model
| |
| |____subscribers_model.js
|
|________________________./router
| |
| |____push.js
| |____subscribe.js
|
|___________________________server.js
now start with create database model for mongodb database. so now i am using Mongoose ODM ORM library for MongoDB which is already installed in project
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const SubscriberSchema = new Schema({
endpoint: String,
keys: Schema.Types.Mixed,
createDate: {
type: Date,
default: Date.now
}
});
mongoose.model('subscribers', SubscriberSchema, 'subscribers');
so now let's come to the config file
cd config
and than open the keys.js file which is already created in this folder
if (process.env.NODE_ENV === 'production') {
module.exports = require('./keys_prod');
} else {
module.exports = require('./keys_dev');
}
and update your keys.js file with this code , actually this code provide smart switch database authentication address between production and development application.
Before update the keys_prod.js and keys_dev.js file generate the VAPID keys for client device browser and between the server running application.
by using this command
./node_modules/.bin/web-push generate-vapid-keys
you will see a two keys are generated one is private and another one is public key
which is show in below.
copy both the keys and paste in to the keys_dev.js or on production enviroment server config.
module.exports = {
//i used mlab database for fast and realiable pace development enviroment
mongoURI: 'mongodb://web-push:webpush123@ds213053.mlab.com:13053/web-push',
privateKey: 'ayTIBl3f0gcI-koFq-ZXPxSR4qicC0GcMNHA1dpHaj0' || process.env.VAPID_PRIVATE_KEY,
publicKey: 'BK3Q7j8fcGFws03RiU5XakzDJ7KGEiRhdIX2H5U8eNmhhkdHT_j0_SD09KL96aFZOH_bsjr3uRuQPTd77SRP3DI' || process.env.VAPID_PUBLIC_KEY
}
process.env.VAPID_PUBLIC_KEY or process.env.VAPID_PRIVATE_KEY understand as production server running environment configuration.
Note: Please Make sure you always use or generate your own VAPID keys for your application for more secure your application and make sure your private keys is not expose on the web.
so now all the important application structure setting is done now start coding in server.js which is exist on top of the project folder
const express = require('express');
const path = require('path');
const mongoose = require('mongoose');
const bodyParser = require('body-parser');
require('./model/subscribers_model');
// Load Routes
const index = require('./router');
// subscriber route load push
const push = require('./router/push');
// subscriber route load
const subscribe = require('./router/subscribe');
// Load Keys
const keys = require('./config/keys');
//Handlebars Helpers
mongoose.Promise = global.Promise;
// Mongoose Connect
mongoose.connect(keys.mongoURI, {
useMongoClient: true
})
.then(() => console.log('MongoDB Connected'))
.catch(err => console.log(err));
//Create Express middleware
const app = express();
app.set('trust proxy', true);
// parse application/json
app.use(bodyParser.json());
// parse application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({
extended: true
}));
// Set static folder
app.use(express.static(path.join(__dirname, 'public')));
// app.set('views', __dirname + '/public/js');
// Set global vars
app.use((req, res, next) => {
res.locals.user = req.user || null;
next();
});
// Use Routes
app.use('/', index);
app.use('/subscribe', subscribe);
app.use('/push', push);
// catch 404 and forward to error handler
app.use(function (req, res, next) {
var err = new Error('Not Found');
err.status = 404;
next(err);
});
// error handlers
// development error handler
// will print stacktrace
if (app.get('env') === 'development') {
app.use(function (err, req, res, next) {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: err
});
});
}
// production error handler
// no stacktraces leaked to user
app.use(function (err, req, res, next) {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: {}
});
});
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Server started on port ${port}`);
});
and now come to the folder router first start with subscribe.js which is already created by command. open this file in new tab and than paste this code in your subscribe.js file
const express = require('express');
const router = express.Router();
const mongoose = require('mongoose');
const Subscription = mongoose.model('subscribers');
//Post route of subscribe url is as http://host:3000/subscribe
router.post('/', (req, res) => {
const subscriptionModel = new Subscription(req.body);
subscriptionModel.save((err, subscription) => {
if (err) {
console.error(`Error occurred while saving subscription. Err: ${err}`);
res.status(500).json({
error: 'Technical error occurred'
});
} else {
res.json({
data: 'Subscription saved.'
});
}
});
});
// fixed the error get request for this route with a meaningful callback
router.get('/', (req, res) => {
res.json({
data: 'Invalid Request Bad'
});
});
module.exports = router;
save the changes and move to next file push.js and paste this code in your already created push.js file by command line
const express = require('express');
const router = express.Router();
const mongoose = require('mongoose');
const Subscription = mongoose.model('subscribers');
const q = require('q');
const webPush = require('web-push');
const keys = require('./../config/keys');
//Post route of push url is as http://host:3000/push
router.post('/', (req, res) => {
const payload = {
title: req.body.title,
message: req.body.message,
url: req.body.url,
ttl: req.body.ttl,
icon: req.body.icon,
image: req.body.image,
badge: req.body.badge,
tag: req.body.tag
};
Subscription.find({}, (err, subscriptions) => {
if (err) {
console.error(`Error occurred while getting subscriptions`);
res.status(500).json({
error: 'Technical error occurred'
});
} else {
let parallelSubscriptionCalls = subscriptions.map((subscription) => {
return new Promise((resolve, reject) => {
const pushSubscription = {
endpoint: subscription.endpoint,
keys: {
p256dh: subscription.keys.p256dh,
auth: subscription.keys.auth
}
};
const pushPayload = JSON.stringify(payload);
const pushOptions = {
vapidDetails: {
subject: "http://example.com",
privateKey: keys.privateKey,
publicKey: keys.publicKey
},
TTL: payload.ttl,
headers: {}
};
webPush.sendNotification(
pushSubscription,
pushPayload,
pushOptions
).then((value) => {
resolve({
status: true,
endpoint: subscription.endpoint,
data: value
});
}).catch((err) => {
reject({
status: false,
endpoint: subscription.endpoint,
data: err
});
});
});
});
q.allSettled(parallelSubscriptionCalls).then((pushResults) => {
console.info(pushResults);
});
res.json({
data: 'Push triggered'
});
}
});
});
// fixed the error get request for this route with a meaningful callback
router.get('/', (req, res) => {
res.json({
data: 'Invalid Request Bad'
});
});
module.exports = router;
again make sure save this code changes in your push.js file with this code now again move to the index.js file and update the code with this below code
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
res.locals.metaTags = {
title: 'web-push-api',
description: 'Web Push Notification Full Stack Application With Node Js Restful API',
keywords: 'Web Push Notification Full Stack Application With Node Js Restful API',
generator: '0.0.0.1',
author: 'Saurabh Kashyap'
};
res.json({
status: 'ok',
message: 'Server is running'
});
});
module.exports = router;
and save the changes in server.js file with above code in server.js file and run command hit this run command
node server.js
please make sure you will see these messages after hitting this command.
press again close the application after checking your application is running correct.
so now server side running application code is done.
Now Let's Begin with next Step
Step 4
creat a new folder in with public name and create file with these commands as below
mkdir public
cd public
touch index.html
touch sw.js
touch app.js
now lets beign the basic html code in index.html file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Web-Push Application with Restful Api</title>
</head>
<body>
<h1>This is a web-push notification example</h1>
<!-- call service worker for register and send subscribe request to the server with javascript -->
<script src="app.js"></script>
</body>
</html>
save this code and move to next file app.js where service worker browser check and register service worker in browser and also send a ajax request to the application API http://host:3000/subscribe for subscribe the service in client browser.
let isSubscribed = false;
let swRegistration = null;
let applicationKey = "put_your_public_key_here";
// Url Encription
function urlB64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/\-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
// Installing service worker
if ('serviceWorker' in navigator && 'PushManager' in window) {
console.log('Service Worker and Push is supported');
navigator.serviceWorker.register('sw.js')
.then(function (swReg) {
console.log('service worker registered');
swRegistration = swReg;
swRegistration.pushManager.getSubscription()
.then(function (subscription) {
isSubscribed = !(subscription === null);
if (isSubscribed) {
console.log('User is subscribed');
} else {
swRegistration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlB64ToUint8Array(applicationKey)
})
.then(function (subscription) {
console.log(subscription);
console.log('User is subscribed');
saveSubscription(subscription);
isSubscribed = true;
})
.catch(function (err) {
console.log('Failed to subscribe user: ', err);
})
}
})
})
.catch(function (error) {
console.error('Service Worker Error', error);
});
} else {
console.warn('Push messaging is not supported');
}
// Send request to database for add new subscriber
function saveSubscription(subscription) {
let xmlHttp = new XMLHttpRequest();
//put here API address
xmlHttp.open("POST", "/subscribe");
xmlHttp.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
xmlHttp.onreadystatechange = function () {
if (xmlHttp.readyState != 4) return;
if (xmlHttp.status != 200 && xmlHttp.status != 304) {
console.log('HTTP error ' + xmlHttp.status, null);
} else {
console.log("User subscribed to server");
}
};
xmlHttp.send(JSON.stringify(subscription));
}
and now save the all files and start coding for a service worker let's begin now
let notificationUrl = '';
//notification registered feature for getting update automatically from server api
self.addEventListener('push', function (event) {
console.log('Push received: ', event);
let _data = event.data ? JSON.parse(event.data.text()) : {};
notificationUrl = _data.url;
event.waitUntil(
self.registration.showNotification(_data.title, {
body: _data.message,
icon: _data.icon,
tag: _data.tag
})
);
});
//notification url redirect event click
self.addEventListener('notificationclick', function (event) {
event.notification.close();
event.waitUntil(
clients.matchAll({
type: "window"
})
.then(function (clientList) {
if (clients.openWindow) {
return clients.openWindow(notificationUrl);
}
})
);
});
nad save the all code . YEAH..!! done. so now we have to check wheater is it working or not. so again run command in your terminal
node server.js
open url: http://localhot:3000 in your browser now
after click on allo you see message like in your browser console
and save the all code . YEAH..!! done. so now we have to check whether is it working or not. so again run command in your terminal
node server.js
open url: http://localhot:3000 in your browser now
after click on allow you see message like in your browser console
and open postman with for send push message to subscibers.
push send example
{
"title": "StoryBook",
"message": "welcome friends",
"url": "https://new-storybook.herokuapp.com",
"ttl": 36000,
"icon": "https://cdn3.iconfinder.com/data/icons/happy-x-mas/501/santa15-128.png",
"badge": "https://cdn3.iconfinder.com/data/icons/happy-x-mas/501/santa15-128.png",
"data":"Hello New World",
"tag": "Book Story"
}
Source Code Here
Congrate you are successfully done !
what if there is millions of user, we need to go through the for loop and send to everybody when new post or blog is created ? I will be scalable way or any other alternative please
Hi Saurabh,
I do not receive any notification but my code is perfectly running
Greetings Saurabh, I am getting state: Rejected, with the following message:
The key in the authorization header does not correspond to the sender ID used to subscribe this user.
This is the full
<addr> {
“state”: “rejected”,
“reason”: {
“status”: false,
“endpoint”: “https://fcm.googleapis.com/fcm/send/ck3zllywyMM:APA91bGbmJr4cpaoQF65XUtaDWzoGr2jEXAC-xOOZqd_t-eTIhx7-MssQCg1OjwvCxYYnovWms0Ks0TOicaFnQScXRmmnpMX9e9ocSOYQTzEdoSPP8DJm5qXUnaY3YOa-fyECNtkjDju”,
“data”: “@{name=WebPushError; message=Received unexpected response code; statusCode=403; headers=; body=the key in the authorization header does not correspond to the sender ID used to subscribe this user. Please ensure you are using the correct sender ID and server Key from the Firebase console.\n; endpoint=https://fcm.googleapis.com/fcm/send/ck3zllywyMM:APA91bGbmJr4cpaoQF65XUtaDWzoGr2jEXAC-xOOZqd_t-eTIhx7-MssQCg1OjwvCxYYnovWms0Ks0TOicaFnQScXRmmnpMX9e9ocSOYQTzEdoSPP8DJm5qXUnaY3YOa-fyECNtkjDju}”
}
}
</addr>