Create a Modern Application with Django and Vue
Tags: django, vue, javascript, python
Published: 2023-04-15 23:50:32

> [Download source code here.](https://www.ericsdevblog.com/posts/create-a-modern-application-with-django-and-vue-1/) ⬅️

> Interested in web development? Level up your skills with [thedevspace.io](https://www.thedevspace.io/)!

Previously, we talked about how to create a web application using [Django](https://www.ericsdevblog.com/posts/django-for-beginners-1/), a full-stack Python-based web framework that follows the MTV design pattern. We call it full-stack because we can create both the frontend and the backend with it.

This solution, however, has one small flaw. When the end user requests a webpage, the page will need to be rendered in the backend, and then the rendered HTML page will be sent to the user. As you can imagine, when you have a lot of users, that will put a lot of pressure on your server.

To solve this problem, developers usually split the application into two parts, the backend and the frontend. This way, when the user requests a webpage, instead of rendering the webpage, the backend only gathers the necessary data and transfers them to the frontend. The client's machine, which usually has a lot more excessive computing power, will use the data to render the webpage inside the browser directly, hence relieving the pressure on the server.

In this tutorial, we are going to discuss how to create a modern single-page application using [Django](https://www.ericsdevblog.com/posts/django-for-beginners-1/) as the backend, [Vue](https://www.ericsdevblog.com/posts/vue-js-for-beginners-1/) as the frontend, and [GraphQL](https://graphql.org/) as the API manipulation language that connects them together.

Before you start, make sure you are familiar with both Django and Vue.js frameworks. If not, please consider going through the following tutorials first:

- [Django for Beginners](https://www.ericsdevblog.com/posts/django-for-beginners-1/)
- [Vue.js for Beginners](https://www.ericsdevblog.com/posts/vuejs-for-beginners-1/)

## A brief review of Django

Let's start with a brief review of the Django framework. Django is a Python-based web framework that follows the MTV architecture.

- The model (M) is an interface that allows us to interact with the database, such as retrieving, creating, updating or deleting records.
- The template (T) is the frontend part of the framework, it is the part that the end-users are going to see.
- The view (V) is the backend logic of the application, it uses the model to interact with the database, such as retrieving the data that is required by the user. Then the view would manipulate the data in some way, and return the result (usually a customized template) to the user.

For this particular tutorial, we are only going to use Django for the backend, which means we are not going to use Django's template or view, and replace them with Vue.js and GraphQL.

Let's start by setting up the Django end.

## Creating a fresh Django project

Personally, I like to separate the `backend` and the `frontend` directories. So this is how I created the project structure:

```text
blog
├── backend
└── frontend
```

Go to the `backend` folder, and create a new Python virtual environment. A Python virtual environment is an isolated environment with a fresh Python install, without the custom packages. When you install packages inside this environment, it will not affect your system's Python environment, which is very important if you are using Linux or macOS, and you don't want to mess with it.

```cmd
cd backend
```

```cmd
python3 -m venv env
```

This command will create a new directory called `env`, and the virtual environment is generated inside. To activate this virtual environment, use the following command:

```cmd
source env/bin/activate
```

If you are using Windows use this command instead. This depends on personal preference, but I do recommend setting up [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) is you are using Windows.

```cmd
env/Scripts/activate
```

After the virtual environment has been activated, your terminal will look like this. Notice the `(env)` in front of the username. This indicates you are currently working in the virtual environment.

![Python virtual environment](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jyaws9nsl3lf43zrxah1.png)

Next, it is time for you to create a new Django project. You should be familiar with this process, if not, please check the previously linked article for details.

```cmd
python -m pip install Django
```

```cmd
django-admin startproject backend
```

Create a new application:

```cmd
python manage.py startapp blog
```

After you are done, the project structure should look like this:

```text
.
├── backend
│   ├── backend
│   ├── blog
│   ├── manage.py
│   └── requirements.txt
└── frontend
```

## Creating models

Recall that the model is an interface which we can use to interact with the database. And one of the greatest feature of Django is that it can automatically detect the changes you made to the models, and generate the corresponding migration files, which we can use to make changes to the database structure.

### The `Site` model

Let's start with the `Site` model, which stores the basic information of your website.

```python
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
```

On line 4, there is an `ImageField` which tells Django to upload the image to `'site/logo/'` directory. To make this work, there are two things you need to do.

First, you must install the [Pillow](https://pillow.readthedocs.io/en/stable/) package. Django requires it in order to process images.

```cmd
python -m pip install Pillow
```

Second, you need a new setting directive in the `settings.py`. You have to tell Django where you are going to store these media files and what URL you are going to use when accessing these files.

```python
import os


# Media Files
MEDIA_ROOT = os.path.join(BASE_DIR, 'mediafiles')
MEDIA_URL = '/media/'
```

This setting means that the media files will be stored inside the `/mediafiles` directory, and we'll need to use the URL prefix `/media/` to access them. For example, the URL `http://localhost:3000/media/example.png` will retrieve the image `/mediafiles/example.png`.

### The `User` model

Next, for the `User` model. Django comes with a built-in `User` model, which offers basic permission and authorization functionalities. However, for this project, let's try something more complicated. You can add a profile avatar, a bio, and some other information. To do that, you need to create a new `User` model that extends to the `AbstractUser` class.

```python
from django.contrib.auth.models import AbstractUser


# New user model
class User(AbstractUser):
    avatar = models.ImageField(
        upload_to='users/avatars/%Y/%m/%d/',
        default='users/avatars/default.jpg'
    )
    bio = models.TextField(max_length=500, null=True)
    location = models.CharField(max_length=30, null=True)
    website = models.CharField(max_length=100, null=True)
    joined_date = models.DateField(auto_now_add=True)

    class Meta:
        verbose_name = 'user'
        verbose_name_plural = '2. Users'

    def __str__(self):
        return self.username
```

Django's `AbstractUser` class looks like this:

```python
class AbstractUser(AbstractBaseUser, PermissionsMixin):
    """
    An abstract base class implementing a fully featured User model with
    admin-compliant permissions.

    Username and password are required. Other fields are optional.
    """
    username_validator = UnicodeUsernameValidator()

    username = models.CharField(
        _('username'),
        max_length=150,
        unique=True,
        help_text=_('Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.'),
        validators=[username_validator],
        error_messages={
            'unique': _("A user with that username already exists."),
        },
    )
    first_name = models.CharField(_('first name'), max_length=150, blank=True)
    last_name = models.CharField(_('last name'), max_length=150, blank=True)
    email = models.EmailField(_('email address'), blank=True)
    is_staff = models.BooleanField(
        _('staff status'),
        default=False,
        help_text=_('Designates whether the user can log into this admin site.'),
    )
    is_active = models.BooleanField(
        _('active'),
        default=True,
        help_text=_(
            'Designates whether this user should be treated as active. '
            'Unselect this instead of deleting accounts.'
        ),
    )
    date_joined = models.DateTimeField(_('date joined'), default=timezone.now)

    objects = UserManager()

    EMAIL_FIELD = 'email'
    USERNAME_FIELD = 'username'
    REQUIRED_FIELDS = ['email']

    class Meta:
        verbose_name = _('user')
        verbose_name_plural = _('users')
        abstract = True

    def clean(self):
        super().clean()
        self.email = self.__class__.objects.normalize_email(self.email)

    def get_full_name(self):
        """
        Return the first_name plus the last_name, with a space in between.
        """
        full_name = '%s %s' % (self.first_name, self.last_name)
        return full_name.strip()

    def get_short_name(self):
        """Return the short name for the user."""
        return self.first_name

    def email_user(self, subject, message, from_email=None, **kwargs):
        """Send an email to this user."""
        send_mail(subject, message, from_email, [self.email], **kwargs)
```

As you can see, it offers some basic fields like `first_name`, `last_name`, etc.

Next, you need to make sure that Django is using this new `User` model as its default `User` model, or the authentication won't work. Go to `settings.py` and add the following directive:

```python
# Change Default User Model
AUTH_USER_MODEL = 'blog.User'
```

### The `Category`, `Tag` and `Post` model

```python
class Category(models.Model):
    name = models.CharField(max_length=200)
    slug = models.SlugField()
    description = models.TextField()

    class Meta:
        verbose_name = 'category'
        verbose_name_plural = '3. Categories'

    def __str__(self):
        return self.name
```

```python
class Tag(models.Model):
    name = models.CharField(max_length=200)
    slug = models.SlugField()
    description = models.TextField()

    class Meta:
        verbose_name = 'tag'
        verbose_name_plural = '4. Tags'

    def __str__(self):
        return self.name

```

```python
class Post(models.Model):
    title = models.CharField(max_length=200)
    slug = models.SlugField()
    content = RichTextField()
    featured_image = models.ImageField(
        upload_to='posts/featured_images/%Y/%m/%d/')
    is_published = models.BooleanField(default=False)
    is_featured = models.BooleanField(default=False)
    created_at = models.DateField(auto_now_add=True)
    modified_at = models.DateField(auto_now=True)

    # Each post can receive likes from multiple users, and each user can like multiple posts
    likes = models.ManyToManyField(User, related_name='post_like')

    # Each post belongs to one user and one category.
    # Each post has many tags, and each tag has many posts.
    category = models.ForeignKey(
        Category, on_delete=models.SET_NULL, null=True)
    tag = models.ManyToManyField(Tag)
    user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)

    class Meta:
        verbose_name = 'post'
        verbose_name_plural = '5. Posts'

    def __str__(self):
        return self.title

    def get_number_of_likes(self):
        return self.likes.count()
```

Notice how the like system is implemented on line 13. It is not a simple `IntegerField`, but instead, it works just like tags. And you can use `get_number_of_likes()` method to get the number of likes for each post.

### The `Comment` model

This time, let's go one step further, and create a comment section for this application.

```python
class Comment(models.Model):
    content = models.TextField(max_length=1000)
    created_at = models.DateField(auto_now_add=True)
    is_approved = models.BooleanField(default=False)

    # Each comment can receive likes from multiple users, and each user can like multiple comments
    likes = models.ManyToManyField(User, related_name='comment_like')

    # Each comment belongs to one user and one post
    user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
    post = models.ForeignKey(Post, on_delete=models.SET_NULL, null=True)

    class Meta:
        verbose_name = 'comment'
        verbose_name_plural = '6. Comments'

    def __str__(self):
        if len(self.content) > 50:
            comment = self.content[:50] + '...'
        else:
            comment = self.content
        return comment

    def get_number_of_likes(self):
        return self.likes.count()
```

## Setup Django admin panel

Finally, it is time to set up the Django admin. Open the `admin.py` file:

```python
from django.contrib import admin
from .models import *

# Register your models here.
class UserAdmin(admin.ModelAdmin):
    list_display = ('username', 'first_name', 'last_name', 'email', 'date_joined')

class CategoryAdmin(admin.ModelAdmin):
    prepopulated_fields = {'slug': ('name',)}


class TagAdmin(admin.ModelAdmin):
    prepopulated_fields = {'slug': ('name',)}


class PostAdmin(admin.ModelAdmin):
    prepopulated_fields = {'slug': ('title',)}
    list_display = ('title', 'is_published', 'is_featured', 'created_at')

class CommentAdmin(admin.ModelAdmin):
    list_display = ('__str__', 'is_approved', 'created_at')


admin.site.register(Site)
admin.site.register(User, UserAdmin)
admin.site.register(Category, CategoryAdmin)
admin.site.register(Tag, TagAdmin)
admin.site.register(Post, PostAdmin)
admin.site.register(Comment, CommentAdmin)
```

For the `CommentAdmin`, `__str__` refers to the `__str__()` method in the `Comment` model. Which will return the first 50 characters concatenated with "`...`".

Now, start the development server and see if everything works:

```cmd
python manage.py runserver
```

![Django Admin](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e4cwz7jsk5632nw9e6rb.png)

Before moving to the next step, remember to add some pseudo information for your blog.

## A brief review on Vue.js

Now that you are done with the backend, it is time to focus on the frontend. In the second part of this article, let's use Vue.js to create the frontend application. Again, we'll start with a brief review. If you've never used the framework before, please go through the previously linked tutorial first.

Vue.js is a front-end [JavaScript](https://www.thedevspace.io/course/javascript-introduction) framework that provides you with a simple component-based system, which allows you to create interactive user interfaces. Component-based means that the root component (`App.vue`) can import other components (files with extension `.vue`), and those components can import more components, which allows you to create very complex systems.

A typical `.vue` file contains three sections, the `<template>` section includes HTML codes, the `<script>` section includes JavaScript Codes, and the `<style>` section includes the CSS codes.

In the `<script>` section, you can declare new bindings inside the `data()` model. These bindings can then be displayed inside the `<template>` section using the double curly braces syntax (`{{ binding }}`). The bindings declared inside the `data()` method will automatically be wrapped inside Vue's reactivity system. Meaning that when the value of the binding changes, the corresponding component will be automatically rerendered, without having to refresh the page.

The `<script>` section can also contain methods other than `data()`, such as `computed`, `props`, `methods` and so on. And the `<template>` also allows us to bind data using directives such as `v-bind`, `v-on` and `v-model`.

## Creating a new Vue.js project

In the [Vue.js For Beginners](https://www.ericsdevblog.com/posts/vuejs-for-beginners-1/) tutorial, we installed and created a Vue app using the Vue command-line tool. This time, we are going to do things differently. We are going to use a frontend build tool called Vite (pronounced as "veet", the French word for fast), which is created by the same author who created Vue.js.

Go into the `frontend` folder, and run the following command:

```cmd
npm init vue@latest
```

You will be prompted with multiple options, for this project, you only need to add Vue Router:

```text
✔ Project name: … <your_project_name>
✔ Add TypeScript? … No / Yes
✔ Add JSX Support? … No / Yes
✔ Add Vue Router for Single Page Application development? … No / Yes
✔ Add Pinia for state management? … No / Yes
✔ Add Vitest for Unit testing? … No / Yes
✔ Add Cypress for both Unit and End-to-End testing? … No / Yes
✔ Add ESLint for code quality? … No / Yes
✔ Add Prettier for code formating? … No / Yes

Scaffolding project in ./<your_project_name>. . .
Done.
```

If you are more comfortable with a strong type language, you can elect to install [TypeScript](https://www.typescriptlang.org). If you need autocorrect and autoformat for your code, you can install ESlint and Prettier as well. This installation process will generate a `package.json` file in your project directory, which stores the required packages and their versions. You need to install these packages inside your project.

```cmd
cd <your_project_name>
```

```cmd
npm install
```

```cmd
npm run dev
```

One more thing before we start creating the frontend app. We are using a CSS framework called [TailwindCSS](https://tailwindcss.com) in this project. To install it, run the following command:

```cmd
npm install -D tailwindcss postcss autoprefixer
```

```cmd
npx tailwindcss init -p
```

This will generate two files, `tailwind.config.js` and `postcss.config.js`. This is not a tutorial on CSS or Tailwind, so I assume you already know how to use them. If not, please read [Tailwind's official documentation](https://tailwindcss.com/docs/editor-setup).

Go to `tailwind.config.js`, and add the path to all of your template files:

```javascript
module.exports = {
  content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
  theme: {
    extend: {},
  },
  plugins: [],
};
```

Create a `./src/index.css` file and add the `@tailwind` directives for each of Tailwind’s layers.

```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```

Import the newly-created `./src/index.css` file into your `./src/main.js` file.

```javascript
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import "./index.css";

const app = createApp(App);

app.use(router);

app.mount("#app");
```

Now you should be able to use Tailwind inside the `.vue` files. Let's test it out.

```vue
<template>
  <header>
    . . .
    <div class="wrapper">
      <HelloWorld msg="You did it!" />
      <h1 class="text-3xl font-bold underline">Hello world!</h1>
      . . .
    </div>
  </header>
  . . .
</template>
```

We added an `<h1>` heading after `<HelloWorld>`, and the heading is using the Tailwind classes.

![Vue Welcome page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hmsbbirqfykpb4eiofwy.png)

### Vue router

Notice that this time, your project directory is a little bit different.

![Vue router](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/72blak3wshtbx655c5gt.png)

Inside the `src` directory, there is a `router` and a `views` folder. The `router` directory contains an `index.js` file. This is where you can define different routes. Each route will point to a view component that is inside the `views` directory, and the view can then extend to other components inside the `components` directory. Vue already provided us with an example of `index.js`:

```javascript
import { createRouter, createWebHistory } from "vue-router";
import HomeView from "../views/HomeView.vue";

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: "/",
      name: "home",
      component: HomeView,
    },
    {
      path: "/about",
      name: "about",
      // route level code-splitting
      // this generates a separate chunk (About.[hash].js) for this route
      // which is lazy-loaded when the route is visited.
      component: () => import("../views/AboutView.vue"),
    },
  ],
});

export default router;
```

To invoke a defined router, look inside the `App.vue` file. Instead of the `<a>` tag, we use `<RouterLink>` which is imported from the `vue-router` package.

```vue
<script setup>
import { RouterLink, RouterView } from "vue-router";
. . .
</script>

<template>
  <header>
    . . .
    <div class="wrapper">
      . . .
      <nav>
        <RouterLink to="/">Home</RouterLink>
        <RouterLink to="/about">About</RouterLink>
      </nav>
    </div>
  </header>

  <RouterView />
</template>
```

When the page is being rendered, the `<RouterView />` tag will be replaced with the corresponding view. If you don't want to import these components, simply use `<router-link to="">` and `<router-view>` tags instead. Personally, I prefer this way because I always forget to import them.

## Creating routes with Vue router

For our blog application, we need to create at least 6 pages. We need a home page that displays a list of recent pages, a categories/tags page that shows all categories/tags, a category/tag page that displays a list of posts that belongs to the category/tag, and finally, a post page that displays the post content as well as the comments.

So, these are the routers I created. The `@` maps to the `src` directory.

```javascript
import { createRouter, createWebHistory } from "vue-router";
import HomeView from "@/views/main/Home.vue";
import PostView from "@/views/main/Post.vue";
import CategoryView from "@/views/main/Category.vue";
import TagView from "@/views/main/Tag.vue";
import AllCategoriesView from "@/views/main/AllCategories.vue";
import AllTagsView from "@/views/main/AllTags.vue";

const routes = [
  {
    path: "/",
    name: "Home",
    component: HomeView,
  },
  {
    path: "/category",
    name: "Category",
    component: CategoryView,
  },
  {
    path: "/tag",
    name: "Tag",
    component: TagView,
  },
  {
    path: "/post",
    name: "Post",
    component: PostView,
  },
  {
    path: "/categories",
    name: "Categories",
    component: AllCategoriesView,
  },
  {
    path: "/tags",
    name: "Tags",
    component: AllTagsView,
  },
];

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

export default router;
```

Please note that in this article, we are only creating the frontend interface, we are not dealing with data transfer just yet, so don't worry about how to find the correct post/category/tag right now.

## Creating views, pages, and components

[This](https://github.com/ericsdevblog/django-vue-starter-blog/tree/main/frontend/src/views) is the frontend UI that I created for this project, you can either use my code directly or if you don't like it, you can follow [this tutorial](https://www.ericsdevblog.com/posts/vuejs-for-beginners-1/) on Vue.js and create your own.

![Homepage](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2v1dmcmzjabgi5ojmepe.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mjhpv1ua78qwekzby5rk.png)

A REST API request usually consists of an endpoint, which points to the server, an [HTTP method](https://www.ericsdevblog.com/posts/javascript-basics-5/), 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. You need to install a new package called `graphene-django`. Run the following command:

```cmd
pip install graphene-django
```

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

```python
INSTALLED_APPS = [
  . . .
  "blog",
  "graphene_django",
]
```

### Configuring `graphene-django`

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

```python
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, 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.

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

```python
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, you must import the necessary packages and models.

Next, `SiteType` class is declared, and this `SiteType` is connected with the `Site` model.

Lastly, there is a `Query` class. This class is what allows you to retrieve information using the GraphQL API. To create or update information, you need to use a different class called `Mutation`, which we'll discuss in the next article.

Inside the `Query` class, there is a `resolve_site` function that returns the first record of the Site model. This method automatically binds with the `site` variable due to its name. This part works exactly the same as the regular [Django QuerySet](https://www.ericsdevblog.com/posts/django-for-beginners-2/#queryset).

### Creating schemas

Now you can do the same for all of the models. To make sure the schema file isn't too big, I separated them into `schema.py`, `types.py` and `queries.py`.

`schema.py`

```python
import graphene
from blog import queries


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

`types.py`

```python
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`

```python
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, you need to tell Django where to find the schema file. Go to `settings.py` and add the following code:

```python
# 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](http://127.0.0.1:8000/graphql). You should see the GraphiQL interface.

![GraphiQL](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3m2h05xy5ublpjpw9ziw.png)

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 you can move on to the frontend, there is still something you 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, you must enable the CORS (cross origin resource sharing) functionality.

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

```cmd
pip install django-cors-headers
```

Add `"corsheaders"` to the `INSTALLED_APPS` variable.

```python
INSTALLED_APPS = [
  . . .
  "corsheaders",
]
```

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

```python
MIDDLEWARE = [
  "corsheaders.middleware.CorsMiddleware",
  . . .
]
```

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

```python
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, install the Apollo library. It allows you to use GraphQL in the Vue app. To do that, run the following command:

```cmd
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:

```javascript
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 and port 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`:

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

```vue
<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 preference to create a separate file for all the queries and then import it into the `.vue` file.

`src/queries.js`

```javascript
import gql from "graphql-tag";

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

`App.vue`

```vue
. . .

<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 left over problem 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 routes, 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.

```javascript
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 (the one that will show a list of all categories), we will pass some information to this `category` variable.

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

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

```javascript
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 you 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 backend and `cd` into the `blog` directory, and then create a file called `mutations.py`. In this example, let's investigate how you can pass data to the backend in order to create a new user.

```python
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, you 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 you create a new item using the Django QuerySet.

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

After that, you must make sure this `mutation.py` file is included in the GraphQL schema. Go to `schema.py`:

```python
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](http://127.0.0.1:8000/graphql) to access the GraphiQL interface.

![Mutation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zxrq9r0gwemqwr748zm6.png)

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

```vue
<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`

```javascript
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 you know how to send data to the backend, user authentication shouldn't be too hard. You ask the user to input their username and password and send that information to the backend, and then in the backend, Django finds the user based on username, and it'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. You 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/](https://jwt.io/).

This token will be saved inside the browser's [local storage](https://blog.logrocket.com/localstorage-javascript-complete-guide/#:~:text=localStorage%20is%20a%20property%20that,the%20browser%20window%20is%20closed.), 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](https://www.ericsdevblog.com/posts/vuejs-for-beginners-2/). 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.

You need a centralized place to store this information (that the user is logged in), and all components should be able to read data from it. To do that, you'll need to use [Pinia](https://pinia.vuejs.org/introduction.html), which is Vue's new official store library created based on Vuex.

### JWT in the Backend

First, let's integrate JWT with the Django backend. To do that, you need to install another package called `django-graphql-jwt`.

```cmd
pip install django-graphql-jwt
```

Then go to `settings.py` and add a middleware as well as authentication backend. The configuration will overwrite Django's default setting, allowing it to use JWT instead.

```python
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:

```python
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wv3hq95k8fflkfuyu29e.png)

_User Authenticated_

![User Authenticated](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yx5llx6cj0auiwrl8p8b.png)

As you can see, the input arguments are username and password, and if the user is authenticated, an encrypted token will be returned. Later, you can 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`:

```python
# 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, you can use `ObtainJSONWebToken` instead.

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

![User auth customization](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fmmnqa52mzwzlge16wo7.png)

### Pinia in the Frontend

Now it's time for us to solve the second problem in the frontend. Let's start by installing Pinia.

```cmd
npm install pinia
```

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

```javascript
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, you only need a `user` store, so create a `user.js` file:

```javascript
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 you declare 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 this case, you are saving/removing the user's token and information.

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

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

```vue
<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, imported the user store you 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, invoke the `setToken()` and `setUser()` actions we just created, this will save the information inside the local storage.

Now, this is how you can log the user in, but what if the user is already signed in? Let's take a look at an example:

```vue
<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, 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.

Now that you know how to retrieve data using queries and how to send data using mutations, you can try something a little bit more challenging. In this article, let's create a comment and a like reaction system for our blog project.

## Creating a comment system

Let's start with the comment section. There are a few things you need to remember before diving into the code. First, for security reasons, only users that are logged in can leave comments. Second, each user can leave multiple comments, and each comment only belongs to one user. Third, each article can have multiple comments, and each comment only belongs to one article. Last but not least, the comment has to be approved by the admin before showing up on the article page.

_Not logged in_

![Comment section not logged in](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0zjkvpiw8yayjmv3hxwz.png)

_Logged in_

![Comment logged in](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xsgd2zr8i566b5imb3x6.png)

### Setting up the backend

With that in mind, let's start by creating the model for the comments. This part should be fairly easy to understand if you already know how to work with Django.

```python
# Comment model
class Comment(models.Model):
    content = models.TextField(max_length=1000)
    created_at = models.DateField(auto_now_add=True)
    is_approved = models.BooleanField(default=False)

    # Each comment belongs to one user and one post
    user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
    post = models.ForeignKey(Post, on_delete=models.SET_NULL, null=True)
```

Next, go ahead and apply the changes you've made to the models. Go to the terminal and run the following commands.

```cmd
python manage.py makemigrations
```

```cmd
python manage.py migrate
```

You also need to set up GraphQL at the backend. You can add a type for the comment model.

```python
class CommentType(DjangoObjectType):
    class Meta:
        model = models.Comment
```

And then the mutation. Note that there are three things Django needs to know to add a comment, the content of the comment, the user that wants to create this comment, and the article that the user is commenting on.

```python
class CreateComment(graphene.Mutation):
    comment = graphene.Field(types.CommentType)

    class Arguments:
        content = graphene.String(required=True)
        user_id = graphene.ID(required=True)
        post_id = graphene.ID(required=True)

    def mutate(self, info, content, user_id, post_id):
        comment = models.Comment(
            content=content,
            user_id=user_id,
            post_id=post_id,
        )
        comment.save()

        return CreateComment(comment=comment)
```

```python
class Mutation(graphene.ObjectType):
    . . .
    create_comment = CreateComment.Field()
```

Remember to add the `CreateComment` class inside the `Mutation` class.

### Setting up the frontend

As for the frontend, let's go to `Post.vue`, this is where the comments are shown. Please note that I removed some unrelated code in the following examples, so that the code snippets won't be too long, but if you wish to have the complete code, you can download the source code [here](https://github.com/ericsdevblog/django-vue-starter-blog).

`Post.vue`

```vue
<script>
import { POST_BY_SLUG } from "@/queries";
import CommentSectionComponent from "@/components/CommentSection.vue";

export default {
  name: "PostView",

  components: { CommentSectionComponent },

  data() {
    return {
      postBySlug: null,
      comments: null,
      userID: null,
    };
  },

  computed: {
    // Filters out the unapproved comments
    approvedComments() {
      return this.comments.filter((comment) => comment.isApproved);
    },
  },

  async created() {
    // Get the post before the instance is mounted
    const post = await this.$apollo.query({
      query: POST_BY_SLUG,
      variables: {
        slug: this.$route.params.slug,
      },
    });
    this.postBySlug = post.data.postBySlug;
    this.comments = post.data.postBySlug.commentSet;
  },
};
</script>
```

`queries.js`

```javascript
export const POST_BY_SLUG = gql`
  query ($slug: String!) {
    postBySlug(slug: $slug) {
      . . .
      commentSet {
        id
        content
        createdAt
        isApproved
        user {
          username
          avatar
        }
        numberOfLikes
        likes {
          id
        }
      }
    }
  }
`;
```

First, in the `created()` hook, you retrieve the requested article as well as the comments using the `POST_BY_SLUG` query, which is shown above. Next, in the `computed` property, you need to filter out the comments that are not approved by the admin. And finally, you pass the comment, the post ID and the user ID to the `CommentSectionComponent`.

`CommentSectionComponent.vue`

```vue
<template>
  <div class="home">
    . . .
    <!-- Comment Section -->
    <!-- Pass the approved comments, the user id and the post id to the comment section component -->
    <comment-section-component
      v-if="this.approvedComments"
      :comments="this.approvedComments"
      :postID="this.postBySlug.id"
      :userID="this.userID"
    ></comment-section-component>
  </div>
</template>
```

Next, let's take a closer look at the comment section component. This component contains two sections, a form that allows the user to leave comments, which is only shown when the user is logged in, and a list of existing comments.

`CommentSection.vue`

```vue
<script>
import { SUBMIT_COMMENT } from "@/mutations";
import CommentSingle from "@/components/CommentSingle.vue";
import { useUserStore } from "@/stores/user";

export default {
  components: { CommentSingle },
  name: "CommentSectionComponent",

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

  data() {
    return {
      commentContent: "",
      commentSubmitSuccess: false,
      user: {
        isAuthenticated: false,
        token: this.userStore.getToken || "",
        info: this.userStore.getUser || {},
      },
    };
  },
  props: {
    comments: {
      type: Array,
      required: true,
    },
    postID: {
      type: String,
      required: true,
    },
    userID: {
      type: String,
      required: true,
    },
  },
  async created() {
    if (this.user.token) {
      this.user.isAuthenticated = true;
    }
  },
  methods: {
    submitComment() {
      if (this.commentContent !== "") {
        this.$apollo
          .mutate({
            mutation: SUBMIT_COMMENT,
            variables: {
              content: this.commentContent,
              userID: this.userID,
              postID: this.postID,
            },
          })
          .then(() => (this.commentSubmitSuccess = true));
      }
    },
  },
};
</script>
```

I assume you already know how to use Pinia to verify if the user is logged in, and how to use `props` to pass information between different components, I'll skip this part, and let's focus on the `submitComment()` method.

When this method is invoked, it will test if the comment is empty, and if not, it will use the `SUBMIT_COMMENT` mutation to create a new comment. The `SUBMIT_COMMENT` mutation is defined as follows:

`mutations.js`

```javascript
export const SUBMIT_COMMENT = gql`
  mutation ($content: String!, $userID: ID!, $postID: ID!) {
    createComment(content: $content, userId: $userID, postId: $postID) {
      comment {
        content
      }
    }
  }
`;
```

The following code is the HTML section of `CommentSection.vue` file. Notice that at the end of this code, we used another component `CommentSingle.vue` to display one single comment.

`CommentSection.vue`

```vue
<template>
  <div class=". . .">
    <p class="font-bold text-2xl">Comments:</p>

    <!-- If the user is not authenticated -->
    <div v-if="!this.user.isAuthenticated">
      You need to
      <router-link to="/account">sign in</router-link>
      before you can leave your comment.
    </div>

    <!-- If the user is authenticated -->
    <div v-else>
      <div v-if="this.commentSubmitSuccess" class="">
        Your comment will show up here after is has been approved.
      </div>
      <form action="POST" @submit.prevent="submitComment">
        <textarea type="text" class=". . ." rows="5" v-model="commentContent" />

        <button class=". . .">Submit Comment</button>
      </form>
    </div>

    <!-- List all comments -->
    <comment-single
      v-for="comment in comments"
      :key="comment.id"
      :comment="comment"
      :userID="this.userID"
    >
    </comment-single>
  </div>
</template>
```

Now, let's take a closer look at the `CommentSingle.vue` file.

`CommentSingle.vue` HTML section

```vue
<template>
  <div class="border-2 p-4">
    <div
      class="flex flex-row justify-start content-center items-center space-x-2 mb-2"
    >
      <img
        :src="`http://127.0.0.1:8000/media/${this.comment.user.avatar}`"
        alt=""
        class="w-10"
      />
      <p class="text-lg font-sans font-bold">
        {{ this.comment.user.username }}
      </p>
    </div>

    <p>
      {{ this.comment.content }}
    </p>
  </div>
</template>
```

`CommentSingle.vue` JavaScript section

```vue
<script>
export default {
  name: "CommentSingleComponent",
  data() {
    return {
      . . .
    };
  },
  props: {
    comment: {
      type: Object,
      required: true,
    },
    userID: {
      type: String,
      required: true,
    },
  },
};
</script>
```

## Creating a like reaction system

![Like system](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tt2l348ed2t68k5ic8pz.png)

As for the like system, there are also a few things you need to keep in mind. First, the user has to be logged in to add a like. Unverified users can only see the number of likes. Second, each user can only send one like to one article, and clicking the like button again would remove the like reaction. Lastly, each article can receive likes from multiple users.

### Setting up the backend

Again, let's start with the models.

Since each article can have many likes from many users, and each user can give many likes to many articles, this should be a [many-to-many relationship](https://www.ericsdevblog.com/posts/django-for-beginners-2/#many-to-many-relation) between `Post` and `User`.

Also notice that this time a `get_number_of_likes()` function is created to return the total number of likes. Remember to apply these changes to the database using the commands we've talked about before.

```python
# Post model

class Post(models.Model):
    . . .

    # Each post can receive likes from multiple users, and each user can like multiple posts
    likes = models.ManyToManyField(User, related_name='post_like')

    . . .

    def get_number_of_likes(self):
        return self.likes.count()

```

Next, we add the types and mutations.

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

    number_of_likes = graphene.String()

    def resolve_number_of_likes(self, info):
        return self.get_number_of_likes()

```

Notice that in line 8, `self.get_number_of_likes()` invokes the `get_number_of_likes()` function you defined in the model.

```python
class UpdatePostLike(graphene.Mutation):
    post = graphene.Field(types.PostType)

    class Arguments:
        post_id = graphene.ID(required=True)
        user_id = graphene.ID(required=True)

    def mutate(self, info, post_id, user_id):
        post = models.Post.objects.get(pk=post_id)

        if post.likes.filter(pk=user_id).exists():
            post.likes.remove(user_id)
        else:
            post.likes.add(user_id)

        post.save()

        return UpdatePostLike(post=post)

```

To add a like to a post, you need to know the `id` of the article, and the `id` of the user that likes this article.

From line 11 to 14, if the post already has a like from the current user, the like will be removed, and if not, a like will be added.

### Setting up the frontend

Next, we need to add a like button to our post page. Go back to `Post.vue`.

`Post.vue` HTML section

```vue
<template>
  <div class="home">
    . . .

    <!-- Like, Comment and Share -->
    <div class=". . .">
      <div v-if="this.liked === true" @click="this.updateLike()">
        <i class="fa-solid fa-thumbs-up">
          <span class="font-sans font-semibold ml-1">{{
            this.numberOfLikes
          }}</span>
        </i>
      </div>
      <div v-else @click="this.updateLike()">
        <i class="fa-regular fa-thumbs-up">
          <span class="font-sans font-semibold ml-1">{{
            this.numberOfLikes
          }}</span>
        </i>
      </div>
      . . .
    </div>

    . . .
  </div>
</template>
```

`Post.vue` JavaScript section

```vue
<script>
import { POST_BY_SLUG } from "@/queries";
import { UPDATE_POST_LIKE } from "@/mutations";
. . .

export default {
  . . .
  async created() {
    . . .
    // Find if the current user has liked the post
    let likedUsers = this.postBySlug.likes;

    for (let likedUser in likedUsers) {
      if (likedUsers[likedUser].id === this.userID) {
        this.liked = true;
      }
    }

    // Get the number of likes
    this.numberOfLikes = parseInt(this.postBySlug.numberOfLikes);
  },

  methods: {
    updateLike() {
      if (this.liked === true) {
        this.numberOfLikes = this.numberOfLikes - 1;
      } else {
        this.numberOfLikes = this.numberOfLikes + 1;
      }
      this.liked = !this.liked;

      this.$apollo.mutate({
        mutation: UPDATE_POST_LIKE,
        variables: {
          postID: this.postBySlug.id,
          userID: this.userID,
        },
      });
    },
  },
};
</script>
```

I deleted some code to make this example shorter, but there are still four things we need to talk about in this example. First, the `POST_BY_SLUG` query that you use to retrieve the article, you need to make sure that it returns the number of likes and the users that already liked the article.

`queries.js`

```javascript
export const POST_BY_SLUG = gql`
  query ($slug: String!) {
    postBySlug(slug: $slug) {
      . . .
      numberOfLikes
      likes {
        id
      }
      . . .
    }
  }
`;
```

Next, in the `created()` hook, after you've retrieved the post, you must determine if the current user is in the list of users that already liked the post.

Then, in the `updateLike()` method, when this method is invoked, it will change the number of likes based on whether or not the user has liked the post.

Finally, the method updates the post's likes in the backend using the `UPDATE_POST_LIKE` mutation.

`mutations.js`

```javascript
export const UPDATE_POST_LIKE = gql`
  mutation ($postID: ID!, $userID: ID!) {
    updatePostLike(postId: $postID, userId: $userID) {
      post {
        id
        title
        likes {
          id
        }
      }
    }
  }
`;
```

## A Challenge

After learning how to create a comment and a like system, let's consider a more challenging task. What if we want to create a nested commenting system, where users can comment on another comment? How can we change our code to make this possible? And how can we create a like system for the comment as well?

The complete implementation of these functionalities are included in the [source code](https://github.com/ericsdevblog/django-vue-starter-blog) of this tutorial.

> If you liked this article, please also check out my other tutorials:
- [Django for Beginners](https://www.ericsdevblog.com/posts/django-for-beginners-1/)
- [Laravel for Beginners](https://www.ericsdevblog.com/posts/laravel-for-beginners-1/)
- [How to Set Up a Web Server](https://www.thedevspace.io/community/set-up-web-server)
- [Web Dev for Beginners](https://www.thedevspace.io/)