© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2021
V. GagliardiDecoupled Django https://doi.org/10.1007/978-1-4842-7144-5_6

6. Decoupled Django with the Django REST Framework

Valentino Gagliardi1  
(1)
Colle di Val D’Elsa, Italy
 
This chapter covers:
  • The Django REST Framework with Vue.js

  • Single-page applications in Django templates

  • Nested DRF serializers

In this chapter, you learn how to use the Django REST Framework to expose a REST API in your Django project, and how to serve a single-page application within Django.

Building the Billing App

In the previous chapter, we created a billing app in the Django project. If you haven’t done this yet, here’s a quick recap. First off, configure DJANGO_SETTINGS_MODULE:
export DJANGO_SETTINGS_MODULE=decoupled_dj.settings.development
Then, run the startapp command to create the app:
python manage.py startapp billing
If you prefer more flexibility but also more typing, you can pass the settings file directly to manage.py:
python manage.py command_name --settings=decoupled_dj.settings.development

Here, command_name is the name of the command you want to run. You can also make a shell function out of this command to avoid typing it again and again.

Note

The rest of this chapter assumes you are in the repo root decoupled-dj, with the Python virtual environment active.

Building the Models

For this app we need a couple of Django models: Invoice for the actual invoice, and ItemLine, which represents a single row in the invoice. Let’s outline the relationships between these models:
  • Each Invoice can have one or many ItemLines

  • An ItemLine belongs to exactly one Invoice

This is a many-to-one (or one-to-many) relationship, which means that ItemLine will have a foreign key to Invoice (if you need a refresher on this topic, check out the resources section at the end of the chapter). In addition, each Invoice is associated with a User (the custom Django user we built in Chapter 3). This means:
  • A User can have many Invoices

  • Each Invoice belongs to one User

To help make sense of this, Figure 6-1 shows the ER diagram on which we will build these Django models.
../images/505838_1_En_6_Chapter/505838_1_En_6_Fig1_HTML.png
Figure 6-1

The ER diagram for the billing application

Having defined the entities, let’s now build the appropriate Django models. Open billing/models.py and define the models as shown in Listing 6-1.
from django.db import models
from django.conf import settings
class Invoice(models.Model):
   class State(models.TextChoices):
       PAID = "PAID"
       UNPAID = "UNPAID"
       CANCELLED = "CANCELLED"
   user = models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
   date = models.DateField()
   due_date = models.DateField()
   state = models.CharField(max_length=15, choices=State.choices, default=State.UNPAID)
class ItemLine(models.Model):
   invoice = models.ForeignKey(to=Invoice, on_delete=models.PROTECT)
   quantity = models.IntegerField()
   description = models.CharField(max_length=500)
   price = models.DecimalField(max_digits=8, decimal_places=2)
   taxed = models.BooleanField()
Listing 6-1

billing/models.py – The Models for the Billing App

Here we take advantage of models.TextChoices, a feature shipped with Django 3.0. As for the rest, they are standard Django fields, with all the relationships set up according to the ER diagram. To add a bit more protection, since we don’t want to delete the invoices or the item lines by accident, we use PROTECT on them.

Enabling the App

When the models are ready, enable the billing app in decoupled_dj/settings/base.py, as shown in Listing 6-2.
INSTALLED_APPS = [
   ...
   "billing.apps.BillingConfig",
]
Listing 6-2

decoupled_dj/settings/base.py - Enabling the Billing App

Finally, you can make and apply the migrations (these are two separate commands):
python manage.py makemigrations
python manage.py migrate

With the app in place, we are now ready to outline the interface, and later the backend code to make it work.

Wireframing the Billing App

Before talking about any frontend library, let’s first see what we are going to build. Figure 6-2 shows a wireframe of the billing app, specifically, the interface for creating a new invoice.
../images/505838_1_En_6_Chapter/505838_1_En_6_Fig2_HTML.jpg
Figure 6-2

The wireframe for the billing app

Having the UI in mind before writing any code is important; this is an approach known as outside-in development. By looking at the interface, we can begin to think about what API endpoint we need to expose. What HTTP calls should we make from the frontend to the backend? First off, we need to fetch a list of clients to populate the select that says “Select a client”. This is a GET call to an endpoint like /billing/api/clients/. As for the rest, it’s almost all dynamic data that must be sent with a POST request once we compile all the fields in the invoice. This could be a request to /billing/api/invoices/. There is also a Send Email button, which should trigger an email. To summarize, we need to make the following calls:
  • GET all or a subset of users from /billing/api/clients/

  • POST data for a new invoice to /billing/api/invoices/

  • POST for sending an email to the client (you will work on this in Chapter 11)

These interactions might sound trivial to any developer familiar with JavaScript. Nevertheless, they will help make sense of the architecture of a typical decoupled project. In the next sections, we pair a JavaScript frontend with a DRF API. Keep in mind that we focus on the interactions and on the architecture between all the moving parts rather than strive for the perfect code implementation.

Note

We don’t have a specialized client model in this project. A client is just a user for Django. We use the term client on the frontend for convenience, while for Django everyone is a User.

Pseudo-Decoupled with the Django REST Framework

We talked about pseudo-decoupled in the previous chapter, as a way to augment the application frontend with JavaScript or to replace the static frontend altogether with a single-page app.

We haven’t touched authentication extensively yet, but in brief, one of the advantages of a pseudo-decoupled approach is that we can use the fantastic built-in Django authentication, based on sessions. In the following sections, we work in practice with one of the most popular JavaScript libraries for building interactive frontends—Vue.js—to see how it fits into Django. Vue.js is a perfect match for a pseudo-decoupled Django project, thanks to its high configurability. If you are wondering, we will cover React later in the book.

Vue.js and Django

Let’s start off with Vue. We want to serve our single-page application from within a Django template.

To do so, we must set up a Vue project. First off, install Vue CLI:
npm install -g @vue/cli
Now we need to create the Vue project somewhere. Vue is highly configurable; in most cases it’s up to you to decide where to put the app. To keep things consistent we create the Vue app inside the billing folder, where our Django app already lives. Move inside the folder and run Vue CLI:
cd billing
vue create vue_spa
The installer will ask whether we want to manually select the features or use the default preset. For our project, we pick the following configurations:
  • Vue 2.x

  • Babel

  • No routing

  • Linter/formatter (ESLint and Prettier)

  • Configuration in dedicated config files

Press Enter and let the installer configure the project. When the package manager finishes pulling in all the dependencies, take a minute to explore the Vue project structure. Once you’re done, you are ready to explore all the steps for making the single-page app work within Django.

To keep things manageable, we target only the development environment for now (we cover production in the next chapter). As anticipated in Chapter 2, Django can serve static files in development with the integrated server. When we run python manage.py runserver, Django collects all the static assets, as long as we configure STATIC_URL. In Chapter 3, we split all the settings for our project, and we configured STATIC_URL as /static/ for development. Out of the box, Django can collect static files from each app folder, and for our billing app, this means we need to put static assets in billing/static.

With a bunch of simple JavaScript files this is easy. You simply place them in the appropriate folder. With a CLI tool like Vue CLI or create-react-app instead, the destination folder for the JavaScript bundle and for all other static assets is already decided for you by the tool. For Vue CLI, this folder is named dist, and it is meant to end up in the same project folder of your single-page app. This is bad for Django, which won’t be able to pick up these static files. Luckily, thanks to Vue’s configurability, we can put our JavaScript build and the template where Django expects them. We can decide where static files and index.html should end up through vue.config.js. Since Vue CLI has an integrated development server with hot reloading, we have two options at this point in development:
  • We serve the app with npm run serve

  • We serve the app through Django’s development server

With the first option, we can run and access the app at http://localhost:8081/ to see changes in real time. With the second option, is convenient to get a more real-world feeling: for example we can use the built-in authentication system. In order to go with the second option, we need to configure Vue CLI.

To start off, in the Vue project folder billing/vue_spa, create an environment file named .env.staging with the following content:
VUE_APP_STATIC_URL=/static/billing/
Note that this is the combination between Django’s STATIC_URL, which we aptly configured in decoupled_dj/settings/.env, and Django’s app folder called billing. Next up, create vue.config.js in the same Vue project folder, with the content shown in Listing 6-3.
const path = require("path");
module.exports = {
 publicPath: process.env.VUE_APP_STATIC_URL,
 outputDir: path.resolve(__dirname, "../static", "billing"),
 indexPath: path.resolve(__dirname, "../templates/", "billing", "index.html")
};
Listing 6-3

billing/vue_spa/vue.config.js – Vue’s Custom Configuration

With this configuration, we tell Vue to:
  • Use the path specified at .env.staging as the publicPath

  • Put static assets to outputDir inside billing/static/billing

  • Put the index.html to indexPath inside billing/templates/billing

This setup respects Django expectations about where to find static files and the main template. publicPath is the path at which the Vue app is expecting to be deployed. In development/staging, we can point to /static/billing/, where Django will serve the files. In production, we provide a different path.

Note

Django is highly configurable in regard to static files and template structure. You are free to experiment with alternative setups. Throughout the book we will adhere to the stock Django structure.

Now you can build your Vue project in “staging” mode (you should run this command from the Vue project folder):
npm run build -- --mode staging
After running the build, you should see Vue files landing in the expected folders:
  • Static assets go in billing/static/billing

  • index.html goes in billing/templates/billing

To test things out, we need to wire up a view and the URLs in Django. First off, in billing/views.py create a subclass of TemplateView to serve Vue’s index.html, as shown in Listing 6-4.
from django.views.generic import TemplateView
class Index(TemplateView):
      template_name = "billing/index.html"
Listing 6-4

billing/views.py - Template View for Serving the App Entry Point

Note

If you like function view more, you can use the render() shortcut with a function view instead of a TemplateView.

Next up, configure the main route in billing/urls.py, as shown in Listing 6-5.
from django.urls import path
from .views import Index
app_name = "billing"
urlpatterns = [
      path("", Index.as_view(), name="index")
]
Listing 6-5

billing/urls.py - URL Configuration

Finally, include the URL for the billing app in decoupled_dj/urls.py, as shown in Listing 6-6.
from django.urls import path, include
urlpatterns = [
   path(
       "billing/",
       include("billing.urls", namespace="billing")
   ),
]
Listing 6-6

decoupled_dj/urls.py - Project URL Configuration

You can now run Django development server in another terminal:
python manage.py runserver
If you visit http://127.0.0.1:8000/billing/, you should see your Vue app up and running, as shown in Figure 6-3.
../images/505838_1_En_6_Chapter/505838_1_En_6_Fig3_HTML.jpg
Figure 6-3

Our Vue app is served by Django’s development server

You may wonder why we use the term staging, and not development, for this setup. What you get out of this configuration, really, is more like a “pre-staging” environment where you can test the Vue app within Django. The drawback of this configuration is that to see changes reflected, we need to rebuild the Vue app every time. Of course nothing stops you from running npm run serve to start the Vue app with the integrated webpack server. In the next sections, we complete the UI for our billing app, and finally the REST backend.

Building the Vue App

Let’s now build our Vue app. To start off, wipe the boilerplate from vue_spa/src/App.vue and start with the code shown in Listing 6-7.
<template>
  <div id="app">
      <InvoiceCreate />
  </div>
</template>
<script>
import InvoiceCreate from "@/components/InvoiceCreate";
export default {
  name: "App",
  components: {
      InvoiceCreate
  }
};
</script>
Listing 6-7

Main Vue Component

Here we include the InvoiceCreate component. Now, create this component in a new file called vue_spa/src/components/InvoiceCreate.vue, (you can also remove HelloWorld.vue). Listing 6-8 shows the template part first.
<template>
 <div class="container">
   <h2>Create a new invoice</h2>
   <form @submit.prevent="handleSubmit">
     <div class="form">
       <div class="form__aside">
         <div class="form__field">
           <label for="user">Select a client</label>
           <select id="user" name="user" required>
             <option value="--">--</option>
             <option v-for="user in users" :key="user.email" :value="user.id">
               {{ user.name }} - {{ user.email }}
             </option>
           </select>
         </div>
         <div class="form__field">
           <label for="date">Date</label>
           <input id="date" name="date" type="date" required />
         </div>
         <div class="form__field">
           <label for="due_date">Due date</label>
           <input id="due_date" name="due_date" type="date" required />
         </div>
       </div>
       <div class="form__main">
         <div class="form__field">
           <label for="quantity">Qty</label>
           <input
             id="quantity"
             name="quantity"
             type="number"
             min="0"
             max="10"
             required
           />
         </div>
         <div class="form__field">
           <label for="description">Description</label>
           <input id="description" name="description" type="text" required />
         </div>
         <div class="form__field">
           <label for="price">Price</label>
           <input
             id="price"
             name="price"
             type="number"
             min="0"
             step="0.01"
             required
           />
         </div>
         <div class="form__field">
           <label for="taxed">Taxed</label>
           <input id="taxed" name="taxed" type="checkbox" />
         </div>
       </div>
     </div>
     <div class="form__buttons">
       <button type="submit">Create invoice</button>
       <button disabled>Send email</button>
     </div>
   </form>
 </div>
</template>
Listing 6-8

Template Section of the Vue Form Component

In this markup we have:
  • The select for choosing the client

  • Two date inputs

  • Inputs for quantity, description, and price

  • A checkbox for taxed

  • Two buttons

Next up we have the logic part, with the quintessential form handling, as shown in Listing 6-9.
<script>
export default {
 name: "InvoiceCreate",
 data: function() {
   return {
     users: [
       { id: 1, name: "xadrg", email: "[email protected]" },
       { id: 2, name: "olcmf", email: "[email protected]" }
     ]
   };
 },
 methods: {
   handleSubmit: function(event) {
     // eslint-disable-next-line no-unused-vars
     const formData = new FormData(event.target);
     // TODO - build the request body
     const data = {};
     fetch("/billing/api/invoices/", {
       method: "POST",
       headers: { "Content-Type": "application/json" },
       body: JSON.stringify(data)
     })
       .then(response => {
         if (!response.ok) throw Error(response.statusText);
         return response.json();
       })
       .then(json => {
         console.log(json);
       })
       .catch(err => console.log(err));
   }
 },
 mounted() {
   fetch("/billing/api/clients/")
     .then(response => {
       if (!response.ok) throw Error(response.statusText);
       return response.json();
     })
     .then(json => {
       this.users = json;
     });
 }
};
</script>
Listing 6-9

JavaScript Section of the Vue Form Component

In this code we have:
  • A users property inside the Vue component state

  • A method for handling the form submit

  • A mounted lifecycle method for fetching data on mount

Also, we target our API endpoints (not yet implemented): /billing/api/clients/ and /billing/api/invoices/. You can notice some fake data in users; this is so we have a minimal usable interface while we wait for building the REST API.

Tip

You can develop the frontend without a backend, with tools like Mirage JS, which can intercept and respond to HTTP calls.

To make the code work, remember to put the template and the script part in order in vue_spa/src/components/InvoiceCreate.vue. With this minimal implementation, you are now ready to start the project in two ways. To build the app and serve it with Django, run the following in the Vue project folder:
npm run build -- --mode staging
Then, start the Django development server and head over to http://localhost:8000/billing/. To run Vue with its development server instead, run the following inside the Vue folder:
npm run serve
The app will start at http://localhost:8081/, but since we don’t have the backend yet, nothing will work for the end user. In the meantime, we can set up the application so that:
  • When launched under Django’s umbrella, it calls /billing/api/clients/ and /billing/api/invoices/

  • When called with the integrated webpack server, it calls http://localhost:8000/billing/api/clients/ and http://localhost:8000/billing/api/invoices/, which are the endpoints where the DRF will listen

To do this, open vue.config.js and add the lines in Listing 6-10 in the configuration.
// omitted
module.exports = {
  // omitted
  devServer: {
      proxy: "http://localhost:8000"
  }
};
Listing 6-10

Development Server Configuration for Vue CLI

This ensures the project works well in staging/production with a pseudo-decoupled setup, and in development as a standalone app. In a minute, we will finally build the REST backend.

Vue.js, Django, and CSS

At this point you may wonder where CSS fits into the big picture. Our Vue component does have some classes, but we didn’t show any CSS pipeline in the previous section.

The reason is that there are at least two approaches for working with CSS in a project like the one we are building. Specifically, you can:
  • Include CSS in a base Django template

  • Include CSS from each single-page app

At the time of this writing, Tailwind is one of the most popular CSS libraries on the Django scene. In a pseudo-decoupled setup, you can configure Tailwind in the main Django project, include the CSS bundle in a base template, and have a single-page Vue app extend the base template. If each single-page app is independent, each one with its own style, you can configure Tailwind and friends individually. Be aware that the maintainability of the second approach might be a bit difficult in the long run.

Note

You can find a minimal CSS implementation for the component in the source code for this chapter at https://github.com/valentinogagliardi/decoupled-dj/tree/chapter_06_decoupled_with_drf.

Building the REST Backend

We left a note in our Vue component that says // TODO - build the request body. This is because with the form we built, we cannot send the request as it is to the Django REST Framework. You’ll see the reason in a moment. In the meantime, with the UI in place, we can wire up the backend with the DRF. Based on the endpoints we call from the UI, we need to expose the following sources:
  • /billing/api/clients/

  • /billing/api/invoices/

Let’s also recap the relationships between all the entities:
  • Each Invoice can have one or many ItemLines

  • An ItemLine belongs to exactly one Invoice

  • A User can have many Invoices

  • Each Invoice belongs to one User

What does this mean? When POSTing to the backend to create a new invoice, Django wants:
  • The user ID to associate the invoice with

  • One or more item lines to associate the invoice with

The user is not a problem because we grab it from the first API call to /billing/api/clients/. Each item and the associated invoice cannot be sent as a whole from the frontend. We need to:
  • Build the correct object in the frontend

  • Adjust the ORM logic in the DRF to save related objects

Building the Serializers

To start off, we need to create the following components in the DRF:
  • A serializer for User

  • A serializer for Invoice

  • A serializer for ItemLine

As a first step let’s install the Django REST Framework:
pip install djangorestframework
Once it’s installed, update requirements/base.txt to include the DRF:
Django==3.1.3
django-environ==0.4.5
psycopg2-binary==2.8.6
uvicorn==0.12.2
djangorestframework==3.12.2
Next up, enable the DRF in decoupled_dj/settings/base.py, as shown in Listing 6-11.
INSTALLED_APPS = [
   ...
   "users.apps.UsersConfig",
   "billing.apps.BillingConfig",
   "rest_framework", # enables DRF
]
Listing 6-11

decoupled_dj/settings/base.py - Django Installed Apps with the DRF Enabled

Now create a new Python package named api in billing so that we have a billing/api folder. In this package, we place all the logic for our REST API. Let’s now build the serializers. Create a new file called billing/api/serializers.py with the content shown in Listing 6-12.
from users.models import User
from billing.models import Invoice, ItemLine
from rest_framework import serializers
class UserSerializer(serializers.ModelSerializer):
   class Meta:
       model = User
       fields = ["id", "name", "email"]
class ItemLineSerializer(serializers.ModelSerializer):
   class Meta:
       model = ItemLine
       fields = ["quantity", "description", "price", "taxed"]
class InvoiceSerializer(serializers.ModelSerializer):
   items = ItemLineSerializer(many=True, read_only=True)
   class Meta:
       model = Invoice
       fields = ["user", "date", "due_date", "items"]
Listing 6-12

billing/api/serializers.py – The DRF Serializers

Here we have three serializers. UserSerializer will serialize our User model. ItemLineSerializer is the serializer for an ItemLine. Finally, InvoiceSerializer will serialize our Invoice model. Each serializer subclasses the DRF’s ModelSerializer, which we encountered in Chapter 3, and has the appropriate fields mapping to the corresponding model. The last serializer in the list, InvoiceSerializer, is interesting because it contains a nested ItemLineSerializer. It’s this serializer that needs some work to comply with our frontend. To see why, let’s build the views.

Building the Views and the URL

Create a new file called billing/api/views .py with the code shown in Listing 6-13.
from .serializers import InvoiceSerializer, UserSerializer, User
from rest_framework.generics import CreateAPIView, ListAPIView
class ClientList(ListAPIView):
   serializer_class = UserSerializer
   queryset = User.objects.all()
class InvoiceCreate(CreateAPIView):
   serializer_class = InvoiceSerializer
Listing 6-13

billing/api/views.py - DRF Views

These views will respond respectively to /billing/api/clients/ and /billing/api/invoices/. Here, ClientList is a subclass of the generic DRF list view. InvoiceCreate instead subclasses the DRF’s generic create view. We are now ready to wire up the URLs for our app. Open billing/urls.py and define your routes as shown in Listing 6-14.
from django.urls import path
from .views import Index
from .api.views import ClientList, InvoiceCreate
app_name = "billing"
urlpatterns = [
   path("", Index.as_view(), name="index"),
   path(
       "api/clients/",
       ClientList.as_view(),
       name="client-list"),
   path(
       "api/invoices/",
       InvoiceCreate.as_view(),
       name="invoice-create"),
]
Listing 6-14

billing/urls.py - URL Patterns for the Billing API

Here, app_name paired with a namespace in the main project URL will allow us to call billing:client-list and billing:invoice-create with reverse(), which is particularly useful in testing. As a last step, you should have the URLs configured in decoupled_dj/urls.py, as shown in Listing 6-15.
from django.urls import path, include
urlpatterns = [
   path(
       "billing/",
       include("billing.urls", namespace="billing")
   ),
]
Listing 6-15

decoupled_dj/urls.py - The Main Project URL Configuration

We are ready to test things out. To create a couple of models in the database, you can launch an enhanced shell (this comes from django-extensions):
python manage.py shell_plus
To create the models, run the following queries (>>> is the shell prompt):
>>> User.objects.create_user(username="jul81", name="Juliana", email="[email protected]")
>>> User.objects.create_user(username="john89", name="John", email="[email protected]")
Exit the shell and start Django:
python manage.py runserver
In another terminal, run the following curl command and see what happens:
curl -X POST --location "http://127.0.0.1:8000/billing/api/invoices/"
   -H "Accept: */*"
   -H "Content-Type: application/json"
   -d "{
         "user": 1,
         "date": "2020-12-01",
         "due_date": "2020-12-30"
       }"
As a response you should see the following output:
{"user":1,"date":"2020-12-01","due_date":"2020-12-30"}
This is the Django REST Framework telling us it created a new invoice in the database. So far so good. How about adding some items to the invoice now? To do so, we need to make the serializer writable. In billing/api/serializers.py, remove read_only=True from the field items so that it looks like Listing 6-16.
class InvoiceSerializer(serializers.ModelSerializer):
   items = ItemLineSerializer(many=True)
   class Meta:
       model = Invoice
       fields = ["user", "date", "due_date", "items"]
Listing 6-16

billing/api/serializers.py - The Serializer for an Invoice, Now with a Writable Relationship

You can test again with curl, this time by passing also two items:
curl -X POST --location "http://127.0.0.1:8000/billing/api/invoices/"
   -H "Accept: application/json"
   -H "Content-Type: application/json"
   -d "{
         "user": 1,
         "date": "2020-12-01",
         "due_date": "2020-12-30",
         "items": [
           {
             "quantity": 2,
             "description": "JS consulting",
             "price": 9800.00,
             "taxed": false
           },
           {
             "quantity": 1,
             "description": "Backend consulting",
             "price": 12000.00,
             "taxed": true
           }
         ]
       }"
At this point everything should blow up, and you should see the following exception:
TypeError: Invoice() got an unexpected keyword argument 'items'
Exception Value: Got a TypeError when calling Invoice.objects.create().

This may be because you have a writable field on the serializer class that is not a valid argument to Invoice.objects.create(). You may need to make the field read-only or override the InvoiceSerializer.create() method to handle this correctly.

Django REST is asking us to tweak create() in InvoiceSerializer so it can accept items alongside with the invoice.

Working with Nested Serializers

Open billing/api/serializers.py and modify the serializer as shown in Listing 6-17.
class InvoiceSerializer(serializers.ModelSerializer):
   items = ItemLineSerializer(many=True)
   class Meta:
       model = Invoice
       fields = ["user", "date", "due_date", "items"]
   def create(self, validated_data):
       items = validated_data.pop("items")
       invoice = Invoice.objects.create(**validated_data)
       for item in items:
           ItemLine.objects.create(invoice=invoice, **item)
       return invoice
Listing 6-17

billing/api/serializers.py - The Serializer for an Invoice, Now with a Customized create()

This is also a good moment to tweak the ItemLine model. As you can see from the serializer, we are using the items field to set related items on a given invoice. The problem is, there is no such field available in the Invoice model. This is because reverse relationships on a Django model are accessible as modelname_set unless configured differently. To fix the field, open billing/models.py and add the related_name attribute to the invoice row, as shown in Listing 6-18.
class ItemLine(models.Model):
   invoice = models.ForeignKey(
     to=Invoice, on_delete=models.PROTECT, related_name="items"
   )
   ...
Listing 6-18

billing/models.py - The ItemLine Model with a related_name

After saving the file, run the migration as follows:
python manage.py makemigrations billing
python manage.py migrate

After starting Django, you should now be able to repeat the same curl request, this time with success. At this stage, we can fix the frontend as well.

Fixing the Vue Frontend

In vue_spa/src/components/InvoiceCreate.vue, locate the line that says // TODO - build the request body and adjust the code as shown in Listing 6-19.
 methods: {
   handleSubmit: function(event) {
     const formData = new FormData(event.target);
     const data = Object.fromEntries(formData);
     data.items = [
       {
         quantity: formData.get("quantity"),
         description: formData.get("description"),
         price: formData.get("price"),
         taxed: Boolean(formData.get("taxed"))
       }
     ];
     fetch("/billing/api/invoices/", {
       method: "POST",
       headers: { "Content-Type": "application/json" },
       body: JSON.stringify(data)
     })
       // omitted;
   }
 }
Listing 6-19

The handleSubmit Method from the Vue Component

For brevity, I show only the relevant portion. Here, we use Object.fromEntries() (ECMAScript 2019) to build an object from our form. We then proceed to add an array of items (it has just one item for now) to the object. We finally send the object as the body payload for fetch. You can run Vue with the integrated server (from within the Vue project folder):
npm run serve

You should see a form that creates an invoice at http://localhost:8080/. Try to fill the form and click on Create Invoice. In the browser console, you should see the response from the Django REST Framework, with the invoice being successfully saved to the database. Great job! We finished the first real feature of this decoupled Django project.

Note

It is a good moment to commit the changes you made so far and to push the work to your Git repo. You can find the source code for this chapter at https://github.com/valentinogagliardi/decoupled-dj/tree/chapter_06_decoupled_with_drf.

Exercise 6-1: Handling Multiple Items

Extend the Vue component to handle multiple items for the invoice. The user should be able to click on a plus (+) button to add more items to the form, which should be sent along with the request.

Summary

This chapter paired up a Vue.js frontend with a Django REST Framework API, with Vue.js served in the same context as the main Django project.

By doing so, you learned how to:
  • Integrate Vue.js into Django

  • Interact with a DRF API from JavaScript

  • Work with nested serializers in the Django REST Framework

In the next chapter, we approach a more real-world scenario. We discuss security and deployment, before moving again to the JavaScript land, with Next.js in Chapter 8.

Additional Resource

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
44.200.210.43