Create a Modern Application with Django and Vue #1

You can download the source code for this tutorial here.

Previously, in the beginner’s roadmap to web development tutorial, we talked about how to create a web application using Django, 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 request 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 it to the frontend. The client’s machine, which usually has a lot more excessive computing power, will use that 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 web application using Django as the backend, Vue as the frontend, and GraphQL as the API manipulation language that connects them together.

A brief review on 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. And finally, 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.

Take a look at this tutorial series if you are interested in learning these concepts in detail.

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. Instead, we are using Vue as the frontend, and use GraphQL to connect the front and the back. We are going to talk about them in the following articles. In this article, let’s start with setting up the Django end.

Creating a fresh Django project

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

1
2
3
blog
--backend
--frontend

Go to the backend folder, and create a Python virtual environment. A Python virtual environment is an isolated environment with a fresh Python install, without all the 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.

1
2
cd backend
python3 -m venv env

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

1
source env/bin/activate

And your terminal will look like this. Notice the (env) in front of the username. This indicates that you are currently working in the virtual environment.

Python virtual environment

Next, it is time for us to create a new Django project. You should already be familiar with this process.

1
2
python -m pip install Django
django-admin startproject backend

Create a new application:

1
2
cd backend
python manage.py startapp blog

Creating models

Next, it is time for us to setup the model. Recall that model is the 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. Again, if you are not sure how this process works, please consider going through the beginner’s tutorial on Django first.

The Site model

We’ll start with the Site model, which stores the basic information of our website.

 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

On line 4, we have an ImageField which will upload the image to 'site/logo/' directory. To make this work, there are two things we need to do. First, we need to install the Pillow package. Django need to use it to deal with images.

1
python -m pip install Pillow

Second, we need a new setting in the settings.py. We need to tell Django where we are storing these media files and what URL we’ll use when accessing these files.

1
2
3
4
5
6
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.

The User model

Next, for the User model. Django comes with a built-in User model, which offers basic permission and authorization functions. However, for our project, we’ll try something more complicated. We’ll add a profile avatar, a bio, and some other information. To do that, we need to create a new User models which extends to the AbstractUser class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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 look 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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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, we 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:

1
2
# Change Default User Model
AUTH_USER_MODEL = 'blog.User'

The Category, Tag and Post model

This part would be very easy for you if you already understand database relations.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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
 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
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 belong 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 we’ll use get_number_of_likes() method to get the number of likes for each post.

The Comment model

This time, we’ll go one step further, and create a comment section for our web application.

 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
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, let’s setup the Django admin. Open the admin.py 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
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 plus “...”.

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

1
python manage.py runserver

Django Admin

Before we move to the next step, remember to add some pseudo information for our blog.

A brief review on Vue.js

Now that we are done with the backend, it is time for us to create the frontend part of our application. In this the second part of this article, we are going to use Vue.js to create the frontend application. Again, let’s start with a brief review. If you’ve never used the framework before, please consider going through the Vue.js For Beginners tutorial first.

Vue.js is a front-end JavaScript framework that provides us with a simple component-based system, which allows us to create complex user interfaces. Component-based means that the root component (App.vue) can import other components (files with extension .vue), and those other components can import more components, which allows us 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, we 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 automatically be rendered, 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. If you don’t know what they are, please consider going through this tutorial first: Vue.js For Beginners.

Creating a new Vue.js project

In the Vue.js For Beginners 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 person who created Vue.js.

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

1
npm init vue@latest

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
✔ 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. If you need auto-correct for your code, you can install ESlint and Prettier as well. This process will generate a package.json file in your project, which stores the required packages and their versions. You need to install these packages inside your project.

1
2
3
cd <your-project-name>
npm install
npm run dev

One more thing before we start creating the frontend app. We are using a CSS framework called TailwindCSS in our project. To install it, run the following command:

1
2
npm install -D tailwindcss postcss autoprefixer
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, I assume you already know how to use them, and what PostCSS is. If not, please consider reading their documentations. Tailwind: (https://tailwindcss.com/docs/editor-setup). PostCSS: (https://github.com/postcss/postcss/tree/main/docs)

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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.

1
2
3
@tailwind base;
@tailwind components;
@tailwind utilities;

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import './main.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 that.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<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

Vue router

Also, notice that this time, our project directory is a little bit different.

Vue router

Inside the src directory, we have a router and a views folder. The router directory contains an index.js file. This is where we can define our routers. Each router 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.

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<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.

 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
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 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 on Vue.js and create your own.

Home Page

Home Page

All Categories

All Categories

All Tags

All Tags

Sign In Page

Sign In Page

Sign Up Page

Sign Up Page

Post Page

Post Page

Comment Section

Comment Section

User Profile Page

User Profile Page

User Profile Page Comment Section

User Profile Page Comment Section


comments powered by Disqus