PresentationInteractive, narrated presentation of the section content.

ContentDetailed description of the section content.

Building Real-Time Applications in Full Stack Development

Picture this: you’re in a group chat planning a study session, but messages take ages to appear. Or imagine a food delivery app that doesn’t notify you when your order is at your door. These delays frustrate users. Real-time applications fix this by delivering instant updates, making apps feel responsive and engaging. Whether it’s a chat app like WhatsApp or push notifications on your phone, real-time tech is everywhere.

In this lecture, we’ll build a real-time chat application and explore mobile push notifications using modern full stack tools. We’ll use Next.js for the front end and implement three approaches: WebSockets with Socket.IO, Server-Sent Events (SSE) with Next.js API routes, and Push Notifications for mobile devices. By the end, you’ll understand how to create apps that update instantly and keep users connected.

A common misconception is that refreshing a page or polling (repeatedly checking for updates) is enough for real-time apps. Polling is slow, wastes server resources, and feels clunky. True real-time apps use WebSockets, SSE, or push notifications to deliver updates without constant requests. Another myth: real-time is only for chat apps. Nope! It’s used in live dashboards, stock tickers, collaborative tools like Google Docs, and mobile notifications for apps like Uber.

Our example will be a study group chat app where users can join a room, send messages, and receive instant updates. We’ll also add push notifications to alert users on their phones when new messages arrive.

Section 1: Understanding Real-Time Technologies

Real-time apps enable instant data exchange between servers and clients. Think of it like a phone call (constant connection) versus texting (send and wait). Here are the technologies we’ll use:

  • WebSockets: A two-way connection for sending and receiving data instantly. Ideal for chat apps.
  • Server-Sent Events (SSE): A one-way connection where the server pushes updates to the client. Great for notifications or live feeds.
  • Push Notifications: Messages sent to mobile devices via a push service, even when the app is closed. Perfect for alerts.
  • Polling (the old way): The client repeatedly asks the server for updates. It’s inefficient and outdated.

Let’s debunk another misconception: some think real-time apps are too complex for beginners. Not true! With libraries like Socket.IO and Next.js’s API routes, you can build real-time features with minimal code. Our chat app will prove it.

Section 2: Approach 1 - WebSockets with Socket.IO and Express

Why WebSockets?

WebSockets create a persistent, two-way connection, allowing instant communication. Socket.IO simplifies WebSocket setup and adds features like automatic reconnection.

How It Works

  1. The client (Next.js) connects to a Socket.IO server (Express).
  2. Users join a chat room and send messages via Socket.IO events.
  3. The server broadcasts messages to all clients in the room.
  4. Clients update their UI instantly.

Step-by-Step Example

We’ll build the chat app with an Express server and a Next.js front end, using TypeScript for type safety.

Backend (Express + Socket.IO with TypeScript)

The server manages connections and broadcasts messages.

// server/index.ts
import express from 'express';
import http from 'http';
import { Server, Socket } from 'socket.io';

const app = express();
const server = http.createServer(app);
const io = new Server(server, {
  cors: { origin: 'http://localhost:3000' },
});

interface Message {
  user: string;
  text: string;
}

interface SendMessagePayload {
  room: string;
  user: string;
  text: string;
}

io.on('connection', (socket: Socket) => {
  console.log('User connected:', socket.id);

  socket.on('joinRoom', (room: string) => {
    socket.join(room);
    socket.broadcast.to(room).emit('message', {
      user: 'System',
      text: `User ${socket.id} joined!`,
    });
  });

  socket.on('sendMessage', ({ room, user, text }: SendMessagePayload) => {
    const message: Message = { user, text };
    io.to(room).emit('message', message);
  });

  socket.on('disconnect', () => {
    console.log('User disconnected:', socket.id);
  });
});

server.listen(4000, () => console.log('Server running on port 4000'));

Frontend (Next.js with TypeScript)

The Next.js app connects to the server, joins a room, and displays messages.

// app/page.tsx
'use client';
import { useState, useEffect } from 'react';
import io, { Socket } from 'socket.io-client';

interface Message {
  user: string;
  text: string;
}

const socket: Socket = io('http://localhost:4000');

export default function Chat() {
  const [room, setRoom] = useState<string>('study-group');
  const [user, setUser] = useState<string>(
    'Student' + Math.floor(Math.random() * 100),
  );
  const [text, setText] = useState<string>('');
  const [messages, setMessages] = useState<Message[]>([]);

  useEffect(() => {
    socket.emit('joinRoom', room);

    socket.on('message', (message: Message) => {
      setMessages((prev) => [...prev, message]);
    });

    return () => {
      socket.off('message');
    };
  }, [room]);

  const sendMessage = () => {
    if (text.trim()) {
      socket.emit('sendMessage', { room, user, text });
      setText('');
    }
  };

  return (
    <div className="p-4 max-w-md mx-auto">
      <h1 className="text-2xl font-bold">Study Group Chat</h1>
      <div className="border p-2 h-64 overflow-y-auto mb-2">
        {messages.map((msg, i) => (
          <div key={i}>
            <strong>{msg.user}:</strong> {msg.text}
          </div>
        ))}
      </div>
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
        className="border p-2 w-full"
        placeholder="Type a message..."
      />
      <button onClick={sendMessage} className="bg-blue-500 text-white p-2 mt-2">
        Send
      </button>
    </div>
  );
}

How It Works

  • The server listens for joinRoom and sendMessage events, with TypeScript interfaces ensuring correct data shapes.
  • The client joins a room (e.g., “study-group”) and updates when new messages arrive.
  • Messages are broadcast to all room members instantly.

Advantages

  • Fast, two-way communication.
  • Perfect for chat, gaming, or collaborative apps.
  • Socket.IO handles fallbacks (e.g., polling if WebSockets fail).

Disadvantages

  • WebSockets can strain servers with many connections.
  • Requires a separate Express server alongside Next.js.

Section 3: Approach 2 - Server-Sent Events with Next.js API Routes

Why SSE?

SSE is simpler than WebSockets and ideal for one-way updates (server to client). Next.js API routes let us implement SSE without a separate server.

How It Works

  1. The client (Next.js) connects to an SSE endpoint (Next.js API route).
  2. The server streams messages to the client as they arrive.
  3. The client updates the UI with new messages.
  4. For sending messages, the client uses a standard POST request.

Step-by-Step Example

We’ll modify the chat app to use SSE for receiving messages, with TypeScript for type safety.

Backend (Next.js API Routes with TypeScript)

Two API routes: one for streaming messages (SSE) and one for sending messages.

// app/api/messages/stream/route.ts
import { NextRequest, NextResponse } from 'next/server';

interface Message {
  user: string;
  text: string;
}

let messages: Message[] = [];
let clients: TransformStreamDefaultController[] = [];

export async function GET() {
  const stream = new ReadableStream({
    start(controller) {
      clients.push(controller);
      controller.enqueue(`data: ${JSON.stringify(messages)}\n\n`);

      const interval = setInterval(() => {
        controller.enqueue(`data: ${JSON.stringify(messages)}\n\n`);
      }, 1000);

      return () => {
        clearInterval(interval);
        clients = clients.filter((c) => c !== controller);
      };
    },
  });

  return new NextResponse(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      Connection: 'keep-alive',
    },
  });
}

export async function POST(request: NextRequest) {
  const { user, text }: Message = await request.json();
  messages.push({ user, text });
  clients.forEach((client) =>
    client.enqueue(`data: ${JSON.stringify(messages)}\n\n`),
  );
  return NextResponse.json({ success: true });
}

Frontend (Next.js with TypeScript)

The client listens to the SSE stream and sends messages via POST.

// app/page.tsx
'use client';
import { useState, useEffect } from 'react';

interface Message {
  user: string;
  text: string;
}

export default function Chat() {
  const [user, setUser] = useState<string>(
    'Student' + Math.floor(Math.random() * 100),
  );
  const [text, setText] = useState<string>('');
  const [messages, setMessages] = useState<Message[]>([]);

  useEffect(() => {
    const eventSource = new EventSource('/api/messages/stream');
    eventSource.onmessage = (event) => {
      const newMessages: Message[] = JSON.parse(event.data);
      setMessages(newMessages);
    };
    return () => eventSource.close();
  }, []);

  const sendMessage = async () => {
    if (text.trim()) {
      await fetch('/api/messages/stream', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ user, text }),
      });
      setText('');
    }
  };

  return (
    <div className="p-4 max-w-md mx-auto">
      <h1 className="text-2xl font-bold">Study Group Chat (SSE)</h1>
      <div className="border p-2 h-64 overflow-y-auto mb-2">
        {messages.map((msg, i) => (
          <div key={i}>
            <strong>{msg.user}:</strong> {msg.text}
          </div>
        ))}
      </div>
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
        className="border p-2 w-full"
        placeholder="Type a message..."
      />
      <button onClick={sendMessage} className="bg-blue-500 text-white p-2 mt-2">
        Send
      </button>
    </div>
  );
}

How It Works

  • The /api/messages/stream route streams messages to clients using SSE.
  • The client listens for updates via EventSource and displays new messages.
  • Sending messages uses a POST request to the same endpoint.

Advantages

  • Simpler than WebSockets for one-way updates.
  • No external server needed; uses Next.js API routes.
  • Lightweight for notifications or feeds.

Disadvantages

  • One-way communication (server to client).
  • Less suited for complex interactions like chat.

Section 4: Approach 3 - Push Notifications for Mobile Devices

Why Push Notifications?

Push notifications alert users on their phones, even when the app is closed. They’re perfect for urgent updates, like new chat messages when the user isn’t active.

How It Works

  1. The client (Next.js) registers for notifications and sends a subscription to the server.
  2. The server stores the subscription and uses it to send push notifications via a service like Web Push.
  3. The push service delivers the notification to the user’s device.

Step-by-Step Example

We’ll add push notifications to our chat app, notifying users of new messages, using TypeScript.

Backend (Next.js API Route with TypeScript)

Handles subscriptions and sends notifications using web-push.

// app/api/notifications/route.ts
import { NextRequest, NextResponse } from 'next/server';
import webPush from 'web-push';

webPush.setVapidDetails(
  'mailto:example@yourdomain.com',
  process.env.VAPID_PUBLIC_KEY!,
  process.env.VAPID_PRIVATE_KEY!,
);

interface PushSubscription {
  endpoint: string;
  keys: {
    p256dh: string;
    auth: string;
  };
}

interface NotificationPayload {
  message: string;
}

let subscriptions: PushSubscription[] = [];

export async function POST(request: NextRequest) {
  const subscription: PushSubscription = await request.json();
  subscriptions.push(subscription);
  return NextResponse.json({ success: true });
}

export async function PUT(request: NextRequest) {
  const { message }: NotificationPayload = await request.json();
  const payload = JSON.stringify({ title: 'New Message', body: message });
  subscriptions.forEach((sub) => {
    webPush.sendNotification(sub, payload).catch((err) => console.error(err));
  });
  return NextResponse.json({ success: true });
}

Frontend (Next.js + Service Worker with TypeScript)

Registers for notifications and displays them.

// app/page.tsx (add to existing chat)
'use client';
import { useState, useEffect } from 'react';

interface Message {
  user: string;
  text: string;
}

export default function Chat() {
  const [user, setUser] = useState<string>(
    'Student' + Math.floor(Math.random() * 100),
  );
  const [text, setText] = useState<string>('');
  const [messages, setMessages] = useState<Message[]>([]);

  useEffect(() => {
    if ('serviceWorker' in navigator && 'PushManager' in window) {
      navigator.serviceWorker.register('/sw.js').then((reg) => {
        reg.pushManager
          .subscribe({
            userVisibleOnly: true,
            applicationServerKey: process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!,
          })
          .then((sub) => {
            fetch('/api/notifications', {
              method: 'POST',
              headers: { 'Content-Type': 'application/json' },
              body: JSON.stringify(sub),
            });
          });
      });
    }
  }, []);

  // ... rest of SSE chat code from Section 3 ...
}
// public/sw.ts
self.addEventListener('push', (event: PushEvent) => {
  const data = event.data?.json();
  if (data) {
    self.registration.showNotification(data.title, { body: data.body });
  }
});

Triggering Notifications

When a message is sent, trigger a notification.

// app/api/messages/stream/route.ts (modified POST)
export async function POST(request: NextRequest) {
  const { user, text }: Message = await request.json();
  messages.push({ user, text });
  clients.forEach((client) =>
    client.enqueue(`data: ${JSON.stringify(messages)}\n\n`),
  );

  await fetch('/api/notifications', {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ message: `${user}: ${text}` }),
  });

  return NextResponse.json({ success: true });
}

How It Works

  • The client registers a service worker and subscribes to push notifications.
  • The subscription is sent to the server and stored.
  • When a new message arrives, the server sends a push notification to all subscribed devices.

Advantages

  • Works even when the app is closed.
  • Engages users with timely alerts.
  • Cross-platform (web and mobile).

Disadvantages

  • Requires user permission.
  • Setup involves VAPID keys and service workers, which adds complexity.

Section 5: Comparing Approaches

ApproachUse CaseProsCons
WebSockets (Socket.IO)Two-way apps (chat, gaming)Fast, bidirectional, reliableResource-intensive, needs separate server
SSE (Next.js)One-way updates (feeds, notifications)Simple, lightweight, no external serverOne-way, less suited for complex interactions
Push NotificationsMobile/web alertsWorks offline, engagingComplex setup, requires permissions

For our chat app, WebSockets are best for real-time messaging, SSE is great for simpler updates, and push notifications keep users informed on the go.

Conclusion

Key Takeaways

  • Real-time apps use WebSockets, SSE, or push notifications for instant updates.
  • WebSockets (Socket.IO) are ideal for two-way communication like chat.
  • SSE is simple for one-way updates and works well with Next.js API routes.
  • Push notifications engage users on mobile devices, even offline.
  • Avoid polling—it’s inefficient and outdated.

Best Practices

  • Use Socket.IO for robust WebSocket apps with reconnection handling.
  • Leverage Next.js API routes for lightweight SSE implementations.
  • Secure push notifications with VAPID keys and test across devices.
  • Monitor server load, especially with WebSockets.

Common Pitfalls

  • Don’t overuse WebSockets for simple updates—SSE is often enough.
  • Ensure push notifications aren’t spammy; respect user preferences.
  • Test real-time apps with multiple users to catch race conditions.

By mastering these techniques, you can build apps that feel fast, responsive, and modern. Try combining WebSockets and push notifications in your chat app to create a seamless user experience!

No hints available.

Discuss with OthersAsk questions, share your thoughts, and discuss with other learners.

Loading discussion ...
Setup
React Fundamentals
Next.js
Advanced React
Databases
React Hooks
Authentication and Authorisation
APIs
CI/CD and DevOps
Testing React
Advanced Topics