The simplest way to build an instant messaging app with Django ๐ฎ
I wrote this tutorial to show you the simplest way to add async, real-time events with Django. This includes:
- no heavy dependencies. No Redis. No extra Django channels installation.
- reading from the Django database in real-time ๐๏ธ. We use the new async Django features
- using the lightest available setup (one
pip
command to install Daphne).- Daphne is fully-integrated into Django.
- Daphne is very easy to deploy in production: 2 lines (see here).
- fast to do โฐ I want to learn things as fast as possible. You probably do too.
Our finished product will look like this:
I've made a easy-to-follow video guide (featuring me ๐๐ฟ) that goes along with the step-by-step instructions. Here's the video:
Motivation:
Whenever I've looked online in the past for a nice example about adding real-time asynchronous events with Django, I've only found lengthy, complex articles - weighed down with dependencies and heavy services to install (like Redis).
Lots of steps.
Not any more.
This guide shows you the simplest way to add real-time events to Django ๐ฎ
Let's go ๐
0. Setup Django and Daphne
pip install django daphnedjango-admin startproject core .python manage.py startapp sim
Note: Make sure to use >=Django 4.2 . Otherwise, you will get errors like TypeError: async_generator object is not iterable
when using async views.
(Thanks to Daniel for pointing this out in the Youtube comments)
Add 'daphne' and your app to INSTALLED_APPS
in core/settings.py
# core/settings.pyINSTALLED_APPS = ['daphne', # Add this at the top.# ...'sim',# ...]
Set ASGI_APPLICATION
in core/settings.py
- Add this line anywhere in the file.
# core/settings.pyASGI_APPLICATION = 'core.asgi.application'
2. Add async and sync Django views to stream data
- Add the below to
sim/views.py
:
from datetime import datetimeimport asynciofrom typing import AsyncGeneratorfrom django.shortcuts import render, redirectfrom django.http import HttpRequest, StreamingHttpResponse, HttpResponsefrom . import modelsimport jsonimport randomdef lobby(request: HttpRequest) -> HttpResponse:if request.method == 'POST':username = request.POST.get('username')if username:request.session['username'] = usernameelse:names = ["Horatio", "Benvolio", "Mercutio", "Lysander", "Demetrius", "Sebastian", "Orsino","Malvolio", "Hero", "Bianca", "Gratiano", "Feste", "Antonio", "Lucius", "Puck", "Lucio","Goneril", "Edgar", "Edmund", "Oswald"]request.session['username'] = f"{random.choice(names)}-{hash(datetime.now().timestamp())}"return redirect('chat')else:return render(request, 'lobby.html')def chat(request: HttpRequest) -> HttpResponse:if not request.session.get('username'):return redirect('lobby')return render(request, 'chat.html')def create_message(request: HttpRequest) -> HttpResponse:content = request.POST.get("content")username = request.session.get("username")if not username:return HttpResponse(status=403)author, _ = models.Author.objects.get_or_create(name=username)if content:models.Message.objects.create(author=author, content=content)return HttpResponse(status=201)else:return HttpResponse(status=200)async def stream_chat_messages(request: HttpRequest) -> StreamingHttpResponse:"""Streams chat messages to the client as we create messages."""async def event_stream():"""We use this function to send a continuous stream of datato the connected clients."""async for message in get_existing_messages():yield messagelast_id = await get_last_message_id()# Continuously check for new messageswhile True:new_messages = models.Message.objects.filter(id__gt=last_id).order_by('created_at').values('id', 'author__name', 'content')async for message in new_messages:yield f"data: {json.dumps(message)}\n\n"last_id = message['id']await asyncio.sleep(0.1) # Adjust sleep time as needed to reduce db queries.async def get_existing_messages() -> AsyncGenerator:messages = models.Message.objects.all().order_by('created_at').values('id', 'author__name', 'content')async for message in messages:yield f"data: {json.dumps(message)}\n\n"async def get_last_message_id() -> int:last_message = await models.Message.objects.all().alast()return last_message.id if last_message else 0return StreamingHttpResponse(event_stream(), content_type='text/event-stream')
3. Add URLs for your async and sync views
- Create
sim/urls.py
and add the following:
from django.urls import pathfrom . import viewsurlpatterns = [path('lobby/', views.lobby, name='lobby'),path('', views.chat, name='chat'),path('create-message/', views.create_message, name='create-message'),path('stream-chat-messages/', views.stream_chat_messages, name='stream-chat-messages'),]
Update core/urls.py
to include the app's URLs
# core/urls.pyfrom django.contrib import adminfrom django.urls import path, includeurlpatterns = [path('admin/', admin.site.urls),path('', include('sim.urls')),]
4. Add your templates, including an EventSource script to receive your server-sent events from Django
- Create a directory named
templates
in thesim
directory. - Create a file named
chat.html
in thetemplates
directory.
<!doctype html><html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Chat</title></head><body><div class="header"><h1>Welcome {{ request.session.username }}</h1></div><div class="container"><div class="messages"><div id="sse-data"></div></div><formx-cloak@submit.prevent="submit"x-data="{state: 'composing', errors: {}}"><div><textareaname="content"@input="state = 'composing'"autofocusplaceholder="Your next message..."></textarea><button class="button">Send</button></div><div x-show="state === 'error'"><p>Error sending your message โ</p></div></form><form action="/lobby/" method="get"><button type="submit">Return to Lobby</button></form></div><script>let eventSourceconst sseData = document.getElementById('sse-data')function startSSE() {eventSource = new EventSource('/stream-chat-messages/')eventSource.onmessage = (event) => {const data = JSON.parse(event.data)const messageHTML = `<div class="message-box"><div class="message-author">${data.author__name}</div><div class="message-content">${data.content}</div></div>`sseData.innerHTML += messageHTML}}// On load, start SSE if the browser supports it.if (typeof EventSource !== 'undefined') {startSSE()} else {sseData.innerHTML ="Whoops! Your browser doesn't receive server-sent events."}</script><script>function submit(event) {event.preventDefault()const formData = new FormData(event.target)const endpointUrl = '/create-message/'fetch(endpointUrl, {method: 'post',body: formData,headers: {'X-CSRFToken': '{{ csrf_token }}',},}).then((response) => {this.state = response.ok ? 'success' : 'error'return response.json()}).then((data) => {this.errors = data.errors || {}})}</script><scriptdefer=""src="https://cdn.jsdelivr.net/npm/alpinejs@3.12.3/dist/cdn.min.js"></script></body></html>
Add a lobby, where users choose a name
- Create a file named
lobby.html
in thetemplates
directory.
<!doctype html><html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Sign-in Page</title><style>body {font-family: 'Helvetica Neue', sans-serif;display: flex;justify-content: center;align-items: center;height: 100vh;background-color: #e8eff1;margin: 0;color: #333;}.sign-in-container {background: #ffffff;padding: 40px 50px;border-radius: 8px;box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);width: 300px;}.sign-in-container h2 {text-align: center;margin-bottom: 30px;font-size: 24px;color: #0a3d62;}.sign-in-container form {display: flex;flex-direction: column;}.sign-in-container input {margin-bottom: 15px;padding: 15px;border: 1px solid #ced6e0;border-radius: 6px;font-size: 16px;}.sign-in-container button {padding: 15px;background-color: #2ecc71;color: white;border: none;border-radius: 6px;cursor: pointer;font-size: 16px;transition: background-color 0.3s;}.sign-in-container button:hover {background-color: #27ae60;}</style></head><body><div class="sign-in-container"><h2>Enter your chat name:</h2><form method="post">{% csrf_token %}<input type="text" name="username" placeholder="Username" required /><button type="submit">Join the chat</button></form></div></body></html>
6. Create Django models to store data to send in real-time
- Add the below to
sim/models.py
:
from django.db import modelsclass Author(models.Model):name = models.CharField(max_length=500)class Message(models.Model):author = models.ForeignKey(Author, on_delete=models.CASCADE)content = models.TextField()created_at = models.DateTimeField(auto_now_add=True)
- Run migrations to create the database table for the new model.
python manage.py makemigrationspython manage.py migrate
7. Run ๐โโ๏ธ
- Run the Django app:
python manage.py runserver
You should see something similar to the examples given below. Note that the Daphne server is working:
Django version 4.2.7, using settings 'core.settings'Starting ASGI/Daphne version 4.0.0 development server at http://127.0.0.1:8000/Quit the server with CONTROL-C.
Access your Django application to see the realtime server-sent events
- Visit
http://127.0.0.1:8000/
in your web browser.
8. Bonus: Add styling to your chat interface
- Add styling to the
chat.html
template to include the chat interface and styling. Here's the full template:
<!doctype html><html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Chat</title><style>body {font-family: 'Arial', sans-serif;background-color: #e8eff1;margin: 0;padding: 0;color: #333;}.header {color: #022c22;font-size: 14px;text-align: center;}.container {max-width: 60%;margin: auto;}.messages {background: #ffffff;border-radius: 8px;padding: 20px;margin-bottom: 30px;box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);font-size: 16px;height: 50vh;overflow-y: scroll;}.message {border-bottom: 1px solid #ced6e0;padding: 15px 0;}.message:last-child {border-bottom: none;}form {display: flex;flex-direction: column;}textarea,input,button {margin-bottom: 15px;padding: 15px;border: 1px solid #ced6e0;border-radius: 6px;font-size: 16px;}.button {background-color: #2ecc71;color: white;border: none;cursor: pointer;transition: background-color 0.3s;}.button:hover {background-color: #27ae60;}.message-box {background: rgba(247, 248, 245, 0.42);border-left: 4px solid rgba(51, 177, 104, 0.42);margin-bottom: 15px;padding: 10px 15px;border-radius: 6px;box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);}.message-author {font-weight: bold;margin-bottom: 5px;}.message-content {font-size: 16px;line-height: 1.4;}textarea {background: #f8f9fa;border: 1px solid #ced4da;box-sizing: border-box;width: 100%;padding: 12px 20px;border-radius: 6px;min-height: 100px;font-size: 16px;line-height: 1.5;resize: none;outline: none;}</style><style>[x-cloak] {display: none !important;}</style></head><body><div class="header"><h1>Welcome {{ request.session.username }}</h1></div><div class="container"><div class="messages"><div id="sse-data"></div></div><formx-cloak@submit.prevent="submit"x-data="{state: 'composing', errors: {}}"><div><textareaname="content"@input="state = 'composing'"autofocusplaceholder="Your next message..."></textarea><button class="button">Send</button></div><div x-show="state === 'error'"><p>Error sending your message โ</p></div></form><form action="/lobby/" method="get"><button type="submit">Return to Lobby</button></form></div><script>let eventSourceconst sseData = document.getElementById('sse-data')function startSSE() {eventSource = new EventSource('/stream-chat-messages/')eventSource.onmessage = (event) => {const data = JSON.parse(event.data)const messageHTML = `<div class="message-box"><div class="message-author">${data.author__name}</div><div class="message-content">${data.content}</div></div>`sseData.innerHTML += messageHTML}}// On load, start SSE if the browser supports it.if (typeof EventSource !== 'undefined') {startSSE()} else {sseData.innerHTML ="Whoops! Your browser doesn't receive server-sent events."}</script><script>function submit(event) {event.preventDefault()const formData = new FormData(event.target)const endpointUrl = '/create-message/'fetch(endpointUrl, {method: 'post',body: formData,headers: {'X-CSRFToken': '{{ csrf_token }}',},}).then((response) => {this.state = response.ok ? 'success' : 'error'return response.json()}).then((data) => {this.errors = data.errors || {}})}</script><scriptdefer=""src="https://cdn.jsdelivr.net/npm/alpinejs@3.12.3/dist/cdn.min.js"></script></body></html>
9. Check out your real-time Django instant messenger app with server-sent events
- Visit
http://127.0.0.1:8000/
in your web browser. - Open several tabs and see the updates in realtime๐ ๐ฎ
Congratulations. You've built a real-time instant messenger app with Django and server-sent events, using the latest async Django features. ๐
Next steps? Deploy ๐
If enough people are interested, I'll write my next guide to show you how to fully deploy this app online.
Edit: Thanks for the interest ๐ Here's the guide: How to deploy a Django instant messenger app with real-time events