Full Stack Development

Real-Time Apps



by Tomas Trescak t.trescak@westernsydney.edu.au

Real-Time

/

Introduction

  • Instant updates enhance user experience
  • 👺 Myth : Polling is enough
  • Let's build a study group chat app

Real-Time

/

Technologies

  • WebSockets : Two-way, persistent connection
  • Server Sent Events (SSE): Server pushes updates to client
  • Push Notifications : Mobile alerts
  • Webhooks : Event-driven HTTP callbacks
  • gRPC : High-performance RPC framework

Real-Time

/

Technologies

WebSockets

/

Server

    // 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(3000, () => console.log('Server running on port 3000'));
  
  • Fast, two-way communication
  • Uses Socket.IO for simplicity
  • Requires Express server

WebSockets

/

Client

    '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:3000');

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}>
        Send
      </button>
    </div>
  );
}
  

SSE

/

Server

    // 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 });
}
  
  • One-way, lightweight updates
  • Uses NextJS API routes
  • Best for server-to-client data

SSE

/

Client

    // 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>
  );
}
  

Real-Time

/

Wrap-Up

  • WebSockets for two-way chat
  • SSE for simple updates
  • Push notifications for engagement
  • Avoid polling