dev-resources.site
for different kinds of informations.
Implementing React and Django with Docker and Nginx
If youâre ever used Django before, youâve probably wanted to integrate it with a framework like React. And if youâve gone down this path, youâve probably spent a few hours looking for an easy way to do this, and youâve probably realized that there really isnât. While there are alternative solutions such as this, I wanted to make my own implementation of React and Django.
Hey you! Before I begin, if youâd like to click off this post and run the project right now, you can! As part of my design philosophy, you can spin up the project with a single command, docker compose up -w
. But if youâd like to stick around and see how this all works, be my guest. Speaking of design philosophy...
A quick note on design philosophy đ§
The goals of this project were simple: make a project that can be run with a single command, ensure that this project still behaves like a normal Django or React project, and maintain the level of security that Django does.
Docker đ
For obvious reasons, everything is dockerized and managed by a single docker compose file (I donât have a production-ready docker compose file yet, as I havenât deployed a project with this).
The docker part of this project is pretty simple, but if youâre curious Iâd invite you to read through the docker compose file yourself. Just for a quick overview, the compose file has 4 containers; one for Nginx, one for PostgreSQL, one for Django, and one for React. The PostgreSQL and React containers spin up first, then the Nginx container, and finally the Django container, which waits to start until PostgreSQL is ready using a health check like so:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DATABASE_USER} -d ${DATABASE_NAME}"]
interval: 1s
retries: 3
start_period: 3s
timeout: 3s
Also, the only port exposed to our computer (localhost
) is 1337
. Any other ports that are âexposedâ in this project are only exposed to other Docker containers.
And finally, the project supports live reloading by default! This way, the project still behaves like a normal Django or React project, which is in line with the design philosophy.
Nginx đ»
Nginx lets us run the frontend and the backend on the same domain. Django isnât very happy when it receives requests from a different domain, so this makes our lives a lot easier.
If youâre confused about what Nginxâs place is in this project, hereâs a little diagram that might help you:
There are a few different routes listed in the Nginx folder, but theyâre pretty simple. Going to /api
, /admin
, or /static
will proxy you to the backend, and going anywhere else (/
) will proxy you to the frontend! Speaking of which, most of our users will be going straight to the frontend, so why not talk about that next?
React (the frontend) đ„ïž
The frontend is just a SPA (Single Page Application) running React, but that doesnât mean itâs exactly simple. Alongside React is React Redux for global state management and React Router V6 for routing purposes. Every interaction that the frontend makes with the backend is through Javscriptâs fetch API (AJAX). For instance, our login function:
async function login({ username, password }) {
const config = {
headers: { "Content-Type": "application/json" },
credentials: "include",
method: "POST",
body: JSON.stringify({ username, password }),
};
const response = await fetch("/api/accounts/login/", config);
if (response.ok) {
store.dispatch(checkAuthenticated(LOGIN_SUCCESS));
} else {
store.dispatch(checkAuthenticated(LOGIN_FAIL));
}
return store.getState().auth.isAuthenticated;
}
It takes in a username and a password, then sends a POST request to our backend. If response.ok
is True, then weâre logged in, and if it isnât, we arenât logged in. This in itself is simple enough, but it gets tricky when you consider how to manage this state across multiple pages. This is where React Redux comes in.
While Iâm not going to go into full detail about how React Redux works, Iâll give a brief overview of how it helps our frontend manage authentication. If you look at the conditionals on the login function, youâll see a store.dispatch
function. If we logged in successfully, the store.dispatch
function tells the store that we are now authenticated, so the next time we want to load a page that requires authentication, we can just âaskâ the store if weâre authenticated before proceeding.
But what about when the user reloads the page? Well, we can just send a request to the backend to check if weâre authenticated, and update the store based on that. This is done in the AuthenticatedRoutes.jsx
file, and itâs also where Iâd like to point out how useful React Router V6 is. I can easily redirect the user to the login page if they arenât logged in, or if they are, I can also choose not to redirect them. Here's an example from AuthenticatedRoutes.jsx
:
const isAuth = store.getState().auth.isAuthenticated;
if (isAuth) {
return children;
}
return <Navigate to="/login" replace={true} />;
}
return <p>Loading</p>;
If you donât know how Django (or any other backend framework) authenticates users, you might be a bit confused. We havenât manually sent anything to the backend that shows proof of authentication (except for a username and password), so how are we authenticating the user? Indeed, there are a few different methods of authentication, but the most popular by far is session authentication. Iâll leave it to the reader to learn about session authentication, but just know that Django sends the session token through cookies, so we donât have to manually send anything.
The CSRF token đȘ
We do have to manually send something from the frontend to the backend though: the CSRF token (again, Iâll let the reader learn about the CSRF token on their own). This is because weâre using the Django Rest Framework (DRF) on top of Django to facilitate communication between the frontend and backend, and DRF requires us to send our CSRF token in the cookies and in the headers. Django sends the CSRF token as a cookie every time we log in, and we have to get the cookie into Javascript so we can send it with every request that needs it. This is quite easy, as we can just use a Javascript function provided by Django themselves:
const getCookie = (name) => {
let cookieValue = null;
if (document.cookie && document.cookie !== "") {
let cookies = document.cookie.split(";");
for (let i = 0; i < cookies.length; i++) {
let cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === name + "=") {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
};
export default getCookie;
Now when we want to make POST requests â or any other kind of request that requires a CSRF token â to the backend, we can just get the CSRF token like so:
const config = {
headers: {
"X-CSRFToken": getCookie("csrftoken"),
},
...
};
Thatâs about it for the frontend!
Django (the backend) đ
The backend is incredibly simple. 90% of the logic is in one file , and itâs only 50 lines of code. For instance, here's the logout function:
@api_view(["POST"])
def logout_user(request):
try:
logout(request)
return Response({"success": "You have been logged out"}, status=200)
except:
return Response({"error": "Something went wrong"}, status=403)
There is one quirk that I would like to mention. Normally, when you send a cookie, Django sets the HTTPOnly
attribute of the cookie to True, meaning that the cookie canât be accessed by Javascript (which can offer a little bit of security). We have to disable this attribute in order to allow the getCookie
function from Djangoâs documentation to actually read the cookie. While this may sound dangerous, Django themselves say that this is perfectly safe.
Wrapping up đ
And⊠thatâs about it. I think Iâve stayed aligned with the design philosophy that I mentioned at the start of this. What do you think? Are there any improvements that you would make (and if there are, please feel free to make a pull request!).
You can view the project on Github here!
Featured ones: