ReactJS is a fantastic frontend framework, and Django is a fantastic backend framework. However, as usual when dealing with anything of more than trivial complexity, it isn’t easy to get the two to place nicely together. It's not like taping a banana to a wall. This is a mid-level tutorial, going beyond what most tutorials deal with for making Django and React work together. I’m not just going to leave you with an incomplete picture here. This is the whole shebang. Get over it and build something useful. It was a major pain in the ass when we did it for my startup the first time, so please use this knowledge wisely and pardon the plug. Lollipop.ai Why even use JSON Web Tokens? Since sessions aren’t typically supported in Apps, and in this we want to be able to build a backend API that can support both web and apps, we are going to use JSON Web Tokens or to handle the authentication hand-off between the front and backends. On top of that, JWT are quite compact and easy to use. For more information, check Auth0’s . JWT description here It’ll only take you 6 steps to do it all! Part 1 - Django: 1. 2. Django Custom User DRF serializers and auth Part 2 - React: 3. inside our Django project as a standalone app 4. , with routing, and the signup & login forms 5. 6. Installing React Preparing React for Authentication Axios for requests and tokens Logging out & blacklisting tokens The completed code lives on and you can just flip through branches to see the code at each step (1–1, 1–2, so on) GitHub here Part 1: Django Backend 1–1) Setting up a Custom User in Django First, make a fresh directory to hold our entire project. $ mkdir django-jwt-react $ cd django-jwt-react Then create our virtual environment and install with . PS: Poetry is better but harder to get that first venv up and running pipenv $ pipenv --python $ pipenv install django djangorestframework djangorestframework-simplejwt 3.7 Note: You may see references online to a package called but it is no longer maintained. Use instead. djangorestframework-jwt djangorestframework-simplejwt Activate the virtual environment and create the Django project. $ pipenv shell $ django-admin startproject djsr Now you should have the below in your directory. django-jwt-react/ -django-jwt-react/ --djsr/ ---djsr/ ----__init__.py ----settings.py ----urls.py ----wsgi.py ---manage.py --Pipfile --Pipfile.lock Most of the time you won’t be using the stock Django user in a project, you’ll be adding custom attributes such as favorite color. But Django doesn’t like it very much when we modify the User model after it has already been created in the database. To avoid those errors we first create our custom user and only then make and run database migrations. Django themselves recommend to do this. This is a good time to create our app in Django. Authentication $ python djsr/manage.py startapp authentication And add it to your in . INSTALLED_APPS settings.py # djsr/djsr/settings.py INSTALLED_APPS = [ , , , , , , ] 'django.contrib.admin' 'django.contrib.auth' 'django.contrib.contenttypes' 'django.contrib.sessions' 'django.contrib.messages' 'django.contrib.staticfiles' 'authentication' Let’s create our custom user model in and add the fav_color attribute, since we really care about colorful users. authentication/models.py # djsr/authentication/models.py django.contrib.auth.models AbstractUser django.db models = models.CharField(blank=True, max_length= ) from import from import ( ): class CustomUser AbstractUser fav_color 120 extends from which gives us access to the standard Django User model attributes and functionalities such as username, password, and so on. We don’t need to add those to ours as a result. Nice. CustomUser AbstractUser And adding it to authentication/admin.py with the most basic . ModelAdmin # djsr/authentication/admin.py django.contrib admin .models CustomUser = CustomUser admin.site.register(CustomUser, CustomUserAdmin) from import from import ( . ): class CustomUserAdmin admin ModelAdmin model Finally in we configure as our . settings.py CustomUser AUTH_USER_MODEL # djsr/djsr/settings.py # ... # Custom user model AUTH_USER_MODEL = "authentication.CustomUser" Now with our custom user setup, we can make and run migrations. While we’re at it, create a superuser too. $ python djsr/manage.py makemigrations $ python djsr/manage.py migrate $ python djsr/manage.py createsuperuser Cool. Now run the server. $ python djsr/manage.py runserver You should see the default Django success page. Note: If you made your migrations before creating the CustomUser, you may have to delete and re-create the database. GitHub code for section 1–1 lives . here 1–2) DRF serializers and auth Great, now that your project is set up with a Custom User, we can use that custom user and Django Rest Framework + DRF Simple JWT to create Javascript Web Token based authentication. We’ve already got those installed. This section will cover: a. Configuring DRF + DRF Simple JWT b. Authenticating and getting Refresh and Access tokens c. Refreshing the tokens d. Customizing the Obtain Token serializer and view to add extra context e. Register a new user f. Creating and testing a protected view 1–2a. Configuring DRF + DRF Simple JWT To get the ball rolling, in let’s configure DRF and Simple JWT. Add to installed apps and the configuration dict. The Django Rest Framework Simple JWT package doesn’t need to be added to the . settings.py “rest_framework” REST_FRAMEWORK INSTALLED_APPS # djsr/djsr/settings.py # Needed SIMPLE_JWT datetime timedelta # ... INSTALLED_APPS = [ ... # add rest_framework ] REST_FRAMEWORK = { : ( , ), : ( , ), # } SIMPLE_JWT = { : timedelta(minutes= ), : timedelta(days= ), : True, : False, : , : SECRET_KEY, : None, : ( ,), : , : , : ( ,), : , } for from import 'rest_framework' 'DEFAULT_PERMISSION_CLASSES' 'rest_framework.permissions.IsAuthenticated' 'DEFAULT_AUTHENTICATION_CLASSES' 'rest_framework_simplejwt.authentication.JWTAuthentication' 'ACCESS_TOKEN_LIFETIME' 5 'REFRESH_TOKEN_LIFETIME' 14 'ROTATE_REFRESH_TOKENS' 'BLACKLIST_AFTER_ROTATION' 'ALGORITHM' 'HS256' 'SIGNING_KEY' 'VERIFYING_KEY' 'AUTH_HEADER_TYPES' 'JWT' 'USER_ID_FIELD' 'id' 'USER_ID_CLAIM' 'user_id' 'AUTH_TOKEN_CLASSES' 'rest_framework_simplejwt.tokens.AccessToken' 'TOKEN_TYPE_CLAIM' 'token_type' By default we’ll only let authenticated viewers access our views, and they can authenticate using JWTAuthentication from the simplejwt package. Configuring Simple JWT can get a little complicated. The key things to note here are that Refresh tokens (which last 14 days) are used to get Access tokens (which last 5 minutes). Only with a valid Access token can the user access a view, otherwise DRF will return a 401 unauthorized error. We rotate the Refresh tokens so that our users don’t have to log in again if they visit within 14 days, for ease of use. You can blacklist the tokens after rotating them, but we won’t cover it here. If you’re not using the stock , and are using something like the email address, then you’ll also want to change the and to correspond to whatever the new user ID field is. user_id USER_ID_FIELD USER_ID_CLAIM Make special note of the “ ” as whatever value you put here must be reflected in React’s headers a bit later on. We set it as , but I’ve seen used as well. AUTH_HEADER_TYPES "JWT" “Bearer” No need to do migrations again. 1-2b. Authenticating and getting Refresh and Access tokens We need to add the DRF Simple JWT URLs into our project to be able to test logging in, first. # djsr/djsr/urls.py django.contrib admin django.urls path, include urlpatterns = [ path( , admin.site.urls), path( , include( )) ] from import from import 'admin/' 'api/' 'authentication.urls' And make a new in the authentication directory so that we can use the twin views supplied by DRF Simple JWT to obtain token pairs and refresh tokens. urls.py # djsr/authentication/urls.py django.urls path rest_framework_simplejwt views jwt_views urlpatterns = [ path( , jwt_views.TokenObtainPairView.as_view(), name= ), # override sjwt stock token path( , jwt_views.TokenRefreshView.as_view(), name= ), ] from import from import as 'token/obtain/' 'token_create' 'token/refresh/' 'token_refresh' Now use CURL with the superuser credentials you set earlier. $ curl --header -X POST http: { : , : } "Content-Type: application/json" //127.0.0.1:8000/api/token/obtain/ --data '{"username":"djsr","password":"djsr"}' "refresh" "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTU2MTYyMTg0OSwianRpIjoiYmE3OWUxZTEwOWJkNGU3NmI1YWZhNWQ5OTg5MTE0NjgiLCJ1c2VyX2lkIjoxfQ.S7tDJaaymUUNs74Gnt6dX2prIU_E8uqCPzMtd8Le0VI" "access" "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTYwNDEyNTQ5LCJqdGkiOiJiMmM0MjM4MzYyZjI0MTJhYTgyODJjMTMwNWU3ZTQwYiIsInVzZXJfaWQiOjF9.0ry66-v6SUxiewAPNmcpRt99D8B8bu-fgfqOCpVnN1k" Boom. Tokens. We’re authenticated! But wait. There are tokens plural. The Refresh token lasts 14 days (we can consider this logged in) but the Access token only lasts a mere 5 minutes. That means, whenever your user tries to access something without a valid Access token, it’ll get rejected, and then you need to send a refresh request from the frontend to the backend to get a new one. Let’s do that with CURL. 1–2c. Refreshing the tokens Take the Refresh token from above and use CURL again: $ curl --header -X POST http: { : , : } "Content-Type: application/json" //127.0.0.1:8000/api/token/refresh/ --data '{"refresh":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTU2MTYyMTg0OSwianRpIjoiYmE3OWUxZTEwOWJkNGU3NmI1YWZhNWQ5OTg5MTE0NjgiLCJ1c2VyX2lkIjoxfQ.S7tDJaaymUUNs74Gnt6dX2prIU_E8uqCPzMtd8Le0VI"}' "access" "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTYwNDEyOTQ0LCJqdGkiOiI1N2ZiZmI3ZGFhN2Y0MzkwYTZkYTc5NDhhMjdhMzMwMyIsInVzZXJfaWQiOjF9.9p-cXSn2uwwW2E0fX1FcOuIkYPcM85rUJvKBhypy1_c" "refresh" "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTU2MTYyMjI0NCwianRpIjoiYWUyZTNiNmRiNTI0NGUyNDliZjAyZTBiMWI3NTFmZjMiLCJ1c2VyX2lkIjoxfQ.peB-nzZRjzgMjcNASp1TZZ510p3lJt7N9SeCWUt0ngI" Look at that, new tokens! If you didn’t have in then the Refresh token would be the same, but since we are rotating, it’s also a fresh Refresh token. As long as the user keeps visiting before this expires, they'll never have to log in again. ROTATE_REFRESH_TOKENS:True settings.py What makes up a JWT anyway? Head over to and plug your token in. For our Refresh token above, after decoding you’ll see. jwt.io Header: { : , : } "typ" "JWT" "alg" "HS256" Payload: { : , : , : , : } "token_type" "refresh" "exp" 1561622244 "jti" "ae2e3b6db5244e249bf02e0b1b751ff3" "user_id" 1 Note that . JTI is contained within the token, along with the type, expiration and any other info you put into it. token != jti And the Access token holds similar information. See how the payload includes the user_id? You can add any information you want with the token, you just have to modify the claim a bit, first. 1–2d. Customizing the Obtain Token Serializer and view Earlier we added a fav_color attribute on our model. First, go into the admin panel and choose a color. CustomUser 127.0.0.1:8000/admin/ The DRF Simple JWT package makes it very easy to develop so that we can send our user’s favorite color in each token by importing and subclassing with the original serializer. custom claims # djsr/authentication/serializers.py rest_framework_simplejwt.serializers TokenObtainPairSerializer = (MyTokenObtainPairSerializer, cls).get_token(user) # Add custom claims token[ ] = user.fav_color token from import ( ): @ ( , ): class MyTokenObtainPairSerializer TokenObtainPairSerializer classmethod def get_token cls user token super 'fav_color' return It needs a view to go along with it. # djsr/authentication/views.py rest_framework_simplejwt.views TokenObtainPairView rest_framework permissions .serializers MyTokenObtainPairSerializer = (permissions.AllowAny,) serializer_class = MyTokenObtainPairSerializer from import from import from import ( ): class ObtainTokenPairWithColorView TokenObtainPairView permission_classes Which needs a new entry in to replace the packaged one. urls.py # djsr/authentication/urls.py django.urls path rest_framework_simplejwt views jwt_views .views ObtainTokenPairWithColorView urlpatterns = [ path( , ObtainTokenPairWithColorView.as_view(), name= ), path( , jwt_views.TokenRefreshView.as_view(), name= ), ] from import from import as from import 'token/obtain/' 'token_create' 'token/refresh/' 'token_refresh' The old Refresh token will still work to get new Access tokens, so at this point you should blacklist all outstanding tokens to effectively log everybody out. To see the new token in action, use CURL again. $ curl --header -X POST http: { : , : } "Content-Type: application/json" //127.0.0.1:8000/api/token/obtain/ --data '{"username":"djsr","password":"djsr"}' "refresh" "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTU2MTYyNjQ5NywianRpIjoiODVhMmRlNWUyNjQ0NGE1ZWFmOGQ1NDAzMmM1ODUxMzIiLCJ1c2VyX2lkIjoxLCJmYXZfY29sb3IiOiIifQ.1eJr6XVZXDm0nmm19tyu9WP9AfdY8Ny_D_tK4Qtvo9E" "access" "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTYwNDE3MTk3LCJqdGkiOiI5ZjE4NmM4OTQ0ZWI0NGYyYmNmYjZiMTQ5MzkyY2Y4YSIsInVzZXJfaWQiOjEsImZhdl9jb2xvciI6IiJ9.Ad2szXkTB4eOqnRk3GIcm1NDuNixZH3rNyf9RIePXCU" There you can see your fav color in the decoded token. Cool. Why is this useful? Depends on your implementation, but it can be nice to have a little extra context along with the token, or to use the custom serializer/view to perform custom actions. This isn’t meant to act as any sort of function. Don't use it like that. get_user_info() Now we can log in an existing user and give them a token. We have two things left to do. Register a user and create a protected view. 1–2e. Registering a user Surprisingly (or not) creating a new user has nothing at all to do with JWT. It’s just vanilla Django Rest Framework. We don’t need to do anything to our model, but we need to make a serializer for it, and put that in a view with a URL. CustomUser First, the model serializer. If you’re unfamiliar with Django Rest Framework, the serializers are essentially responsible for taking JSON and turning it into usable Python data structures, and then taking action with that. For more on serializers, take a look at the very good . This serializer we’re using is SUPER typical. CustomUserSerializer DRF docs # djsr/authentication/serializers.py rest_framework_simplejwt.serializers TokenObtainPairSerializer rest_framework serializers .models CustomUser # ... class CustomUserSerializer(serializers.ModelSerializer): email = serializers.EmailField( required=True ) username = serializers.CharField() password = serializers.CharField(min_length= , write_only=True) = CustomUser fields = ( , , ) extra_kwargs = { : { : True}} def create(self, validated_data): password = validated_data.pop( , None) instance = self.Meta.model(**validated_data) # long the fields are the same, we can just use password is not None: instance.set_password(password) instance.save() instance from import from import from import "" " Currently unused in preference of the below. " "" 8 : class Meta model 'email' 'username' 'password' 'password' 'write_only' 'password' as as this if return For our viewset, rather than use a , we create our own view with just a POST endpoint. We would have a different endpoint for any GET requests for the objects. ModelViewSet CustomUser In since REST_FRAMEWORK’s permissions defaults are for views to be accessible to authenticated users only, we have to explicitly set the permissions to , otherwise a new user trying to sign up and pay you would get an unauthorized error. Bad juju. settings.py AllowAny When you feed data to a model serializer like we are doing here, as long as the serializer has a or method, you can use to magically create (or update) the corresponding object (in our case, ) and return the instance. create() update() serializer.save() CustomUser Docs. # djsr/authentication/views.py rest_framework_simplejwt.views TokenObtainPairView rest_framework status, permissions rest_framework.response Response rest_framework.views APIView .serializers MyTokenObtainPairSerializer, CustomUserSerializer = MyTokenObtainPairSerializer = (permissions.AllowAny,) def post(self, request, format= ): serializer = CustomUserSerializer(data=request.data) serializer.is_valid(): user = serializer.save() user: json = serializer.data Response(json, status=status.HTTP_201_CREATED) Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) from import from import from import from import from import ( ): class ObtainTokenPairWithColorView TokenObtainPairView serializer_class ( ): class CustomUserCreate APIView permission_classes 'json' if if return return And finally in we add the new view. urls.py # djsr/authentication/urls.py django.urls path rest_framework_simplejwt views jwt_views .views ObtainTokenPairWithColorView, CustomUserCreate urlpatterns = [ path( , CustomUserCreate.as_view(), name= ), path( , ObtainTokenPairWithColorView.as_view(), name= ), path( , jwt_views.TokenRefreshView.as_view(), name= ), ] from import from import as from import 'user/create/' "create_user" 'token/obtain/' 'token_create' 'token/refresh/' 'token_refresh' View + Serializer + URL = good to go. You’re going to have to make sure to test it at this point, since if this view is restricted to authenticated users only, you’ll get exactly 0 new users. They would be unhappy and your odds of getting an investor go way up when you have users. That means, more CURLing. $ curl --header -X POST http: { : , : } "Content-Type: application/json" //127.0.0.1:8000/api/user/create/ --data '{"email":"ichiro@mariners.com","username":"ichiro1","password":"konnichiwa"}' "email" "ichiro@mariners.com" "username" "ichiro1" Bingo! Works just fine. The final step for us on the Django Rest Framework side is to create a protected view for us to try to access. 1–2f. Creating and testing a protected view The only way for us to tell if all of this is working or not is to create a dummy view… let’s call it HelloWorld… and protect it as we try to access it with and without our Javascript Web Token Authentication Tokens. Let’s just make the simplest possible view. # djsr/authentication/views.py ... class HelloWorldView(APIView): def get(self, request): Response(data={ : }, status=status.HTTP_200_OK) return "hello" "world" And adding it to . urls.py # djsr/authentication/urls.py django.urls path rest_framework_simplejwt views jwt_views .views ObtainTokenPairWithColorView, CustomUserCreate, HelloWorldView urlpatterns = [ path( , CustomUserCreate.as_view(), name= ), path( , ObtainTokenPairWithColorView.as_view(), name= ), path( , jwt_views.TokenRefreshView.as_view(), name= ), path( , HelloWorldView.as_view(), name= ) ] from import from import as from import 'user/create/' "create_user" 'token/obtain/' 'token_create' 'token/refresh/' 'token_refresh' 'hello/' 'hello_world' Back to CURL. If we did this right, our API request without the token will fail. $ curl --header -X GET http: { : } "Content-Type: application/json" //127.0.0.1:8000/api/hello/ "detail" "Authentication credentials were not provided." As expected. Now with the credentials. Make sure to refresh them first, odds are the Access token has expired. Or try with your new user. $ curl --header -X POST http: { : , : } $ curl --header -X GET http: { : } $ curl --header --header -X GET http: { : } "Content-Type: application/json" //127.0.0.1:8000/api/token/obtain/ --data '{"username":"ichiro1","password":"konnichiwa"}' "refresh" "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTU2MTYzODQxNiwianRpIjoiMGM5MjY5NWE0ZGQwNDUyNzk2YTM5NTY3ZDMyNTRkYzgiLCJ1c2VyX2lkIjoyLCJmYXZfY29sb3IiOiIifQ.sV6oNQjQkWw2F3NLMQh5VWWleIxB9OpmIFvI5TNsUjk" "access" "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTYwNDI5MTE2LCJqdGkiOiI1NWVlZDA4MGQ2YTg0MzI4YTZkZTE0Mjg4ZjE3OWE0YyIsInVzZXJfaWQiOjIsImZhdl9jb2xvciI6IiJ9.LXqfhFifGDA6Qg8s4Knl1grPusTLX1lh4YKWuQUuv-k" "Content-Type: application/json" //127.0.0.1:8000/api/hello/ "detail" "Authentication credentials were not provided." "Content-Type: application/json" "Authorization: JWT eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTYwNDI5MTE2LCJqdGkiOiI1NWVlZDA4MGQ2YTg0MzI4YTZkZTE0Mjg4ZjE3OWE0YyIsInVzZXJfaWQiOjIsImZhdl9jb2xvciI6IiJ9.LXqfhFifGDA6Qg8s4Knl1grPusTLX1lh4YKWuQUuv-k" //127.0.0.1:8000/api/hello/ "hello" "world" First we logged new user Ichiro in, then tried a GET request on our protected endpoint. It’s still rejected, because we didn’t actually pass the token, even though we can see it with our naked eyes. It has to be passed in the header. This is why it’s key to remember that earlier in settings.py we set in the . “JWT” AUTH_HEADER_TYPES In the header, the token MUST be preceeded by . Or whatever you set as. Otherwise, no dice. This will become important when we connect the frontend. “Authorization: JWT “ + access token AUTH_HEADER_TYPES When we do that, as you can see, we were able to get the view’s response of ! {"hello":"world"} Amazing. Now we can authenticate custom users using JWT Refresh and Access tokens, and let them access protected views only when transferring an Access token in the header. What if you waited 5 minutes and tried to use an expired Access Token? $ curl -Type: application/json Authorization: JWT eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTYwNDI5MTE2LCJqdGkiOiI1NWVlZDA4MGQ2YTg0MzI4YTZkZTE0Mjg4ZjE3OWE0YyIsInVzZXJfaWQiOjIsImZhdl9jb2xvciI6IiJ9.LXqfhFifGDA6Qg8s4Knl1grPusTLX1lh4YKWuQUuv-k detail Given token not valid any token type code token_not_valid messages token_class AccessToken token_type access message Token is invalid or expired " --header " " -X GET http://127.0.0.1:8000/api/hello/ {" ":" for "," ":" "," ":[{" ":" "," ":" "," ":" "}]} In the Django server console you'll be able to see the magic: Unauthorized: hello/ [ /Jun/ : : ] /api/ 13 2019 12 53 29 "GET /api/hello/ HTTP/1.1" 401 183 The view would be inaccessible, and you’d have to refresh. It’s super tedious to do manually with CURL, but within a frontend framework, it’s easily automated. Next we’ll continue and create a frontend using ReactJS to consume our API. GitHub code for section 1–2 . lives here Part 2: React Frontend 2–1) Installing React inside our Django project as a standalone app Setting up a frontend framework to work with Django can be done any number of ways. They could be completely separate, only coming into contact over the API. In that case, local development would run a Django dev server and a separate React dev server, while deployment would involve a frontend deployed independently of the backend. React on AWS S3 for example, while Django is hosted on an EC2 server. It would require some fun CORS configurations. On the other end of the spectrum, React could be intertwined more deeply within Django’s own templating system, so that Django handles serving the templates but you can still use React magic. It somewhat defeats the purpose of having a frontend framework in the first place. This tutorial uses a middle approach — which is to install React inside a standalone Django app. CORS headers don’t need much work, and you get the full benefits of the React framework. There are definite caveats. The same server would be responsible for serving all the data, which could slow it down. URL routing is a little tricky, as well, if you want to have some Django URLs available, such as a health check or /admin. Initial setup as well is something of a pain. We’ll walk through it. First thing to do is make a new Django app to hold React. $ cd djsr $ python manage.py startapp frontend And add it to your in . INSTALLED_APPS settings.py In frontend make a file, which will act as the base template for React along with a regular Django rendered index view. In this particular template, Django template context processors are available to use, which can be incredibly useful if you want to control how React behaves from within . Let’s prepare it like a standard Django base template, for now. templates/frontend/index.html settings.py <!-- djsr/frontend/templates/frontend/index.html --> <html> {% load static %} <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="{% static 'frontend/style.css' %}"> <title>DRF + React = Winning the game</title> </head> <body> <div id="root" class="content"> This will be the base template. </div> </body> </html> <!DOCTYPE html> And make a minimal style.css while you’re at it. #root{ background-color:rebeccapurple; } // djsr/fontend/static/frontend/style.css We finish this with a view and updated URLs. # djsr/djsr/urls.py django.contrib admin django.urls path, include urlpatterns = [ path( , admin.site.urls), path( , include( )), path( , include( )) ] from import from import 'admin/' 'api/' 'authentication.urls' '' 'frontend.urls' Make sure to put this include at the end of . This way, anything not matching Django URLs will be handled by the frontend, which lets us use React’s router to manage frontend views while still hosting it on the same server. Thus nicely avoiding CORS madness. urlpatterns # djsr/frontend/views.py django.shortcuts render # Create your views here. def index(request): render(request, , context=None) from import return 'frontend/index.html' This view renders the that will be the base template for everything React. All it needs to do is render the template. Could add context if desired. index.html Finally we add this view to the frontend URLs. We have to add it twice, first to catch the empty URL, e.g. and second to catch every other URL e.g. . https://lollipop.ai https://lollipop.ai/lollisignup/ # djsr/frontend/urls.py urlpatterns = [ path( , index_view), # the empty url url(r , index_view) # all other urls ] '' for '^.*/$' for With that final touch, run the server again and see if the index renders correctly. $ cd ../.. (so you 're in project root: django-jwt-react) $ python djsr/manage.py runserver Navigate to and see this beautiful mostly empty page rendered succesfully. And you’ll see the same at thus showing us we didn’t mess up. Not yet, at least. Cool. Now we can start playing with React. http://127.0.0.1/ http://127.0.0.1:8000/asdjfklasdjfklasdfjklasdf/ Note that the regex ends with . Django by default appends a / to the end of each URL, so when we do routing in React, each path will need a / at the end of it. This helps enforce that discipline. /$ Now we can get to the dirty business of setting React up in the frontend app. This isn’t a straightforward task, we cannot just rely on Create React App. Instead, we create our own toolchain, adapting the by . great tutorial Jedai Saboteur Make sure you’re at the root folder of the app (with the Pipfile). First create the file. The answers to the questions here don’t matter much, so just leave them at pretty much the default. package.json $ npm init Now within the Django app, create a directory. Within the directory create another directory called public to hold the compiled React files. frontend src This will hold our React components. static/frontend Now the Django app directory should look like this: djsr +-- authentication/ +-- djsr/ +-- frontend/ | +-- migrations/ | +-- src/ | +-- / | | +-- frontend/ | | | +-- public/ | | | +-- style.css | +-- templates/ | | +-- frontend/ | | | +-- index.html +--db.sqlite3 +--manage.py static Open up and add this kinda awkward line to the bottom of the body. We can still use Django’s templating magic in this html file. index.html <script type= src= > "text/javascript" "{% static 'frontend/public/main.js' %}" </ > script When compiled, the React components we make will be contained in . You can rename this file to whatever should you so desire. main.js To make that work even better in production, you should split main.js into bundles if it gets too fat. This line is the key intersection between Django and React for us. With our Django-served index.html loading main.js we can serve any awesome frikken React wizardry we’d like. And we’d like to do a lot of that wizardry indeed. With our ready and waiting, we have to figure out how to create and compile something for it to grab and present. This requires Babel and Webpack. index.html Babel Install , , @babel/core @babel/preset-env @babel-preset-react $ npm install --save-dev @babel/core@ @babel/preset-env@ @babel/preset-react@ 7.4 .5 7.4 .5 7.0 .0 Babel takes the code we write in whatever we use and turns it into JavaScript browsers love. is for modern Javascript ES6+ and for JSX. If you are coming from the Angular world and want to use TypeScript (and why wouldn’t you? It’s great) install instead. We’ll stay away from TypeScript in this tutorial. babel/preset-env babel/preset-react babel/preset-typescript To use our freshly installed Babel, we must create a file in the project root, next to . .babelrc package.json { : [ , ] } "presets" "@babel/preset-env" "@babel/preset-react" Not really any way to test this quite yet. Webpack Webpack is a bundler. It takes our modules (dependencies and custom code both) and converts them into static assets. This infographic from their website does a good job of illustrating how it works. https://webpack.js.org This is what will take our various React components and (after Babel has had its way with the JS(X)) will turn it all into nice static files to load as browsers are used to. Let’s install it and a few more packages that enable us to use it with Babel. $ npm install --save-dev webpack webpack-cli babel-loader + babel-loader@ + webpack-cli@ + webpack@ 8.0 .6 3.3 .4 4.35 .0 We are going to let Django do CSS loading within , but if you wish, Webpack is more than capable of taking over that loading functionality as well. It can also provide a dev server, but Django can handle that nearly as well. It just won’t detect changes to the src files (which to be fair can get quite tedious). index.html Let’s create our Webpack configuration file at the root, next to . webpack.config.js packages.json path = ( ); .exports = { : , : path.resolve(__dirname, ), : { path: path.resolve(__dirname, ), publicPath: , : , }, : { rules: [ { test: , exclude: , use: { : , : { : [ ]} }, } ], }, }; const require 'path' module mode "development" entry 'djsr/frontend/src/index.js' output // options related to how webpack emits results // where compiled files go "djsr/frontend/static/frontend/public/" // 127.0.0.1/static/frontend/public/ where files are served from "/static/frontend/public/" filename 'main.js' // the same one we import in index.html module // configuration regarding modules // regex test for js and jsx files /\.(js|jsx)?$/ // don't look in the node_modules/ folder /node_modules/ // for matching files, use the babel-loader loader "babel-loader" options presets "@babel/env" Looking at the exported configuration object, we see a few things that are important to note. The entry point is where Webpack will find the start of our React app and bundle from there. We have to use absolute paths for Webpack, and the path module makes that very easy when we use them in the entry and output settings. Setting mode to “development” versus “production” and comes along with some optimizations that help either way. Stick to development for now. Within output, there are 3 relevant settings. path is the location where files are emitted after compilation. is essentially setting the location where the application will find the React static files. In development, just set it to where they’re emitted. In production using a CDN, the pattern here would be where STATIC_PATH is wherever your Django project saves static files after running . filename is of course the name of the file emitted after Webpack is done compiling it. publicPath STATIC_PATH/{{path after emitting}} collectstatic Since we’re letting Django handle a lot of the loading duties, we only use one rule within the module setting. For all or files found after entry, the files will be loaded and transformed by babel-loader. We explicitly exclude the folder from being tested. It would be worse than committing it to Git! .js .jsx /node_modules/ If you’re coming from a Django development background, you are probably quite shocked by all this configuring before we even get to do ANY React coding. Yeah, it’s a lot. It’ll be worth it! We have finally reached the point where we can install React. React $ npm install --save react react-dom + react@ + react-dom@ 16.8 .6 16.8 .6 With React installed we can finally make that we referenced in . This is the beginning of our React app, and for now will just import . index.js webpack.config.js React {render} App ; render( // djsr/frontend/src/index.js import from 'react' import from 'react-dom' import from './components/App' , document.getElementById('root')); < /> App See the part? That renders the App in place of the #root div we made in . It used to say “This will be the base template” but after we’re done here — very soon — we’ll see something else. getElementByID(‘root’) index.html We’re forward thinking people, so create a directory and our within it. components/ App.js React, { Component} ; { render(){ ( <h1>Ahhh after 10,000 years I'm free. Time to conquer the Earth!</h1> ); } } App; // djsr/frontend/src/components/App.js import from "react" class App extends Component return < = > div className "site" </ > div export default This is our first React component! Awesome. Note that React uses rather than class. className Since we aren’t using the Webpack development server, we have to compile manually a different way. Let’s be lazy about this and make it a script. Open up and add . package.json ”build”: “webpack — config webpack.config.js” { : , : , : , : , : { : , : }, : , : , : { : , : , : , : , : , : }, : { : , : } } // package.json "name" "django-jwt-react" "version" "1.0.0" "description" "To learn how to merge React and Django" "main" "index.js" "scripts" "build" "webpack --config webpack.config.js" "test" "test" "author" "" "license" "ISC" "devDependencies" "@babel/core" "^7.4.5" "@babel/preset-env" "^7.4.5" "@babel/preset-react" "^7.0.0" "babel-loader" "^8.0.6" "webpack" "^4.35.0" "webpack-cli" "^3.3.4" "dependencies" "react" "^16.8.6" "react-dom" "^16.8.6" And finally. $ npm run build If it worked, take a look at . Is there a file there? There should be. djsr/frontend/public/ main.js If we run Django, we should see the new line of text. Let’s test that. $ python djsr/manage.py runserver That wasn’t simple nor easy, but it worked. Django is now serving our React app, which is living happily in our frontend app. Way to keep up this far. GitHub code for section 2–1 . lives here 2–2) Preparing React Our end goal is to authenticate with React and Django. We’ve already made all the endpoints we need to on the Django side. Before we write the authentication bits on the frontend, we have to do some legwork. Namely: Routing for components, setting up Axios, and making our form views. We’re going to skip over a lot of the complexities about client-side routing in React for now. I may expand upon it in a later article if there’s demand, but there are some really already, although this may be one of the first that touches React Router 5. good resources out there Routing Install . We use the -dom variant since we’re building a website, not an app. For the curious making a React Native app, you’d use react-router-native. react-router-dom $ npm install --save react-router-dom + react-router-dom@ 5.0 .1 With react-router-dom 4 & 5, for projects that’ll be used through a browser like our own, we have 2 kinds of browsers available to us. and . Based on the history API, is the preferred choice when the React app is a Single-Page App ( served each time by the server) or is backed by a dynamic server to handle all requests; urls are formatted without a hash — . HashRouter urls are formatted with a — . is more commonly used when the server only serves static pages and static assets. Thanks to Django, we have a dynamic server, so we can use the . BrowserRouter HashRouter BrowserRouter index.html # www.lollipop.ai/bananaphone/ /#/ www.lollipop.ai/#/bananaphone/ HashRouter BrowserRouter We need to add it to as well, wrapping our App like so, since render only expects to receive 1 component. index.js React {render} {BrowserRouter} App ; render(( <App /> ), .getElementById( )); // djsr/frontend/src/index.js import from 'react' import from 'react-dom' import from 'react-router-dom' import from './components/App' < > BrowserRouter </ > BrowserRouter document 'root' With the imported and updated to use it, the next step is to create another couple components we can render depending on the URLs. BrowserRouter index.js Make 2 new files: and in . login.js signup.js components/ React, { Component } ; { (props){ (props); } render() { ( <h2>Login page</h2> ) } } Login; // djsr/frontend/components/login.js import from "react" class Login extends Component constructor super return < > div </ > div export default And: React, { Component } ; { (props){ (props); } render() { ( <h2>Signup page</h2> ) } } Signup; // djsr/frontend/components/signup.js import from "react" class Signup extends Component constructor super return < > div </ > div export default Pretty much the simplest React components you can write. It has its constructor, renders some HTML, and exports itself. With our components ready, let’s import , , and , which we’ll need . Switch Route Link React, { Component} ; { Switch, Route, Link } ; Login ; Signup ; { render() { ( <main> <h1>Ahhh after 10,000 years I'm free. Time to conquer the Earth!</h1> <Switch> <Route exact path={"/login/"} component={Login}/> <Route exact path={"/signup/"} component={Signup}/> <Route path={"/"} render={() => <div>Home again</div>}/> </Switch> </main> </div> ); } } export default App; // djsr/frontend/src/components/App.js import from "react" import from "react-router-dom" import from "./login" import from "./signup" class App extends Component return < = > div className "site" Using the HTML element is a good habit, so we add that for our main content. Within that we add a block which lets React know that in that space we will switch rendered components depending on the defined routes. <main> <Switch> URL routing in React Switches are very similar to Django’s path syntax. Through the use of the property, when the URL path is matched exactly, the relevant component is rendered. All other paths go to home (or better yet, a future 404 page). exact With this, we should be able to test the URLs by typing them into the address bar in our browser. Navigate to and see what you have there. http://127.0.0.1:8000/signup Looks like it’s working. Man, that's ugly. You can build better looking websites right? It’s tedious to type a URL each time we want to test it, so let’s add a couple links to App.js to save us some keystrokes. React, { Component} ; { Switch, Route, Link } ; Login ; Signup ; { render() { ( <nav> <Link className={"nav-link"} to={"/"}>Home</Link> <Link className={"nav-link"} to={"/login/"}>Login</Link> <Link className={"nav-link"} to={"/signup/"}>Signup</Link> </nav> <main> <h1>Ahhh after 10,000 years I'm free. Time to conquer the Earth!</h1> <Switch> <Route exact path={"/login/"} component={Login}/> <Route exact path={"/signup/"} component={Signup}/> <Route path={"/"} render={() => <div>Home again</div>}/> </Switch> </main> </div> ); } } export default App; // djsr/frontend/src/components/App.js import from "react" import from "react-router-dom" import from "./login" import from "./signup" class App extends Component return < = > div className "site" Still using good habits, we of course put it all in a <nav> element. For now, add the class for some bare-minimum "styling." In React, don’t use <a href=””> , use React router’s <Link> component, which we have to for React router to work correctly with the history API. “nav-link” #root{ background-color:rebeccapurple; color:white; } .nav-link{ :white; border: px solid white; padding: em; } /* djsr/frontend/static/frontend/style.css */ color 1 1 Yes, everything is still incredibly ugly. Maybe I should do something about that later. First, confirm the s do what they’re supposed to do. <Link> $ npm run build $ python djsr/manage.py runserver Looks good again. Routing complete! Let’s flesh out those forms. Forms Two forms are needed for Login and Signup. Signup has username, email, and password fields while login only needs username and password. I really love how React makes forms so friendly to both developers and users. I started this form by ripping the form out of React’s . It’s got almost everything we need to start. own docs React, { Component } ; { (props) { (props); .state = { : , : }; .handleChange = .handleChange.bind( ); .handleSubmit = .handleSubmit.bind( ); } handleChange(event) { .setState({[event.target.name]: event.target.value}); } handleSubmit(event) { alert( + .state.username + + .state.password); event.preventDefault(); } render() { ( <form onSubmit={this.handleSubmit}> <label> Username: <input name="username" type="text" value={this.state.username} onChange={this.handleChange}/> </label> <label> Password: <input name="password" type="password" value={this.state.password} onChange={this.handleChange}/> </label> <input type="submit" value="Submit"/> </form> </div> ) } } export default Login; // djsr/frontend/src/components/login.js import from "react" class Login extends Component constructor super this username "" password "" this this this this this this this 'A username and password was submitted: ' this " " this return Login < > div First, look at the HTML of the form. When the form is submitted, it triggers the onSubmit method, which we have written as . This method will handle what we want it to do after the user submits the form. In this case, what we want it to do is alert us with the data that has been input. Normally a submission would trigger a reload or redirect, so by adding in that unwanted behavior can be stopped. handleSubmit preventDefault() To the two other input fields (username, password) we set the onChange property to a method called . Whenever the content of that input field changes, when a keystroke occurs, it triggers the handleChange method to do what we want it to do, which is to update the local component state to match the entered text value for each input field using . handleChange setState If you aren’t careful, you could end up with a handleSomething method for each input field. However, since we want to stick to DRY principles, we can cleverly just give each input a name, and ensure thelocal component state matches that name. With that, we can get away with only one method, using one line of code. Not exactly a Python one-liner, but it scratches that itch. handleChange It’s super important to bind to each class method in the constructor, or else this will be undefined in the callback. Very annoying to forget. this The updated signup page is essentially the same, but with a bonus email input field (and matching component state). React, { Component } ; { (props){ (props); .state = { : , : , : }; .handleChange = .handleChange.bind( ); .handleSubmit = .handleSubmit.bind( ); } handleChange(event) { .setState({[event.target.name]: event.target.value}); } handleSubmit(event) { alert( + .state.username + + .state.password + + .state.email); event.preventDefault(); } render() { ( <form onSubmit={this.handleSubmit}> <label> Username: <input name="username" type="text" value={this.state.username} onChange={this.handleChange}/> </label> <label> Email: <input name="email" type="email" value={this.state.email} onChange={this.handleChange}/> </label> <label> Password: <input name="password" type="password" value={this.state.password} onChange={this.handleChange}/> </label> <input type="submit" value="Submit"/> </form> </div> ) } } export default Signup; // djsr/frontend/src/components/signup.js import from "react" class Signup extends Component constructor super this username "" password "" email "" this this this this this this this 'A username and password was submitted: ' this " " this " " this return Signup < > div Test it by building and running the Django server, plus a refresh. $ npm run build $ pythong djsr/manage.py runserver If you’ve been keeping up, there should be a super attractive alert, and maybe even a warning from your browser asking if you want it to block this site from displaying alerts. Thanks Firefox for protecting me from this annoying signup page. Ok, so our React frontend can now handle routing and we can use our signup and login forms — kinda. This next part is my least favorite. Axios. GitHub code for section 2–2 . lives here 2–3) Axios for requests and tokens Within Javascript circles, there are two primary ways to GET/POST/UPDATE/DELETE/whatever data from/to a REST API: and JavaScript’s included . Personally, I have had more success with Axios. Both use Promises, and Internet Explorer doesn’t like either of them (but it seems to like Fetch a teeny bit less). I would like to explore GraphQL later on in a followup article. Axios Fetch We want to use Axios for: POSTing to to create a userPOSTing to to login a user and obtain a JWT token pairPOSTing to to refresh the JWT token pairGETting from the protected to see what the backend secretly has to say /api/user/create/ /api/token/obtain/ /api/token/refresh/ /api/hello/ Scroll back up and take a look at our CURL commands. To access the protected view, the JWT token has to be sent in the header. Axios will need to take care of that in addition to sending along the POSTed data. Whenever the JWT access token expires, we don’t want to make the user get a new one manually, since it expires every 5 minutes. That user would run away. Axios will have to handle acquiring a new token automatically, as well. It’s a tall task. First step — install Axios: $ npm install --save axios + axios@ 0.19 .0 Axios has two superpowers useful here. First, we can with custom configurations that can be used throughout the website — this is where we’ll set it to send JWT headers. Second, we can make a custom for the new Axios instance. The interceptor lets us “do stuff” while handling the request — this is where we’ll handle refreshing tokens. create a standalone instance interceptor Create a file within called . src/ axiosApi.js axios axiosInstance = axios.create({ : , : , : { : + localStorage.getItem( ), : , : } }); // djsr/frontend/src/axiosApi.js import from 'axios' const baseURL 'http://127.0.0.1:8000/api/' timeout 5000 headers 'Authorization' "JWT " 'access_token' 'Content-Type' 'application/json' 'accept' 'application/json' To create an Axios instance, import it and use the method with the custom configuration to set the defaults. Set the to where the backend API is served from. The headers are important. In the dict sets the as so for the Authorization header here it has to be the same. .create() baseURL settings.py SIMPLE_JWT AUTH_HEADER_TYPES ‘JWT’ Don’t neglect to add the space after JWT in axiosAPI.js. Also do NOT a space in settings.py. Same same, but different, but still same. Each time Axios gets a token, it stores the access_token in local storage. We initiate the creation of the Axios instance by getting that token. If there’s no token in local storage, don’t even worry about it for the header. It will be set every time a user logs in. To test anything, we still have to write the methods to login and signup. Then we do still need to tackle logging out as well. Can’t forget that! Logging in Within our original file, we can now improve to POST to the Django backend’s token creation endpoint and get a token pair. login.js handleSubmit /api/token/obtain/ React, { Component } ; axiosInstance ; { (props) { (props); .state = { : , : }; .handleChange = .handleChange.bind( ); .handleSubmit = .handleSubmit.bind( ); } handleChange(event) { .setState({[event.target.name]: event.target.value}); } handleSubmit(event) { event.preventDefault(); { response = axiosInstance.post( , { : .state.username, : .state.password }); axiosInstance.defaults.headers[ ] = + response.data.access; localStorage.setItem( , response.data.access); localStorage.setItem( , response.data.refresh); data; } (error) { error; } } render() { ( <form onSubmit={this.handleSubmit}> <label> Username: <input name="username" type="text" value={this.state.username} onChange={this.handleChange}/> </label> <label> Password: <input name="password" type="password" value={this.state.password} onChange={this.handleChange}/> </label> <input type="submit" value="Submit"/> </form> </div> ) } } export default Login; // djsr/frontend/src/components/login.js import from "react" import from "../axiosApi" class Login extends Component constructor super this username "" password "" this this this this this this this try const '/token/obtain/' username this password this 'Authorization' "JWT " 'access_token' 'refresh_token' return catch throw return Login < > div Very simple upgrade. Our prior version of the view already set the username and password in state, so now all that is left to do is to import the custom Axios instance, and post the username and password. If it works, the response will contain a pair of brand new JWT tokens. We want to save both of them to local storage, however. Why? We’ll get to that. Since access to views is determined by the access token, that’s the one that needs to go in every header. Run the standard routine, and then navigate to the endpoint and try to login: /login/ $ npm run build $ python djsr/manage.py runserver Django says all OK. Firefox got an OK, too. Oh hey look a token pair!!!!!!!! Testing a protected view Token pair in hand. Can we see the protected view yet? Nope. Nothing in React actually tries to get that info yet. Time for another React component. Make a file called in the components folder. Within this component we want to do only 1 thing: GET a message from a protected API endpoint on the backend, and display it. We will use our custom Axios instance again. hello.js There are a couple tricky parts. React, { Component } ; axiosInstance ; { (props) { (props); .state = { : , }; .getMessage = .getMessage.bind( ) } getMessage(){ { response = axiosInstance.get( ); message = response.data.hello; .setState({ : message, }); message; } (error){ .log( , .stringify(error, , )); error; } } componentDidMount(){ messageData1 = .getMessage(); .log( , .stringify(messageData1, , )); } render(){ ( <p>{this.state.message}</p> ) } } Hello; // djsr/frontend/src/components/hello.js import from "react" import from "../axiosApi" class Hello extends Component constructor super this message "" this this this try let '/hello/' const this message return catch console "Error: " JSON null 4 throw // It's not the most straightforward thing to run an async method in componentDidMount // Version 1 - no async: Console.log will output something undefined. const this console "messageData1: " JSON null 4 return < > div </ > div export default By now you should be familiar with the contents of the method. We set message in the , and later on in , we render whatever the message is. constructor() state render Without going into too much detail about the React lifecycle, it’s intuitive enough just to say that when the component is mounted/loaded, the method is fired. Since we want to get this message RIGHT AWAY, this is the right place to trigger our GET request. componentDidMount Just, first, gotta add this component to the Navbar and Switch in . No big deal. App.js React, { Component} ; { Switch, Route, Link } ; Login ; Signup ; Hello ; { render() { ( <nav> <Link className={"nav-link"} to={"/"}>Home</Link> <Link className={"nav-link"} to={"/login/"}>Login</Link> <Link className={"nav-link"} to={"/signup/"}>Signup</Link> <Link className={"nav-link"} to={"/hello/"}>Hello</Link> </nav> <main> <h1>Ahhh after 10,000 years I'm free. Time to conquer the Earth!</h1> <Switch> <Route exact path={"/login/"} component={Login}/> <Route exact path={"/signup/"} component={Signup}/> <Route exact path={"/hello/"} component={Hello}/> <Route path={"/"} render={() => <div>Home again</div>}/> </Switch> </main> </div> ); } } export default App; // djsr/frontend/src/components/App.js import from "react" import from "react-router-dom" import from "./login" import from "./signup" import from "./hello" class App extends Component return < = > div className "site" Build and recompile the server. Navigate to and see what happens. /hello/ It doesn’t work! Number 1 — the access token has expired, so there’s a 401 error. Number 2 — React tried to assign without waiting for the response. message = response.data.hello Either one of those would have taken down the site. You can confirm that #2 would break it even with a fresh access token by logging in again. Or, actually, not. . We couldn’t see it at the time since it didn’t outright break anything, but earlier in , although we POSTed and got the token and can see it in the console, it wasn’t actually getting set to localStorage or the header of the axiosInstance. This isn’t the first time that we tried to assign a value before waiting for the response login.js Change to see how the in is logged as undefined. getMessage() access_token localStorage ... getMessage(){ { header = localStorage.getItem( ); .log(header); } (error){ .log( , .stringify(error, , )); error; } } ... // djsr/frontend/src/components/hello.js try const "access_token" console // let response = axiosInstance.get('/hello/'); // const message = response.data.hello; // this.setState({ // message: message, // }); // return message; catch console "Error: " JSON null 4 throw With no token set in the headers, you can’t test #1 either. How do we fix that? Async/Await & .then We need to tell React to wait for a response before continuing along through a method, assigning values that haven’t been returned in a response yet. You can add a callback function using promises, which is the other way to do it. It would look something like this: .then() { (props) { ... this.handleSubmitWThen = .handleSubmitWThen.bind( ); } ... handleSubmitWThen(event){ event.preventDefault(); axiosInstance.post( , { : .state.username, : .state.password }).then( { axiosInstance.defaults.headers[ ] = + result.data.access; localStorage.setItem( , result.data.access); localStorage.setItem( , result.data.refresh); } ).catch ( { error; }) } render() { ( <form onSubmit={this.handleSubmitWThen}> ... </form> ) } } // djsr/frontend/src/components/login.js class Login extends Component constructor this this '/token/obtain/' username this password this => result 'Authorization' "JWT " 'access_token' 'refresh_token' => error throw return Login < > div </ > div This code works, so you can totally ignore the following ES7 async/await version if you so choose. You could also use try to use React hooks, but they seem to still be not entirely ready as of June 2019. Sidenote: , , and do work together pretty well. I just prefer putting everything is standard blocks. .then() .catch() .finally() try/catch/finally Let’s use the newer ES7 async/await with try/catch/finally blocks instead to stay on top of things. They work great, normally. However, as always, there’s another caveat. We have to configure webpack. But first, the async . handleSubmit ... async handleSubmit(event) { event.preventDefault(); { data = axiosInstance.post( , { : .state.username, : .state.password }); axiosInstance.defaults.headers[ ] = + data.access; localStorage.setItem( , data.access); localStorage.setItem( , data.refresh); data; } (error) { error; } } ... render() { ( <form onSubmit={this.handleSubmit}> ... </form> ) } } // djsr/frontend/src/components/login.js try const await '/token/obtain/' username this password this 'Authorization' "JWT " 'access_token' 'refresh_token' return catch throw return Login < > div </ > div Just need to add at the beginning when declaring the method, and at the part you want the code to wait, add an . It’s that easy. The rest of the code can be written like it’s synchronous, and that includes errors. Very clean. That cleanliness makes it my personal preference. Don’t neglect to change the back to . async await onSubmit handleSubmit Babel again Since we’re using a custom Webpack configuration, using async/await won’t work without some additional elbow grease. Right now, the console will show us a big red error and React will break. Googling around will lead you to leading to the answer. Just add to the entry line of or import it. We had better install that package. ReferenceError: regeneratorRuntime is not defined various StackOverflow Q&As babel-polyfill webpack.config.js $ npm install --save-dev babel-polyfill + babel-polyfill@ 6.26 .0 The cleanest way is to just add that as an entry point. So let’s do that. path = ( ); .exports = { : , : [ , path.resolve(__dirname, )], ... } // webpack.config.js const require 'path' module mode "development" entry 'babel-polyfill' 'djsr/frontend/src/index.js' Yes, you can have multiple entries for entry. Just make sure the final entry in the list is for the actual file. index.js Ok. So now we should be able to use Async/await in our components. Build it and run the server. $ npm run build $ python djsr/manage.py runserver Login again and navigate to and check the console — we should see that it now runs succesfully with the async/await method working on login. Cool. /hello/ GETting from a Protected API endpoint With async working with , storing the token properly and including it in the headers, it can now be used as authentication for accessing protected API endpoints in Django. Login.js Finally. All this work to get right here. is nearly ready. Set free the commented-out lines and add async + await. hello.js ... async getMessage(){ { response = axiosInstance.get( ); message = response.data.hello; .setState({ : message, }); message; } (error){ .log( , .stringify(error, , )); error; } } ... // djsr/frontend/src/components/hello.js try let await '/hello/' const this message return catch console "Error: " JSON null 4 throw Test it out. Unless you test within 5 minutes of logging in, you’ll get a 401 Unauthorized rejection. You can verify it in the Django console or the browser console. /hello/ Unauthorized: hello/ /api/ "GET /api/hello/ HTTP/1.1" 401 183 It’s a bit vague, but this 401 is due to the access token we’re sending along with the request exceeding its short 5 minute lifespan. DRF rejects the request with a 401 error. It's kind of like forgetting the water is boiling for spaghetti, and you forget, and then the water has completely evaporated and you have to start all over again. Login one more time, get a fresh token, and you’ll see the message “world” load. Should a user have to login every 5 minutes? No way. Let’s fix that. Axios interceptor With instance and refresh token in hand, let’s add the interceptor. It isn’t as robust as I would like, so if anybody can suggest improvements, please drop them in the comments. axios axiosInstance = axios.create({ : , : , : { : + localStorage.getItem( ), : , : } }); axiosInstance.interceptors.response.use( response, error => { originalRequest = error.config; (error.response.status === && error.response.statusText === ) { refresh_token = localStorage.getItem( ); axiosInstance .post( , { : refresh_token}) .then( { localStorage.setItem( , response.data.access); localStorage.setItem( , response.data.refresh); axiosInstance.defaults.headers[ ] = + response.data.access; originalRequest.headers[ ] = + response.data.access; axiosInstance(originalRequest); }) .catch( { .log(err) }); } .reject(error); } ); axiosInstance // djsr/frontend/src/axiosApi.js import from 'axios' const baseURL 'http://127.0.0.1:8000/api/' timeout 5000 headers 'Authorization' "JWT " 'access_token' 'Content-Type' 'application/json' 'accept' 'application/json' => response const if 401 "Unauthorized" const 'refresh_token' return '/token/refresh/' refresh ( ) => response 'access_token' 'refresh_token' 'Authorization' "JWT " 'Authorization' "JWT " return => err console return Promise export default Every single time a request is made with the Axios instance we created earlier, if there are no errors, it just works as expected. When there errors, first we get the configuration from the errant request to use later. Then, we must see what kind of error code is being sent along with it. Django Rest Framework sends a 401 Unauthorized error status when a user isn’t authorized to view a protected view. are If the error is a 401, we need to refresh it, which means we need to have the refresh token handy to get a new token pair. POST the token with the custom AxiosInstance and barring any surprises, we’ll have a pair of refreshed tokens to save to localStorage. New tokens in hand, send on the original request, and it should work. In theory. Build it and refresh to see. Take a look at what happens in the Django dev server console: /hello/ Unauthorized: hello/ /api/ "GET /api/hello/ HTTP/1.1" 401 183 "POST /api/token/refresh/ HTTP/1.1" 200 491 "GET /api/hello/ HTTP/1.1" 200 17 Note that sometimes I have had problems using the interceptor method when the request itself has errors, and it results in a bit of a loop. There you have it. Users can login and access protected data with their access token getting refreshed each time it expires. Not easy. Signup Let’s flesh out Signup with a functional method to actually talk to the backend. It’s very similar to the Login component. handleSubmit() ... async handleSubmit(event) { event.preventDefault(); { response = axiosInstance.post( , { : .state.username, : .state.email, : .state.password }); response; } (error) { .log(error.stack); } } ... // djsr/frontend/src/components/signup.js try const await '/user/create/' username this email this password this return catch console This seems to work. Now, try to sign up again in a fresh browser or clear your cookies. Private window may even work. Pay attention to the dev server console. Unauthorized: ser/create/ Bad Request: token/refresh/ /api/u "POST /api/user/create/ HTTP/1.1" 401 183 /api/ "POST /api/token/refresh/ HTTP/1.1" 400 43 But wait, we set to for the view. Shouldn’t it work? It worked fine for CURL. permission_classes AllowAny CustomUserCreate # djsr/authentication/views.py = (permissions.AllowAny,) def post(self, request, format= ): serializer = CustomUserSerializer(data=request.data) serializer.is_valid(): user = serializer.save() user: json = serializer.data Response(json, status=status.HTTP_201_CREATED) Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) ( ): class CustomUserCreate APIView permission_classes 'json' if if return return Turns out, this view was missing something. CustomUserCreate # djsr/authentication/views.py = (permissions.AllowAny,) authentication_classes = () def post(self, request, format= ): serializer = CustomUserSerializer(data=request.data) serializer.is_valid(): user = serializer.save() user: json = serializer.data Response(json, status=status.HTTP_201_CREATED) Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) ( ): class CustomUserCreate APIView permission_classes 'json' if if return return We need to specify an empty list or tuple for in addition to setting to convince DRF to open up a view to the public. Try again. No more . authentication_classes permission_classes 401 Unauthorized error Errors Speaking of errors, how can we handle errors from DRF? At a minimum we should present the field errors for the Signup component. The quickest way to do that is to combine with some ternary operators within the rendered HTML. setState ... async handleSubmit(event) { event.preventDefault(); { response = axiosInstance.post( , { : .state.username, : .state.email, : .state.password }); response; } (error) { .log(error.stack); .setState({ :error.response.data }); } } render() { ( <form onSubmit={this.handleSubmit}> <label> Username: <input name="username" type="text" value={this.state.username} onChange={this.handleChange}/> { this.state.errors.username ? this.state.errors.username : null} </label> <label> Email: <input name="email" type="email" value={this.state.email} onChange={this.handleChange}/> { this.state.errors.email ? this.state.errors.email : null} </label> <label> Password: <input name="password" type="password" value={this.state.password} onChange={this.handleChange}/> { this.state.errors.password ? this.state.errors.password : null} </label> <input type="submit" value="Submit"/> </form> </div> ) } ... // djsr/frontend/src/components/signup.js try const await '/user/create/' username this email this password this return catch console this errors return Signup < > div For a properly made DRF API View, when encountering errors, it will return those errors in JSON form in the response. We log the error and set the state directly to the JSON object containing the error messages. Every time the state is set, it triggers a re-render of the component — in this case that would render the error messages. { .state.errors.password ? .state.errors.password : } this this null Ternary operators like this are done in this format: { ? (content to show True) : (content to show False) Boolean if if So if a password doesn’t pass the password validator on the backed Serializer, it would send an error which we store in the state at state.errors.password and if that is present, it shows the error text. Otherwise it shows nothing. Question for the readers: Is there a different way to build forms with error handling based on some kind of formset for Django and React? Right now it’s quite a manual approach, isn’t it? Now that we can login, signup, get and refresh tokens, handle basic errors and view content with proper authorization. It’s time for the final step: Logging Out. GitHub code for section 2–3 . lives here 2–4) Logging out and blacklisting tokens According to the JWT documentation, there isn’t really a way to log out in the conventional sense. The tokens cannot be forced to expire before their time, and if you delete the token in a users’s , but they somehow come up with a token that hasn’t yet expired(even that same one), they are able once again to access the website as a logged-in user. localStorage There are a few ways to tackle logging out. Just delete the tokens in localStorage1 + blacklist that token1 + blacklist all tokens for the user Deleting the tokens in alone means every token is still valid to use on the site. localStorage The way we have set the project up for this tutorial, each time a user needs to use their refresh token to get a fresh access token, they are issued a fresh PAIR of tokens. A new refresh token with a new validity included, and the old pair is . The number of tokens per user will grow very quickly, so blacklisting whatever token is currently in use doesn’t do much more than merely deleting the token... Unless of course you blacklist each old token after rotation, . which you should Finally, blacklisting every single token for a user would force them to login again on every single device, not just the device they’re currently logged into. What to do? Let’s tackle option 2 as an exercise. First, take a look at settings.py. Ah, it looks like we are NOT blacklisting after rotation. Better change that by adding the blacklist app from and setting to True. django-rest-framework-simplejwt BLACKLIST_AFTER_ROTATION # djsr/djsr/settings.py INSTALLED_APPS = ( ... , ... } ... SIMPLE_JWT = { : timedelta(minutes= ), : timedelta(days= ), : True, : True, : , : SECRET_KEY, : None, : ( ,), : , : , : ( ,), : , } 'rest_framework_simplejwt.token_blacklist' 'ACCESS_TOKEN_LIFETIME' 5 'REFRESH_TOKEN_LIFETIME' 14 'ROTATE_REFRESH_TOKENS' 'BLACKLIST_AFTER_ROTATION' 'ALGORITHM' 'HS256' 'SIGNING_KEY' 'VERIFYING_KEY' 'AUTH_HEADER_TYPES' 'JWT' 'USER_ID_FIELD' 'id' 'USER_ID_CLAIM' 'user_id' 'AUTH_TOKEN_CLASSES' 'rest_framework_simplejwt.tokens.AccessToken' 'TOKEN_TYPE_CLAIM' 'token_type' The blacklist relies on saving blacklisted tokens in the database, so you know you have a migration to run. $ python djsr/manage.py migrate Now each time a token pair gets refreshed, the old ones get blacklisted. First, an API view for blacking out tokens: # djsr/authentication/views.py ... from rest_framework_simplejwt.tokens RefreshToken ... class LogoutAndBlacklistRefreshTokenForUserView(APIView): permission_classes = (permissions.AllowAny,) authentication_classes = () def post(self, request): : refresh_token = request.data[ ] token = RefreshToken(refresh_token) token.blacklist() Response(status=status.HTTP_205_RESET_CONTENT) except Exception e: Response(status=status.HTTP_400_BAD_REQUEST) import try "refresh_token" return as return The view accepts a POSTed , uses that to create a object for access to the blacklist class method, and blacklists it. refresh_token RefreshToken Add it to urls.py. # djsr/authentication/urls.py .views ObtainTokenPairWithColorView, CustomUserCreate, HelloWorldView, LogoutAndBlacklistRefreshTokenForUserView urlpatterns = [ ... path( , LogoutAndBlacklistRefreshTokenForUserView.as_view(), name= ) ] from import 'blacklist/' 'blacklist' Next we need to add a button to our navbar to delete the localStorage tokens and to post the token to a blackout API view, which we will make shortly. This will go in for now, but making a dedicated Nav component at this point also makes sense. App.js ... import axiosInstance ; { () { (); .handleLogout = .handleLogout.bind( ); } handleLogout() { { response = axiosInstance.post( , { : localStorage.getItem( ) }); localStorage.removeItem( ); localStorage.removeItem( ); axiosInstance.defaults.headers[ ] = ; response; } (e) { .log(e); } }; render() { ( <nav> <Link className={"nav-link"} to={"/"}>Home</Link> <Link className={"nav-link"} to={"/login/"}>Login</Link> <Link className={"nav-link"} to={"/signup/"}>Signup</Link> <Link className={"nav-link"} to={"/hello/"}>Hello</Link> <button onClick={this.handleLogout}>Logout</button> </nav> // djsr/frontend/src/components/App.js from "../axiosApi" class App extends Component constructor super this this this async try const await '/blacklist/' "refresh_token" "refresh_token" 'access_token' 'refresh_token' 'Authorization' null return catch console return < = > div className "site" ... The button triggers when clicked. Handle logout posts the refresh token to the blackout API View to black it out, and then deletes access and refresh tokens from , while resetting the Authorization header for the axios instance. Need to do both, otherwise Axios will still be able to get authorized access to protected view. handleLogout() localStorage Build it, run it, and test it by viewing before logging in, while logged in, and after logging out. /hello/ Clicking the logout button doesn’t trigger any kind of global refresh for the site, and clicking the link to the /hello/ page also doesn’t refresh the component if you’re already there, so you’ll may have to manually refresh to see the message disappear. Code for this section is on . GitHub here 3) Bugfixes If you've been testing what we've written so far, you'll have noticed that certain conditions, such as using the wrong password, can cause throw the Axios interceptor into an infinite loop of trying to get a new refresh token but never getting it. . Bad juju in the comments down below generously provided that solves this problem and adds a check the token's expiration time before making a request to begin with. Thanks! Mosta a fix To improve a liiiitle bit further upon that, we can also reject any 401 errors thrown by the token refresh endpoint itself while redirecting the user to the login page. Incorporating both changes into the interceptor, here is the final : (note the change to the baseURL in the axiosInstance) djsr/frontend/frontend/src/axiosApi.js axios baseURL = axiosInstance = axios.create({ : baseURL, : , : { : localStorage.getItem( ) ? + localStorage.getItem( ) : , : , : } }); axiosInstance.interceptors.response.use( response, error => { originalRequest = error.config; (error.response.status === && originalRequest.url === baseURL+ ) { .location.href = ; .reject(error); } (error.response.data.code === && error.response.status === && error.response.statusText === ) { refreshToken = localStorage.getItem( ); (refreshToken){ tokenParts = .parse(atob(refreshToken.split( )[ ])); now = .ceil( .now() / ); .log(tokenParts.exp); (tokenParts.exp > now) { axiosInstance .post( , { : refreshToken}) .then( { localStorage.setItem( , response.data.access); localStorage.setItem( , response.data.refresh); axiosInstance.defaults.headers[ ] = + response.data.access; originalRequest.headers[ ] = + response.data.access; axiosInstance(originalRequest); }) .catch( { .log(err) }); } { .log( , tokenParts.exp, now); .location.href = ; } } { .log( ) .location.href = ; } } .reject(error); } ); axiosInstance // djsr/frontend/src/axiosApi.js import from 'axios' const 'http://127.0.0.1:8000/api/' const baseURL timeout 5000 headers 'Authorization' 'access_token' "JWT " 'access_token' null 'Content-Type' 'application/json' 'accept' 'application/json' => response const // Prevent infinite loops if 401 'token/refresh/' window '/login/' return Promise if "token_not_valid" 401 "Unauthorized" const 'refresh_token' if const JSON '.' 1 // exp date in token is expressed in seconds, while now() returns milliseconds: const Math Date 1000 console if return '/token/refresh/' refresh ( ) => response 'access_token' 'refresh_token' 'Authorization' "JWT " 'Authorization' "JWT " return => err console else console "Refresh token is expired" window '/login/' else console "Refresh token not available." window '/login/' // specific error handling done elsewhere return Promise export default That’s it! Final code on GitHub here In conclusion Thanks for coding along with me! Follow me on Hackernoon @Toruitas or on Twitter: @Stuart_Leitch as I work on stuff like this, machine learning, philosophy in the digital age, and Creative Coding at my startup Lollipop.ai and at University of the Art London's Creative Computing Institute . If you’ve made it this far, you’ve got a React frontend hosted by and interacting with a nice Django Rest Framework based API backend using JWT for authorization. It’s got essentials like Client-side routing, automatic token refreshing, and field errors. Plus nice-to-haves like token blacklisting and ES7 async/await syntax. Well done. If you want to use this as a starting point, do it! The GitHub repo is here For potential followup articles, we could expand this by refactoring and including Redux for state management — it’s what I actually use regularly but would be too much to fit into a single article. I also want to explore how to adapt Django and React to using GraphQL.