In this tutorial, we will walk through the step by step to create a simple chat application, where users can exchange messages in real time.
Prerequisites
Before we begin, you should have the following installed on your system:
Python 3.6 or higher
Node.js 14 or higher
Django 3 or higher
Django Channels 3 or higher
React.js
PostgreSQL
WebSocket API
Setting up the Backend — Django
- Create a new Django project by running the following command:
django-admin startproject chatapp
- Create a new Django app within your project by running the following command:
python manage.py startapp chat
- Install Django Channels and REST Framework using pip:
pip install channels djangorestframework psycopg2
- Add the following lines to your project’s
settings.py
file:
INSTALLED_APPS = [
# ...
'chat',
'channels',
'rest_framework',
]
ASGI_APPLICATION = 'chatapp.asgi.application'
The purpose of using ASGI is to enable asynchronous features within your Django project, such as handling WebSockets, HTTP2, and other protocols that require asynchronous communication.
- Create a new file called
routing.py
in your project's root directory with the following contents:
from channels.routing import ProtocolTypeRouter, URLRouter
from django.urls import re_path
from chat.consumers import ChatConsumer
websocket_urlpatterns = [
re_path(r'chat/', ChatConsumer.as_asgi()),
]
- Update the
asgi.py
file in your project's root directory with the following contents:
import os
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from . import routing
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "chatapp.settings")
application = ProtocolTypeRouter({
"http": get_asgi_application(),
"websocket": AuthMiddlewareStack(
URLRouter(
routing.websocket_urlpatterns
)
),
})
We’re using the ProtocolTypeRouter
to route HTTP and WebSocket requests to the appropriate handler.
- Create a new file named
models.py
in thechat
directory.
from django.db import models
class Message(models.Model):
username = models.CharField(max_length=255)
content = models.TextField()
timestamp = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "chat_message"
ordering = ('timestamp',)
This Message
model defines the fields for a message.
- Create a new file named
serializers.py
in thechat
directory.
from rest_framework import serializers
from .models import Message
class MessageSerializer(serializers.ModelSerializer):
class Meta:
model = Message
fields = ('id', 'username', 'content', 'timestamp')
read_only_fields = ('id', 'timestamp')
This MessageSerializer
class describes how the Message
model is serialized and deserialized.
- Create a new file named
views.py
in thechat
directory.
from rest_framework import generics
from .models import Message
from .serializers import MessageSerializer
class MessageList(generics.ListCreateAPIView):
queryset = Message.objects.all()
serializer_class = MessageSerializer
ordering = ('-timestamp',)
This MessageList
class defines a view that lists all messages and allows new messages to be created.
- Add the following line to the
urls.py
file in thechat
directory.
from django.urls import path
from .views import MessageList
urlpatterns = [
path('messages/', MessageList.as_view()),
]
This defines the URL for the MessageList
view.
- To include the chat app URLs and Web Socket routing in the root URL configuration, we need to add the
include()
function inurls.py
located in the project's root directory.
from django.contrib import admin
from django.urls import path, include
from . import routing
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('chat.urls')),
path('ws/', include(routing.websocket_urlpatterns)),
]
- Create a new file called
consumers.py
in your app's directory with the following contents:
import json
from channels.generic.websocket import AsyncWebsocketConsumer
from channels.layers import get_channel_layer
from channels.db import database_sync_to_async
from .models import Message
class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.room_group_name = 'chat'
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
await self.accept()
async def disconnect(self, close_code):
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
@database_sync_to_async
def create_message(self, message, username):
message_obj = Message.objects.create(
content=message,
username=username
)
return message_obj
async def receive(self, text_data):
data = json.loads(text_data)
message = data['message']
username = data['username']
# Create a new message object and save it to the database
message_obj = await self.create_message(message, username)
# Send the message to the group
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'chat_message',
'message': message_obj.content,
'username': message_obj.username,
'timestamp': str(message_obj.timestamp)
}
)
async def chat_message(self, event):
message = event['message']
username = event['username']
timestamp = event['timestamp']
# Send the message to the websocket
await self.send(text_data=json.dumps({
'message': message,
'username': username,
'timestamp': timestamp
}))
The ChatConsumer
class handles WebSocket connections. The connect()
method is called when a new WebSocket connection is established. It adds the channel to the chat
group, which allows the server to send messages to all connected clients. The disconnect()
method removes the channel from the group when the connection is closed. The receive()
method is called when the server receives a WebSocket message. It sends the message to all connected clients using the group_send()
method. The chat_message()
method is called when the server receives a message from the chat
group. It sends the message to the WebSocket connection using the send()
method.
To make inserting a message into the database an asynchronous operation, we import database_sync_to_async
from channels.db
, and then define a new method called create_message
which uses the @database_sync_to_async
decorator to make it asynchronous.
- Start the development server:
$ python manage.py makemigrations
$ python manage.py migrate
$ python manage.py runserver
Your Django Channels server is now running and listening for WebSocket connections at ws://localhost:8000/ws/chat/
.
Setting up the Frontend — React
- Create a new React project.
$ npx create-react-app chatapp-frontend
- Install React WebSocket.
$ npm install --save react-websocket
- Create a new file named
Chat.js
in thesrc
directory.
import React, { useState, useEffect } from "react";
import { w3cwebsocket as W3CWebSocket } from "websocket";
function Chat() {
const [socket, setSocket] = useState(null);
const [username, setUsername] = useState("");
const [message, setMessage] = useState("");
const [messages, setMessages] = useState([]);
useEffect(() => {
// Get the username from local storage or prompt the user to enter it
const storedUsername = localStorage.getItem("username");
if (storedUsername) {
setUsername(storedUsername);
} else {
const input = prompt("Enter your username:");
if (input) {
setUsername(input);
localStorage.setItem("username", input);
}
}
// Connect to the WebSocket server with the username as a query parameter
const newSocket = new WebSocket(`ws://localhost:8000/ws/chat/`);
setSocket(newSocket);
newSocket.onopen = () => console.log("WebSocket connected");
newSocket.onclose = () => console.log("WebSocket disconnected");
// Clean up the WebSocket connection when the component unmounts
return () => {
newSocket.close();
};
}, [username]);
useEffect(() => {
if (socket) {
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
setMessages((prevMessages) => [...prevMessages, data]);
};
}
}, [socket]);
const handleSubmit = (event) => {
event.preventDefault();
if (message && socket) {
const data = {
message: message,
username: username,
};
socket.send(JSON.stringify(data));
setMessage("");
}
};
return (
<div className="chat-container">
<div className="chat-header">Chat</div>
<div className="message-container">
{messages.map((message, index) => (
<div key={index} className="message">
<div className="message-username">{message.username}:</div>
<div className="message-content">{message.message}</div>
<div className="message-timestamp">{message.timestamp}</div>
</div>
))}
</div>
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Type a message..."
value={message}
onChange={(event) => setMessage(event.target.value)}
/>
<button type="submit">Send</button>
</form>
</div>
);
}
export default Chat;
4. Open the App.js
file and replace its contents with the following.
import React from 'react';
import Chat from './Chat';
function App() {
return (
<div className="App">
<Chat />
</div>
);
}
export default App;
Start the React development server.
$ cd chatapp-frontend
$ npm start
7. Open a web browser and navigate to http://localhost:3000/
Congratulations, you have created a live updating chat application using Django Channels and React web sockets!
Leave a comment if you need the full source code.