← Back to Tutorials
Node.js

Build a Real-Time Notification System

Difficulty: Intermediate Est. Time: ~3 hours

Introduction

Real-time notifications are essential for modern web applications. From social media alerts to chat messages, users expect instant updates without refreshing the page.

In this tutorial, we'll build a notification system using WebSockets for real-time communication between server and clients.

What You'll Build
  • WebSocket server for real-time communication
  • Notification management system
  • User connection tracking
  • Real-time notification delivery
  • Frontend notification UI
What You'll Learn
  • WebSocket fundamentals
  • Socket.io library
  • Real-time event handling
  • User authentication with WebSockets
  • Push notifications

Project Overview

Our notification system will have several key features:

Core Features

  • Real-time Delivery - Instant notifications via WebSockets
  • Multi-user Support - Handle multiple connected users
  • Notification Types - Support different notification categories
  • Offline Support - Store notifications for offline users
  • Read Status - Track read/unread notifications

Prerequisites

  • Node.js installed - Version 14 or higher
  • Express.js - Basic knowledge
  • JavaScript - ES6+ syntax
  • Code editor - VS Code recommended

Project Setup

Bash
# Create project directory
mkdir notification-system
cd notification-system

# Initialize Node.js
npm init -y

# Install dependencies
npm install express socket.io cors

Project Structure

File Structure
notification-system/
├── public/
│   ├── index.html
│   ├── admin.html
│   └── style.css
├── server.js
├── notifications.js
└── package.json

Server Setup

Let's create the Express server with Socket.io integration.

JavaScript
// server.js
const express = require('express');
const http = require('http');
const { Server } = require("socket.io");
const cors = require('cors');
const path = require('path');

const app = express();
const server = http.createServer(app);
const io = new Server(server, {
    cors: {
        origin: "*",
        methods: ["GET", "POST"]
    }
});

app.use(cors());
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));

const NotificationManager = require('./notifications');
const notificationManager = new NotificationManager();

// Track connected users
const connectedUsers = new Map();

io.on('connection', (socket) => {
    console.log(`User connected: ${socket.id}`);
    
    socket.on('join', (userId) => {
        connectedUsers.set(userId, socket.id);
        socket.userId = userId;
        
        console.log(`User ${userId} joined`);
        
        // Send unread notifications
        notificationManager.getUnreadNotifications(userId)
            .then(notifications => {
                socket.emit('unread_notifications', notifications);
            });
    });

    socket.on('send_notification', (data) => {
        const { recipientId, type, title, message } = data;
        
        notificationManager.createNotification({
            recipientId,
            type,
            title,
            message
        }).then(notification => {
            io.emit('new_notification', notification);
            
            // Send to specific user if online
            const recipientSocket = connectedUsers.get(recipientId);
            if (recipientSocket) {
                io.to(recipientSocket).emit('notification', notification);
            }
        });
    });

    socket.on('mark_read', (notificationId) => {
        notificationManager.markAsRead(notificationId);
        socket.emit('notification_read', notificationId);
    });

    socket.on('disconnect', () => {
        if (socket.userId) {
            connectedUsers.delete(socket.userId);
            console.log(`User ${socket.userId} disconnected`);
        }
    });
});

// REST API endpoints
app.get('/api/notifications/:userId', (req, res) => {
    notificationManager.getNotifications(req.params.userId)
        .then(notifications => res.json(notifications));
});

app.post('/api/notifications', (req, res) => {
    const { recipientId, type, title, message } = req.body;
    
    notificationManager.createNotification({
        recipientId,
        type,
        title,
        message
    }).then(notification => {
        io.emit('new_notification', notification);
        res.json(notification);
    });
});

const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
});

WebSocket Implementation

The notification manager handles storing and retrieving notifications.

JavaScript
// notifications.js
class NotificationManager {
    constructor() {
        this.notifications = [];
        this.notificationId = 0;
    }

    createNotification(data) {
        const notification = {
            id: ++this.notificationId,
            recipientId: data.recipientId,
            type: data.type || 'info',
            title: data.title,
            message: data.message,
            read: false,
            createdAt: new Date()
        };
        
        this.notifications.push(notification);
        return Promise.resolve(notification);
    }

    getNotifications(userId) {
        const userNotifications = this.notifications
            .filter(n => n.recipientId === userId)
            .sort((a, b) => b.createdAt - a.createdAt);
        return Promise.resolve(userNotifications);
    }

    getUnreadNotifications(userId) {
        const unread = this.notifications
            .filter(n => n.recipientId === userId && !n.read)
            .sort((a, b) => b.createdAt - a.createdAt);
        return Promise.resolve(unread);
    }

    markAsRead(notificationId) {
        const notification = this.notifications.find(n => n.id === notificationId);
        if (notification) {
            notification.read = true;
        }
        return Promise.resolve(notification);
    }

    markAllAsRead(userId) {
        this.notifications
            .filter(n => n.recipientId === userId)
            .forEach(n => n.read = true);
        return Promise.resolve();
    }

    deleteNotification(notificationId) {
        const index = this.notifications.findIndex(n => n.id === notificationId);
        if (index > -1) {
            this.notifications.splice(index, 1);
        }
        return Promise.resolve();
    }
}

module.exports = NotificationManager;
WebSocket Events
  • join - User joins, receives unread notifications
  • send_notification - Send a new notification
  • mark_read - Mark notification as read
  • new_notification - Broadcast to all users
  • notification - Direct message to user

Notification System Features

Let's add more advanced notification features.

JavaScript
// Advanced notification features
class AdvancedNotificationManager {
    constructor() {
        this.notifications = [];
        this.subscribers = new Map();
    }

    // Create notification with priority
    createPriorityNotification(data) {
        const notification = {
            id: Date.now(),
            recipientId: data.recipientId,
            type: data.type,
            priority: data.priority || 'normal',
            title: data.title,
            message: data.message,
            read: false,
            archived: false,
            createdAt: new Date(),
            expiresAt: data.expiresAt || null
        };
        
        this.notifications.push(notification);
        this.notifySubscribers(notification);
        
        return notification;
    }

    // Subscribe to notification types
    subscribe(userId, callback, types = []) {
        if (!this.subscribers.has(userId)) {
            this.subscribers.set(userId, []);
        }
        this.subscribers.get(userId).push({ callback, types });
    }

    unsubscribe(userId) {
        this.subscribers.delete(userId);
    }

    notifySubscribers(notification) {
        this.subscribers.forEach((subs, userId) => {
            subs.forEach(sub => {
                if (sub.types.includes(notification.type) || sub.types.length === 0) {
                    sub.callback(notification);
                }
            });
        });
    }

    // Get notification statistics
    getStats(userId) {
        const userNotifs = this.notifications.filter(n => n.recipientId === userId);
        
        return {
            total: userNotifs.length,
            unread: userNotifs.filter(n => !n.read).length,
            byType: this.groupBy(userNotifs, 'type'),
            byPriority: this.groupBy(userNotifs, 'priority')
        };
    }

    groupBy(arr, key) {
        return arr.reduce((acc, item) => {
            acc[item[key]] = (acc[item[key]] || 0) + 1;
            return acc;
        }, {});
    }

    // Clean up expired notifications
    cleanupExpired() {
        const now = new Date();
        this.notifications = this.notifications.filter(n => 
            !n.expiresAt || new Date(n.expiresAt) > now
        );
    }
}

Building the Frontend

Let's create the client-side code to receive real-time notifications.

HTML
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Notifications</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <h1>Notifications</h1>
        
        <div class="user-info">
            <input type="text" id="userId" placeholder="Enter your user ID" value="user1">
            <button onclick="join()" class="btn">Join</button>
        </div>

        <div class="notification-panel">
            <div class="notification-header">
                <h2>Notifications</h2>
                <span id="unreadCount" class="badge">0</span>
                <button onclick="markAllRead()" class="btn-small">Mark all read</button>
            </div>
            <div id="notifications" class="notification-list"></div>
        </div>
    </div>

    <script src="/socket.io/socket.io.js"></script>
    <script>
        const socket = io();
        let currentUser = null;

        function join() {
            currentUser = document.getElementById('userId').value;
            socket.emit('join', currentUser);
            loadNotifications();
        }

        function loadNotifications() {
            fetch(\`/api/notifications/\${currentUser}\`)
                .then(res => res.json())
                .then(notifications => displayNotifications(notifications));
        }

        function displayNotifications(notifications) {
            const container = document.getElementById('notifications');
            const unreadCount = notifications.filter(n => !n.read).length;
            
            document.getElementById('unreadCount').textContent = unreadCount;
            
            container.innerHTML = notifications.map(notif => \`
                <div class="notification \${notif.read ? 'read' : 'unread'}" 
                     data-id="\${notif.id}">
                    <div class="notification-type"\>\${notif.type}</div>
                    <h3>\${notif.title}</h3>
                    <p>\${notif.message}</p>
                    <small>\${new Date(notif.createdAt).toLocaleString()}</small>
                    <button onclick="markRead(\${notif.id})" class="btn-small">
                        \${notif.read ? 'Read' : 'Mark Read'}
                    </button>
                </div>
            \`).join('');
        }

        function markRead(id) {
            socket.emit('mark_read', id);
            const el = document.querySelector(\`[data-id="\${id}"]\`);
            el.classList.remove('unread');
            el.classList.add('read');
            updateUnreadCount();
        }

        function markAllRead() {
            fetch(\`/api/notifications/\${currentUser}\`)
                .then(res => res.json())
                .then(notifications => {
                    notifications.forEach(n => {
                        if (!n.read) markRead(n.id);
                    });
                });
        }

        function updateUnreadCount() {
            const unread = document.querySelectorAll('.notification.unread').length;
            document.getElementById('unreadCount').textContent = unread;
        }

        // Socket event listeners
        socket.on('new_notification', (notification) => {
            if (notification.recipientId === currentUser) {
                addNotificationToList(notification);
            }
        });

        socket.on('notification', (notification) => {
            addNotificationToList(notification);
        });

        function addNotificationToList(notification) {
            const container = document.getElementById('notifications');
            const html = \`
                <div class="notification unread" data-id="\${notification.id}">
                    <div class="notification-type"\>\${notification.type}</div>
                    <h3>\${notification.title}</h3>
                    <p>\${notification.message}</p>
                    <small>\${new Date().toLocaleString()}</small>
                </div>
            \`;
            container.insertAdjacentHTML('afterbegin', html);
            updateUnreadCount();
            
            // Show browser notification
            if (Notification.permission === 'granted') {
                new Notification(notification.title, { body: notification.message });
            }
        }

        // Request notification permission
        if ('Notification' in window && Notification.permission !== 'granted') {
            Notification.requestPermission();
        }
    </script>
</body>
</html>
Browser Notifications

You can use the browser's Notification API to show desktop notifications even when the tab is not active.

Testing

Bash
# Start the server
node server.js

# Test with multiple browser tabs
# 1. Open http://localhost:3000 in two tabs
# 2. Enter different user IDs in each tab
# 3. Send notification via API

# Send notification via curl
curl -X POST http://localhost:3000/api/notifications \
  -H "Content-Type: application/json" \
  -d '{
    "recipientId": "user1",
    "type": "info",
    "title": "New Message",
    "message": "You have a new message!"
  }'
Testing Checklist
  • ✓ User can join and receive notifications
  • ✓ Real-time delivery works
  • ✓ Mark as read works
  • ✓ Multiple users supported
  • ✓ Browser notifications appear

Summary

Congratulations! You've built a complete real-time notification system.

What You Built

  • WebSocket Server - Real-time communication
  • Notification Manager - Store and manage notifications
  • Real-time Delivery - Instant push notifications
  • Frontend UI - User notification panel

Next Steps

  • Add notification categories (email, SMS)
  • Implement notification preferences
  • Add database persistence
  • Implement notification grouping

Continue Learning

Try these tutorials:

  • Build a Chat Application
  • Build a REST API with JWT
  • Build a Todo App with Authentication