This tutorial will explain how to build a chat application with Python, Django and React.
Unlike other tutorials, I’m not using Python/Django for WebSocket connections. While doing so may seem cool from a tech perspective, it's pretty sluggish and expensive – especially if you have a halfway decent number of users. Languages such as C++, Go and Elixir are much better at handling the core of chat.
In this tutorial, we will use Stream, an API for chat that takes care of WebSocket connections and other heavy lifting using Go, Raft and RocksDB.
Table of Contents:
- React Chat Demo UI
- Django/Python Setup
- User Auth
- Django Rest Framework
- Generating Tokens to Access Stream's Chat Server
- Integrating Auth in React
- Sending a Message from the Python Server
- Final Thoughts
The GitHub repo for the code below can be found at https://github.com/GetStream/python-chat-example.
Let's code! 🤓
Step 1 – React Chat Demo UI
Before we start thinking about the Python chat side of things let's spin up a simple React frontend, so we have something nice and visual to look at:
$ yarn global add create-react-app
$ brew install node && brew install yarn # skip if installed
$ create-react-app chat-frontend
$ cd chat-frontend
$ yarn add stream-chat-react
Replace the code in src/App.js
with:
import React from "react";
import {
Chat,
Channel,
ChannelHeader,
Thread,
Window
} from "stream-chat-react";
import { MessageList, MessageInput } from "stream-chat-react";
import { StreamChat } from "stream-chat";
import "stream-chat-react/dist/css/index.css";
const chatClient = new StreamChat("qk4nn7rpcn75"); // Demo Stream Key
const userToken =
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiY29vbC1za3ktOSJ9.mhikC6HPqPKoCP4aHHfuH9dFgPQ2Fth5QoRAfolJjC4"; // Demo Stream Token
chatClient.setUser(
{
id: "cool-sky-9",
name: "Cool sky",
image: "https://getstream.io/random_svg/?id=cool-sky-9&name=Cool+sky"
},
userToken
);
const channel = chatClient.channel("messaging", "godevs", {
// image and name are required, however, you can add custom fields
image:
"https://cdn.chrisshort.net/testing-certificate-chains-in-go/GOPHER_MIC_DROP.png",
name: "Talk about Go"
});
const App = () => (
<Chat client={chatClient} theme={"messaging light"}>
<Channel channel={channel}>
<Window>
<ChannelHeader />
<MessageList />
<MessageInput />
</Window>
<Thread />
</Channel>
</Chat>
);
export default App;
Next, run yarn start
to see the chat in action!
Step 2 - Django/Python Setup (skip if you already have it)
Make sure you have Python 3.7 up and running.
$ brew install python3
$ pip install virtualenv virtualenvwrapper
$ export WORKON_HOME=~/Envs
$ source /usr/local/bin/virtualenvwrapper.sh
$ mkvirtualenv chatexample -p `which python3`
$ workon chatexample
If that does not work, please try this snippet:
$ python3 -m venv chatexample
$ source chatexample/bin/activate
Now that you’re in your virtual env you should see python 3 when you run:
$ python --version
To kick off a new Django project, use the following snippet:
$ pip install django
$ django-admin startproject mychat
And to start your app:
$ cd mychat
$ python manage.py runserver
Now, when you open http://localhost:8000
, you should see this:
Step 3 - User Auth
As a next step lets setup Django’s user auth.
$ python manage.py migrate
$ python manage.py createsuperuser
$ python manage.py runserver
Visit http://localhost:8000/admin/
and login. Voila!
You should see the Django admin screen as shown below:
Step 4 - Django Rest Framework
One of my favorite packages for integrating react with Django is Django Rest Framework. To make everything work, we will need to create endpoints for:
- User Signup
- User Login
We could build those ourselves; however, there is a package called Djoser that has already solved this problem. It configured the necessary API endpoints for user registration, login, password reset, etc.
To install Djoser, use the following snippet:
$ pip install djangorestframework djoser
Then, edit urls.py
and change the file to contain:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('auth/', include('djoser.urls')),
path('auth/', include('djoser.urls.authtoken')),
]
Once complete, edit settings.py
and make the following changes:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'rest_framework.authtoken',
'djoser',
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.TokenAuthentication',
)
}
For more on the API endpoints that Djoser exposes, have a look at this:
https://djoser.readthedocs.io/en/latest/sample_usage.html
Now, let’s go ahead and test the registration endpoint:
$ curl -X POST http://127.0.0.1:8000/auth/users/ --data 'username=djoser&password=alpine12'
Step 5 - Generating Tokens to Access Stream's Chat Server
Now we need to customize the Djoser views to generate tokens for Stream. Let’s get started.
Let's organize our files a bit and create a chat app folder in our project (make sure that you are in the correct directory):
$ python manage.py startapp auth
Install stream-chat:
$ pip install stream-chat
Create a custom serializer in auth/serializers.py
with the following logic:
from djoser.serializers import TokenSerializer
from rest_framework import serializers
from djoser.conf import settings as djoser_settings
from stream_chat import StreamChat
from django.conf import settings
class StreamTokenSerializer(TokenSerializer):
stream_token = serializers.SerializerMethodField()
class Meta:
model = djoser_settings.TOKEN_MODEL
fields = ('auth_token','stream_token')
def get_stream_token(self, obj):
client = StreamChat(api_key=settings.STREAM_API_KEY, api_secret=settings.STREAM_API_SECRET)
token = client.create_token(obj.user.id)
return token
And last, use the custom serializer by updating your settings.py
file:
STREAM_API_KEY = YOUR_STREAM_API_KEY # https://getstream.io/dashboard/
STREAM_API_SECRET = YOUR_STREAM_API_SECRET
DJOSER = {
'SERIALIZERS': {
'token': 'auth.serializers.StreamTokenSerializer',
}
}
Rerun your migration:
$ python manage.py migrate
To verify that it works, hit the login endpoint with a POST request:
$ curl -X POST http://127.0.0.1:8000/auth/token/login/ --data 'username=djoser&password=alpine12'
Both the auth_token
and stream_token
should be returned.
Step 6 - Integrating Auth in React
Adding an auth later to the frontend is an essential step for obvious reasons. In our case, it’s especially useful because we can fetch a user token from the backend API (powered by Python) and dynamically use it when sending messages.
First, install the CORS middleware package for Django:
$ pip install django-cors-headers
Then, modify your settings.py
to reference the djors-cors-header
middleware:
INSTALLED_APPS = (
...
'corsheaders',
...
)
MIDDLEWARE = [
...
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
...
]
And finally, add the following to your settings.py
file:
CORS_ORIGIN_ALLOW_ALL = True
The next step requires a few modifications to be made to your frontend. To start, you will want to ensure that you have all of the dependencies installed via yarn:
$ yarn add axios react-dom react-router-dom
Next, create the following files within your src/
directory:
- AuthedRoute.js
- UnauthedRoute.js
- withSession.js
- Login.js
- Chat.js
App.js
import React from "react";
import { BrowserRouter as Router, Switch } from "react-router-dom";
import Chat from "./Chat";
import Login from "./Login";
import UnauthedRoute from "./UnauthedRoute";
import AuthedRoute from "./AuthedRoute";
const App = () => (
<Router>
<Switch>
<UnauthedRoute path="/auth/login" component={Login} />
<AuthedRoute path="/" component={Chat} />
</Switch>
</Router>
);
export default App;
AuthedRoute.js
import React from "react";
import { Redirect, Route } from "react-router-dom";
const AuthedRoute = ({ component: Component, loading, ...rest }) => {
const isAuthed = Boolean(localStorage.getItem("token"));
return (
<Route
{...rest}
render={props =>
loading ? (
<p>Loading...</p>
) : isAuthed ? (
<Component history={props.history} {...rest} />
) : (
<Redirect
to={{
pathname: "/auth/login",
state: { next: props.location }
}}
/>
)
}
/>
);
};
export default AuthedRoute;
UnauthedRoute.js
import React from "react";
import { Redirect, Route } from "react-router-dom";
const AuthedRoute = ({ component: Component, loading, ...rest }) => {
const isAuthed = Boolean(localStorage.getItem("token"));
return (
<Route
{...rest}
render={props =>
loading ? (
<p>Loading...</p>
) : !isAuthed ? (
<Component history={props.history} {...rest} />
) : (
<Redirect
to={{
pathname: "/"
}}
/>
)
}
/>
);
};
export default AuthedRoute;
withSession.js
import React from "react";
import { withRouter } from "react-router";
export default (Component, unAuthed = false) => {
const WithSession = ({ user = {}, streamToken, ...props }) =>
user.id || unAuthed ? (
<Component
userId={user.id}
user={user}
session={window.streamSession}
{...props}
/>
) : (
<Component {...props} />
);
return withRouter(WithSession);
};
Login.js
import React, { Component } from "react";
import axios from "axios";
class Login extends Component {
constructor(props) {
super(props);
this.state = {
loading: false,
email: "",
password: ""
};
this.initStream = this.initStream.bind(this);
}
async initStream() {
await this.setState({
loading: true
});
const base = "http://localhost:8000";
const formData = new FormData();
formData.set("username", this.state.email);
formData.set("password", this.state.password);
const registration = await axios({
method: "POST",
url: `${base}/auth/users/`,
data: formData,
config: {
headers: { "Content-Type": "multipart/form-data" }
}
});
const authorization = await axios({
method: "POST",
url: `${base}/auth/token/login/`,
data: formData,
config: {
headers: { "Content-Type": "multipart/form-data" }
}
});
localStorage.setItem("token", authorization.data.stream_token);
await this.setState({
loading: false
});
this.props.history.push("/");
}
handleChange = e => {
this.setState({
[e.target.name]: e.target.value
});
};
render() {
return (
<div className="login-root">
<div className="login-card">
<h4>Login</h4>
<input
type="text"
placeholder="Email"
name="email"
onChange={e => this.handleChange(e)}
/>
<input
type="password"
placeholder="Password"
name="password"
onChange={e => this.handleChange(e)}
/>
<button onClick={this.initStream}>Submit</button>
</div>
</div>
);
}
}
export default Login;
Chat.js
import React, { Component } from "react";
import {
Chat,
Channel,
ChannelHeader,
Thread,
Window
} from "stream-chat-react";
import { MessageList, MessageInput } from "stream-chat-react";
import { StreamChat } from "stream-chat";
import "stream-chat-react/dist/css/index.css";
class App extends Component {
constructor(props) {
super(props);
this.client = new StreamChat("<YOUR_STREAM_APP_ID>");
this.client.setUser(
{
id: "cool-sky-9",
name: "Cool Sky",
image: "https://getstream.io/random_svg/?id=cool-sky-9&name=Cool+sky"
},
localStorage.getItem("token")
);
this.channel = this.client.channel("messaging", "godevs", {
image:
"https://cdn.chrisshort.net/testing-certificate-chains-in-go/GOPHER_MIC_DROP.png",
name: "Talk about Go"
});
}
render() {
return (
<Chat client={this.client} theme={"messaging light"}>
<Channel channel={this.channel}>
<Window>
<ChannelHeader />
<MessageList />
<MessageInput />
</Window>
<Thread />
</Channel>
</Chat>
);
}
}
export default App;
Be sure to replace YOUR_STREAM_APP_ID
with a valid Stream App ID which can be found on the dashboard.
Restart your frontend application and you should be hit with an auth wall! Enter your email and password and a token will be requested and stored in local storage.
Step 7 - Sending a Message from the Python chat server
Occasionally, you will want to write to the chat API using your backend Python-based server. Here’s a quick management command that you can use:
Verify that installed apps looks like this in settings.py
:
INSTALLED_APPS = [
'corsheaders',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'rest_framework.authtoken',
'djoser',
]
Next, create the directory chat/management/commands
. In that directory, add a file called broadcast.py
with this content:
from django.core.management.base import BaseCommand, CommandError
from django.conf import settings
from stream_chat import StreamChat
class Command(BaseCommand):
help = 'Broadcast the message on your channel'
def add_arguments(self, parser):
parser.add_argument('--message')
def handle(self, *args, **options):
client = StreamChat(api_key=settings.STREAM_API_KEY, api_secret=settings.STREAM_API_SECRET)
client.update_user({"id": "system", "name": "The Server"})
channel = client.channel("messaging", "kung-fu")
channel.create("system")
response = channel.send_message({"text": "AMA about kung-fu"}, 'system')
self.stdout.write(self.style.SUCCESS('Successfully posted a message with id "%s"' % response['message']['id']))
You can try posting a message to the chat like this:
$ python manage.py broadcast --message hello
And you should see a response like this:
Final Thoughts
I hope you enjoyed this tutorial on building a chat application with Django, Python and React!
For an interactive tour of Stream Chat, please have a look at our API Tutorial on the Stream website. If you are interested in digging into the code for Stream Chat React Components, the full docs can be found here. If you are interested in building chat on top of Stream, we offer various SDKs for popular languages and frameworks with our latest being iOS (Swift).
Happy coding! ✌
Top comments (25)
Thanks for putting this together. I was able to complete steps 1-5. When I tried to complete Step 6 I get the following error message:
Error: userToken does not have a user_id or is not matching with user.id
StreamChat.setUser
node_modules/stream-chat/dist/browser.es.js:3186
new App
localhost:3001/static/js/main.chun...
Any suggestions on what I might be doing wrong?
I ran into this same issue. Couldn't figure out if it was because the chat app was named auth but not listed as an installed app, or if it was because the UnauthedRoute.js has the function name as the one in AuthedRoute.js. If it has something to do with the encoded user id in the generated token not matching the cool-sky-9 id user listed in the demo. Did everything I could to troubleshoot it and couldn't figure it out. I just know that no API call is being made. Honestly at a loss here on getting this to work.
Edit: I figure it out! The issue is that the key on the backend is being generated with obj.user.id in the serializer. This needs to be changed to obj.user.username if you want to match the username on the front end. In this particular demo cool-sky-9 or whatever have you. This will let you authenticate. The only issue you should run into after this is that you need your API keys from the CHAT part of the website. To have the chat functionality there is a free trial otherwise it's $499 a month from what I saw. So if you're looking at this for a personal project or something fun make sure you don't get charged!
I had the same issue and this was the solution. The 'id' value from client.setUser must match the argument for client.createToken
const streamToken = client.createToken('exactNameHere');
await client.setUser(
{
id: 'exactNameHere'
},
Same error here.
Error: userToken does not have a user_id or is not matching with user.id
All I changed:
mychat/settings.py on line 47 and 48 => 4tk***, zgbw6*********************************
chat-frontend/src/Chat.js on line 17 => this.client = new StreamChat("60XXX");
I have the same error, did you guys find a fix ?
Great tuto though.
Happy to help. Any chance your API key is mixed up / not the same between your app and server-side code? This would throw that kind of error.
I have the same issue and I am using the following:
For the Frontend
this.client = new StreamChat("69XXX");
For the Backend at settings.py
STREAM_API_KEY = "g4eXXXXXXXXX"
STREAM_API_SECRET = "4wyXXXXXXXXX..."
I have the Chat.js and settings.py files updated with the same API key. Is there somewhere else the API key needs to be set that I missed?
The problem is that the Django User ID is an integer and the Chat is missing some code.
Following my examples:
github.com/rhenter/stream-chat-api
github.com/rhenter/stream-chat-react
PS: In the React Project I didn't create a Custom Readme, so you must to add you credentials on Chat.js if you wore to try to use
nice job on the writing Nick.
Thank you!
Thanks for reminding me how easy it is to do APIs with Django! I have an upcoming project that needs an admin frontend, a batch-ish portion, and an API all three; going with Django will make it easy to just get it done.
How to deploy it on Heroku?
Should I use 2 plans?
Or I can deploy it in one plan?
I want to use Django with React, but got confused about the deployment, and how to set Static and Media, and combining templates and components.
Because React and Django have different folder location.
Especially, I've developed an app using Django, and want to using React on the way and not from the start.
Or I should stick with only Jquery Ajax instead of React for my front end?
Please advise. Thanks
At this stage, Django Channels ( WebSocket support ) is more matured and can able to use in production.
Django Channels work, but the performance just isn't great. It's a good fit if your app is small. If it grows you'll want to use C++, Java, Elixir or Golang (my favorite) for websockets in order to be cost effective.
What benchmarks are you using to determine the performance evaluation?
True, synchronous code running on an internal thread executor on an event loop will be slower than fully async, but still curious.
And which version of channels did you try most recently?
Building a live chat app is a fun and a proper challenge. It's not your simple management system and its something unique. Thanks Nick for sharing this tutorial.
Nice tuto, Thanks!
Happy to put it out in the wild! Enjoy!
Have questions? Drop them here! I'm happy to help you out!
Cool way to use it as I had problems in using web sockets with Django in the past.
Due to the complexity and rewrite of the libraries with very little tutorial information.
Website to Learn Python, Machine Learning And Data Science.
bit.ly/codeperfectplus