GraphQL — Another flavour in a world of APIs

Having multiple URLs to manage resources or sending multiple requests to retrieve some data, as well as maintain your documentation updated could be a characteristic you don’t like from a REST API and a huge challenge within your team. Probably, there already exists a framework that can help you with those issues. Let’s know GraphQL

If you are interested in other frameworks and tools you could use for your APIs you can check my other posts related to gRPC and RESTful…

GraphQL

GraphQL provides the query language and the runtime required to fulfil your request with your data and also provides a full description of the data you could access behind your API server. The users can ask for the exact information they need and they can retrieve multiple information in only one request. In this way, it’s easier (or should be) for you to upgrade your APIs over time.

Supported Languages

There are many tools and libraries (servers and clients) to help you get started with GraphQL in all sorts of languages. You can check the list here.

Schema

It’s the core of any GraphQL server implementation because it describes the functionalities available to the client applications that connect to it. The schema is a description of the data that clients can request and also defines the queries and mutations used to read and write data.

Resolver

A resolver is a function that is executed to return the data for a single attribute of a type/object.

Operations in GraphQL

There are three types of operations that GraphQL models:

  • querya read-only fetch.
  • mutation an operation followed by a fetch.
  • subscriptionis a long-lived request that fetches data in response to source events.

Queries

A query is a component (simple string) that the GraphQL server can parse and respond to with data. There, you define the schemas and fields you want to retrieve. Below, a simple example.

query {
health
}

Mutations

The mutations are used to modify the server-side data. If queries are the GraphQL equivalent to GET calls in REST, then mutations represent the state-changing methods in REST (DELETE, PUT, REMOVE).

// Example of a mutation to create a person...
mutation {
add_person (fullName: "...", age: 25) {
person {
uuid
fullName
age
}
}
}

If the mutation field returns an object type, you can ask for nested fields. This can be useful for fetching the new state of an object after an update or another action.

Subscriptions

Like queries, subscriptions allows fetching data. They can maintain an active connection to the server, enabling the server to push updates to the subscription’s result. They could be useful for notifications in real-time about changes to back-end data.

Introspection

The GraphQL service supports introspection over its schemas. The types and fields required by the GraphQL introspection system that is used in the same context as user-defined types and fields are prefixed with “__”. This is in order to avoid naming collisions with user-defined types. That’s why any Name within a GraphQL type system must not start with two underscores “__”.

Imagine you don’t know the service and want to know the available schemas and the mutations you can execute…

{
__schema {
queryType {
name
fields {
name
description
}
}
mutationType {
name
description
fields {
name
description
}
}
}
}

Getting…

{
"data": {
"__schema": {
"queryType": {
"name": "Query",
"fields": [
{
"name": "health",
"description": null
},
{
"name": "ticket",
"description": null
},
...
]
},
"mutationType": {
"name": "Mutations",
"description": null,
"fields": [
{
"name": "create_ticket",
"description": "It creates a new Ticket..."
},
...
]
}
}
}
}

Or you can inspect the fields inside a specific data type.

{
__type(name: "Ticket") {
name
fields {
name
description
type {
name
}
}
}
}

Authentication | Authorization

In this topic, you could implement your own mechanism of authentication and authorization. Maybe using JSON Web Tokens to achieve the authorization mechanism through a middleware or you can rely on some existing libraries, like:

GraphQL over REST

There are some characteristics for which people usually claim that GraphQL is a better alternative to a RESTful API.

  • Standardization: Frequently developers don’t find the way to follow the REST principles and create very bad RESTful APIs (Don’t blame REST for this). In the GraphQL environment, the specification is defined.
  • Development Environment: GraphQL provides query validation, code completion as well as documentation. But, take into consideration that the documentation level depends on the Backend, everything is not by magic.
  • Retrieve only what you want: You are able to request/retrieve the exact information you need. Taking advantage of the documentation, validation and code completion, you can get the fields and attributes (You could implement the same result in REST) and know the type of those elements.
  • Lower Network Overhead: You can get everything you want using only one request.
  • One version: It’s append/update only and you don’t need to manage new versions.
  • One Access Point: You have only one URL to manage all your resources and actions.

Graphene

Graphene-Python is a library for building GraphQL APIs in Python easily, its main goal is to provide a simple but extendable API for making developers’ lives easier

Graphene has integrations with multiple popular frameworks like:

A Dummy Project — with Graphene

Let’s create an API that should allow us to manage people (create, list and remove) as well as manage a ticket (returning the expected termination day depending on the story points).

Below is the project structure…

graphql-python-example
├── README.md
├── requirements.txt
├── server.py
├── service
│ ├── controllers.py
│ ├── __init__.py
│ ├── models.py
│ ├── mutations.py
│ └── schema.py
└── test.py

As usual, we need a virtual environment as well as some external libraries the project requires…

virtualenv --python=python3.8 .venv
source .venv/bin/activate
pip install --upgrade pip
pip install urllib3 graphene flask_graphql flask

First, let’s create the server using Flask and Flask-GraphQL which gives us the possibility to have GraphQL running on top of the Flask service.

# server.py
# -*- coding: utf-8 -*-

from flask import Flask
from flask_graphql import GraphQLView

from service.schema import schema

app = Flask(__name__)


@app.route("/")
def health():
return "OK"


app.add_url_rule(
"/graphql",
view_func=GraphQLView.as_view(
"graphql",
schema=schema,
graphiql=True)
)


if __name__ == "__main__":
app.run(port=3001)

Graphene will be used to create the models, the mutations and the schema required for the service…

# models.py
# -*- coding: utf-8 -*-

from graphene import String, Int, ObjectType


class Ticket(ObjectType):
name = String()
description = String()
story_points = Int()


class Person(ObjectType):
uuid = String()
full_name = String()
age = Int()

The mutations…

# mutations.py
# -*- coding: utf-8 -*-

from datetime import datetime, timedelta
from graphene import Mutation, String, Int, Field
from service.models import Person

from .controllers import controller


class CreateTicket(Mutation):
class Input:
name = String()
description = String()
story_points = Int()

expected_dateline = String()

@staticmethod
def mutate(root, info, name, description, story_points):
expected_dateline = datetime.utcnow() + timedelta(days=story_points)
return CreateTicket(expected_dateline.strftime("%Y-%m-%d %H:%M:%S"))


class AddPerson(Mutation):
class Input:
full_name = String()
age = Int()

person = Field(lambda: Person)

@staticmethod
def mutate(root, info, full_name, age):
return AddPerson(controller.add_person(full_name=full_name, age=age))


class RemovePerson(Mutation):
class Input:
uid = String()

person = Field(lambda: Person)

@staticmethod
def mutate(root, info, uid):
return RemovePerson(controller.remove_person(uid))

The data layer (a dummy and a very simple one)…

# controllers.py
# -*- coding: utf-8 -*-

from random import randint
from typing import Dict
from uuid import uuid4

from service.models import Person


class DataController:
people: Dict[str, Person] = {}

def __init__(self):
for x in range(10):
uuid_ = str(uuid4())
self.people[uuid_] = Person(
uuid=uuid_,
full_name=f"some_name_{x}",
age=randint(25, 50)
)

def add_person(self, full_name: str, age: int):
uuid_ = str(uuid4())

record = Person(
uuid=uuid_,
full_name=full_name,
age=age
)

self.people[uuid_] = record
return record

def get_by_id(self, uuid_: str):
return self.people.get(uuid_, None)

def get_all(self, limit: int = 10, offset: int = 0):
data = list(self.people.values())
end, size = limit + offset, len(data)
return data[offset:end if end < size else size]

def remove_person(self, uuid_: str):
return self.people.pop(uuid_, None)


controller = DataController()

The Schema…

# schema.py
# -*- coding: utf-8 -*-

from graphene import Field, ObjectType, Schema, String, List, Int

from service.controllers import controller
from service.models import Ticket, Person
from service.mutations import CreateTicket, AddPerson, RemovePerson


class Query(ObjectType):
health = String()

@staticmethod
def resolve_health(root, info):
return f"OK"

ticket = Field(Ticket, name=String())

@staticmethod
def resolve_ticket(root, info, name):
return Ticket(
name="SomeName",
description="...",
story_points=3
)

person = Field(Person, uid=String())

@staticmethod
def resolve_person(root, info, uid):
return controller.get_by_id(uid)

people = List(Person, limit=Int(), offset=Int())

@staticmethod
def resolve_people(root, info, limit: int = 10, offset: int = 0):
return controller.get_all(limit, offset)


class Mutations(ObjectType):
create_ticket = CreateTicket.Field(
name="create_ticket",
description="It creates a new Ticket...")

add_person = AddPerson.Field(
name="add_person",
description="It adds a person to the system...")

remove_person = RemovePerson.Field(
name="remove_person",
description="It remove the person from the database...")


schema = Schema(query=Query, mutation=Mutations)

As you can notice there is a lot of code in the project. Let’s create and execute some queries and mutations to test the service.

http://localhost:3001/graphql

Performance Comparison

In a previous post, were compared some different frameworks in terms of speed, the results were:

FastAPI                 Flask                   gRPC
2.5365726947784424 3.588320016860962 0.6878361701965332
2.4663445949554443 3.4863481521606445 0.7431104183197021
2.8214125633239746 3.522181749343872 0.719865083694458

Let’s check the performance of the GraphQL server running on top of Flask by sending 2000 (same number of the previous test) requests. Below, the code used to send the request…

# test.py
# -*- coding: utf-8 -*-

import json
from time import time

import urllib3

http = urllib3.PoolManager()


def main():
query = {
"query": """mutation {
create_ticket (name: \"T-0001\", description: \"...\", storyPoints: 5) {
expectedDateline
}
}"""
}

start = time()

for _ in range(2000):
http.request(
"POST",
"http://localhost:3001/graphql",
headers={"Content-Type": "application/json"},
body=json.dumps(query)
)

print(time() - start)


if __name__ == "__main__":
main()

Below, the results and as you can notice the execution time is almost duplicated…

6.15591287612915
6.1439385414123535
5.966768741607666

Conclusions

GraphQL is without any doubt a great tool, but if you ask me, I still prefer a RESTful API service. With GraphQL you need to write about the double of code and the performance is not good compared with REST and gRPC. But, there are some benefits like auto-documentation and a single entry point as well as the possibility to retrieve multiple information in only one request.

But as I always say, focusing on the requirements is the best way to choose the tool you need for your current use case…

Well, that’s all for today, hoping this information may be useful for some of you when you face the challenge of selecting the engine for your next API service. This post put an end to a series of three posts related to API frameworks…

Code repository: https://github.com/alekglez/graphql-python-example.git

Cloud & Solutions & Data Architect | Python Developer | Serverless Advocate