DEV Community 👩‍💻👨‍💻

saurabharch
saurabharch

Posted on

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.
web-push-how-working.png

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
popup-notify.png

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:

  1. IDE Visual Studio Code
  2. Nodejs
  3. Git
  4. MongoDB
  5. Postman

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

git-init-commands.png
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

add-new-file-app.png
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

creat-config-dir.png

mkdir model
cd model
touch subscribers_model.js

creat-model-dir.png

mkdir router
cd router
touch push.js
touch subscriber.js
touch index.js

creat-router-dir.png

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.
create-vapid-key.png
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.
server-running-command.jpg

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

create-public-dir.png
creat-public-dir-files.png

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
puh-popup.png

after click on allo you see message like in your browser console

Top comments (0)

🤔 Did you know?

 
🎙 DEV hosts some podcasts that you can find on our Podcasts page.