In our messenger app, messages are stacked by conversations between two participants. You start a conversation providing the user you want to chat with, the conversations is created (if not exists already) and you can start sending messages to that conversations.
On the front-end we’re interested in showing a list of the lastest conversations. There we’ll show the last message of it and the name and avatar of the other participant.
In this post, we’ll code the endpoints to start a conversation, list the latest and find a single one.
These three endpoints require authentication so we use the `guard()` middleware. There is a new middleware that checks for the request content type JSON.
Messages are for the next post, but I define the struct now since we are using it. Most of the fields are the same as the database table. We have `Mine` to tell if the message is owned by the current authenticated user and `ReceiverID` will be used to filter messanges once we add realtime capabilities.
Lets write the HTTP handler then. It’s quite long but don’t be scared.
```
func createConversation(w http.ResponseWriter, r *http.Request) {
var input struct {
Username string `json:"username"`
}
defer r.Body.Close()
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
respondError(w, fmt.Errorf("could not insert participants: %v", err))
return
}
if err = tx.Commit(); err != nil {
respondError(w, fmt.Errorf("could not commit tx to create conversation: %v", err))
return
}
conversation.OtherParticipant = &otherParticipant
respond(w, conversation, http.StatusCreated)
}
```
For this endpoint you do a POST request to `/api/conversations` with a JSON body containing the username of the user you want to chat with.
So first it decodes the request body into an struct with the username. Then it validates that the username is not empty.
```
type Errors struct {
Errors map[string]string `json:"errors"`
}
```
This is the `Errors` struct. It’s just a map. If you enter an empty username you get this JSON with a `422 Unprocessable Entity` error.
```
{
"errors": {
"username": "Username required"
}
}
```
Then, we begin an SQL transaction. We only received an username, but we need the actual user ID. So the first part of the transaction is to query for the id and avatar of that user (the other participant). If the user is not found, we respond with a `404 Not Found` error. Also, if the user happens to be the same as the current authenticated user, we respond with `403 Forbidden`. There should be two different users, not the same.
Then, we try to find a conversation those two users have in common. We use `INTERSECT` for that. If there is one, we redirect to that conversation `/api/conversations/{conversationID}` and return there.
If no common conversation was found, we continue by creating a new one and adding the two participants. Finally, we `COMMIT` the transaction and respond with the newly created conversation.
### Get Conversations
This endpoint `/api/conversations` is to get all the conversations of the current authenticated user.
```
func getConversations(w http.ResponseWriter, r *http.Request) {
respondError(w, fmt.Errorf("could not iterate over conversations: %v", err))
return
}
respond(w, conversations, http.StatusOK)
}
```
This handler just does a query to the database. It queries to the conversations table with some joins… First, to the messages table to get the last message. Then to the participants, but it adds a condition to a participant whose ID is not the one of the current authenticated user; this is the other participant. Then it joins to the users table to get his username and avatar. And finally joins with the participants again but with the contrary condition, so this participant is the current authenticated user. We compare `messages_read_at` with the message `created_at` to know whether the conversation has unread messages. And we use the message `user_id` to check if it’s “mine” or not.
Note that this query assumes that a conversation has just two users. It only works for that scenario. Also, if you want to show a count of the unread messages, this design isn’t good. I think you could add a `unread_messages_count``INT` field on the `participants` table and increment it each time a new message is created and reset it when the user read them.
Then it iterates over the rows, scan each one to make an slice of conversations and respond with those at the end.
### Get Conversation
This endpoint `/api/conversations/{conversationID}` respond with a single conversation by its ID.
```
func getConversation(w http.ResponseWriter, r *http.Request) {
LEFT JOIN messages ON conversations.last_message_id = messages.id
INNER JOIN participants other_participants
ON other_participants.conversation_id = conversations.id
AND other_participants.user_id != $1
INNER JOIN users other_users ON other_participants.user_id = other_users.id
INNER JOIN participants auth_user
ON auth_user.conversation_id = conversations.id
AND auth_user.user_id = $1
WHERE conversations.id = $2
`, authUserID, conversationID).Scan(
&conversation.HasUnreadMessages,
&otherParticipant.ID,
&otherParticipant.Username,
&otherParticipant.AvatarURL,
); err == sql.ErrNoRows {
http.Error(w, "Conversation not found", http.StatusNotFound)
return
} else if err != nil {
respondError(w, fmt.Errorf("could not query conversation: %v", err))
return
}
conversation.ID = conversationID
conversation.OtherParticipant = &otherParticipant
respond(w, conversation, http.StatusOK)
}
```
The query is quite similar. We’re not interested in showing the last message, so we omit those fields, but we need the message to know whether the conversation has unread messages. This time we do a `LEFT JOIN` instead of an `INNER JOIN` because the `last_message_id` is `NULLABLE`; in other case we won’t get any rows. We use an `IFNULL` in the `has_unread_messages` comparison for that reason too. Lastly, we filter by ID.
If the query returns no rows, we respond with a `404 Not Found` error, otherwise `200 OK` with the found conversation.
* * *
Yeah, that concludes with the conversation endpoints.
Wait for the next post to create and list messages 👋