Create a Modern Application with Django and Vue #2

You can download the source code for this tutorial here.

In part two, we are going to talk about how to connect the backend and the frontend. Currently, the industry standard is to use something called REST API, which stands for representational state transfer application programming interface. API refers to the connection between two software applications, and REST refers to a specific architecture that this type of connection follows.

API

A REST API request usually consists of an endpoint, which points to the server, an HTTP method, a header and a body. The header provides meta information such as caching, user authentication and AB testing, and the body contains data that the client wants to send to the server.

However, REST API has one small flaw, it is impossible to design APIs that only fetch the exact data that the client requires, so it is very common for the REST API to overfetch or underfetch. GraphQL was created to solve this problem. It uses schemas to make sure that with each request, it only fetches data that is required, we’ll see how this works later.

Setting up GraphQL with Django

Let’s start by setting up GraphQL in the backend. We need to install a new package called graphene-django. Run the following command:

1
pip install graphene-django

Next, go to settings.py and find the INSTALLED_APPS variable. We need to add graphene-django inside so that Django is able to find this module.

1
2
3
4
5
INSTALLED_APPS = [
  ...
  "blog",
  "graphene_django",
]

Configuring graphene-django

There are still a few things we need to do before we can use GraphQL. First, we need to setup a URL pattern to serve the GraphQL APIs. Go to urls.py and add the following code:

1
2
3
4
5
6
7
from django.views.decorators.csrf import csrf_exempt
from graphene_django.views import GraphQLView

urlpatterns = [
    ...
    path("graphql", csrf_exempt(GraphQLView.as_view(graphiql=True))),
]

Next, we need to create the schemas and tell Django where to find them in the settings.py. GraphQL schemas define a pattern that allows Django to translate the database models into GraphQL and vice versa. Let’s take the Site model as an example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Site(models.Model):
    name = models.CharField(max_length=200)
    description = models.TextField()
    logo = models.ImageField(upload_to='site/logo/')

    class Meta:
        verbose_name = 'site'
        verbose_name_plural = '1. Site'

    def __str__(self):
        return self.name

Create a schema.py file inside the blog directory.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import graphene
from graphene_django import DjangoObjectType
from blog import models

# Define type
class SiteType(DjangoObjectType):
    class Meta:
        model = models.Site

# The Query class
class Query(graphene.ObjectType):
    site = graphene.Field(types.SiteType)

    def resolve_site(root, info):
        return (
            models.Site.objects.first()
        )

As you can see, this file is divided into three parts. First, we import the necessary packages and models. Next, we define a SiteType, and this type is bonded with the Site model. And then, we have a Query class. This class is what allows us to retrieve information using the GraphQL API. To create or update information, we need to use a different class called Mutation, which we’ll discuss in the next article.

Inside the Query class, we have a resolve_site function that binds with the site variable, which returns the first record of the Site model. This part works exactly the same as the regular Django QuerySet.

Creating schemas

Now we can do the same for all of our models. To make sure the schema file isn’t too long, I separated them into schema.py, types.py and queries.py.

schema.py

1
2
3
4
5
import graphene
from blog import queries


schema = graphene.Schema(query=queries.Query)

types.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import graphene
from graphene_django import DjangoObjectType
from blog import models


class SiteType(DjangoObjectType):
    class Meta:
        model = models.Site


class UserType(DjangoObjectType):
    class Meta:
        model = models.User


class CategoryType(DjangoObjectType):
    class Meta:
        model = models.Category


class TagType(DjangoObjectType):
    class Meta:
        model = models.Tag


class PostType(DjangoObjectType):
    class Meta:
        model = models.Post 

queries.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import graphene
from blog import models
from blog import types


# The Query class
class Query(graphene.ObjectType):
    site = graphene.Field(types.SiteType)
    all_posts = graphene.List(types.PostType)
    all_categories = graphene.List(types.CategoryType)
    all_tags = graphene.List(types.TagType)
    posts_by_category = graphene.List(types.PostType, category=graphene.String())
    posts_by_tag = graphene.List(types.PostType, tag=graphene.String())
    post_by_slug = graphene.Field(types.PostType, slug=graphene.String())

    def resolve_site(root, info):
        return (
            models.Site.objects.first()
        )

    def resolve_all_posts(root, info):
        return (
            models.Post.objects.all()
        )

    def resolve_all_categories(root, info):
        return (
            models.Category.objects.all()
        )

    def resolve_all_tags(root, info):
        return (
            models.Tag.objects.all()
        )

    def resolve_posts_by_category(root, info, category):
        return (
            models.Post.objects.filter(category__slug__iexact=category)
        )

    def resolve_posts_by_tag(root, info, tag):
        return (
            models.Post.objects.filter(tag__slug__iexact=tag)
        )

    def resolve_post_by_slug(root, info, slug):
        return (
            models.Post.objects.get(slug__iexact=slug)
        ) 

Finally, we need to tell Django where to find the schema file. Go to settings.py and add the following code:

1
2
3
4
# Configure GraphQL
GRAPHENE = {
    "SCHEMA": "blog.schema.schema",
}

To verify that the schemas work, open your browser and go to http://127.0.0.1:8000/graphql. You should see the GraphiQL interface.

GraphiQL

Notice how we are retrieving information in this example, it’s the GraphQL language, and it is how we are going to retrieve data in the frontend, which you’ll see later.

Setting up CORS

Before we can move onto the frontend, there is still something we need to take care of. By default, data can only be transferred within the same application for security reasons, but in our case we need the data to flow between two applications. To tackle this problem, we need to enable the CORS (cross origin resource sharing) functionality.

First, install the django-cors-headers package. Inside the backend app, run the following command:

1
pip install django-cors-headers

Add "corsheaders" to the INSTALLED_APPS variable.

1
2
3
4
INSTALLED_APPS = [
  ...
  "corsheaders",
]

Then add "corsheaders.middleware.CorsMiddleware" to the MIDDLEWARE variable:

1
2
3
4
MIDDLEWARE = [
  "corsheaders.middleware.CorsMiddleware",
  ...
]

And finally, add the following code to the settings.py.

1
2
CORS_ORIGIN_ALLOW_ALL = False
CORS_ORIGIN_WHITELIST = ("http://localhost:8080",) # Matches the port that Vue.js is using

Setting up Apollo with Vue.js

Now it’s time for us to move to the frontend. First, we need to install a library called Apollo. It allows us to use GraphQL in the Vue app. To do that, run the following command:

1
npm install --save graphql graphql-tag @apollo/client

Under the src directory, create a new file called apollo-config.js and add the following code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import { ApolloClient, createHttpLink, InMemoryCache } from '@apollo/client/core'

// HTTP connection to the API
const httpLink = createHttpLink({
  uri: 'http://127.0.0.1:8000/graphql', // Matches the url that Django is using
})

// Cache implementation
const cache = new InMemoryCache()

// Create the apollo client
const apolloClient = new ApolloClient({
  link: httpLink,
  cache,
})

Then go to main.js and import the apolloClient:

1
2
import { apolloClient } from "@/apollo-config";
createApp(App).use(router).use(apolloClient).mount("#app");

Now we can use the GraphQL language we just saw to retrieve data from the backend. Let’s see an example. Go to App.vue, and here we’ll retrieve the name of our website.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<template>
  <div class="container mx-auto max-w-3xl px-4 sm:px-6 xl:max-w-5xl xl:px-0">
    <div class="flex flex-col justify-between h-screen">
      <header class="flex flex-row items-center justify-between py-10">
        <div class="nav-logo text-2xl font-bold">
          <router-link to="/" v-if="mySite">{{ mySite.name }}</router-link>
        </div>
        ...
      </header>
      ...
    </div>
  </div>
</template>

<script>
import gql from "graphql-tag";

export default {
  data() {
    return {
      mySite: null,
    };
  },

  async created() {
    const siteInfo = await this.$apollo.query({
      query: gql`
        query {
          site {
            name
          }
        }`,
    });
    this.mySite = siteInfo.data.site;
  },
};
</script> 

It is my personal habit to create a separate file for all the queries and then import it into the .vue file.

src/queries.js

1
2
3
4
5
6
7
8
9
import gql from "graphql-tag";

export const SITE_INFO = gql`
  query {
    site {
      name
    }
  }
`;

App.vue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
...

<script>
import { SITE_INFO } from "@/queries";


export default {
  data() {
    return {
      mySite: null,
    };
  },

  async created() {
    const siteInfo = await this.$apollo.query({
      query: SITE_INFO,
    });
    this.mySite = siteInfo.data.site;
  },
};
</script> 

The category page

Now we have a problem left over from the previous article. When we invoke a router, how does the router know which page should be returned? For instance, when we click on a link Category One, a list of posts that belong to category one should be returned, but how does the router know how to do that? Let’s see an example.

First, in the router/index.js file where we defined all of our routers, we should set a segment of the URL pattern as a variable. In the following example, the word after /category/ will be assigned to the variable category. This variable will be accessible in the CategoryView component.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { createRouter, createWebHistory } from "vue-router";
...

const routes = [
  {
    path: "/",
    name: "Home",
    component: HomeView,
  },
  {
    path: "/category/:category",
    name: "Category",
    component: CategoryView,
  },
  ...
];

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes,
});

export default router;

Next, in the AllCategories view, we will pass some information to this category variable.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<template>
  <div class="flex flex-col place-content-center place-items-center">
    <div class="py-8 border-b-2">
      <h1 class="text-5xl font-extrabold">All Categories</h1>
    </div>
    <div class="flex flex-wrap py-8">
      <router-link
        v-for="category in this.allCategories"
        :key="category.name"
        class="..."
        :to="`/category/${category.slug}`"
        >{{ category.name }}</router-link
      >
    </div>
  </div>
</template>

In the Category view, we can access this category variable using this.$route property.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<script>
// @ is an alias to /src
import PostList from "@/components/PostList.vue";
import { POSTS_BY_CATEGORY } from "@/queries";


export default {
  components: { PostList },
  name: "CategoryView",

  data() {
    return {
      postsByCategory: null,
    };
  },

  async created() {
    const posts = await this.$apollo.query({
      query: POSTS_BY_CATEGORY,
      variables: {
        category: this.$route.params.category,
      },
    });
    this.postsByCategory = posts.data.postsByCategory;
  },
};
</script>

And finally, the corresponding posts can be retrieved using the POSTS_BY_CATEGORY query.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
export const POSTS_BY_CATEGORY = gql`
  query ($category: String!) {
    postsByCategory(category: $category) {
      title
      slug
      content
      isPublished
      isFeatured
      createdAt
    }
  }
`;

With this example, you should be able to create the tag and post page.

Creating and updating information with mutations

From the previous section, we learned that we can use queries to retrieve information from the backend and send it to the frontend. However, in a modern web application, it is very common for us to send information from the frontend to the backend. To do that, we need to talk about a new concept called mutation.

Let’s go back to the blog directory and create a file called mutations.py. In this example, we are going to create a new user.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import graphene
from blog import models, types


# Mutation sends data to the database
class CreateUser(graphene.Mutation):
    user = graphene.Field(types.UserType)

    class Arguments:
        username = graphene.String(required=True)
        password = graphene.String(required=True)
        email = graphene.String(required=True)

    def mutate(self, info, username, password, email):
        user = models.User(
            username=username,
            email=email,
        )
        user.set_password(password)
        user.save()

        return CreateUser(user=user) 

On line 7, recall that the UserType is tied with the User model.

Line 9 to 12, to create a new user, we need to pass three arguments, username, password and email.

Line 15 to 18, this should be very familiar to you, it is the same way we create a new item using the Django QuerySet.

Line 19, this line of code sets the password. For security reasons, we can not save the user’s original password in the database, and the set_password() method can make sure it is encrypted.

After that, we need to make sure this mutation is included in the GraphQL schema. Go to schema.py:

1
2
3
4
5
import graphene
from blog import queries, mutations


schema = graphene.Schema(query=queries.Query, mutation=mutations.Mutation)

To make sure it works, open your browser and go to http://127.0.0.1:8000/graphql to access the GraphiQL interface.

Mutation

1
2
3
4
5
6
7
8
mutation {
  createUser(username: "testuser2022", email: "testuser2022@test.com", password: "testuser2022") {
    user {
      id
      username
    }
  }
}

I think you already know how to use this in the frontend. As an example, this is what I did.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<script>
import { USER_SIGNUP } from "@/mutations";

export default {
  name: "SignUpView",

  data() {...},

  methods: {
    async userSignUp() {
      // Register user
      const user = await this.$apollo.mutate({
        mutation: USER_SIGNUP,
        variables: {
          username: this.signUpDetails.username,
          email: this.signUpDetails.email,
          password: this.signUpDetails.password,
        },
      });
     // Do something with the variable user
     ...
    },
  },
};
</script> 

src/mutations.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import gql from "graphql-tag";

export const USER_SIGNUP = gql`
  mutation ($username: String!, $email: String!, $password: String!) {
    createUser(username: $username, email: $email, password: $password) {
      user {
        id
        username
      }
    }
  }
`;

User authentication with Django and Vue.js

Now that we know how to send data to the backend, user authentication shouldn’t be too hard. We ask the user to input their username and password and send that information to the backend, and then in the backend, we find the user based on username, and we’ll try to match the password with the one stored in the database. If the match is successful, the user is logged in.

However, in practice, this plan has some problems. First, sending the user password back and forth isn’t exactly safe. We need some way to encrypt the data. The most commonly used method is JWT, which stands for JSON Web Token. It encrypts JSON information into a token. You can see an example here: https://jwt.io/.

This token will be saved inside the browser’s local storage, and as long as there is a token present, the user will be considered logged in.

The second problem is caused by Vue’s component system. We know that each component is independent. If one component changes, it does not affect the others. However, in this case, we want all components to share the same state. If the user is logged in, we want all components to recognize the user’s state as logged in.

We need a centralized place to store this information (that the user is logged in), and we want all components to read data from it. To do that, we’ll need to use Pinia, which is Vue’s new official store library created based on Vuex.

JWT in the Backend

First, let’s integrate JWT with our Django backend. To do that, we can install another package called django-graphql-jwt.

1
pip install django-graphql-jwt

The go to settings.py and add a middleware as well as authentication backend.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
MIDDLEWARE = [
    "django.contrib.auth.middleware.AuthenticationMiddleware",
]

# Configure GraphQL

GRAPHENE = {
    "SCHEMA": "blog.schema.schema",
    'MIDDLEWARE': [
        'graphql_jwt.middleware.JSONWebTokenMiddleware',
    ],
}

# Auth Backends

AUTHENTICATION_BACKENDS = [
    'graphql_jwt.backends.JSONWebTokenBackend',
    'django.contrib.auth.backends.ModelBackend',
]

To use this package, go to mutations.py and add the following code:

1
2
3
4
5
6
7
import graphql_jwt


class Mutation(graphene.ObjectType):
    token_auth = graphql_jwt.ObtainJSONWebToken.Field()
    verify_token = graphql_jwt.Verify.Field()
    refresh_token = graphql_jwt.Refresh.Field()

We can test it in the GraphiQL interface.

Wrong Password

User Auth Wrong Password

User Authenticated

User Authenticated

As you can see, the input arguments are username and password, and if the user is authenticated, an encrypted token will be returned. Later, we’ll save this token in the browser’s local storage.

If you want, you can also customize the behaviour of ObtainJSONWebToken. Go back to mutations.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Customize the ObtainJSONWebToken behavior to include the user info

class ObtainJSONWebToken(graphql_jwt.JSONWebTokenMutation):
    user = graphene.Field(types.UserType)

    @classmethod
    def resolve(cls, root, info, **kwargs):
        return cls(user=info.context.user)

class Mutation(graphene.ObjectType):
    token_auth = ObtainJSONWebToken.Field()

Notice that the ObtainJSONWebToken extends to the default JSONWebTokenMutation, and then in the Mutation class, we’ll use ObtainJSONWebToken instead.

Now we can make GraphQL return more information about the user.

User auth customization

Pinia in the Frontend

Now it’s time for us to solve the second problem at the frontend. We’ll start by installing Pinia.

1
npm install pinia

Then, go to main.js and make sure that our app is using Pinia.

1
2
3
import { createPinia } from "pinia";

createApp(App).use(createPinia()).use(router).use(apolloProvider).mount("#app");

Go back to the src directory and create a folder called stores. This is where we’ll put all of our stores. For now, we only need a user store, so let’s create a user.js file.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import { defineStore } from "pinia";

export const useUserStore = defineStore({
  id: "user",
  state: () => ({
    token: localStorage.getItem("token") || null,
    user: localStorage.getItem("user") || null,
  }),
  getters: {
    getToken: (state) => state.token,
    getUser: (state) => JSON.parse(state.user),
  },
  actions: {
    setToken(token) {
      this.token = token;

      // Save token to local storage
      localStorage.setItem("token", this.token);
    },
    removeToken() {
      this.token = null;
    
      // Delete token from local storage
      localStorage.removeItem("token");
    },
    setUser(user) {
      this.user = JSON.stringify(user);
    
      // Save user to local storage
      localStorage.setItem("user", this.user);
    },
    removeUser() {
      this.user = null;
    
      // Delete user from local storage
      localStorage.removeItem("user");
    },

  },
});

Notice that this store consists of mainly three sections, state, getters and actions. If you already know how to create a Vue application, this should be fairly easy to understand.

state is like the data() method in a Vue component, it is where we declear variables, except these variables will be accessible to all components. In our example, Vue will first try to get the token from the local storage, if the token does not exist, the variable will be assigned the value null.

getters are the equivalent of the computed variables. It performs simple actions, usually just returning the value of a state. Again, it is accessible to all components and pages.

And finally actions are like the methods in a Vue component. They usually perform some action using the states. In our case, we are saving/removing the user’s token and information.

One more thing we need to notice is that we cannot save objects inside the local storage, only strings. That is why we have to use stringify() and parse() to turn the data into a string and then back into an object.

Next, we need to use this store when we log the user in. I created a SignIn.vue file like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<script>
import { useUserStore } from "@/stores/user";
import { USER_SIGNIN } from "@/mutations";


export default {
  name: "SignInView",

  setup() {
    const userStore = useUserStore();
    return { userStore };
  },

  data() {
    return {
      signInDetails: {
        username: "",
        password: "",
      },
    };
  },

  methods: {
    async userSignIn() {
      const user = await this.$apollo.mutate({
        mutation: USER_SIGNIN,
        variables: {
          username: this.signInDetails.username,
          password: this.signInDetails.password,
        },
      });
      this.userStore.setToken(user.data.tokenAuth.token);
      this.userStore.setUser(user.data.tokenAuth.user);
    },
  },
};
</script> 

Line 2, we imported the user store we just created.

Line 9-12, call the user store in the setup hook, this makes Pinia easier to work with without any additional map functions.

Line 32-33, involk the setToken() and setUser() actions we just created, this will save the information inside the local storage.

Now, this is how we can log the user in, but what if the user is already signed in? Let see an example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<script>
import { SITE_INFO } from "@/queries";
import { useUserStore } from "@/stores/user";


export default {
  setup() {
    const userStore = useUserStore();
    return { userStore };
  },

  data() {
    return {
      menuOpen: false,
      mySite: null,
      user: {
        isAuthenticated: false,
        token: this.userStore.getToken || "",
        info: this.userStore.getUser || {},
      },
      dataLoaded: false,
    };
  },

  async created() {
    const siteInfo = await this.$apollo.query({
      query: SITE_INFO,
    });
    this.mySite = siteInfo.data.site;

    if (this.user.token) {
      this.user.isAuthenticated = true;
    }

  },

  methods: {
    userSignOut() {
      this.userStore.removeToken();
      this.userStore.removeUser();
    },
  },
};
</script> 

Line 18-19, we first try to get the token and user info from the store.

Line 31-33, if the token exists, then the user is considered as authenticated.

Line 38-41, this method will log the user out when invoked.

comments powered by Disqus