Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/khushboodaryani/Chat-App/llms.txt

Use this file to discover all available pages before exploring further.

Chat App persists every exchange in MongoDB and delivers new messages instantly over a persistent Socket.io connection. Conversations are created lazily — the first message between two users automatically provisions the shared conversation document, so there is no separate “start chat” step.

Conversation Model

A Conversation document ties two users together and acts as a container for their message history.
FieldTypeDescription
participants[ObjectId]Array of exactly two User _id references
messages[ObjectId]Ordered array of Message _id references
A Message document stores the payload for a single chat bubble:
FieldTypeDescription
senderIdObjectIdUser who sent the message
receiverIdObjectIdUser who should receive it
messagestringPlain-text message body
createdAt / updatedAtDateMongoose timestamps
The server queries conversations with { participants: { $all: [senderId, receiverId] } }, so the participant order in the array does not matter.

Sending a Message

Endpoint: POST /api/messages/send/:receiverId
Auth: Required (protectRoute middleware)
Body: { "message": "Hello!" }
1

Locate or create the conversation

The backend searches for an existing Conversation whose participants array contains both senderId (from req.user._id) and :receiverId. If none exists, it creates one on the spot.
2

Create the Message document

A new Message is instantiated with senderId, receiverId, and the message string from the request body.
3

Link message to conversation

The new message’s _id is pushed into conversation.messages so the history stays ordered.
4

Persist both documents in parallel

await Promise.all([conversation.save(), newMessage.save()]);
Running the two saves concurrently reduces write latency compared to awaiting them sequentially.
5

Emit to receiver's socket (if online)

const receiverSocketId = getReceiverSocketId(receiverId);
if (receiverSocketId) {
  io.to(receiverSocketId).emit("newMessage", newMessage);
}
If the receiver has an active Socket.io connection their socket ID is looked up from userSocketMap and the message is emitted directly to that socket — no polling required.
6

Respond with the new Message

The controller returns the saved Message document with status 201 Created.
Request example
POST /api/messages/send/664a1f2e8c3b2a001e4f9abc
Content-Type: application/json

{
  "message": "Hey, are you free this weekend?"
}
Response 201 Created
{
  "_id": "664b3c1a7e2d4b001f8a12cd",
  "senderId": "664a1f2e8c3b2a001e4f9000",
  "receiverId": "664a1f2e8c3b2a001e4f9abc",
  "message": "Hey, are you free this weekend?",
  "createdAt": "2024-05-20T10:32:14.000Z",
  "updatedAt": "2024-05-20T10:32:14.000Z"
}

Fetching Message History

Endpoint: GET /api/messages/:id
Auth: Required
The backend looks up the Conversation for the current user and the target user (:id), then calls .populate("messages") to replace the ObjectId references with full Message documents. If no conversation exists yet, an empty array [] is returned — not a 404 error. The useGetMessages hook fetches history automatically whenever selectedConversation changes:
// frontend/src/hooks/useGetMessages.js
useEffect(() => {
  const getMessages = async () => {
    setLoading(true);
    try {
      const res = await fetch(`/api/messages/${selectedConversation._id}`);
      const data = await res.json();
      if (data.error) throw new Error(data.error);
      setMessages(data);
    } catch (error) {
      toast.error(error.message);
    } finally {
      setLoading(false);
    }
  };

  if (selectedConversation?._id) getMessages();
}, [selectedConversation?._id, setMessages]);

Incoming Message Notifications

useListenMessages registers a Socket.io listener for the "newMessage" event while a conversation is open:
// frontend/src/hooks/useListenMessages.js
socket?.on("newMessage", (newMessage) => {
  newMessage.shouldShake = true;          // triggers shake animation in the UI
  const sound = new Audio(notificationSound);
  sound.play();                           // plays /assets/sounds/notification.mp3
  setMessages([...messages, newMessage]);
});
Two things happen when a message arrives in real time:
  • Notification soundnotification.mp3 is played so the user is alerted even if the window is in the background.
  • Shake animation flagshouldShake = true is set on the message object. UI components can check this property to apply a CSS shake animation to the new bubble.
useListenMessages cleans up with socket?.off("newMessage") in the effect’s return function, so the listener is never duplicated across re-renders.

The sidebar search is a client-side filter — no extra API call is made. SearchInput holds a text state and, on form submission, filters the already-loaded conversations list by fullName:
// frontend/src/components/sidebar/SearchInput.jsx
const conversation = conversations.find((c) =>
  c.fullName.toLowerCase().includes(search.toLowerCase())
);

if (conversation) {
  setSelectedConversation(conversation);
  setSearch("");
} else {
  toast.error("No such user found!");
}
The search input requires at least 3 characters before attempting a match. Shorter queries are rejected with a toast notification to avoid false positives on very short names.

Frontend Messaging Hooks

HookWhat it doesReturns
useSendMessagePOSTs a message to /api/messages/send/:id and appends the result to the Zustand messages array{ sendMessage, loading }
useGetMessagesFetches full message history for selectedConversation on mount and when the conversation changes{ messages, loading }
useListenMessagesSubscribes to the "newMessage" socket event; plays a sound and sets shouldShakevoid (no return value)

Using useSendMessage in a component

import { useState } from "react";
import useSendMessage from "../hooks/useSendMessage";

const MessageInput = () => {
  const [text, setText] = useState("");
  const { sendMessage, loading } = useSendMessage();

  const handleSubmit = async (e) => {
    e.preventDefault();
    if (!text.trim()) return;
    await sendMessage(text);
    setText("");
  };

  return (
    <form onSubmit={handleSubmit} className="flex gap-2">
      <input
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="Type a message…"
        className="input input-bordered flex-1"
      />
      <button type="submit" disabled={loading} className="btn btn-primary">
        Send
      </button>
    </form>
  );
};

export default MessageInput;

Zustand Conversation Store

Global messaging state is managed by the useConversation Zustand store, making it accessible from any component without prop drilling:
// frontend/src/zustand/useConversation.js
const useConversation = create((set) => ({
  selectedConversation: null,
  setSelectedConversation: (selectedConversation) => set({ selectedConversation }),
  messages: [],
  setMessages: (messages) => set({ messages }),
}));
State keyTypeDescription
selectedConversationobject | nullThe contact/conversation currently open in the chat panel
messagesarrayThe message history for selectedConversation

Build docs developers (and LLMs) love