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
- pipenv
- django 2.0 or greater
- graphene
- 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)
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"))
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)
]
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
}
Make a request for it
fetchImage(name: "Bike") {
url
}
}
## Get exactly what you want ##
{
"image": {
"url": "https://google.com"
}
}
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...
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
Install dependencies ....
pipenv install django graphene graphene-django
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
Move into the django project and start the development server...
cd server
python manage.py runserver
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.
Navigate to localhost:8000
on your browser, you should see the following
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
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)
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'
This is a basic schema implementation that defines only one field car
and returns a string when that field is queried on client side.
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)
Edit settings.py
# Previous code here .....
GRAPHENE = {
'SCHEMA': 'server.schema.schema',
'MIDDLEWARE': (
'graphene_django.debug.DjangoDebugMiddleware',
)
}
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)),
]
In your console, migrate and then start the development server
python manage.py migrate
python manage.py runserver
Navigate to localhost:8000/graphql
, you'll be presented with a graphql IDE.
Let's make a query.
query{
car
}
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"
}
}
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()
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
}
}
]
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()
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
}
}
{
"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
}
]
}
}
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()
Now query for a car in the database.
query{
car(id: 1){
name
model
price
}
}
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)
Update schema.py in project directory ...
class Mutation(schema.Mutation, graphene.ObjectType):
pass
schema = graphene.Schema(query=Query, mutation=Mutation)
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
}
}
}
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
}
}
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.
Top comments (8)
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).
Thanks a lot for the feedback, would definitely consider using ariadne in my next project... ๐๐
Not saying you shouldn't use graphene, of course, but at least do try Ariadne and compare the two :)
Very nice post, i enjoyed reading it, thanks ๐
Thanks...
Excellent writeup! I did a similar one for Django with Ariadne if anyone is interested: perandrestromhaug.com/posts/guide-...
Thanks, I loved the article...
Touched areas I couldn't cover in this post.
Pretty nice intro of a Graphene-Django implementation of a GraphQL API
Thanks