DEV Community

Cover image for An Intro to GraphQL with Django
Ibaakee Ledum
Ibaakee Ledum

Posted on

An Intro to GraphQL with Django

In this post, I'll be showing us how to implement a basic GraphQL server in Django, and query for data using a built in GraphiQL IDE that comes along with it.

Requirements

  1. pipenv
  2. django 2.0 or greater
  3. graphene
  4. graphene-django

Note: This post requires you to have a working knowledge of how Django works,
if you are still into the basics check out this
resource to get more knowledge on the subject.

What is GraphQL ?

GraphQL is a query language that provides a complete and understandable description of the data in your API, giving clients the power to ask for exactly what they need.

Let's lay some ground work before moving further .....
Take a look at this code snippet

from django.http import JsonResponse
from car.models import Car

def list_post(request):

    cars = Car.objects.all()
    data = {
        "cars":list(cars.values())
    }

    return JsonResponse(data=data)
Enter fullscreen mode Exit fullscreen mode

This is a basic implementation of a REST API that sends data back to client in JSON format.
Using this we can write seperate CRUD endpoints

def list_post(request):

    cars = Car.objects.all()
    data = {
        "cars":list(cars.values())
    }

    return JsonResponse(data=data)

def create_post(request):

    data = request.POST
    car = Car.objects.create(id=data.id, name=data.name)

    response = {
        "car":{
            'id': car.id,
            'name': car.name
        }
    }
    return JsonResponse(data=response)

def update_post(request, id):
    data = request.POST

    car = Car.objects.get(id=id)
    car.id = data.id
    car.name = data.name
    car.save()

    response = {
        "car":{
            'id': car.id,
            'name': car.name
        }
    }
    return JsonResponse(data=response)

def delete_post(request, id):
    car = Car.objects.get(id=id)
    car.delete()
    return JsonResponse(data=dict(status="Car successfully deleted"))
Enter fullscreen mode Exit fullscreen mode

Registering with urls.py we get....

from application import views

urlpatterns = [
    # Previous code here,
    path('posts', views.post_list),
    path('post/delete/<int:id>', views.delete_post),
    path('post/create', views.create_post),
    path('post/update/<int:id>', views.update_post)
]
Enter fullscreen mode Exit fullscreen mode

This is a simple REST API implementation giving you four distinct endpoints for performing a basic CRUD operation.

Now, I know that at first this might seem okay to some, but trust me, in the long run, managing different endpoints increases complexity.

Sure, we can try fitting everything into a single request endpoint and handle it all with if-else statements, but in the long run, consistent updates makes your code less readable, since that request endpoint is resolving to perform many roles at the same time.

What if we had a simpler way to describe our data, so when we need to perform CRUD operations, we just send in a description of what we are trying to get / change and receive back exactly what we wanted.

Using just a single endpoint and a type system, we can fetch for as many resources as we want.

This is where GraphQL comes in.......

Declare a type...

type Image{
    name: String
    url: String
    width: Int
    height: Int
}
Enter fullscreen mode Exit fullscreen mode

Make a request for it

fetchImage(name: "Bike") {
    url
  }
}
Enter fullscreen mode Exit fullscreen mode

## Get exactly what you want ##

{
  "image": {
    "url": "https://google.com"
  }
}
Enter fullscreen mode Exit fullscreen mode

Note here that we can specify other fields and receive back exactly what was been specified, as opposed to a fixed resource of REST API data of which we don't need most values all the time. That's one of the perks of using graphql.

Let's build our first Django GraphQL server...

Happy

If you don't know how to use pipenv , here's a great guide you can follow to get started with the basics.

Create project directory graphql and move into it.

mkdir graphql

cd graphql
Enter fullscreen mode Exit fullscreen mode

Install dependencies ....

pipenv install django graphene graphene-django
Enter fullscreen mode Exit fullscreen mode

Note: Running the above command generates two files Pipfile and Pipfile.lock, make sure installation runs without errors.

Once installation is done, check if virtual environment is already activated, else run pipenv shell to activate it.

Start a new Django project

django-admin startproject server
Enter fullscreen mode Exit fullscreen mode

Move into the django project and start the development server...

cd server

python manage.py runserver

Enter fullscreen mode Exit fullscreen mode
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).

You have 17 unapplied migration(s). Your project may not work 
properly until you apply the migrations for app(s): admin,
 auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.

January 21, 2020 - 03:40:47
Django version 3.0.2, using settings 'server.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
Enter fullscreen mode Exit fullscreen mode

Navigate to localhost:8000 on your browser, you should see the following

Django welcome page

Hit Ctrl-C to shutdown server.

Now in order for us to make requests to our graphql server, we must declare a schema file, this is how graphql knows what we are querying for or what we are trying to change.

Start a new application

python manage.py startapp car
Enter fullscreen mode Exit fullscreen mode

In the new app, create a new file called schema.py, and write the following code.

import graphene
from graphene import ObjectType, Schema

class Query(ObjectType):
    car = graphene.String()

    def resolve_car(self, info, root):
        return f"Mercedes Benz | Model:23qwer | Color: Black"

schema = Schema(query=Query)
Enter fullscreen mode Exit fullscreen mode

In the code snippet above, we defined a car field that returns a string when queried, note that for the query to work it has to be resolved through a function that takes in the data and gives an output.

Run python manage.py shell to open a python shell and type the following:


>>> from car.schema import schema

>>> query = "{ car }"
>>> result = schema.execute(query)
>>> print(result.data['car'])

>>> 'Mercedes Benz | Model:23qwer | Color: Black'
Enter fullscreen mode Exit fullscreen mode

This is a basic schema implementation that defines only one field car and returns a string when that field is queried on client side.

Alt Text

Create a schema.py file in server folder where settings.py is located and include the following:


from graphene import ObjectType, Schema

from car.schema import schema


class Query(schema.Query, ObjectType):
    pass


schema = Schema(query=Query)

Enter fullscreen mode Exit fullscreen mode

Edit settings.py


# Previous code here .....

GRAPHENE = {
    'SCHEMA': 'server.schema.schema',
    'MIDDLEWARE': (
        'graphene_django.debug.DjangoDebugMiddleware',
    )
}
Enter fullscreen mode Exit fullscreen mode

Add car and graphene_django to INSTALLED_APPS, then update urls.py in server folder

from django.contrib import admin
from django.urls import path

from graphene_django.views import GraphQLView

urlpatterns = [
    # Previous code here ....
    path('graphql', GraphQLView.as_view(graphiql=True)),
]
Enter fullscreen mode Exit fullscreen mode

In your console, migrate and then start the development server

python manage.py migrate

python manage.py runserver
Enter fullscreen mode Exit fullscreen mode

Navigate to localhost:8000/graphql, you'll be presented with a graphql IDE.

Let's make a query.

query{
    car
}
Enter fullscreen mode Exit fullscreen mode

Click on the play button, the server returns a response containing the exact string gotten earlier when we queried it with the shell.

{
  "data": {
    "car": "Mercedes Benz | Model:23qwer | Color: Black"
  }
}
Enter fullscreen mode Exit fullscreen mode

With this knowledge, let's build a basic car model in django, write queries and test it out in graphql IDE.

Edit models.py file in the car application

class Car(models.Model):
    name = models.CharField(max_length=40)
    model = models.CharField(max_length=40)
    price = models.IntegerField()
Enter fullscreen mode Exit fullscreen mode

Create a folder named fixtures in car application folder, add data.json to it and paste the following code snippet.

[
  {
    "model":"car.Car",
    "pk":1,
    "fields":{
        "name": "Tesla Motors",
        "model": "Model S",
    "price": 80000
    }
  },
  {
    "model":"car.Car",
    "pk":2,
    "fields":{
        "name": "Aston Martin",
        "model": "Vantage N430",
    "price": 90000
    }
  },
  {
    "model":"car.Car",
    "pk":3,
    "fields":{
        "name": "General Motors",
        "model": "Cadillac Escalade",
    "price": 79000
    }
  },
  {
    "model":"car.Car",
    "pk":3,
    "fields":{
        "name": "Toyota",
        "model": "Prius V",
    "price": 31700
    }
  }
]
Enter fullscreen mode Exit fullscreen mode

Migrate application

python manage.py makemigrations

python manage.py migrate

Load the fixtures into database

python manage.py loadfixtures data.json

Now, let's create a new field to fetch all cars from the database, edit your car application schema.py file

# Previous code here

from graphene_django import DjangoObjectType
from car.models import Car


class CarType(DjangoObjectType):
    class Meta:
        model = Car

class Query(ObjectType):
    # Previous fields here ...
    all_cars = graphene.List(CarType)

    # Previous resolver function
    def resolve_all_cars(self, info, **kwargs):
        return Car.objects.all() 
Enter fullscreen mode Exit fullscreen mode

Django comes with it's own ObjectType so displaying and resolving model fields would be made easier.
We could create our own ObjectType to mimic fields in django models, but constant updates to model fields require updating our defined ObjectType and that becomes a hassle in the long run.

Note we defined a field all_cars which returns a list of all cars in our database.
To view more scalar types we could use in our graphql application, go here.

Start your development server and navigate to localhost:8000/graphql

Let's query for all the cars in the database.

query{
  allCars{
    name
    model
    price
  }
}
Enter fullscreen mode Exit fullscreen mode
{
  "data": {
    "allCars": [
      {
        "name": "Tesla Motors",
        "model": "Model S",
        "price": 80000
      },
      {
        "name": "Aston Martin",
        "model": "Vantage N430",
        "price": 90000
      },
      {
        "name": "General Motors",
        "model": "Cadillac Escalade",
        "price": 79000
      },
      {
        "name": "Toyota",
        "model": "Prius V",
        "price": 31700
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Let's update our schema file, update car field and resolver method to return a single car type

class Query(ObjectType):
    car = graphene.Field(CarType, id=graphene.Int())
    all_cars = graphene.List(CarType)

    def resolve_car(self, info, **kwargs):
        id = kwargs.get("id")
        if id is not None:
            return Car.objects.get(id=id)

    def resolve_all_cars(self, info, **kwargs):
        return Car.objects.all()
Enter fullscreen mode Exit fullscreen mode

Now query for a car in the database.

query{
  car(id: 1){
    name
    model
    price
  }
}
Enter fullscreen mode Exit fullscreen mode

It gives you a single car object.

Query this same endpoint multiple times with different data types, it gives you exactly what you asked for..

That's the power of graphql

Mutation

Mutations in graphql are just like queries but they alter data.

Create an object, Update an object, Delete an object, all these are mutations we are making to our data.

Let's add a Mutation to add new Car type to our database

class CarInput(graphene.InputObjectType):
    name = graphene.String()
    model = graphene.String()
    price = graphene.Int()


class AddCar(graphene.Mutation):
    class Arguments:
        input = CarInput(required=True)

    car = graphene.Field(CarType)

    @staticmethod
    def mutate(root, info, input=None):
        car = Car(
            name=input.name, 
            model=input.model, 
            price=input.price)
        car.save()
        return AddCar(car=car)

class Mutation(graphene.Mutation):
    add_car = AddCar.Field()

schema = Schema(query=Query, mutation=Mutation)
Enter fullscreen mode Exit fullscreen mode

Update schema.py in project directory ...


class Mutation(schema.Mutation, graphene.ObjectType):
    pass


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

Enter fullscreen mode Exit fullscreen mode

Start development server, let's add a new car to our database.

mutation {
  addCar(input: {name: "Volkswagen", model: "RED23X", price: 50000}) {
    car {
      name
      model
      price
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

In the mutation specified above, you can see that the addCar field accepted just one argument, input as specified in the Arguments class subclassed in the AddCar Mutation Type.

If there are no errors, you would see the car you just created returned back as an object.

Now if we query for that same car, we should see the data returned back as an object.

query{
  car(id: 4){
    name
    model
    price
  }
}
Enter fullscreen mode Exit fullscreen mode

GraphQL is a very powerful query language for APIs, it's simplicity and the way you are able comprehend data in your API is off the charts.

You ask for a cat, you get exactly that, you ask for a dog with 2 tails, hell you get that too πŸ˜‚, you can send multiple queries in one query and still graphql finds a way to resolve your data and give you exactly what you asked for, no growing endpoints, no need for redundant data, no stress 😎.

REST is good, GraphQL is better 😏.

Graphql has it's own minor problems, but that's how it is with api patterns,feel free to explore and search for better ways to improve your data structure and fetching.

Thank you for staying till the end of this article, I really appreciate you taking out your time to go through this basic tutorial and I hope you are able to implement a basic GraphQL server on your own using django.

Alt Text

Resources

  1. Graphene Django

  2. GraphQL

Latest comments (8)

Collapse
 
artu_hnrq profile image
Arthur Henrique

Pretty nice intro of a Graphene-Django implementation of a GraphQL API
Thanks

Collapse
 
pastromhaug profile image
Per-Andre Stromhaug

Excellent writeup! I did a similar one for Django with Ariadne if anyone is interested: perandrestromhaug.com/posts/guide-...

Collapse
 
pydorax profile image
Ibaakee Ledum

Thanks, I loved the article...
Touched areas I couldn't cover in this post.

Collapse
 
tchi91 profile image
Fethi TChi

Very nice post, i enjoyed reading it, thanks πŸ‘Œ

Collapse
 
pydorax profile image
Ibaakee Ledum

Thanks...

Collapse
 
patryktech profile image
Patryk

Having tried both Graphene and Ariadne, I have decided to use Ariadne for all my GraphQL projects.

Graphene makes it easier to resolve data coming from your models (i.e. requires less code), but I don't like the massive multiple inheritance mess it is. If you ever want to extend anything, Ariadne is much easier. (And I am not the first person to say that).

Collapse
 
pydorax profile image
Ibaakee Ledum • Edited

Thanks a lot for the feedback, would definitely consider using ariadne in my next project... πŸ‘πŸ‘

Collapse
 
patryktech profile image
Patryk

Not saying you shouldn't use graphene, of course, but at least do try Ariadne and compare the two :)