Build a Blog Using Django, Vue, and GraphQL

Build a Blog Using Django, Vue, and GraphQL

by Dane Hillard May 24, 2021 advanced api django front-end

Are you a regular Django user? Do you find yourself wanting to decouple your back end and front end? Do you want to handle data persistence in the API while displaying the data in a single-page app (SPA) in the browser using a client-side framework like React or Vue? You’re in luck. This tutorial will take you through the process of building a Django blog back end and a Vue front end, using GraphQL to communicate between them.

Projects are an effective way to learn and solidify concepts. This tutorial is structured as a step-by-step project so you can learn in a hands-on way and take breaks as needed.

In this tutorial, you’ll learn how to:

  • Translate your Django models into a GraphQL API
  • Run the Django server and a Vue application on your computer at the same time
  • Administer your blog posts in the Django admin
  • Consume a GraphQL API in Vue to show data in the browser

You can download all the source code you’ll use to build your Django blog application by clicking the link below:

Remove ads

Demo: A Django Blog Admin, a GraphQL API, and a Vue Front End

Blog applications are a common starter project because they involve create, read, update, and delete (CRUD) operations. In this project, you’ll use the Django admin to do the heavy CRUD lifting and focus on providing a GraphQL API for your blog data.

Here’s a demonstration of the completed project in action:

Next, you’ll make sure you have all the necessary background information and tools before diving into building your blog application.

Project Overview

You’ll create a small blogging application with some rudimentary features. Authors can write many posts. Posts can have many tags and can be either published or unpublished.

You’ll build the back end of this blog in Django, complete with an admin for adding new blog content. Then you’ll expose the content data as a GraphQL API and use Vue to display that data in the browser. You’ll accomplish this in several high-level steps:

  1. Set up the Django blog
  2. Create the Django blog admin
  3. Set up Graphene-Django
  4. Set up django-cors-headers
  5. Set up Vue.js
  6. Set up Vue Router
  7. Create the Vue Components
  8. Fetch the data

Each section will provide links to any necessary resources and give you a chance to pause and come back as needed.

Prerequisites

You’ll be best equipped for this tutorial if you already have a solid foundation in some web application concepts. You should understand how HTTP requests and responses and APIs work. You can check out Python & APIs: A Winning Combo for Reading Public Data to understand the details of using GraphQL APIs vs REST APIs.

Because you’ll use Django to build the back end for your blog, you’ll want to be familiar with starting a Django project and customizing the Django admin. If you haven’t used Django much before, you might also want to try building another Django-only project first. For a good introduction, check out Get Started with Django Part 1: Build a Portfolio App.

Because you’ll be using Vue on the front end, some experience with reactive JavaScript will also help. If you’ve only used a DOM manipulation paradigm with a framework like jQuery in the past, the Vue introduction is a good foundation.

Familiarity with JSON is also important because GraphQL queries are JSON-like and return data in JSON format. You can read about Working with JSON Data in Python for an introduction. You’ll also need to install Node.js to work on the front end later in this tutorial.

Step 1: Set Up the Django Blog

Before going too far, you’ll need a directory in which you can organize the code for your project. Start by creating one called dvg/, short for Django-Vue-GraphQL:

$ mkdir dvg/
$ cd dvg/

You’ll also be completely splitting up the front-end and back-end code, so it’s a good idea to start creating that separation right off the bat. Create a backend/ directory in your project directory:

$ mkdir backend/
$ cd backend/

You’ll put your Django code this directory, completely isolated from the Vue code you’ll create later in this tutorial.

Install Django

Now you’re ready to start building the Django application. To separate the dependencies for this project from your other projects, create a virtual environment in which you’ll install your project’s requirements. You can read more about virtual environments in Python Virtual Environments: A Primer. The rest of the tutorial assumes you will run commands related to Python and Django within your active virtual environment.

Now that you have a virtual environment in which to install requirements, create a requirements.txt file in the backend/ directory and define the first requirement you’ll need:

Django==3.1.7

Once you’ve saved the requirements.txt file, use it to install Django:

(venv) $ python -m pip install -r requirements.txt

Now you’ll be able to start creating your Django project.

Create the Django Project

Now that Django is installed, use the django-admin command to initialize your Django project:

(venv) $ django-admin startproject backend .

This creates a manage.py module and a backend package in the backend/ directory, so your project directory structure should now look like this:

dvg
└── backend
    ├── manage.py
    ├── requirements.txt
    └── backend
        ├── __init__.py
        ├── asgi.py
        ├── settings.py
        ├── urls.py
        └── wsgi.py

This tutorial won’t cover or need all these files, but it won’t hurt for them to be present.

Run Django Migrations

Before adding anything specific to your application, you should also run Django’s initial migrations. If you haven’t dealt with migrations before, then check out Django Migrations: A Primer. Run the migrations using the migrate management command:

(venv) $ python manage.py migrate

You should see a long list of migrations, each with an OK after it:

Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying sessions.0001_initial... OK

This will create an SQLite database file called db.sqlite3 that will also store the rest of the data for your project.

Create a Superuser

Now that you have the database, you can create a superuser. You’ll need this user so you can eventually log in to the Django admin interface. Use the createsuperuser management command to create one:

(venv) $ python manage.py createsuperuser

You’ll be able to use the username and password you supplied in this step to log in to the Django admin in the next section.

Step 1 Summary

Now that you’ve installed Django, created the Django project, run the Django migrations, and created a superuser, you have a fully functioning Django application. You should now be able to start the Django development server and view it in your browser. Start the server using the runserver management command, which will listen to port 8000 by default:

(venv) $ python manage.py runserver

Now visit http://localhost:8000 in your browser. You should see the Django splash page, indicating that the installation worked successfully. You should also be able to visit http://localhost:8000/admin, where you’ll see a login form.

Use the username and password you made for your superuser to log into the Django admin. If everything’s working, then you’ll be taken to the Django admin dashboard page. This page will be pretty bare for the moment, but you’ll make it much more interesting in the next step.

Step 2: Create the Django Blog Admin

Now that you have the foundations of your Django project in place, you’re ready to start creating some of the core business logic for your blog. In this step you’ll create the data models and the administrative configuration for authoring and managing blog content.

Create the Django Blog Application

Keep in mind that a single Django project can contain many Django applications. You should separate your blog-specific behavior into its own Django application so that it remains distinct from any future applications you build into your project. Create the application using the startapp management command:

(venv) $ python manage.py startapp blog

This will create a blog/ directory with several skeleton files:

blog
├── __init__.py
├── admin.py
├── apps.py
├── migrations
│   └── __init__.py
├── models.py
├── tests.py
└── views.py

You’ll make changes and additions to some of these files later in this tutorial.

Enable the Django Blog Application

Creating a Django application doesn’t make it available in your project by default. To make sure the project knows about your new blog application, you’ll need to add it to the list of installed applications. Update the INSTALLED_APPS variable in backend/settings.py:

INSTALLED_APPS = [
  ...
  "blog",
]

This will help Django discover information about your application, such as the data models and URL patterns it contains.

Create the Django Blog Data Models

Now that Django can discover your blog application, you can create the data models. You’ll create three models to start:

  1. Profile stores additional information about blog users.
  2. Tag represents a category in which blog posts can be grouped.
  3. Post stores the content and metadata of each blog post.

You’ll add each of these models to blog/models.py. First, import Django’s django.db.models module:

from django.db import models

Each of your models will inherit from the models.Model class.

The Profile Model

The Profile model will have a few fields:

  • user is a one-to-one association to the Django user with which the profile is associated.
  • website is an optional URL where you can learn more about the user.
  • bio is an optional, tweet-sized blurb to learn more about the user quickly.

You’ll first need to import the settings module from Django:

from django.conf import settings

Then create the Profile model, which should look like the following snippet:

class Profile(models.Model):
    user = models.OneToOneField(
        settings.AUTH_USER_MODEL,
        on_delete=models.PROTECT,
    )
    website = models.URLField(blank=True)
    bio = models.CharField(max_length=240, blank=True)

    def __str__(self):
        return self.user.get_username()

The __str__ method will make the Profile objects you create appear in a more human-friendly manner on the admin site.

The Tag Model

The Tag model has just one field, name, which stores a short, unique name for the tag. Create the Tag model, which should look like the following snippet:

class Tag(models.Model):
    name = models.CharField(max_length=50, unique=True)

    def __str__(self):
        return self.name

Again, __str__ will make the Tag objects you create appear in a more human-friendly manner on the admin site.

The Post Model

The Post model, as you might imagine, is the most involved. It will have several fields:

Field name Purpose
title The unique title of the post to display to readers
subtitle An optional clarifier of the post’s content to help readers understand if they want to read it
slug A unique, readable identifier for the post to use in URLs
body The post’s content
meta_description An optional description to use for search engines like Google
date_created A timestamp of the post’s creation
date_modified A timestamp of the post’s most recent edit
publish_date An optional timestamp when the post goes live
published Whether the post is currently available to readers
author A reference to the user profile who wrote the post
tags The list of tags associated with the post, if any

Because blogs usually show the most recent posts first, you’ll also want the ordering to be by published date, with the most recent first. Create the Post model, which should look like the following snippet:

class Post(models.Model):
    class Meta:
        ordering = ["-publish_date"]

    title = models.CharField(max_length=255, unique=True)
    subtitle = models.CharField(max_length=255, blank=True)
    slug = models.SlugField(max_length=255, unique=True)
    body = models.TextField()
    meta_description = models.CharField(max_length=150, blank=True)
    date_created = models.DateTimeField(auto_now_add=True)
    date_modified = models.DateTimeField(auto_now=True)
    publish_date = models.DateTimeField(blank=True, null=True)
    published = models.BooleanField(default=False)

    author = models.ForeignKey(Profile, on_delete=models.PROTECT)
    tags = models.ManyToManyField(Tag, blank=True)

The on_delete=models.PROTECT argument for author ensures that you won’t accidentally delete an author who still has posts on the blog. The ManyToManyField relationship to Tag allows you to associate a post with zero or more tags. Each tag can be associated to many posts.

Create the Model Admin Configuration

Now that your models are in place, you’ll need to tell Django how they should be displayed in the admin interface. In blog/admin.py, start by importing Django’s admin module and your models:

from django.contrib import admin

from blog.models import Profile, Post, Tag

Then create and register the admin classes for Profile and Tag, which only need the model specified:

@admin.register(Profile)
class ProfileAdmin(admin.ModelAdmin):
    model = Profile

@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
    model = Tag

Just like the model, the admin class for Post is more involved. Posts contain a lot of information, so it helps to be more judicious about what information you display to avoid crowding the interface.

In the list of all posts, you’ll specify that Django should only show the following information about each post:

  1. ID
  2. Title
  3. Subtitle
  4. Slug
  5. Publish date
  6. Publish status

To make browsing and editing posts more fluid, you’ll also tell the Django admin system to take the following actions:

  • Allow filtering the post list by posts that are published or unpublished.
  • Allow filtering posts by publish date.
  • Allow editing all the displayed fields, with the exception of the ID.
  • Allow searching for posts using the title, subtitle, slug, and body.
  • Prepopulate the slug field using the title and subtitle fields.
  • Use the publish date of all posts to create a browsable date hierarchy.
  • Show a button at the top of the list to save changes.

Create and register the PostAdmin class:

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    model = Post

    list_display = (
        "id",
        "title",
        "subtitle",
        "slug",
        "publish_date",
        "published",
    )
    list_filter = (
        "published",
        "publish_date",
    )
    list_editable = (
        "title",
        "subtitle",
        "slug",
        "publish_date",
        "published",
    )
    search_fields = (
        "title",
        "subtitle",
        "slug",
        "body",
    )
    prepopulated_fields = {
        "slug": (
            "title",
            "subtitle",
        )
    }
    date_hierarchy = "publish_date"
    save_on_top = True

You can read more about all the options the Django admin has to offer in Customize the Django Admin With Python.

Create the Model Migrations

Django has all the information it needs to administer and persist your blog content, but you’ll first need to update the database to support these changes. Earlier in this tutorial, you ran Django’s migrations for its built-in models. Now, you’ll create and run migrations for your models.

First, create the migrations using the makemigrations management command:

(venv) $ python manage.py makemigrations
Migrations for 'blog':
  blog/migrations/0001_initial.py
    - Create model Tag
    - Create model Profile
    - Create model Post

This creates a migration whose name is 0001_initial.py by default. Run this migration using the migrate management command:

(venv) $ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, blog, contenttypes, sessions
Running migrations:
  Applying blog.0001_initial... OK

Note that the migrations should say OK after the migration name.

Step 2 Summary

You now have all your data models in place, and you’ve configured the Django admin so that you can add and edit those models.

Start or restart the Django development server, visit the admin interface at http://localhost:8000/admin, and explore what’s changed. You should see links to the list of tags, profiles, and posts and links to add or edit each of them. Try adding and editing a few of each to see how the admin interface responds.

Step 3: Set Up Graphene-Django

At this point, you’ve completed enough of the back end that you could decide to go headlong in the Django direction. You could use Django’s URL routing and templating engine to build pages that would show all the post content you create in the admin to readers. Instead, you’ll wrap the back end you’ve created in a GraphQL API so you can eventually consume it from the browser and provide a richer client-side experience.

GraphQL allows you to retrieve only the data you need, which can be useful compared to very large responses that are common in RESTful APIs. GraphQL also provides more flexibility about projecting data, so you can often retrieve data in new ways without changing the logic of the service providing the GraphQL API.

You’ll use Graphene-Django to integrate what you’ve created so far into a GraphQL API.

Install Graphene-Django

To get started with Graphene-Django, first add it to your project’s requirements file:

graphene-django==2.14.0

Then install it using the updated requirements file:

(venv) $ python -m pip install -r requirements.txt

Add "graphene_django" to the INSTALLED_APPS variable in your project’s settings.py module so Django will find it:

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

Graphene-Django is now installed and ready to be configured.

Configure Graphene-Django

To get Graphene-Django working in your project, you’ll need to configure a few pieces:

  1. Update settings.py so the project knows where to look for GraphQL information.
  2. Add a URL pattern to serve the GraphQL API and GraphiQL, GraphQL’s explorable interface.
  3. Create the GraphQL schema so Graphene-Django knows how to translate your models into GraphQL.

Update Django Settings

The GRAPHENE setting configures Graphene-Django to look in a particular place for your GraphQL schema. Point it to the blog.schema.schema Python path, which you’ll create shortly:

GRAPHENE = {
  "SCHEMA": "blog.schema.schema",
}

Note that this addition may cause Django to produce an import error, which you’ll resolve when you create your GraphQL schema.

Add a URL Pattern for GraphQL and GraphiQL

To let Django serve the GraphQL endpoint and the GraphiQL interface, you’ll add a new URL pattern to backend/urls.py. You’ll point the URL at Graphene-Django’s GraphQLView. Because you’re not using the Django template engine’s cross-site request forgery (CSRF) protection features, you’ll also need to import Django’s csrf_exempt decorator to mark the view as exempt from CSRF protection:

from django.views.decorators.csrf import csrf_exempt
from graphene_django.views import GraphQLView

Then, add the new URL pattern to the urlpatterns variable:

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

The graphiql=True argument tells Graphene-Django to make the GraphiQL interface available.

Create the GraphQL Schema

Now you’ll create the GraphQL schema, which should feel similar to the admin configuration you created earlier. The schema consists of several classes that are each associated with a particular Django model and one to specify how to resolve a few important types of queries you’ll need in the front end.

Create a new schema.py module in the blog/ directory. Import Graphene-Django’s DjangoObjectType, your blog models, and the Django User model:

from django.conf import settings
from graphene_django import DjangoObjectType

from blog import models

Create a corresponding class for each of your models and the User model. They should each have a name that ends with Type because each one represents a GraphQL type. Your classes should look like the following:

class UserType(DjangoObjectType):
    class Meta:
        model = settings.AUTH_USER_MODEL

class AuthorType(DjangoObjectType):
    class Meta:
        model = models.Profile

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

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

You’ll need to create a Query class that inherits from graphene.ObjectType. This class will bring together all the type classes you created, and you’ll add methods to it to indicate the ways in which your models can be queried. You’ll need to import graphene first:

import graphene

The Query class is made up of a number of attributes that are either graphene.List or graphene.Field. You’ll use graphene.Field if the query should return a single item and graphene.List if it will return multiple items.

For each of these attributes, you’ll also create a method to resolve the query. You resolve a query by taking the information supplied in the query and returning the appropriate Django queryset in response.

The method for each resolver must start with resolve_, and the rest of the name should match the corresponding attribute. As an example, the method to resolve the queryset for the all_posts attribute must be named resolve_all_posts.

You’ll create queries to get:

  • All the posts
  • An author with a given username
  • A post with a given slug
  • All posts by a given author
  • All posts with a given tag

Create the Query class now. It should look like the following snippet:

class Query(graphene.ObjectType):
    all_posts = graphene.List(PostType)
    author_by_username = graphene.Field(AuthorType, username=graphene.String())
    post_by_slug = graphene.Field(PostType, slug=graphene.String())
    posts_by_author = graphene.List(PostType, username=graphene.String())
    posts_by_tag = graphene.List(PostType, tag=graphene.String())

    def resolve_all_posts(root, info):
        return (
            models.Post.objects.prefetch_related("tags")
            .select_related("author")
            .all()
        )

    def resolve_author_by_username(root, info, username):
        return models.Profile.objects.select_related("user").get(
            user__username=username
        )

    def resolve_post_by_slug(root, info, slug):
        return (
            models.Post.objects.prefetch_related("tags")
            .select_related("author")
            .get(slug=slug)
        )

    def resolve_posts_by_author(root, info, username):
        return (
            models.Post.objects.prefetch_related("tags")
            .select_related("author")
            .filter(author__user__username=username)
        )

    def resolve_posts_by_tag(root, info, tag):
        return (
            models.Post.objects.prefetch_related("tags")
            .select_related("author")
            .filter(tags__name__iexact=tag)
        )

You now have all the types and resolvers for your schema, but remember that the GRAPHENE variable you created points to blog.schema.schema. Create a schema variable that wraps your Query class in graphene.Schema to tie it all together:

schema = graphene.Schema(query=Query)

This variable matches the "blog.schema.schema" value you configured for Graphene-Django earlier in this tutorial.

Step 3 Summary

You’ve fleshed out your blog’s data model, and now you’ve also wrapped your data model with Graphene-Django to serve that data as a GraphQL API.

Run the Django development server and visit http://localhost:8000/graphql. You should see the GraphiQL interface with some commented text that explains how to use the tool.

Expand the Docs section in the top right of the screen and click query: Query. You should see each of the queries and types that you configured in your schema.

If you haven’t created any test blog content yet, do so now. Try the following query, which should return a list of all the posts you’ve created:

{
  allPosts {
    title
    subtitle
    author {
      user {
        username
      }
    }
    tags {
      name
    }
  }
}

The response should return a list of posts. The structure of each post should match the shape of the query, like the following example:

{
  "data": {
    "allPosts": [
      {
        "title": "The Great Coney Island Debate",
        "subtitle": "American or Lafayette?",
        "author": {
          "user": {
            "username": "coney15land"
          }
        },
        "tags": [
          {
            "name": "food"
          },
          {
            "name": "coney island"
          }
        ]
      }
    ]
  }
}

If you’ve saved some posts and you see them in the response, then you’re ready to continue on.

Step 4: Set Up django-cors-headers

You’ll need to take one more step before you can call the back-end work complete. Because the back end and the front end will run on different ports locally, and because they might run on entirely separate domains in a production environment, cross-origin resource sharing (CORS) comes into play. Without handling CORS, requests from the front end to the back end will generally be blocked by your browser.

The django-cors-headers project makes dealing with CORS fairly painless. You’ll use this to tell Django to respond to requests even if they come from another origin, which will allow the front end to communicate properly with the GraphQL API.

Install django-cors-headers

First, add django-cors-headers to your requirements file:

django-cors-headers==3.6.0

Then install it using the updated requirements file:

(venv) $ python -m pip install -r requirements.txt

Add "corsheaders" to the INSTALLED_APPS list in your project’s settings.py module:

INSTALLED_APPS = [
  ...
  "corsheaders",
]

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

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

The django-cors-headers documentation suggests putting the middleware as early in the MIDDLEWARE list as possible. You can put it at the very top of the list in this project.

Configure django-cors-headers

CORS exists for a good reason. You don’t want to expose your application to be used from just anywhere on the Internet. You can use two settings to very precisely define how much you want to open up the GraphQL API:

  1. CORS_ORIGIN_ALLOW_ALL defines whether Django should be all open or all closed by default.
  2. CORS_ORIGIN_WHITELIST defines which domains from which your Django application will allow requests.

Add the following settings to settings.py:

CORS_ORIGIN_ALLOW_ALL = False
CORS_ORIGIN_WHITELIST = ("http://localhost:8080",)

These settings will allow requests only from your front end, which you’ll eventually run on port 8080 locally.

Step 4 Summary

The back end is complete! You have a working data model, a working admin interface, a working GraphQL API that you can explore using GraphiQL, and the ability to query the API from the front end you’ll build next. This is a great place to pause if you haven’t taken a break in a while.

Step 5: Set Up Vue.js

You’ll use Vue as the front end for your blog. To set up Vue, you’ll create the Vue project, install a couple of important plugins, and run the Vue development server to make sure your application and its dependencies are working together properly.

Create the Vue Project

Much like Django, Vue provides a command-line interface for creating a project without starting totally from scratch. You can pair this with Node’s npx command to bootstrap JavaScript-based commands others have published. Using this approach, you won’t need to manually install the variety of individual dependencies that you need to get a Vue project up and running. Use npx to create your Vue project now:

$ cd /path/to/dvg/
$ npx @vue/cli create frontend --default
...
🎉  Successfully created project frontend.
...
$ cd frontend/

This will create a frontend/ directory alongside your existing backend/ directory, install a number of JavaScript dependencies, and create some skeleton files for the application.

Install Vue Plugins

You’ll need some plugins for Vue to do proper browser routing and interact with your GraphQL API. These plugins sometimes affect your files, so it’s good to install them near the beginning so they don’t overwrite anything, then configure them later on. Install the Vue Router and Vue Apollo plugins, choosing the default options when prompted:

$ npx @vue/cli add router
$ npx @vue/cli add apollo

These commands will take some time to install dependencies, and they’ll add or change some of the files in the project to configure and install each plugin in your Vue project.

Step 5 Summary

You should now be able to run the Vue development server:

$ npm run serve

You now have your Django application running at http://localhost:8000 and your Vue application running at http://localhost:8080.

Visit http://localhost:8080 in your browser. You should see the Vue splash page, which indicates that you’ve installed everything successfully. If you see the splash page, you’re ready to start creating some of your own components.

Step 6: Set Up Vue Router

An important part of client-side applications is handling routing without having to make new requests to the server. A common solution for this in Vue is the Vue Router plugin, which you installed earlier. You’ll use Vue Router instead of normal HTML anchor tags to link to different pages of your blog.

Create Routes

Now that you’ve installed Vue Router, you need to configure Vue to use Vue Router. You’ll also need to configure Vue Router with the URL paths it should route.

Create a router.js module in the src/ directory. This file will hold all the configuration about which URLs map to which Vue components. Start by importing Vue and Vue Router:

import Vue from 'vue'
import VueRouter from 'vue-router'

Add the following imports, which each correspond to a component you’ll create shortly:

import Post from '@/components/Post'
import Author from '@/components/Author'
import PostsByTag from '@/components/PostsByTag'
import AllPosts from '@/components/AllPosts'

Register the Vue Router plugin:

Vue.use(VueRouter)

Now you’ll create the list of routes. Each route has two properties:

  1. path is a URL pattern that optionally contains capture variables similar to Django URL patterns.
  2. component is the Vue component to display when the browser navigates to a route matching the path pattern.

Add these routes as a routes variable. They should look like the following:

const routes = [
  { path: '/author/:username', component: Author },
  { path: '/post/:slug', component: Post },
  { path: '/tag/:tag', component: PostsByTag },
  { path: '/', component: AllPosts },
]

Create a new instance of VueRouter and export it from the router.js module so other modules can use it:

const router = new VueRouter({
  routes: routes,
  mode: 'history',
})
export default router

You’ll import the router variable in another module in the next section.

Install the Router

At the top of src/main.js, import the router from the module you created in the previous section:

import router from '@/router'

Then pass the router to the Vue instance:

new Vue({
  router,
  ...
})

This completes the configuration of Vue Router.

Step 6 Summary

You’ve created the routes for your front end, which map a URL pattern to the component that will show up at that URL. The routes won’t work just yet because they point to components that don’t yet exist. You’ll create these components in the next step.

Step 7: Create the Vue Components

Now that you’ve got Vue up and running with routes that will go to your components, you can start creating the components that will eventually display data from the GraphQL endpoint. At the moment, you’ll just make them display some static content. The table below describes the components you’ll create:

Component Displays
AuthorLink A link to a given author’s page (used in Post and PostList)
PostList A given list of blog posts (used in AllPosts, Author, and PostsByTag)
AllPosts A list of all posts, with the most recent first
PostsByTag A list of posts associated with a given tag, with the most recent first
Post The metadata and content for a given post
Author Information about an author and a list of posts they’ve written

You’ll update these components with dynamic data in the next step.

The PostList Component

The PostList component accepts a posts prop, whose structure corresponds to the data about posts in your GraphQL API. The component also accepts a Boolean showAuthor prop, which you’ll set to false on the author’s page because it’s redundant information. The component should display the following features:

  • The title and subtitle of the post, linking them to the post’s page
  • A link to the author of the post using AuthorLink (if showAuthor is true)
  • The date the post was published
  • The meta description for the post
  • The list of tags associated with the post

Create a PostList.vue SFC in the src/components/ directory. The component template should look like the following:

<template>
  <div>
    <ol class="post-list">
      <li class="post" v-for="post in publishedPosts" :key="post.title">
          <span class="post__title">
            <router-link
              :to="`/post/${post.slug}`"
            >{{ post.title }}: {{ post.subtitle }}</router-link>
          </span>
          <span v-if="showAuthor">
            by <AuthorLink :author="post.author" />
          </span>
          <div class="post__date">{{ displayableDate(post.publishDate) }}</div>
        <p class="post__description">{{ post.metaDescription }}</p>
        <ul>
          <li class="post__tags" v-for="tag in post.tags" :key="tag.name">
            <router-link :to="`/tag/${tag.name}`">#{{ tag.name }}</router-link>
          </li>
        </ul>
      </li>
    </ol>
  </div>
</template>

The PostList component’s JavaScript should look like the following:

<script>
import AuthorLink from '@/components/AuthorLink'

export default {
  name: 'PostList',
  components: {
    AuthorLink,
  },
  props: {
    posts: {
      type: Array,
      required: true,
    },
    showAuthor: {
      type: Boolean,
      required: false,
      default: true,
    },
  },
  computed: {
    publishedPosts () {
      return this.posts.filter(post => post.published)
    }
  },
  methods: {
    displayableDate (date) {
      return new Intl.DateTimeFormat(
        'en-US',
        { dateStyle: 'full' },
      ).format(new Date(date))
    }
  },
}
</script>

The PostList component receives its data as a prop instead of using GraphQL directly.

You can add some optional CSS styling to make the list of posts a bit more readable once they’re rendered:

<style>
.post-list {
  list-style: none;
}

.post {
  border-bottom: 1px solid #ccc;
  padding-bottom: 1rem;
}

.post__title {
  font-size: 1.25rem;
}

.post__description {
  color: #777;
  font-style: italic;
}

.post__tags {
  list-style: none;
  font-weight: bold;
  font-size: 0.8125rem;
}
</style>

These styles add some spacing, remove some clutter, and differentiate different pieces of information to help with scannability.

The AllPosts Component

The next component you’ll create is a list of all the posts on the blog. It needs to display two pieces of information:

  1. A Recent Posts heading
  2. The list of posts, using PostList

Create the AllPosts.vue SFC in the src/components/ directory. It should look like the following:

<template>
  <div>
    <h2>Recent posts</h2>
    <PostList v-if="allPosts" :posts="allPosts" />
  </div>
</template>

<script>
import PostList from '@/components/PostList'

export default {
  name: 'AllPosts',
  components: {
    PostList,
  },
  data () {
    return {
        allPosts: null,
    }
  },
}
</script>

You’ll populate the allPosts variable dynamically with a GraphQL query later in this tutorial.

The PostsByTag Component

The PostsByTag component is very similar to the AllPosts component. The heading text differs, and you’ll query for a different set of posts in the next step.

Create the PostsByTag.vue SFC in the src/components/ directory. It should look like the following:

<template>
  <div>
    <h2>Posts in #{{ $route.params.tag }}</h2>
    <PostList :posts="posts" v-if="posts" />
  </div>
</template>

<script>
import PostList from '@/components/PostList'

export default {
  name: 'PostsByTag',
  components: {
    PostList,
  },
  data () {
    return {
      posts: null,
    }
  },
}
</script>

You’ll populate the posts variable with a GraphQL query later in this tutorial.

The Author Component

The Author component acts as an author’s profile page. It should display the following information:

  • A heading with the author’s name
  • A link to the author’s website, if provided
  • The author’s biography, if provided
  • The list of posts by the author, with showAuthor set to false

Create the Author.vue SFC in the src/components/ directory now. It should look like the following:

<template>
  <div v-if="author">
    <h2>{{ displayName }}</h2>
    <a
      :href="author.website"
      target="_blank"
      rel="noopener noreferrer"
    >Website</a>
    <p>{{ author.bio }}</p>

    <h3>Posts by {{ displayName }}</h3>
    <PostList :posts="author.postSet" :showAuthor="false" />
  </div>
</template>

<script>
import PostList from '@/components/PostList'

export default {
  name: 'Author',
  components: {
    PostList,
  },
  data () {
    return {
      author: null,
    }
  },
  computed: {
    displayName () {
      return (
        this.author.user.firstName &&
        this.author.user.lastName &&
        `${this.author.user.firstName} ${this.author.user.lastName}`
      ) || `${this.author.user.username}`
    },
  },
}
</script>

You’ll populate the author variable dynamically with a GraphQL query later in this tutorial.

The Post Component

Just like the data model, the Post component is the most interesting because it has the responsibility of displaying all the post’s information. The component should display the following information about the post:

  • Title and subtitle, as a heading
  • Author, as a link using AuthorLink
  • Publication date
  • Meta description
  • Content body
  • List of associated tags, as links

Because of your data modeling and component architecture, you may be surprised at how little code this requires. Create the Post.vue SFC in the src/components/ directory. It should look like the following:

<template>
  <div class="post" v-if="post">
      <h2>{{ post.title }}: {{ post.subtitle }}</h2>
      By <AuthorLink :author="post.author" />
      <div>{{ displayableDate(post.publishDate) }}</div>
    <p class="post__description">{{ post.metaDescription }}</p>
    <article>
      {{ post.body }}
    </article>
    <ul>
      <li class="post__tags" v-for="tag in post.tags" :key="tag.name">
        <router-link :to="`/tag/${tag.name}`">#{{ tag.name }}</router-link>
      </li>
    </ul>
  </div>
</template>

<script>
import AuthorLink from '@/components/AuthorLink'

export default {
  name: 'Post',
  components: {
    AuthorLink,
  },
  data () {
    return {
      post: null,
    }
  },
  methods: {
    displayableDate (date) {
      return new Intl.DateTimeFormat(
        'en-US',
        { dateStyle: 'full' },
      ).format(new Date(date))
    }
  },
}
</script>

You’ll populate the post variable dynamically with a GraphQL query later in this tutorial.

The App Component

Before you can see the outcomes of your labor, you need to update the App component that your Vue setup command created. Instead of showing the Vue splash page, it should show the AllPosts component.

Open the App.vue SFC in the src/ directory. You can delete all the content inside it because you’ll need to replace it with code that displays the following features:

  • A heading with the title of your blog that links to the home page
  • <router-view>, a Vue Router component that renders the right component for the current route

Your App component should look like the following:

<template>
    <div id="app">
        <header>
          <router-link to="/">
            <h1>Awesome Blog</h1>
          </router-link>
        </header>
        <router-view />
    </div>
</template>

<script>
export default {
  name: 'App',
}
</script>

You can also add some optional CSS styling to polish up the display a bit:

<style>
* {
  margin: 0;
  padding: 0;
}

body {
  margin: 0;
  padding: 1.5rem;
}

* + * {
  margin-top: 1.5rem;
}

#app {
  margin: 0;
  padding: 0;
}
</style>

These styles give a bit of breathing room to most elements on the page and remove the space around the whole page that most browsers add by default.

Step 7 Summary

If you haven’t used Vue much before, this step might have been a lot to digest. You’ve reached an important milestone, though. You have a working Vue application, complete with routes and views ready to display data.

You can confirm your application is working by starting the Vue development server and visiting http://localhost:8080. You should see the title of your blog and a Recent Posts heading. If you do, then you’re ready to take on the final step, where you’ll use Apollo to query your GraphQL API to bring the front end and back end together.

Step 8: Fetch the Data

Now that you’ve got everything prepped for displaying data when it’s available, it’s time to fetch that data from your GraphQL API.

Apollo makes querying GraphQL APIs more convenient. The Vue Apollo plugin you installed earlier integrates Apollo into Vue, making it that much more convenient to query GraphQL from within your Vue project.

Configure Vue Apollo

Vue Apollo is mostly configured out of the box, but you’ll need to tell it the right endpoint to query. You may also want to turn off the WebSocket connection it tries to use by default because this generates noise in the Network and Console tabs of your browser. Edit the apolloProvider definition in the src/main.js module to specify the httpEndpoint and wsEndpoint properties:

new Vue({
  ...
  apolloProvider: createProvider({
    httpEndpoint: 'http://localhost:8000/graphql',
    wsEndpoint: null,
  }),
  ...
})

Now you’re ready to start adding the queries to populate your pages. You’ll do this by adding a created() function to several of your SFCs. created() is a special Vue lifecycle hook that executes when a component is about to render on the page. You can use this hook to query for the data you want to render so that it becomes available as your component renders. You’ll create a query for the following components:

  • Post
  • Author
  • PostsByTag
  • AllPosts

You can start by creating the Post query.

The Post Query

The query for an individual post accepts the slug of the desired post. It should return all the necessary pieces of information to display the post information and content.

You’ll use the $apollo.query helper and the gql helper to build the query in the Post component’s created() function, ultimately using the response to set the component’s post so it can be rendered. created() should look like the following:

<script>
  ...
  async created () {
    const post = await this.$apollo.query({
        query: gql`query ($slug: String!) {
          postBySlug(slug: $slug) {
            title
            subtitle
            publishDate
            metaDescription
            slug
            body
            author {
              user {
                username
                firstName
                lastName
              }
            }
            tags {
              name
            }
          }
        }`,
        variables: {
          slug: this.$route.params.slug,
        },
    })
    this.post = post.data.postBySlug
  },
  ...
</script>

This query pulls in most of the data about the post and its associated author and tags. Notice that the $slug placeholder is used in the query, and the variables property passed to $apollo.query is used to populate the placeholder. The slug property matches the $slug placeholder by name. You’ll see this pattern again in some of the other queries.

The Author Query

Whereas in the query for Post you fetched the single post’s data and some nested data about the author, in the Author query you’ll need to fetch the author data and the list of all posts by the author.

The author query accepts the username of the desired author and should return all the necessary pieces of information to display the author and their list of posts. It should look like the following:

<script>
  ...
  async created () {
    const user = await this.$apollo.query({
      query: gql`query ($username: String!) {
        authorByUsername(username: $username) {
          website
          bio
          user {
            firstName
            lastName
            username
          }
          postSet {
            title
            subtitle
            publishDate
            published
            metaDescription
            slug
            tags {
              name
            }
          }
        }
      }`,
      variables: {
        username: this.$route.params.username,
      },
    })
    this.author = user.data.authorByUsername
  },
  ...
</script>

This query uses postSet, which might look familiar if you’ve done some Django data modeling in the past. The name “post set” comes from the reverse relationship Django creates for a ForeignKey field. In this case, the post has a foreign key relationship to its author, which has a reverse relationship with the post called post_set. Graphene-Django has automatically exposed this as postSet in the GraphQL API.

The PostsByTag Query

The query for PostsByTag should feel pretty similar to the first queries you created. This query accepts the desired tag and returns the list of matching posts. created() should look like the following:

<script>
  ...
  async created () {
    const posts = await this.$apollo.query({
      query: gql`query ($tag: String!) {
        postsByTag(tag: $tag) {
          title
          subtitle
          publishDate
          published
          metaDescription
          slug
          author {
            user {
              username
              firstName
              lastName
            }
          }
          tags {
            name
          }
        }
      }`,
      variables: {
        tag: this.$route.params.tag,
      },
    })
    this.posts = posts.data.postsByTag
  },
  ...
</script>

You might notice that some pieces of each query look pretty similar to one another. Although it won’t be covered in this tutorial, you can use GraphQL fragments to reduce duplication in your query code.

The AllPosts Query

The query for AllPosts doesn’t require any input information and returns the same set of information as the PostsByTag query. It should look like the following:

<script>
  ...
  async created () {
    const posts = await this.$apollo.query({
      query: gql`query {
        allPosts {
          title
          subtitle
          publishDate
          published
          metaDescription
          slug
          author {
            user {
              username
              firstName
              lastName
            }
          }
          tags {
            name
          }
        }
      }`,
    })
    this.allPosts = posts.data.allPosts
  },
  ...
</script>

This is the last query for now, but you should revisit the last couple of steps to let them sink in. If you want to add new pages with new views of your data in the future, it’s a matter of creating a route, a component, and a query.

Step 8 Summary

Now that each component is fetching the data it needs to display, you’ve arrived at a functioning blog. Run the Django development server and the Vue development server. Visit http://localhost:8080 and browse through your blog. If you can see authors, posts, tags, and the content of a post in your browser, you’re golden!

Next Steps

You started by creating a Django blog back end to administer, persist, and serve the data for a blog. Then you created a Vue front end to consume and display that data. You made the two communicate with GraphQL using Graphene and Apollo.

You may already be wondering what you can do next. To further validate that your blog is working as expected, you could try the following:

  • Add more users and posts to see them split up by author.
  • Make some posts unpublished to confirm that they don’t show up on the blog.

If you’re feeling confident and adventurous with what you have going, you can also take this system of yours even further:

  • Expand your data model to create new behavior in your Django blog.
  • Create new queries to provide interesting views on your blog’s data.
  • Explore GraphQL mutations to write data in addition to reading it.
  • Add CSS to your single-file components to make the blog more eye-catching.

The data modeling and component architecture you’ve put together is remarkably extensible, so take it as far as you like!

If you want to make your Django application ready for prime time, read Deploying Django + Python3 + PostgreSQL to AWS Elastic Beanstalk or Development and Deployment of Django on Fedora. You can use Amazon Web Services or something like Netlify to deploy your Vue project as well.

Conclusion

You’ve seen how you can use GraphQL for building typed, flexible views of your data. You can use these same techniques on an existing Django application you’ve built or one you plan to build. Like other APIs, you can use yours in most any client-side framework as well.

In this tutorial, you learned how to:

  • Build the Django blog data model and admin interface
  • Wrap your data model in a GraphQL API using Graphene-Django
  • Create and route to separate Vue components for each view of the data
  • Query the GraphQL API dynamically to populate your Vue components using Apollo

You covered a lot of ground, so try to identify some new ways to use these concepts in different contexts to solidify your learning. Happy coding, and happy blogging!

You can download the complete source code for this project by clicking the link below:

🐍 Python Tricks 💌

Get a short & sweet Python Trick delivered to your inbox every couple of days. No spam ever. Unsubscribe any time. Curated by the Real Python team.

Python Tricks Dictionary Merge

About Dane Hillard

Dane Hillard Dane Hillard

Dane is a Lead Web Application Developer at ITHAKA and is currently writing Publishing Python Packages.

» More about Dane

Each tutorial at Real Python is created by a team of developers so that it meets our high quality standards. The team members who worked on this tutorial are:

Master Real-World Python Skills With Unlimited Access to Real Python

Join us and get access to hundreds of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

Master Real-World Python Skills
With Unlimited Access to Real Python

Join us and get access to hundreds of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

What Do You Think?

Real Python Comment Policy: The most useful comments are those written with the goal of learning from or helping out other readers—after reading the whole article and all the earlier comments. Complaints and insults generally won’t make the cut here.

What’s your #1 takeaway or favorite thing you learned? How are you going to put your newfound skills to use? Leave a comment below and let us know.

Keep Learning

Related Tutorial Categories: advanced api django front-end