Building a Collaborative Chatroom: An Exploration of Full-Stack Features

Programming is a collaborative process that transforms ideas into reality. In this blog, we’ll discuss the implementation of a Chatroom application, designed to enhance real-time communication. This project showcases the synergy between frontend, backend, and database layers, demonstrating how individual features and APIs come together to create a seamless experience.


Purpose of the Program

The Chatroom enables users to send, edit, and delete messages within different channels, which are actually random questions generated through the user interests. It fosters collaboration, exploration, and innovation through dynamic interactions.

Individual Features

  1. Frontend: User interactions like sending messages and fetching chat history.
  2. API Integration: RESTful communication for CRUD operations.
  3. Backend Model: Data storage and retrieval via SQLAlchemy models.

Input/Output Requests

Live Input Example: Sending Messages

Here’s how the frontend fetches messages in a specific channel using a GET request.

The outputted JSON is the interated through, the various output properties are then put into HTML objects, and then appended to the DOM.

async function fetchData(channelId) {
    try {
        // Pass channelId as a query parameter
        const response = await fetch(`${pythonURI}/api/chat?id=${channelId}`, {
            ...fetchOptions,
            method: 'GET',
            headers: {
                'Content-Type': 'application/json'
            }
        });
        if (!response.ok) {
            throw new Error('Failed to fetch chats: ' + response.statusText);
        }
        const chatData = await response.json();
        const chatBox = document.getElementById('chatBox');
        chatBox.innerHTML = '';
        chatData.forEach(chatItem => {
            const messageElement = document.createElement('div');
            messageElement.className = 'chat-message';
            messageElement.id = `chat-${chatItem.id}`;
            messageElement.innerHTML = `
                <div class="message-content">
                    <p><strong>${chatItem.message}</strong></p>
                    <button class="edit-button" onclick="editMessage(${chatItem.id})">Edit</button>
                    <button class="delete-button" onclick="deleteMessage(${chatItem.id})">Delete</button>
                </div>
            `;
            chatBox.appendChild(messageElement);
        });
        chatBox.scrollTop = chatBox.scrollHeight; // Scroll to the bottom of the chat box
    } catch (error) {
        console.error('Error fetching data:', error);
    }
}
    window.updateAIQuestionAndCreateChannel = updateAIQuestionAndCreateChannel;

For data management purposes, it is important to be able to initialize, backup, and restore data from a database. My feature meets these requirements, as evidenced by the code below.

Initializing Code


#File Location: model/chat.py

def initChats():
    """
    Initializes the Chat table and adds tester data to the table.

    Uses:
        The db ORM methods to create the table.

    Instantiates:
        Chat objects with tester data.

    Raises:
        IntegrityError: An error occurred when adding the tester data to the table.
    """
    with app.app_context():
        """Create database and tables"""
        db.create_all()
        """Tester data for table"""
        chats = [
            Chat(message="Hello, world!", user_id=1, channel_id=1),
            Chat(message="How's everyone doing?", user_id=2, channel_id=1),
            Chat(message="Welcome to the new channel!", user_id=3, channel_id=2),
            Chat(message="Let's discuss the project.", user_id=1, channel_id=2),
            Chat(message="Testing the chat functionality.", user_id=4, channel_id=1),
            Chat(message="This is for general discussions.", user_id=2, channel_id=3),
        ]

        for chat in chats:
            try:
                chat.create()
                print(f"Record created: {repr(chat)}")
            except IntegrityError:
                db.session.remove()
                print(f"Record exists or error: {chat._message}")

Backup Code

For the purposes of length, I will only show code relevant to my feature.


# File Location: main.py

def extract_data():
    data = {}
    with app.app_context():
        data['chat'] = [chat.read() for chat in Chat.query.all()]
    return data

def save_data_to_json(data, directory='backup'):
    if not os.path.exists(directory):
        os.makedirs(directory)
    for table, records in data.items():
        with open(os.path.join(directory, f'{table}.json'), 'w') as f:
            json.dump(records, f)
    print(f"Data backed up to {directory} directory.")

def backup_data():
    data = extract_data()
    save_data_to_json(data)
    backup_database(app.config['SQLALCHEMY_DATABASE_URI'], app.config['SQLALCHEMY_BACKUP_URI'])

The backup_data() function is called to backup the data.

Restore Code

For the purposes of length, I will only show code relevant to my feature.


# File Location: model/chat.py

@staticmethod
def restore(data):
    """
    Restore chats from a list of dictionaries.
    Args:
        data (list): A list of dictionaries containing chat data.
    Returns:
        dict: A dictionary of restored chats keyed by message ID.
    """
    restored_chats = {}
    for chat_data in data:
        _ = chat_data.pop('id', None)  # Remove 'id' from chat_data if present
        message = chat_data.get("message", None)
        chat = Chat.query.filter_by(_message=message).first()
        if chat:
            chat.update(chat_data)
        else:
            chat = Chat(**chat_data)
            chat.create()
        restored_chats[message] = chat
    return restored_chats

# File Location: main.py

def load_data_from_json(directory='backup'):
    data = {}
    for table in ['chat']:
        with open(os.path.join(directory, f'{table}.json'), 'r') as f:
            data[table] = json.load(f)
    return data

# Restore data to the new database
def restore_data(data):
    with app.app_context():
        _ = Chat.restore(data['chat'])
    print("Data restored to the new database.")

@custom_cli.command('restore_data')
def restore_data_command():
    data = load_data_from_json()
    restore_data(data)

The restore_data method is used to restore the JSON data back to the actual database, an important part of data management.


List and Error Codes

Postman is used as a tool to test backend API endpoints. Let me make a GET request to the Chat API and we will recieve the chat message in JSON format, along with a error code, 200, which represents a successful request.

GET Request and Response: to /api/chat

image.png

Error Response

Here is what happens when there are invalid or missing parameters. It should return an error code of 400, which indicates a malformed request.

image.png

Lists, Dictionaries, Database

Rows

In the chat application, database queries are executed to retrieve rows of data, representing multiple chat messages or entities. These rows are returned as Python lists. This functionality is done by third party tools such as SQLAlchemy, which allows us to use Python code to manage SQL Databases.

Example:

chats = Chat.query.filter_by(_channel_id=channel_id).all()

Explanation:

  • Chat.query: Refers to the SQLAlchemy query interface tied to the Chat model.
  • filter_by(_channel_id=channel_id): Filters chat messages where the channel_id matches the input.
  • all(): Fetches all results as a list of Chat objects.

Result: This query returns a Python list of Chat objects, where each object represents a row in the database.

To convert rows to JSON format to be sent to the frontend, we use the jsonify method to transform it into a dictionary.

return jsonify([chat.read() for chat in chats])

CRUD Class for Columns


# File Location: model/chat.py

def create(self):
    """
    Creates a new chat message in the database.

    Returns:
        Chat: The created chat object, or None on error.
    """
    try:
        db.session.add(self)
        db.session.commit()
    except IntegrityError as e:
        db.session.rollback()
        logging.warning(f"IntegrityError: Could not create chat with message '{self._message}' due to {str(e)}.")
        return None
    return self

def read(self):
    """
    Retrieves chat message data as a dictionary.

    Returns:
        dict: A dictionary containing the chat message data, including user and channel names.
    """
    user = User.query.get(self._user_id)
    channel = Channel.query.get(self._channel_id)
    return {
        "id": self.id,
        "message": self._message,
        "user_name": user.name if user else None,
        "channel_name": channel.name if channel else None
    }

def update(self, data):
    """
    Updates the chat message object with new data.

    Args:
        data (dict): A dictionary containing the new data for the chat message.

    Returns:
        Chat: The updated chat message object, or None on error.
    """
    if 'message' in data:
        self._message = data['message']

    try:
        db.session.commit()
    except IntegrityError as e:
        db.session.rollback()
        logging.warning(f"IntegrityError: Could not update chat with ID '{self.id}' due to {str(e)}.")
        return None
    return self

def delete(self):
    """
    Deletes the chat message from the database.
    """
    try:
        db.session.delete(self)
        db.session.commit()
    except Exception as e:
        db.session.rollback()
        raise e

Through these four functions, CRUD funcionality is established. These 4 methods in the model can be called by importing the Chat model itself, and then calling the methods as you wish.

Algorithmic Design

Handling API Requests in Classes

The _CRUD class defines CRUD operations:


#File Location: api/chat.py

class _CRUD(Resource):
    @token_required()
    def post(self):
        """
        Create a new chat message.
        """
        current_user = g.current_user
        data = request.get_json()

        # Validate the presence of required fields
        if not data or 'message' not in data or 'channel_id' not in data:
            return {'message': 'Message and Channel ID are required'}, 400

        # Create a new chat object
        chat = Chat(message=data['message'], user_id=current_user.id, channel_id=data['channel_id'])
        chat.create()
        return jsonify(chat.read())

    @token_required()
    def get(self):
        """
        Retrieve all chat messages by channel ID.
        """
        # Extract channel_id from query parameters
        channel_id = request.args.get('id')
        if not channel_id:
            return {'message': 'Channel ID is required'}, 400

        # Query all chat messages for the given channel_id
        chats = Chat.query.filter_by(_channel_id=channel_id).all()
        if not chats:
            return {'message': 'No chat messages found for this channel'}, 404

        # Return the list of chats in JSON format
        return jsonify([chat.read() for chat in chats])


    @token_required()
    def put(self):
        """
        Update a chat message.
        """
        current_user = g.current_user
        data = request.get_json()

        # Validate the presence of required fields
        if 'id' not in data:
            return {'message': 'Chat ID is required'}, 400
        if 'message' not in data:
            return {'message': 'Chat message is required'}, 400

        # Find the chat message by ID
        chat = Chat.query.get(data['id'])
        if not chat:
            return {'message': 'Chat message not found'}, 404

        # Update the chat message using the model's update method
        updated_chat = chat.update({'message': data['message']})
        if updated_chat:
            return jsonify(updated_chat.read())
        else:
            return {'message': 'Failed to update chat message'}, 500

    @token_required()
    def delete(self):
        """
        Delete a chat message by ID.
        """
        data = request.get_json()
        if 'id' not in data:
            return {'message': 'Chat ID is required'}, 400

        # Find the chat message by ID
        chat = Chat.query.get(data['id'])
        if not chat:
            return {'message': 'Chat message not found'}, 404

        # Delete the chat message
        chat.delete()
        return {'message': 'Chat message deleted'}, 200

Let’s analyze one of them more deeply.

Sequencing, Selection, and Iteration

Sequencing

Sequencing refers to the order in which the operations are performed:

  • Extract Parameter: The method starts by extracting the channel_id from the query parameters.
  • Query Database: Using SQLAlchemy, it fetches all chat messages for the given channel_id.
  • Format Response: Converts the list of Chat objects into JSON format for the API response.

Selection

Selection is evident in the conditional checks:

  • Check for Missing channel_id: If the channel_id is not provided, the method returns a 400 Bad Request error.
  • Check for Empty Results: If no chat messages are found for the given channel_id, the method returns a 404 Not Found error.

Iteration

The method uses iteration to process each chat message:

List Comprehension: [chat.read() for chat in chats] loops through all Chat objects retrieved from the database and formats them into dictionaries using the read() method.


Parameters and Return Type

Parameters

The get method receives data via query parameters:

  1. id (Query Parameter): Represents the channel_id used to filter chat messages.

Return Type

The method uses jsonify to return a JSON response:

  1. Success Response:
    [
        {
            "id": 1,
            "message": "Hello, world!",
            "user_name": "User1",
            "channel_name": "General"
        },
        {
            "id": 2,
            "message": "How's everyone doing?",
            "user_name": "User2",
            "channel_name": "General"
        }
    ]
    
  2. Error Responses:
    • Missing Parameter: (Error Code 400)
      {
          "message": "Channel ID is required"
      }
      
    • No Messages Found: (Error Code 404)
      {
          "message": "No chat messages found for this channel"
      }
      

The get method showcases:

  1. Sequencing to structure the retrieval, validation, and formatting of data.
  2. Selection to handle missing parameters and empty results with appropriate error messages.
  3. Iteration to transform the database rows into a structured JSON response.

Here is my code block

def get(self):
    """
    Retrieve all chat messages by channel ID.
    """
    # Extract channel_id from query parameters
    channel_id = request.args.get('id')
    if not channel_id:
        return {'message': 'Channel ID is required'}, 400

    # Query all chat messages for the given channel_id
    chats = Chat.query.filter_by(_channel_id=channel_id).all()
    if not chats:
        return {'message': 'No chat messages found for this channel'}, 404

    # Return the list of chats in JSON format
    return jsonify([chat.read() for chat in chats])

The selection occurs when the chats are selected from a channel, iteration occurs when the chats are recursively outputted, and sequencing is the whole structure of the get block, which gets the chats from a channel.

This ensures the API is robust and user-friendly while adhering to RESTful principles.


Call to Algorithm Request

Definition of Code Block to Make a Request

The frontend makes an API call using JavaScript’s fetch function. Here is how the POST method is used to create a new chat message

async function sendMessage() {
    const messageInput = document.getElementById('messageInput').value.trim();
    if (messageInput) {
        const postData = { message: messageInput, channel_id: selectedChannelId };
        try {
            const response = await fetch(`${pythonURI}/api/chat`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(postData),
            });

            const result = await response.json();
            if (response.ok) {
                console.log('Message posted successfully:', result);
                // Handle success by updating the UI
                updateChatBox(result);
            } else {
                console.error('Failed to post message:', result.message);
                alert(result.message); // Display error message
            }
        } catch (error) {
            console.error('Error making request:', error);
            alert('An error occurred while sending the message.');
        }
    }
}

  1. API Endpoint: The request is sent to the backend API endpoint at /api/chat via a POST request. This endpoint corresponds to the _CRUD.post method in the backend

  2. JSON Payload:
    • message: The actual content of what the user has typed
    • channel_id: The channel ID of where the message should go

    Example Payload:

    {
        "message": "Hello this is Mihir testing a chat",
        "channel_id": 1
    }
    
  3. Sequencing in the Backend:
    • Validate the request body by checking whether message and channel_id actually exist
    • Create a Chat object and save it to the database using the create method.
    • Return the created object as a JSON response.

Return/Response from the Method

Success Response

When the request is successful, the server returns:

  1. HTTP Status Code: 200 OK.
  2. JSON Response:
    {
        "id": 8,
        "message": "Hello this is Mihir testing a chat",
        "user_name": "Nicholas Tesla",
        "channel_name": "Announcements"
    }
    

Handling Success in Frontend:

if (response.ok) {
    const postId = result.id;
    const tempElement = document.getElementById(tempId);
    tempElement.id = `post-${postId}`;
    tempElement.querySelector('.edit-button').setAttribute('onclick', `editMessage(${postId})`);
    tempElement.querySelector('.delete-button').setAttribute('onclick', `deleteMessage(${postId})`);
    console.log('Message posted successfully:', result);
}

Error Response

If the request fails due to missing or invalid data, the server returns:

  1. HTTP Status Code: 400 Bad Request.
  2. JSON Response:
    {
        "message": "Message and Channel ID are required"
    }
    

Handling Errors in Frontend:

I have used try catch statements in JavaScript to catch any potential errors, and simply remove that chat from the frontend to make sure the site doesnt go down and the user can still easily use the platform.

catch (error) {
    console.error('Error adding post:', error);
    document.getElementById(tempId).remove();
}

Summary

  • Request Definition: fetch calls the backend API with the required payload.
  • Return Handling: Successful responses update the chat UI, while error responses display appropriate messages.
  • Dynamic Responses: Changing the payload or input can lead to different responses (e.g., success, validation errors, or server issues), ensuring robustness and flexibility in the application.

Overall, I have had a lot of fun building this project, as it has taught me the ins and outs of a fullstack application. It has been extremely interesting and rewarding to build a site from scratch that is completely fullstack, as we use these types of web pages every day.