TypeScript: Building a Simple Chat Application with WebSockets

In today’s interconnected world, real-time communication is more crucial than ever. From instant messaging apps to collaborative tools, the ability to exchange information instantly is a cornerstone of modern software. This tutorial will guide you through building a simple chat application using TypeScript and WebSockets, enabling you to understand the fundamentals of real-time communication and apply these concepts to your own projects.

Why WebSockets?

Traditional web applications often rely on HTTP for communication, which is inherently request-response based. This means the client has to initiate every communication. For real-time applications, this is inefficient and leads to techniques like long polling or server-sent events, which have their limitations. WebSockets, on the other hand, provide a persistent, two-way communication channel between the client and the server. This allows for low-latency, real-time data transfer, making them ideal for chat applications, online games, and live data dashboards.

What You’ll Learn

This tutorial will cover the following key aspects:

  • Setting up a basic Node.js server with TypeScript support.
  • Implementing WebSocket connections using the `ws` library.
  • Creating a simple client-side application to connect to the server.
  • Handling message exchange between clients and the server.
  • Understanding the basics of WebSocket protocols.

Prerequisites

To follow this tutorial, you should have a basic understanding of:

  • JavaScript and TypeScript syntax.
  • Node.js and npm (Node Package Manager).
  • Familiarity with the command line.

Setting Up the Server (Backend)

1. Project Initialization

First, create a new project directory and initialize a Node.js project:

mkdir typescript-chat-app
cd typescript-chat-app
npm init -y

2. Installing Dependencies

Next, install the necessary dependencies. We’ll need the `ws` library for WebSocket functionality and `typescript` and `ts-node` for TypeScript support:

npm install ws typescript ts-node --save-dev

3. Configuring TypeScript

Create a `tsconfig.json` file in your project root to configure TypeScript:

{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"]
}

This configuration sets up the TypeScript compiler to transpile your `.ts` files into `.js` files in the `dist` directory. It also enables strict mode for better code quality.

4. Creating the Server File

Create a `src` directory, and inside it, create a file named `server.ts`. This file will contain the server-side code.

// src/server.ts
import * as WebSocket from 'ws';

const wss = new WebSocket.Server({
  port: 8080,
});

interface Client {
  ws: WebSocket;
  id: string;
}

let clients: Client[] = [];

wss.on('connection', ws => {
  console.log('Client connected');
  const clientId = generateClientId();
  const client: Client = {
    ws: ws,
    id: clientId,
  };
  clients.push(client);

  ws.on('message', message => {
    console.log(`Received: ${message}`);
    // Broadcast the message to all connected clients
    broadcast(message, clientId);
  });

  ws.on('close', () => {
    console.log('Client disconnected');
    clients = clients.filter(c => c.id !== clientId);
  });

  ws.on('error', error => {
    console.error('WebSocket error:', error);
    clients = clients.filter(c => c.id !== clientId);
  });

  ws.send(`Welcome to the chat! Your ID is: ${clientId}`);
});

function broadcast(message: WebSocket.Data, senderId: string): void {
  clients.forEach(client => {
    if (client.id !== senderId && client.ws.readyState === WebSocket.OPEN) {
      client.ws.send(message);
    }
  });
}

function generateClientId(): string {
  return Math.random().toString(36).substring(2, 15);
}

console.log('WebSocket server started on port 8080');

Let’s break down this code:

  • We import the `ws` library to create a WebSocket server.
  • We create a `WebSocket.Server` instance, listening on port 8080.
  • We define a `Client` interface to hold the WebSocket connection and a unique ID.
  • The `wss.on(‘connection’, …)` handler is triggered when a client connects. We assign a unique ID to the client.
  • Inside the connection handler:
    • `ws.on(‘message’, …)`: Handles incoming messages. It logs the message and then calls the `broadcast` function to send the message to all other connected clients.
    • `ws.on(‘close’, …)`: Handles client disconnection. It removes the client from the `clients` array.
    • `ws.on(‘error’, …)`: Handles any WebSocket errors.
    • `ws.send(…)`: Sends a welcome message to the connecting client, including their assigned ID.
  • The `broadcast` function iterates through all connected clients and sends the received message to everyone except the sender, ensuring the message is only sent to open connections.
  • `generateClientId()` generates a random ID for each client.

5. Running the Server

To run the server, use the following command in your terminal:

npx ts-node src/server.ts

This command uses `ts-node` to execute your TypeScript code directly without the need for compilation. You should see “WebSocket server started on port 8080” in your console.

Setting Up the Client (Frontend)

1. Creating the HTML File

Create an `index.html` file in the root of your project. This file will contain the HTML structure for your chat application.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Simple Chat App</title>
  <style>
    body {
      font-family: sans-serif;
    }
    #chat-container {
      width: 80%;
      margin: 20px auto;
      border: 1px solid #ccc;
      padding: 10px;
    }
    #messages {
      height: 300px;
      overflow-y: scroll;
      margin-bottom: 10px;
      border: 1px solid #eee;
      padding: 5px;
    }
    .message {
      margin-bottom: 5px;
      padding: 5px;
      border-radius: 5px;
    }
    .sender {
      font-weight: bold;
    }
    .self {
      background-color: #e0f7fa;
    }
    .other {
      background-color: #f0f0f0;
    }
  </style>
</head>
<body>
  <div id="chat-container">
    <div id="messages"></div>
    <input type="text" id="message-input" placeholder="Type your message...">
    <button id="send-button">Send</button>
  </div>
  <script src="./index.js"></script>
</body>
</html>

This HTML provides the basic structure: a container for the chat, a message display area, an input field, and a send button. It also includes some basic CSS for styling.

2. Creating the Client-Side TypeScript File

Create an `index.ts` file in your project root. This file will contain the client-side TypeScript code.


// index.ts
const messagesContainer = document.getElementById('messages') as HTMLDivElement;
const messageInput = document.getElementById('message-input') as HTMLInputElement;
const sendButton = document.getElementById('send-button') as HTMLButtonElement;

let ws: WebSocket;
let clientId: string | null = null;

function connect() {
  ws = new WebSocket('ws://localhost:8080');

  ws.onopen = () => {
    console.log('Connected to WebSocket server');
  };

  ws.onmessage = (event) => {
    const message = event.data;
    if (message.startsWith('Welcome to the chat!')) {
      const match = message.match(/Your ID is: (.*)/);
      if (match) {
        clientId = match[1];
      }
    }
    displayMessage(message);
  };

  ws.onclose = () => {
    console.log('Disconnected from WebSocket server');
    displayMessage('Disconnected from server.');
    setTimeout(connect, 5000); // Attempt to reconnect after 5 seconds
  };

  ws.onerror = (error) => {
    console.error('WebSocket error:', error);
    displayMessage('Error connecting to server.');
  };
}

function displayMessage(message: string) {
  const messageElement = document.createElement('div');
  messageElement.classList.add('message');

  if (clientId && message.includes(clientId)) {
    messageElement.classList.add('self');
  } else {
    messageElement.classList.add('other');
  }

  if (clientId && message.includes(clientId)) {
    const parts = message.split(': ');
    if (parts.length > 1) {
      const senderId = parts[0];
      const messageText = parts.slice(1).join(': ');
      messageElement.innerHTML = `<span class="sender">You</span>: ${messageText}`;
    } else {
      messageElement.textContent = message;
    }
  } else {
    const parts = message.split(': ');
    if (parts.length > 1) {
      const senderId = parts[0];
      const messageText = parts.slice(1).join(': ');
      messageElement.innerHTML = `<span class="sender">${senderId}</span>: ${messageText}`;
    } else {
      messageElement.textContent = message;
    }
  }

  messagesContainer.appendChild(messageElement);
  messagesContainer.scrollTop = messagesContainer.scrollHeight; // Auto-scroll to bottom
}

sendButton.addEventListener('click', () => {
  const message = messageInput.value;
  if (ws && ws.readyState === WebSocket.OPEN && message) {
    ws.send(`${clientId ? clientId : 'Guest'}: ${message}`);
    messageInput.value = '';
  }
});

connect();

Let’s break down this client-side code:

  • We get references to the HTML elements: the message container, input field, and send button.
  • `connect()`: This function establishes the WebSocket connection to the server.
  • Inside `connect()`:
    • `ws = new WebSocket(‘ws://localhost:8080’);`: Creates a new WebSocket instance, connecting to the server.
    • `ws.onopen`: This event handler is triggered when the connection is successfully established.
    • `ws.onmessage`: This event handler is triggered when a message is received from the server. It parses the message and displays it in the chat window. If the message starts with “Welcome to the chat!”, the client ID is extracted.
    • `ws.onclose`: This event handler is triggered when the connection is closed. It displays a disconnection message and attempts to reconnect after 5 seconds.
    • `ws.onerror`: This event handler is triggered if an error occurs.
  • `displayMessage(message)`: This function creates a new message element, adds the appropriate CSS classes for styling (self or other), and appends it to the message container. It also handles the welcome message.
  • The `sendButton.addEventListener(‘click’, …)`: This event listener sends the message entered in the input field to the server when the send button is clicked, including the client ID.
  • `connect()`: Initiates the connection to the WebSocket server when the page loads.

3. Compiling the Client-Side TypeScript

To compile the TypeScript code into JavaScript, you’ll need to use the TypeScript compiler. Open your terminal and run the following command from the project root:

npx tsc index.ts

This will generate a `index.js` file in your project root, which you’ll link in your `index.html` file.

4. Linking the JavaScript File

Make sure your `index.html` file includes the compiled JavaScript file:

<script src="./index.js"></script>

This line should be placed just before the closing `</body>` tag.

Testing the Application

Open `index.html` in your web browser. Open multiple browser windows or tabs to simulate multiple users. You should be able to type messages in the input field and see them appear in all connected clients in real-time. You’ll also see your assigned Client ID in the welcome message.

Common Mistakes and How to Fix Them

1. Server Not Running

Mistake: The chat application does not work, and no messages are displayed.

Fix: Make sure your server is running. Open your terminal and run `npx ts-node src/server.ts`. If the server isn’t running, the client won’t be able to connect.

2. CORS Issues

Mistake: You see a “Cross-Origin Request Blocked” error in your browser’s console.

Fix: WebSockets, like other network requests, are subject to the same-origin policy. If your client and server are running on different domains or ports, you’ll encounter CORS (Cross-Origin Resource Sharing) issues. For this simple example, we are running the client and server on the same machine. In a production environment, you will need to configure CORS on your server to allow connections from your client’s origin.

3. Incorrect WebSocket URL

Mistake: The client fails to connect to the server.

Fix: Double-check the WebSocket URL in your client-side code (`ws = new WebSocket(‘ws://localhost:8080’);`). Ensure that the protocol (`ws://`), hostname (`localhost`), and port (`8080`) match your server configuration.

4. TypeScript Compilation Errors

Mistake: The client-side code doesn’t work, and you see errors in your browser’s console related to JavaScript not being properly loaded or errors related to type mismatches.

Fix: Make sure you have compiled the TypeScript code correctly using `npx tsc index.ts`. If there are any type errors, fix them in your `.ts` files. Check your browser’s developer console for any JavaScript errors. Also, check that the paths in your HTML file to the JavaScript files are correct.

5. Incorrect Message Handling

Mistake: Messages are not displaying correctly, or are not being broadcasted to all users.

Fix: Carefully review your message handling logic in both the server (`broadcast` function) and the client (`displayMessage` function). Ensure that messages are being sent to the correct clients and that the client-side code is parsing and displaying the messages correctly. Double-check your conditional statements and string manipulation.

Enhancements and Next Steps

This is a basic chat application. Here are some ideas for enhancements and further learning:

  • **Usernames:** Implement a system for users to enter usernames and display them alongside their messages.
  • **Private Messaging:** Add the ability for users to send private messages to specific individuals.
  • **Message History:** Store and retrieve message history when a user connects. This could be done using a simple in-memory store on the server or a database.
  • **Error Handling:** Improve error handling on both the client and server. For example, handle connection loss more gracefully.
  • **Styling:** Enhance the CSS for a better user experience.
  • **Deployment:** Deploy your application to a hosting platform like Heroku or AWS.
  • **Security:** Implement security measures to prevent malicious attacks (e.g., input validation, authentication).
  • **Use a Framework:** Consider using a framework like React or Vue.js for a more structured and scalable frontend.

Key Takeaways

  • WebSockets provide a persistent, two-way communication channel, enabling real-time applications.
  • Node.js and the `ws` library are powerful tools for building WebSocket servers.
  • TypeScript enhances code quality and maintainability.
  • Understanding the client-server interaction is crucial for real-time applications.

FAQ

Q: What is the difference between WebSockets and HTTP?

A: HTTP is a request-response protocol, where the client initiates every communication. WebSockets, on the other hand, establish a persistent, two-way connection, allowing for real-time data transfer without the overhead of repeated requests.

Q: Why use TypeScript for this project?

A: TypeScript adds type safety to your code, making it easier to catch errors during development. It also improves code readability and maintainability.

Q: How can I handle user authentication in a real-world chat application?

A: You would typically integrate user authentication with your server. When a client connects, they would be required to authenticate (e.g., using a username and password). Upon successful authentication, the server would associate the WebSocket connection with the authenticated user.

Q: What are some common use cases for WebSockets?

A: WebSockets are used in a variety of applications, including chat applications, online games, live data dashboards, collaborative editing tools, and real-time monitoring systems.

Q: How can I deploy this application?

A: You can deploy your application to a cloud platform like Heroku, AWS, or Google Cloud. You’ll need to package your server-side code (Node.js) and frontend code (HTML, CSS, JavaScript) and configure the platform to run your application. You’ll also need to configure a domain name and SSL certificate (for secure connections).

Building a chat application with WebSockets in TypeScript is a great way to learn about real-time communication and expand your web development skills. By following the steps outlined in this tutorial, you’ve gained a solid foundation for building interactive and engaging applications. Remember that this is just a starting point, and there’s much more to explore. Continue to experiment, build upon this foundation, and adapt these techniques to create your own innovative projects. The world of real-time web applications is vast and exciting, and with the skills you’ve acquired, you’re well-equipped to dive in and make your mark.