DECOUPLING DJANGO WITH DJANGO REST (and a sprinkle of React)

Pycon X - Florence

5 May 2019

Valentino Gagliardi

JavaScript Developer and Trainer

 

I 💚 Python

 

https://www.valentinog.com/blog/

Such kid! Much wow!

The plan

  1. introduction to decoupling
  2. a (re)introduction to Django REST
  3. setting up a JavaScript front-end
  4. focus on testing
  5. conclusion and resources

Goal of this talk

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.

1. Introduction

A bit of backstory

What is "decoupling"?

Exposing data and contents "outside" Django templates, with the help of a REST API.

Decoupling Django means exposing your models as JSON.

JSON

Why decoupling?

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.

Challenges

A decoupled approach imposes challenges!

  • search engine optimization (SSR or die)
  • authentication
  • logic duplication (errors and form validation)
  • more testing
  • more developers

When to decouple?

These are good signs that you will benefit from decoupling:

  • lot of  JS-driven interactions
  • you're building a mobile app
  • you're building a dashboard for internal use

When NOT to decouple

These are good signs that you don't need to decouple:

  • the application has little or no JS-driven interactions
  • you're concerned about SEO
  • you want to decouple just because everyone is doing the same ("Medium" driven development)

2. Django REST framework

Django REST framework

Django REST framework is a Django application for building REST APIs on top of Django.

pip install djangorestframework

Django models

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)

DRF serializers

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

DRF function views

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)

DRF generic views

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()

Wiring up the urls

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()),
]

The result

3. Setting up the frontend

What is React?

React is a front-end library for building composable, reusable, user interfaces.

npm i react react-dom

React components

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!

Let's glue things together

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 ...

#1 create-react-app

Approach #1: A CRA React app in app_name/

#1 create-react-app

<!-- 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:

#2 Custom webpack project

Approach #2: a custom webpack project in app_name/

#2 Custom webpack project

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
  }
};

#2 Custom webpack project

<!-- 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')

#2 Custom webpack project

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?

#2.1 django-webpack-loader

😭 

Learning from others

Maybe we can build a "django-webpacker"?

The winner is ...

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/

Do you even authenticate?

Authentication plays an important role in a decoupled setup.

  • good ol' Django session authentication
  • token based authentication (JWT)
pip install djangorestframework_simplejwt

4. Testing

Testing the front-end

A lot of options for testing the front-end:

  • Selenium, (cannot stub the API, semi-auto waiting)
  • Cypress, (can stub the API, automatic waiting)
  • Splinter, (Selenium abstraction)

UI testing with Cypress

  
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");
});

Testing the API

A lot of options for testing the API too:

  • APISimpleTestCase: (no db queries)
  • APITestCase: (db is available)
  • APILiveServerTestCase

API testing in DRF

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)

API testing in DRF

# 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"]}])

Demo time!

5. Conclusions and resources

Conclusions

Django REST framework is great!

  • configuration over coding
  • built-in pagination
  • authentication and permissions
  • throttling

Where to go from here

You may want to investigate more advanced topics:

  • DRF router
  • DRF viewset

Resources

Get in touch!

Thank you!