Slide 1
-----------
Part 1: Imagine planning a study session in a group chat, but messages lag. Frustrating, right? Real-time apps like WhatsApp solve this with instant updates. In this lecture, we’ll build a chat app using NextJS and explore WebSockets, SSE, and push notifications. Let’s make apps feel alive and responsive!
Slide 2
-----------
Part 1: Instant updates enhance user experience. Real-time apps, like WhatsApp or live sports apps, keep users engaged by delivering updates instantly. Imagine a group chat where messages lag—frustrating! Real-time tech fixes this!
Part 2: It's a common myth that polling is enough. Polling, or repeatedly checking for updates, is slow and wastes resources. Imagine 1000 users checking for new messages every second. What a waste of resources. True real-time uses WebSockets, SSE, or push notifications for instant data.
Part 3: Our goal is to build a study group chat app. We’ll create a chat app where students join a room, send messages, and get instant updates, plus mobile notifications, using NextJS and TypeScript.
Slide 3
-----------
Part 1: There are few technologies that power real-time applications.
Part 2: WebSockets create two-way, persistent connections and allow servers and clients to send messages anytime, perfect for chat apps where users need instant back-and-forth.
Part 3: Server-sent events, or SSE, push updates to the client. They are lightweight, ideal for one-way updates like live feeds or notifications, and work natively with NextJS.
Part 4: Push notifications or mobile alerts reach users even when the app is closed, which is great for alerting users about new chat messages.
Part 5: Webhooks let servers notify clients of events via HTTP, useful for triggering actions like sending a chat message to an external service.
Part 6: Last, gRPC is a high-performance RPC framework. gRPC uses HTTP/2 for fast, typed communication, suited for backend services that need to push real-time chat data efficiently. In this lecture, we will focus on the first three technologies, web sockets, SSE and push notifications.
Slide 4
-----------
Part 1: Each real-time technology has a different communication protocol and aims at different scenarios. Please study them on your own time or when you need to dig deeper.
Slide 5
-----------
Part 1: Let's build backend for our chat application, using WebSockets.
Part 2: WebSockets keep an open connection, allowing instant message exchange, which is ideal for our chat app’s real-time needs.
Part 3: We will use SocketIO package for simplicity as SocketIO simplifies WebSocket setup with features like reconnection, making it beginner-friendly.
Part 4: We’ll run a separate Express server alongside NextJS to handle WebSocket connections. Please not that you can not deploy this app to Vercel, as Vercel only supports server or lambda functions. If you want to deploy this, you will need a constantly running instance, such as EC2 on AWS.
Part 5: This backend sets up a SocketIO server to handle real-time chat, allowing users to join rooms and exchange messages instantly.
Part 6: We use the socketIO and express packages to create this server.
Part 7: We launch a new server instance and set the CORS rules disabling requests from other apps, possibly spamming our server chat rooms.
Part 8: Then, we define the types for messages exchanged between client and server assuring type safety.
Part 9: This handles new user connections, creating a new socket and logging a message to the console.
Part 10: We use the created socket to add message handlers, such as this one which handles the request for user to join a particular chat room, notifying all users in that room that the user has joined.
Part 11: This handler notifies all users in the room that a new message has been sent by a particular users
Part 12: And last, we have a handler when the user disconnects from the socket. The socketIO provides a natural interface to chat applications creation.
Slide 6
-----------
Part 1: The client will be a standard NextJS app.
Part 2: We use the client version of SocketIO library
Part 3: We use the same message type as on the server.
Part 4: Here, we initiate a connection to the socket server, running on port 3000.
Part 5: We remember, which room we are currently in, setting the default to study group.
Part 6: In the useEffect hook, we handle the WebSocket communication, joining the room from our state and listening to all incoming messages while updating the message state. Observe how we notify the socket about disconnection each time we execute this use effect.
Part 7: Also, note, how we execute this connection initiation only when the desired room change.
Part 8: The send message handler sends the message to the active socket.
Part 9: All that remains is to render the UI and hook all handlers. Pretty simple, huh?
Slide 7
-----------
Part 1: Let's explore Server Side Events or SSE and ways to implement them on server,
Part 2: Server-Side Events are one-way, lightweight updates that let the server push messages to clients, perfect for simpler real-time updates like our chat’s message feed.
Part 3: SSE uses NextJS API routes. There is no separate server needed, simplifying our stack.
Part 4: SSE are best for server-to-client data. Unlike WebSockets, SSE is one-way, ideal for notifications or feeds but less suited for two-way chat.
Part 5: Let's check out the server implementation of SSE.
Part 6: We use the same message type as in the previous example.
Part 7: We keep all connected clients in an array. Again, you will need a constantly running server, not a Vercel deployment.
Part 8: We see that the implementation uses a native NextJS route accepting GET command.
Part 9: When user visits this URL, the SSE create a readable stream where we push all the messages coming to the server,
Part 10: Last, we need to return this readable stream setting the appropriate headers so that the client knows how to handle them.
Part 11: The POST handler adds a new message to the list of messages, which are then streamed to the client. As you can see, we are always streaming all the messages. Not very efficient. Let's check out the client now!
Slide 8
-----------
Part 1: This is the NextJS front end to our SSE. It listens to the SSE stream, using EventSource to receive messages from the server and update the chat UI in real-time. Unlike WebSockets, it sends messages using a standard HTTP POST request, keeping the setup simple.
Part 2: On the initial component render, we create a new Event Source connecting to our API and listening to the incoming messages.
Part 3: To send the chant message, we post the message to the designated chat route.
Part 4: The UI closely resembles our previous example, displaying the list of messages along with the input field to type and send a message. This setup is considerably easier than WebSockets, although it has the limitation of a one-way communication protocol.
Slide 9
-----------
Part 1: Let's wrap up real-time apps.
Part 2: Use WebSockets for fast, bidirectional apps like our study group chat. You will need a real-time server hosted on your own server or EC2 AWS instance.
Part 3: Use SSE for simple updates. NextJS API routes make SSE easy for one-way feeds or notifications.
Part 4: Push notifications are great for engagement. They alert users offline with notifications while keeping them relevant. Although we did not show you an example of Push Notifications in this presentation, the lecture description illustrates how to implement it with service workers and the web push package.
Part 5: And remember, polling is inefficient; always choose WebSockets, SSE, or notifications for real-time. That's it for now!
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 CaseProsConsWebSockets (Socket.IO)Two-way apps (chat, gaming)Fast,
bidirectional, reliableResource-intensive, needs separate serverSSE
(Next.js)One-way updates (feeds, notifications)Simple, lightweight, no external
serverOne-way, less suited for complex interactionsPush 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!
Maggie is a generative AI that can help you understand the course content better. You can ask her questions about the lecture, and she will try to answer them. You can also see the questions asked by other students and her responses.
Join the discussion to ask questions, share your thoughts, and discuss with other learners