Django for Beginners #2 - The MTV Structure
Tags: django, python, webdev, beginners
Published: 2023-03-28 14:25:00

> [Download source code here.](https://www.ericsdevblog.com/posts/django-for-beginners-1?ref=Dev_Community/) ⬅️

Django is a web dev framework designed based on the MTV (Model-Template-View) structure. In this structure, the model is in charge of interacting with our database, each model should correspond to one database table. The template is the frontend part of the application, it is what the users are going to see. And finally, the view is the backend logic of the app, it is usually in charge of retrieving data from the database through the models, putting them in the corresponding view, and eventually returning the rendered template to the user. In this article, we will discuss how this architecture works in your Django application.

## Let's start with the models

The model is one of the best features of Django. For other web frameworks, you need to create both a model and a migration file. The migration file is a schema for the database, which describes the structure (column names and column types) of the database. The model provides an interface that handles the data manipulation based on that schema.

But for Django, you only need a model, and the corresponding migration files can be generated with a simple command, saving you a lot of time.

Each app has one `models.py` file, and all the models related to the app should be defined inside. Each model corresponds to a migration file, which corresponds to a database table. To differentiate tables for different apps, a prefix will be automatically assigned to each table. For our `blog` app, the corresponding database table will have the prefix `blog_`.

Here is an example of a model:

`blog/models.py`

```python
from django.db import models

class Person(models.Model):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=30)
```

You can then generate a migration file using the following command:

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

And the generated migration file should look like this:

`blog/migrations/0001_initial.py`

```python
# Generated by Django 4.1.2 on 2022-10-19 23:13

from django.db import migrations, models


class Migration(migrations.Migration):

    initial = True

    dependencies = []

    operations = [
        migrations.CreateModel(
            name="Person",
            fields=[
                (
                    "id",
                    models.BigAutoField(
                        auto_created=True,
                        primary_key=True,
                        serialize=False,
                        verbose_name="ID",
                    ),
                ),
                ("first_name", models.CharField(max_length=30)),
                ("last_name", models.CharField(max_length=30)),
            ],
        ),
    ]

```

The migration file is a schema that describes how the database table should look, you can use the following command to apply this schema:

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

Your database (`db.sqlite3`) should look like this:

![database blog person](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9kppmqbtdq9n3xt8ne9n.png)

As a beginner, you should never try to edit or delete the migration files, just let Django do everything for you unless you absolutely know what you are doing. In most cases, Django can detect the changes you've made in the models (even if you deleted something) and generate the migration files accordingly.

In our example, this model will create a database table `person`, and inside the table, there will be three columns, each named `id`, `first_name`, and `last_name`. The `id` column is created automatically, as seen in the migration file. The `id` column is, by default, used as the primary key for indexing.

`CharField()` is called a field type and it defines the type of the column. `max_length` is called a field option, and it specifies extra information about that column. You can find a reference of all field types and field options [here](https://docs.djangoproject.com/en/4.1/ref/models/fields/).

### Model field types and options

To save us some time, I will only introduce some most commonly used field types.

| Field Type                           | Description                                                                                                                                                            |
| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `BigAutoField`                       | Creates an integer column that automatically increments. Usually used for the `id` column.                                                                             |
| `BooleanField`                       | Creates a Boolean value column, with values `True` or `False`.                                                                                                         |
| `DateField` and `DateTimeField`      | As their names suggest, adds dates and times.                                                                                                                          |
| `FileField` and `ImageField`         | Creates a column that stores the path, which points to the uploaded file or image.                                                                                     |
| `IntegerField` and `BigIntegerField` | Integer has values from -2147483648 to 2147483647. Big integer has values from -9223372036854775808 to 9223372036854775807                                             |
| `SlugField`                          | Slug is usually a URL-friendly version of the title/name.                                                                                                              |
| `CharField` and `TextField`          | CharField and TextField both create a column for storing strings, except TextField corresponds to a larger text box in the Django admin, which we'll talk about later. |

And field options.

| Field Option | Description                                                                                                                        |
| :----------- | :--------------------------------------------------------------------------------------------------------------------------------- |
| `blank`      | Allows this field to have an empty entry.                                                                                          |
| `choices`    | Gives this field multiple choices, you'll see how this works after we get to Django Admin.                                         |
| `default`    | Gives the field a default value.                                                                                                   |
| `unique`     | Makes sure that every item in the column is unique. Usually used to slug and other fields that are supposed to have unique values. |

Makes sure that every item in the column is unique. Usually used to slug and other fields that are supposed to have unique values.

### Meta options

You can also add a `Meta` class inside the model class, which contains extra information about this model, such as database table name, ordering options, and human-readable singular and plural names.

```python
class Category(models.Model):
    priority = models.IntegerField()

    class Meta:
        ordering = ["priority"]
        verbose_name_plural = "categories"
```

### Model methods

Model methods are functions defined inside the model class. These functions allow us to perform custom actions on the current instance of the model object. For example:

```python
class Person(models.Model):
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    birth_date = models.DateField()

    def baby_boomer_status(self):
        "Returns the person's baby-boomer status."
        import datetime
        if self.birth_date < datetime.date(1945, 8, 1):
            return "Pre-boomer"
        elif self.birth_date < datetime.date(1965, 1, 1):
            return "Baby boomer"
        else:
            return "Post-boomer"
```

When the `baby_boomer_status()` method is called, Django will examine the person's birth date and return the person's baby-boomer status.

Objects, methods, and properties are very important concepts in programming languages, and you can read [this article](https://www.w3schools.com/python/python_classes.asp) for more information.

### Model inheritance

In most web applications, you'll need more than one model, and some of them will have common fields. In this case, you can create a parent model which contains the common fields, and make other models inherit from the parent.

```python
class CommonInfo(models.Model):
    name = models.CharField(max_length=100)
    age = models.PositiveIntegerField()

    class Meta:
        abstract = True

class Student(CommonInfo):
    home_group = models.CharField(max_length=5)
```

Notice that `CommonInfo` is marked as an abstract model, which means this model doesn't really correspond to an individual model, instead, it is used as a parent to other models.

To verify this, generate a new migration file:

`blog/migrations/0002_student.py`

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

```python
# Generated by Django 4.1.2 on 2022-10-19 23:28

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ("blog", "0001_initial"),
    ]

    operations = [
        migrations.CreateModel(
            name="Student",
            fields=[
                (
                    "id",
                    models.BigAutoField(
                        auto_created=True,
                        primary_key=True,
                        serialize=False,
                        verbose_name="ID",
                    ),
                ),
                ("name", models.CharField(max_length=100)),
                ("age", models.PositiveIntegerField()),
                ("home_group", models.CharField(max_length=5)),
            ],
            options={
                "abstract": False,
            },
        ),
    ]

```

As you can see, only a `Student` table is created.

### Database relations

So far, we've only talked about how to create individual tables, however, in most applications, these tables aren't entirely independent, and there are usually relations between different tables. For instance, you could have a category that has multiple posts, a post that belongs to a specific user, etc. So how can you describe such relations in Django?

There are primarily three types of database relations, one-to-one, many-to-one, and many-to-many.

**One-to-one relation**

The one-to-one relationship should be the easiest to understand. For example, each person could have one phone, and each phone could belong to one person. We can describe this relation in the models like this:

```python
class Person(models.Model):
    name = models.CharField(max_length=100)

class Phone(models.Model):
    person = models.OneToOneField('Person', on_delete=models.CASCADE)
```

The `OneToOneField` field type works just like any other field type, except it takes at least two arguments. The first argument is the name of the model with whom this model has a relationship. And the second argument is `on_delete`, which defines the action Django will take when data is deleted. This has more to do with SQL than Django, so we are not going into this topic in detail, but if you are interested, [here](https://docs.djangoproject.com/en/4.1/ref/models/fields/#django.db.models.ForeignKey.on_delete) are some available values for `on_delete`.

You can now generate and apply migrations for these models and see what happens. If you run into problems running these commands, simply delete the `db.sqlite3` file and the migration files to start over.

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

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

![one to one relation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6a9cyhmxaqp7btcxciyc.png)

As you can see, the `OneToOneField` created a `person_id` column inside the `blog_phone` table, and this column will store the `id` of the person that owns this phone.

**Many-to-one relation**

Each category can have many posts, and each post belongs to one category. This relation is referred to as a Many-to-one relation.

```python
class Category(models.Model):
    name = models.CharField(max_length=100)

class Post(models.Model):
    category = models.ForeignKey('Category', on_delete=models.CASCADE)
```

`ForeignKey` will create a `category_id` column in the `blog_post` table, which stores the `id` of the category that this post belongs to.

**Many-to-many relation**

Many-to-many relation is slightly more complicated. For example, every article could have multiple tags, and each tag could have multiple articles.

```python
class Tag(models.Model):
    name = models.CharField(max_length=100)

class Post(models.Model):
    tags = models.ManyToManyField('Tag')
```

Instead of creating a new column, this code will create a new table called `post_tags`, which contains two columns, `post_id`, and `tag_id`. This allows you to locate all the tags associated with a particular post, and vice versa.

To make things clearer, imagine we have a table like this:

| post_id | tag_id |
| :------ | :----- |
| 1       | 1      |
| 2       | 1      |
| 3       | 2      |
| 1       | 2      |
| 2       | 3      |

For a post with `id=1`, there are two tags, each with `id=1` and `id=2`. If we want to do things backward and find posts through a tag, we can see that for a tag with `id=2`, there are two posts, `id=3` and `id=1`.

## Now the view layer

The view layer is one of the most important components in a Django application, it is where we write all the backend logic. The most common thing a view is supposed to do is retrieve data from the database through the corresponding model, process the retrieved data, put them in the corresponding location in the template, and finally, render and return that template back to the user.

Of course, retrieving data is not the only thing we can do with a view function. In most web applications, there are four most basic operations we can do with data, create, read, update and delete them. These operations put together are often referred to as CRUD. We are going to investigate all of them in this tutorial.

We've already discussed models in the previous section, but we still don't know how to retrieve or store data using the model. Django offers us a very simple API to help us with that, which is called the [`QuerySet`](https://docs.djangoproject.com/en/4.1/topics/db/queries/).

Suppose that this is your model:

`blog/models.py`

```python
class Category(models.Model):
    name = models.CharField(max_length=100)


class Tag(models.Model):
    name = models.CharField(max_length=200)


class Post(models.Model):
    title = models.CharField(max_length=255)
    content = models.TextField()
    pub_date = models.DateField()
    category = models.ForeignKey(Category, on_delete=models.CASCADE)
    tags = models.ManyToManyField(Tag)
```

With the help of `QuerySet`, you can manipulate data through this model inside the view functions, which are located inside the `blog/views.py` file.

### Creating and saving data

Let's say you are trying to create a new category, this is what you can do:

```python
# import the Category model
from blog.models import Category

# create a new instance of Category
category = Category(name="New Category")

# save the newly created category to database
category.save()
```

This should be very easy for you to understand if you are familiar with the concept of object-oriented programming. If not, [here](https://realpython.com/python3-object-oriented-programming/) is an article on that subject.

In this example, we simply created a new instance of the `Category` object and used the `save()` method, which belongs to that object, to save the information to the database.

Now, what about relations? For example, there is a many-to-one relationship between category and post, which we defined using the `ForeignKey()` field. We assume there are enough records in the database.

```python
from blog.models import Category, Post

# Post.objects.get(pk=1) is how we retrieve the post with pk=1,
# where pk stands for primary key, which is usually the id unless otherwise specified.
post = Post.objects.get(pk=1)

# retrieve the category with name="New Category"
new_category = Category.objects.get(name="New Category")

# assign new_category to the post's category field and save it
post.category = new_category
post.save()
```

There is also a many-to-many relation between posts and tags.

```python
from blog.models import Tag, Post

post1 = Post.objects.get(pk=1) # Retrieve post 1

tag1 = Tag.objects.get(pk=1) # Retrieve tag 1
tag2 = Tag.objects.get(pk=2) # Retrieve tag 2
tag3 = Tag.objects.get(pk=3) # Retrieve tag 3
tag4 = Tag.objects.get(pk=4) # Retrieve tag 4
tag5 = Tag.objects.get(pk=5) # Retrieve tag 5

post.tags.add(tag1, tag2, tag3, tag4, tag5) # Add tag 1-5 to post 1
```

### Retrieving data

Retrieving objects is slightly more complicated than what we've just seen. Imagine we have thousands of records in our database. How do we find one particular record, if we don't know the `id`? Or, what if we want a collection of records that fits particular criteria instead of one record?

**QuerySet Methods**

`QuerySet` methods allow you to retrieve data based on certain criteria. And they can be accessed using the attribute `objects`. We've already seen an example, `get()`, which is used to retrieve one particular record.

```python
first_tag = Tag.objects.get(pk=1)
new_category = Category.objects.get(name="New Category")
```

You can also retrieve all records using the `all()` method.

```python
Post.objects.all()
```

The `all()` method returns what we call a QuerySet, it is a collection of records. And you can further refine that collection by chaining a `filter()` or `exclude()` method.

```python
Post.objects.all().filter(pub_date__year=2006)
```

This will return all the posts that are published in the year 2006. And `pub_date__year` is called a field lookup, we'll discuss this topic in detail later.

Or we can exclude the posts that are published in the year 2006.

```python
Post.objects.all().exclude(pub_date__year=2006)
```

Besides `get()`, `all()`, `filter()` and `exclude()`, there are lots of other QuerySet methods just like them. We can't talk about all of them here, but if you are interested, [here](https://docs.djangoproject.com/en/4.1/ref/models/querysets/#methods-that-return-new-querysets) is a full list of all QuerySet methods.

**Field Lookups**

Field Lookups are the keyword arguments for methods `get()`, `filter()` and `exclude()`. If you are familiar with SQL clauses, they work just like the SQL `WHERE` clause. And they take the form `fieldname__lookuptype=value`. Notice that it is a **double underscore** in between.

```python
Post.objects.all().filter(pub_date__lte='2006-01-01')
```

In this example, `pub_date` is the field name, and `lte` is the lookup type, which means **less than or equal to**. This code will return all the posts where the `pub_date` is less than or equal to `2006-01-01`.

[Here](https://docs.djangoproject.com/en/4.1/ref/models/querysets/#field-lookups) is a list of all field lookups you can use.

Field lookups can also be used to find records that have a relationship with the current record. For example:

```python
Post.objects.filter(category__name='Django')
```

This line of code will return all posts that belong to the category whose name is `"Django"`.

This works backward too. For instance, we can return all the categories that have at least one post whose title contains the word `"Django"`.

```python
Category.objects.filter(post__title__contains='Django')
```

We can go across multiple relations as well.

```python
Category.objects.filter(post__author__name='Admin')
```

This will return all categories that own posts, which are published by the user `Admin`. In fact, you can chain as many relationships as you want.

### Deleting Objects

The method we use to delete a record is conveniently named `delete()`. The following code will delete the post that has `pk=1`.

```python
post = Post.objects.get(pk=1)
post.delete()
```

We can also delete multiple records together.

```python
Post.objects.filter(pub_date__year=2005).delete()
```

This will delete all posts that are published in the year 2005.

However, what if the record we are deleting relates to another record? For example, here we are trying to delete a category with multiple posts.

```python
category = Category.objects.get(pk=1)
category.delete()
```

By default, Django emulates the behaviour of the SQL constraint `ON DELETE CASCADE`, which means all the posts that belong to this category also be deleted. If you wish to change that, you can change the `on_delete` option to something else. [Here](https://docs.djangoproject.com/en/4.1/ref/models/fields/#django.db.models.ForeignKey.on_delete) is a reference of all available options for `on_delete`.

### The view function

So far, we've only seen some snippets showing what you can do inside a view function, but what does a complete view function look like? Well, here is an example. In Django, all the views are defined inside the `views.py` file.

```python
from django.shortcuts import render
from blog.models import Post

# Create your views here.
def my_view(request):
    posts = Post.objects.all()

    return render(request, 'blog/index.html', {
        'posts': posts,
    })
```

There are two things you need to pay attention to in this example.

First, notice that this view function takes an input `request`. This variable `request` is an `HttpRequest` object, and it is automatically passed to the view from our URL dispatcher. If you are not familiar with HTTP, you should go through [this article](https://www.ericsdevblog.com/posts/javascript-basics-5?ref=Dev_Community/) first.

The `request` contains a lot of information about the current HTTP request. For example, we can access the HTTP request method and write different codes for different methods.

```python
if request.method == 'GET':
    do_something()
elif request.method == 'POST':
    do_something_else()
```

[Here](https://docs.djangoproject.com/en/4.1/ref/request-response/#httprequest-objects) is a list of all the information you can access from the `request`.

Second, notice that a shortcut called `render()` is imported, and it is then used to pass the variable `posts` to the template `blog/index.html`.

This is called a shortcut because, by default, you are supposed to load the template with the `loader()` method, render that template with the retrieved data, and return an `HttpResponse` object. Django simplified this process with the `render()` shortcut. To make life easier for you, I'm not going to talk about the complex way here, since we will not use it in this tutorial anyway.

[Here](https://docs.djangoproject.com/en/4.0/topics/http/shortcuts/) is a list of all shortcut functions in Django.

## The Django template system

Now, let's talk about templates. The template layer is the frontend part of a Django application, which is why the template files are all [HTML](https://www.ericsdevblog.com/posts/html-basics-1?ref=Dev_Community/) code since they are what you see in the browser, but things are slightly more complicated than that. If it contains only HTML codes, the entire website would be static, and that is not what we want. So the template would have to tell the view function where to put the retrieved data.

### Configurations

Before we start, there is something you need to change in the `settings.py`, you must tell Django where you are putting the template files.

First, create the `templates` directory. I choose to put it under the root directory of the project, but you can move it somewhere else if you want.

```text
.
├── blog
├── db.sqlite3
├── djangoBlog
├── env
├── manage.py
├── mediafiles
├── staticfiles
└── templates
```

Go to `settings.py` and find `TEMPLATES`.

```python
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [
            'templates',
        ],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]
```

Change the `DIRS` option, which points to the template folder. Now let's verify that this setting works. Create a new URL pattern that points to a `test()` view.

`djangoBlog/urls.py`

```python
from django.urls import path
from blog import views

urlpatterns = [
    path('test/', views.test),
]
```

Create the `test()` view:

`blog/views.py`

```python
def test(request):
    return render(request, 'test.html')
```

Go to the `templates` folder and create a `test.html` template:

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Test Page</title>
  </head>
  <body>
    <p>This is a test page.</p>
  </body>
</html>
```

Start the dev server and go to [http://127.0.0.1:8000/](http://127.0.0.1:8000/).

![Django template](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4isnbhb4iu237an7t4vj.png)

### The Django template language

Now let's discuss Django's template engine in detail. Recall that we can send data from the view to the template like this:

```python
def test(request):
    return render(request, 'test.html', {
        'name': 'Jack'
    })
```

The string `'Jack'` is assigned to the variable `name` and passed to the template. And inside the template, we can display the `name` using double curly braces, `{{ }}`.

```django
<p>Hello, {{ name }}</p>
```

Refresh the browser, and you will see the output.

![Display Data](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pd0wofpnoaabqe59k4jo.png)

However, in most cases, the data that is passed to the template is not a simple string. For example:

```python
def test(request):
    post = Post.objects.get(pk=1)

    return render(request, 'test.html', {
        'post': post
    })
```

In this case, the `post` variable is, in fact, a [dictionary](https://www.w3schools.com/python/python_dictionaries.asp). You can access the items in that dictionary like this in the template:

```django
{{ post.title }}
{{ post.content }}
{{ post.pub_date }}
```

**Filters**

Filters transform the values of variables. For example, we have a variable `django`, with the value `'the web framework for perfectionists with deadlines'`. If we put a `title` filter on this variable:

```django
{{ django|title }}
```

The template will be rendered into:

```html
The Web Framework For Perfectionists With Deadlines
```

[Here](https://docs.djangoproject.com/en/4.1/ref/templates/builtins/#built-in-filter-reference) is a full list of all built-in filters in Django.

**Tags**

Tags add programming language features such as flow control and loops to HTML code, which will save us a lot of time and resources, we don't have to write the same code over and over again. All tags are defined using `{% %}`.

For example, this is a `for` loop:

```django
<ul>
    {% for athlete in athlete_list %}
        <li>{{ athlete.name }}</li>
    {% endfor %}
</ul>
```

This is an `if` statement:

```django
{% if somevar == "x" %}
  This appears if variable somevar equals the string "x"
{% endif %}
```

And this is an `if-else` statement:

```django
{% if athlete_list %}
    Number of athletes: {{ athlete_list|length }}
{% elif athlete_in_locker_room_list %}
    Athletes should be out of the locker room soon!
{% else %}
    No athletes.
{% endif %}
```

[Here](https://docs.djangoproject.com/en/4.1/ref/templates/builtins/#built-in-tag-reference) is a full list of all built-in tags in Django. There are lots of other useful filters and tags in Django template system, we'll talk about them as we encounter specific problems in the future.

### The inheritance system

The primary benefit of using the Django template is that you do not need to write the same code over and over again. For example, in a typical web application, there is usually a navigation bar and a footer, which will appear on every page. Repeating all of these code on every page will make it very difficult for maintenance. Django offers us a very easy way to solve this problem.

Let’s create a `layout.html` file in the templates folder. As the name suggests, this is the place where we define the layout of our template. To make this example easier to read, I skipped the code for the footer and navbar.

`layout.html`

```django
<!DOCTYPE html>
<html>
<head>
    {% block meta %} {% endblock %}
    <!-- Import CSS here -->
</head>
<body>

<div class="container">
    <!-- Put the navbar here -->

    {% block content %} {% endblock %}

    <!-- Put the footer here -->
</div>
</body>
</html>
```

Notice that in this file, we defined two blocks, `meta` and `content`, using the `{% block ... %}` tag. To use this layout, we define a `home.html` template.

`home.html`

```django
{% extends 'layout.html' %}

{% block meta %}
    <title>Page Title</title>
    <meta charset="UTF-8">
    <meta name="description" content="Free Web tutorials">
    <meta name="keywords" content="HTML, CSS, JavaScript">
    <meta name="author" content="John Doe">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
{% endblock %}

{% block content %}
<p>This is the content section.</p>
    {% include 'vendor/sidebar.html' %}
{% endblock %}
```

When this template is called, Django will first find the `layout.html` file, and fill out the `meta` and `content` blocks with information on this `home.html` page.

Also, notice there is something else in this template. `{% include 'vendor/sidebar.html' %}` tells Django to look for the `templates/vendor/sidebar.html` and place it here.

`sidebar.html`

```html
<p>This is the sidebar.</p>
```

It is not exactly a sidebar, but we can use it to demonstrate this inheritance system works. Also make sure your view is correct.

```python
from django.shortcuts import render

def home(request):
    return render(request, 'home.html')
```

And make sure your URL dispatcher points to this view.

```python
path('home/', views.home),
```

Open your browser and go to `http://127.0.0.1:8000/home`, and you should see the following page:

![template layout](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t6wtb4kbaysuaieo0xts.png)

> If you liked this article, please also check out my other tutorials:
> - [Laravel for Beginners](https://www.ericsdevblog.com/posts/laravel-for-beginners-1?ref=Dev_Community/)
> - [Vue.js for Beginners](https://www.ericsdevblog.com/posts/vuejs-for-beginners-1?ref=Dev_Community/)
> - [Create a Modern Application with Django and Vue](https://www.ericsdevblog.com/posts/create-a-modern-application-with-django-and-vue-1?ref=Dev_Community/)
> - [Beginner’s Roadmap to Web Development](https://www.ericsdevblog.com/series/beginners-roadmap-to-web-development?ref=Dev_Community/)