I wrote this tutorial to show you the simplest way to add async, real-time events with Django. This includes:
pip
command to install Daphne). 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:
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 ๐
pip install django daphne
django-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)
INSTALLED_APPS
in core/settings.py
# core/settings.py
INSTALLED_APPS = [
'daphne', # Add this at the top.
# ...
'sim',
# ...
]
ASGI_APPLICATION
in core/settings.py
# core/settings.py
ASGI_APPLICATION = 'core.asgi.application'
sim/views.py
:from datetime import datetime
import asyncio
from typing import AsyncGenerator
from django.shortcuts import render, redirect
from django.http import HttpRequest, StreamingHttpResponse, HttpResponse
from . import models
import json
import random
def lobby(request: HttpRequest) -> HttpResponse:
if request.method == 'POST':
username = request.POST.get('username')
if username:
request.session['username'] = username
else:
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 data
to the connected clients.
"""
async for message in get_existing_messages():
yield message
last_id = await get_last_message_id()
# Continuously check for new messages
while 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 0
return StreamingHttpResponse(event_stream(), content_type='text/event-stream')
sim/urls.py
and add the following:from django.urls import path
from . import views
urlpatterns = [
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'),
]
core/urls.py
to include the app's URLs# core/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('sim.urls')),
]
templates
in the sim
directory.chat.html
in the templates
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>
<form x-cloak
@submit.prevent="submit" x-data="{state: 'composing', errors: {}}">
<div>
<textarea name="content" @input="state = 'composing'" autofocus placeholder="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 eventSource;
const 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>
<script defer="" src="https://cdn.jsdelivr.net/npm/alpinejs@3.12.3/dist/cdn.min.js"></script>
</body>
</html>
lobby.html
in the templates
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>
sim/models.py
:from django.db import models
class 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)
python manage.py makemigrations
python manage.py migrate
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.
http://127.0.0.1:8000/
in your web browser.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>
<form x-cloak
@submit.prevent="submit" x-data="{state: 'composing', errors: {}}">
<div>
<textarea name="content" @input="state = 'composing'" autofocus placeholder="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 eventSource;
const 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>
<script defer="" src="https://cdn.jsdelivr.net/npm/alpinejs@3.12.3/dist/cdn.min.js"></script>
</body>
</html>
http://127.0.0.1:8000/
in your web browser.Congratulations. You've built a real-time instant messenger app with Django and server-sent events, using the latest async Django features. ๐
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
Do you also dream of creating Django products so quickly they break the space-time continuum? Yeah, me too. We're like Django wizards, eager to turn our magical ideas into reality at the snap of our fingers.
Well, let me introduce you to the magic wand I'm building: Photon Designer. It's a visual editor that puts the 'fast' in 'very fast.' When Photon Designer gets going, it slings Django templates at you faster than light escaping a black hole.
Warning: may cause excessive joy and productivity.