Technology

Building Real-Time Applications with WebSockets

Learn how to build real-time chat applications using WebSockets, Socket.IO, and Next.js. From basic concepts to advanced implementation patterns, discover how to create responsive, real-time user experiences.

15 min read
Published

Complete Tutorial Code

Follow along with the complete source code for this WebSocket tutorial. Includes a full real-time chat application with Next.js and Socket.IO.

View on GitHub

Introduction

WebSockets have revolutionized how we build real-time applications on the web. Unlike traditional HTTP requests that follow a request-response pattern, WebSockets enable bidirectional communication between clients and servers, making them perfect for chat applications, live updates, collaborative editing, and real-time gaming.

This comprehensive tutorial will guide you through building a complete real-time chat application from scratch. We'll explore WebSocket fundamentals, implement a Socket.IO server, and create a responsive React interface that demonstrates real-time messaging capabilities.

What are WebSockets?

WebSockets provide a persistent, full-duplex communication channel between a client and server. Once established, both the client and server can send data to each other at any time, enabling real-time communication without the overhead of traditional HTTP polling.

Traditional HTTP

Client → Server (Request)
Server → Client (Response)
Connection Closed

Request-response cycle with connection overhead

WebSocket

Client ↔ Server Persistent Connection Real-time Data Flow Bidirectional Communication

Persistent connection for real-time communication

Project Overview

Our WebSocket tutorial demonstrates a real-time chat application that showcases the power of bidirectional communication. The application features instant messaging, connection status indicators, and a clean, responsive interface built with modern web technologies.

Key Features

Real-time messaging: Send and receive messages instantly across all connected clients
Connection status: Visual indicator showing connection state
Message history: Persistent chat history during the session
Responsive UI: Clean, modern interface built with Tailwind CSS
TypeScript support: Full type safety throughout the application
Auto-scroll: Messages automatically scroll to the latest message

Tech Stack

Our WebSocket application uses a modern tech stack that provides excellent developer experience and production-ready performance:

Frontend: Next.js 16, React 19, TypeScript
Backend: Node.js with Socket.IO server
Styling: Tailwind CSS
Real-time Communication: Socket.IO
Development: tsx for TypeScript execution

Project Structure

The application follows a clean, modular architecture with clear separation of concerns:

websocket-tutorial/
├── src/
│   ├── app/
│   │   ├── page.tsx          # Main chat interface
│   │   ├── layout.tsx        # App layout
│   │   └── globals.css       # Global styles
│   ├── hooks/
│   │   └── useSocket.ts      # Custom hook for Socket.IO connection
│   └── types/
│       └── message.ts        # Message type definitions
├── server.ts                 # Socket.IO server with Next.js integration
├── package.json
└── README.md

How It Works

Server Side Implementation

The server integrates Socket.IO with Next.js to handle both traditional HTTP requests and WebSocket connections. It creates an HTTP server that listens for chat message events from clients and broadcasts received messages to all connected clients.

// server.ts
import { createServer } from 'http';
import { Server } from 'socket.io';
import next from 'next';

const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handler = app.getRequestHandler();

app.prepare().then(() => {
  const server = createServer(handler);
  const io = new Server(server);

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

    socket.on('chat message', (msg) => {
      // Broadcast message to all connected clients
      io.emit('chat message', msg);
    });

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

  server.listen(3000, () => {
    console.log('Server running on http://localhost:3000');
  });
});

Client Side Implementation

The client side uses a custom React hook to manage the WebSocket connection, track connection status, handle incoming messages, and provide a clean API for sending messages.

Custom Socket Hook

// src/hooks/useSocket.ts
import { useEffect, useState } from 'react';
import { io, Socket } from 'socket.io-client';
import { Message } from '@/types/message';

export const useSocket = () => {
  const [socket, setSocket] = useState<Socket | null>(null);
  const [isConnected, setIsConnected] = useState(false);
  const [messages, setMessages] = useState<Message[]>([]);

  useEffect(() => {
    const socketInstance = io();

    socketInstance.on('connect', () => {
      setIsConnected(true);
    });

    socketInstance.on('disconnect', () => {
      setIsConnected(false);
    });

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

    setSocket(socketInstance);

    return () => socketInstance.close();
  }, []);

  const sendMessage = (text: string) => {
    if (socket && text.trim()) {
      const message: Message = {
        id: Date.now().toString(),
        text,
        timestamp: new Date(),
        isSent: true,
      };
      socket.emit('chat message', message);
    }
  };

  return { socket, isConnected, messages, sendMessage };
};

Message Type Definition

// src/types/message.ts
export interface Message {
  id: string;
  text: string;
  timestamp: Date | string;
  isSent: boolean;
}

Main Chat Interface

The main chat component uses the custom hook to provide a complete chat interface with message display, input handling, and connection status indicators.

// src/app/page.tsx
'use client';

import { useState, useRef, useEffect } from 'react';
import { useSocket } from '@/hooks/useSocket';

export default function ChatPage() {
  const { isConnected, messages, sendMessage } = useSocket();
  const [inputValue, setInputValue] = useState('');
  const messagesEndRef = useRef<HTMLDivElement>(null);

  const scrollToBottom = () => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  };

  useEffect(() => {
    scrollToBottom();
  }, [messages]);

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (inputValue.trim()) {
      sendMessage(inputValue);
      setInputValue('');
    }
  };

  return (
    <div className="flex flex-col h-screen max-w-2xl mx-auto p-4">
      <div className="flex items-center gap-2 mb-4">
        <div className={`w-3 h-3 rounded-full ${
          isConnected ? 'bg-green-500' : 'bg-red-500'
        }`} />
        <span className="text-sm text-gray-600">
          {isConnected ? 'Connected' : 'Disconnected'}
        </span>
      </div>

      <div className="flex-1 overflow-y-auto mb-4 space-y-2">
        {messages.map((message) => (
          <div
            key={message.id}
            className={`flex ${
              message.isSent ? 'justify-end' : 'justify-start'
            }`}
          >
            <div
              className={`max-w-xs px-4 py-2 rounded-lg ${
                message.isSent
                  ? 'bg-blue-500 text-white'
                  : 'bg-gray-200 text-gray-800'
              }`}
            >
              {message.text}
            </div>
          </div>
        ))}
        <div ref={messagesEndRef} />
      </div>

      <form onSubmit={handleSubmit} className="flex gap-2">
        <input
          type="text"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          className="flex-1 px-4 py-2 border rounded-lg"
          placeholder="Type a message..."
          disabled={!isConnected}
        />
        <button
          type="submit"
          disabled={!isConnected || !inputValue.trim()}
          className="px-6 py-2 bg-blue-500 text-white rounded-lg disabled:opacity-50"
        >
          Send
        </button>
      </form>
    </div>
  );
}

Key Concepts Demonstrated

WebSocket Connection

Establishing and maintaining real-time connections between client and server.

Event-driven Communication

Using Socket.IO events for efficient message passing and real-time updates.

Broadcasting

Sending messages to all connected clients simultaneously for real-time collaboration.

State Management

Managing connection state and message history with React hooks and TypeScript.

Getting Started

Ready to build your own real-time application? Follow these steps to get the WebSocket tutorial running on your local machine:

Prerequisites

  • • Node.js (version 18 or higher)
  • • npm or yarn

Installation Steps

  1. 1
    Clone the repository:
    git clone https://github.com/audoir/websocket-tutorial.git
  2. 2
    Navigate to the project directory:
    cd websocket-tutorial
  3. 3
    Install dependencies:
    npm install
  4. 4
    Start the development server:
    npm run dev
  5. 5
    Open your browser and navigate to:

Testing the Application

To see the real-time capabilities in action, follow these steps:

  1. 1. Open multiple browser tabs/windows to http://localhost:3000
  2. 2. Type a message in one tab and press Send
  3. 3. Observe the message appearing in all other tabs in real-time
  4. 4. Notice the connection status indicator
  5. 5. Try refreshing a tab to see reconnection behavior

Available Scripts

npm run devStart development server with hot reload
npm run buildBuild the application for production
npm startStart the production server
npm run lintRun ESLint for code quality checks

Learning Objectives

By completing this tutorial, you will have gained hands-on experience with:

  • • Setting up a WebSocket server with Socket.IO
  • • Integrating WebSocket communication in a React application
  • • Managing real-time state updates with custom hooks
  • • Building a responsive chat interface with Tailwind CSS
  • • Handling connection states and error scenarios
  • • TypeScript best practices for real-time applications
  • • Event-driven architecture patterns
  • • Broadcasting messages to multiple clients

Production Considerations

When deploying WebSocket applications to production, consider these important factors:

Scaling: Use Redis adapter for Socket.IO to enable horizontal scaling across multiple server instances.
Authentication: Implement proper authentication and authorization for WebSocket connections.
Rate Limiting: Protect against spam and abuse with message rate limiting.
Error Handling: Implement robust error handling and reconnection logic for network failures.
Monitoring: Set up monitoring and logging for WebSocket connections and message throughput.

Conclusion

WebSockets open up a world of possibilities for building interactive, real-time applications. By providing persistent, bidirectional communication channels, they enable experiences that were previously impossible or impractical with traditional HTTP-based architectures.

The chat application tutorial demonstrates fundamental patterns that you can apply to build various real-time features: live notifications, collaborative editing, real-time dashboards, multiplayer games, and much more. The combination of Socket.IO, React, and TypeScript provides a robust foundation for production-ready real-time applications.

About the Author

Wayne Cheng is the founder and AI app developer at Audoir, LLC. Prior to founding Audoir, he worked as a hardware design engineer for Silicon Valley startups and an audio engineer for creative organizations. He holds an MSEE from UC Davis and a Music Technology degree from Foothill College.

Further Exploration

To continue your WebSocket journey, explore the complete tutorial repository and experiment with extending the chat application. Consider adding features like user authentication, message persistence, file sharing, or video calling to deepen your understanding of real-time communication.

For more AI-powered development tools and tutorials, visit Audoir .