Pycon X - Florence
5 May 2019
Give you an overview/ guideline for structuring a Django project with React (or any JavaScript framework/library).
Outline challenges and tradeoffs of a decoupled approach.
By the end of the talk you should be able to configure a new Django REST project with a front-end library for interacting with the API.
Exposing data and contents "outside" Django templates, with the help of a REST API.
Decoupling Django means exposing your models as JSON.
Because of flexibility... Back-end and front-end developers can work in isolation. Also, no need to fight build tools.
But a decoupled approach can give a lot of headaches! Code duplication, more testing, more developers.
A decoupled approach imposes challenges!
These are good signs that you will benefit from decoupling:
These are good signs that you don't need to decouple:
Django REST framework is a Django application for building REST APIs on top of Django.
pip install djangorestframework
But first ... Django models!
from django.db import models
"""
some other model here!
"""
class Link(models.Model):
title = models.TextField(max_length=200, unique=True)
url = models.URLField(max_length=250, unique=True)
tags = models.ManyToManyField(Tag)
created_at = models.DateField(auto_now_add=True)
Serializers are a layer between Django models and the outside world (they mirror Django forms).
from rest_framework import serializers
from .models import Link
class LinkSerializer(serializers.ModelSerializer):
class Meta:
model = Link
fields = ('title', 'url', 'tags')
tags = serializers.StringRelatedField(many=True)
fields = '__all__' # Don't do that
Views in DRF can be written as plain functions.
from rest_framework.decorators import api_view
from rest_framework.response import Response
from .models import Link
from .serializers import LinkSerializer
@api_view(['GET'])
def link_list(request):
links = Link.objects.all()
serializer = LinkSerializer(links, many=True)
return Response(serializer.data)
But even better with Generic Views.
from rest_framework.generics import ListAPIView
from .models import Link
from .serializers import LinkSerializer
class LinkList(ListAPIView):
serializer_class = LinkSerializer
queryset = Link.objects.all()
After creating a serializer and a view we can expose the API endpoint.
# links/urls.py
from django.urls import path
from .views import LinkList
PREFIX = 'api'
urlpatterns = [
path(f'{PREFIX}/link/', LinkList.as_view()),
]
React is a front-end library for building composable, reusable, user interfaces.
npm i react react-dom
import React from "react";
function Form(props) {
return <form onSubmit={props.onSubmit}>
{props.children}
</form>;
};
<Form onSubmit={handleSubmit}>
<Input/>
<Button/>
</Form>
React components are ... JavaScript functions!
And now comes the hard part. Many ways for pairing up a JavaScript frontend to a Django REST API.
A closer look at two practical approaches ...
Approach #1: A CRA React app in app_name/
<!-- How about these? -->
<script src="/static/js/2.3310f33a.chunk.js"></script>
<script src="/static/js/main.c82d5dba.chunk.js"></script>
<!-- We do need these instead -->
{% load static %}
<script src="{% static "links/js/2.3310f33a.chunk.js" %}"></script>
<script src="{% static "links/js/main.c82d5dba.chunk.js" %}"></script>
But when running npm run build you get these files:
Approach #2: a custom webpack project in app_name/
We have control over the bundle path! Ok for development:
// webpack.config.js
const path = require("path");
module.exports = {
entry: "./index.js",
output: {
filename: "main.js",
path: path.resolve(__dirname,
"../static/custom_webpack_conf_2/js")
},
module: {
// other stuff
}
};
<!-- Seems good! -->
{% load static %}
<script src="{% static "links/js/main.js" %}"></script>
But now ... how about splitChunks and code splitting?
"""
Don't forget to expose a view!
"""
def index(request):
return render(request, 'custom_webpack_conf_2/index.html')
With splitChunks you can aggressively split your bundle. Ending up with a lot of files ...
How do we inject these files into our Django template?
😭
Maybe we can build a "django-webpacker"?
Fully decoupled front-end! Best of both worlds, you don't fight tools.
Don't forget to enable django-cors-headers! https://github.com/ottoyiu/django-cors-headers/
Authentication plays an important role in a decoupled setup.
pip install djangorestframework_simplejwt
A lot of options for testing the front-end:
it("Test description (user story)", () => {
cy.server();
cy.visit('/');
cy.route({
method: "POST", url: "**/api/user/token/", status: 401,
response: { access: undefined, refresh: undefined }
});
const emailInput = 'input[name="username"]';
const passwordInput = 'input[name="password"]';
cy.get(emailInput).type("xsasasa");
cy.get(passwordInput).type("changeme");
cy.get("[data-testid=submit]").click();
cy.get("[data-testid=error-message]")
.contains("Unauthorized");
});
A lot of options for testing the API too:
from rest_framework.test import APITestCase
from django.contrib.auth.models import User
from ..models import Link, Tag
class SimpleTest(APITestCase):
def setUp(self):
self.path = '/api/link2/'
self.link = Link.objects.create(
title="Some title",
url="someurl"
)
self.tags = [Tag.objects.create(name=tag)
for tag in ['django', 'python']]
[self.link.tags.add(tag) for tag in self.tags]
def test_guest_cannot_see_links(self):
response = self.client.get(self.path)
self.assertEqual(response.status_code, 401)
# more stuff here
def test_authenticated_user(self):
user = User.objects.create_user(username='valentino',
password='changeme')
self.client.force_authenticate(user=user)
response = self.client.get(self.path)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, [{"id": 1,
"title": "Some title",
"url": "someurl",
"tags": ["django", "python"]}])
Django REST framework is great!
You may want to investigate more advanced topics: