This post is part of the FastAPI series.
This is another post related to FastAPI(indirectly) in which I am going to discuss how to use GraphQL based APIs to access and manipulate data. I already have discussed how you can make Rest API in the FastAPI framework in the previous post. We will be learning some basics of GraphQL and how graphene helps us to use Python for writing a GraphQL based application. So, let’s get started!
What is GraphQL
From the official website:
GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools
GraphQL was internally developed by Lee Byron and his team in 2012 and used in many internal projects including the Facebook mobile app and later it was released publicly in 2015.
The official website provides an excellent resource for learning different parts of GraphQL which you can refer to for details. Here, I will be more discussing the implementation in Python. Don’t worry, I will be sharing a few bits of the basics anyway.
Before I move forward, let’s set up our local dev environment. Like previous FastAPI posts, I will be using Pipenv
here as well. I am not going to repeat all steps but the installation of Graphene.
What is Graphene
From the official website:
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.
In nutshell, it helps you to set up GraphQL features easily.
GraphQL provides a playground for testing your GraphQL queries. It inputs the URL of the GraphQL server and lets you test queries. I will be setting up the playground first and then we will start coding our application. The application is the same: the contact management system.
import graphene from fastapi import FastAPI from starlette.graphql import GraphQLApp class Query(graphene.ObjectType): hello = graphene.String(name=graphene.String(default_value="stranger")) def resolve_hello(self,info,name): return "Hello " + name app = FastAPI(title='ContactQL', description='GraphQL Contact APIs', version='0.1') @app.get("/") async def root(): return {"message": "Contact Applications!"} app.add_route("/graphql", GraphQLApp(schema=graphene.Schema(query=Query)))
After importing graphene
I am import GraphQLApp
. GraphQL facility was actually provided by Starlette. Since FastAPI is using Starlette hence it is available for us automatically.
When I access http://localhost:8080/grapgql
it shows the following interface:
Don’t panic about what is written here. All I want to reveal that the fancy interface you are seeing above is available only because of the line app.add_route("/graphql", GraphQLApp(schema=graphene.Schema(query=Query)))
OK so now we have our playground ready for running different queries. It’s time to learn the basics of GraphQL queries.
Queries
The very first thing you need to know about GraphQL is querying a graphql server. You follow a certain syntax:
{ hello }
The thing you are seeing above is actually calling an object, in this case, hello
is an object. You pass a JSONish like query to graphql server and it returns:
{ "data": { "hello": "Hello stranger" } }
As you can see the graphql query is pretty much like the returned JSON.
query { hello }
You can explicitly pass the query
keyword with the query for better understanding.
Alright, the above query was executed in a fancy GraphQL interactive interface but life is not so fancy in real. In order for frontend apps to interact with your graphql server, you have to access it like you’d do with a typical REST API. Below is the cURL version of the above:
curl 'http://127.0.0.1:8000/graphql' \ -H 'Content-Type: application/json' \ --data-raw '{"query":"{hello}"}' \ --compressed
And in Postman it looks like:
Sounds cool, No? The GET
version opens the interactive client while POST
call actually is used for executing GraphQL queries. In production code, you might not like to provide the graphiql interface to your users. You can simply disable it by passing graphiql=False
in GraphQLApp
constructor. But I am keeping it as it for testing purposes.
Let’s start the real application. In the previous post, I made the necessary DB and model-related files so I am just using them here. I have copied the models
folder and database.py
file here. The current folder structure looks like the below:
OK, so now I am going to make a few changes in main.py
file.
import graphene from fastapi import FastAPI from starlette.graphql import GraphQLApp from models.contact import list_contacts class Contact(graphene.ObjectType): first_name = graphene.String() last_name = graphene.String() email = graphene.String() class QueryContact(graphene.ObjectType): contacts = graphene.List(Contact) @staticmethod def resolve_contacts(self, info): records = [] contacts = list_contacts() for c in contacts: records.append({'first_name': c.first_name, 'last_name': c.last_name, 'email': c.email}) return records
After importing all necessary stuff I am creating a class Contact
of type graphene.ObjectType
. The reason for doing this is that we have to expose certain fields of contacts. Since the purpose is to show the list of contacts which we then pass to graphene.List
that accepts a class type. You can learn more about it here.
After creating the class I am creating a field contacts
in QueryContact
class which is of type List
. You can pass any type in it. In our case, it is of Contact
type.
The code in the resolver method is self-explanatory. It is fetching records from contacts
table and appending in records
list table
here, the root object is contacts
. In case you wonder why is it showing contacts. It is because we defined a field with the name in QueryContact
class of type List
. All the sub-fields are being fetched from the class of type Contact
which itself is of ObjectType
.
So whatever the field(make sure they are part of the system) you pass they’d be visible here. Below are the cURL and Python requests version of executing the above, just in case one needs to integrate graphql in a system.
The above one was returning all items but what if I want to return a single item or items that match certain criteria? Graphql gives you an option to pass arguments in your queries. This is you do it.
{ contacts(id:1) { firstName email } }
As you see I am passing a parameter, id
here to filter out results. The code will now look like this:
class QueryContact(graphene.ObjectType): contacts = graphene.List(Contact, id=graphene.Int()) @staticmethod def resolve_contacts(self, info, id=0): records = [] result = None if id == 0: contacts = list_contacts() for c in contacts: records.append({'first_name': c.first_name, 'last_name': c.last_name, 'email': c.email}) result = records elif id > 0: contact = get_contact(id) if contact is not None: result = [{'first_name': contact.first_name, 'last_name': contact.last_name, 'email': contact.email}] else: result = [] return result
the contacts
field has passed an argument id
of type graphene,Int()
. Once done, we’d be passing a default id
parameter in resolve_contacts
method. The rest of the code should be clear. I am checking if an id
is being passed then call get_contact
otherwise list_contacts()
. Since we are already using contacts
of type list so even a single record returns it’d be wrapped in a list
.
Cool, isn’t it? If you want to map these queries with RDBMS’ SQL queries so it’d be like: SELECT first_name,email from contacts
and for a single query, it’d be SELECT first_name,email from contacts where id = 2
.
Mutation
So far we are pulling the data, what if we want to change it, mutation allows you to manipulate data. The mutation query syntax looks like the below:
mutation { createContact(firstName:"Musab",lastName:"Siddiqi",email:"musab@gmail.com",phone:"123-0393") { id firstName } }
Let’s write the code!
from models.contact import list_contacts, get_contact, create_contact class CreateContact(graphene.Mutation): # These fields will be displayed after successful insert id = graphene.Int() first_name = graphene.String() class Arguments: first_name = graphene.String() last_name = graphene.String() email = graphene.String() phone = graphene.String() def mutate(self, info, first_name, last_name, email, phone): new_contact = create_contact(first_name=first_name, last_name=last_name, email=email, phone=phone, status=1) if new_contact is None: raise Exception('Contact with the given email already exists') return CreateContact(id=new_contact.id, first_name=new_contact.first_name) class Mutation(graphene.ObjectType): create_contact = CreateContact.Field() app.add_route("/graphql", GraphQLApp(graphiql=True, schema=graphene.Schema(query=QueryContact, mutation=Mutation)))
So I created a CreateContact
class that inherited a class Mutation
.
First I defined a couple of fields. These fields actually show the data after the data insertion or deletion/updation(Yes, the mutation is available for any kind of data manipulation). Then, Arguments
class is used to pass arguments, these arguments contain the pieces of info which will be inserted. The mutate
method actually is similar to the resolve_ methods. The return of this method is the object of the class CreateContact
. We are doing this because we have to show those fields in return. Then, I am creating a class inherited ObjectType
. The class contains the field create_contact
which is of type CreateContact
. Finally, graphene.Schema
will be passed another parameter that sets the Mutation
class. The resultant of the code will be the below:
First, it creates a new record. When you try to insert the same record w.r.t email it raises an exception, cool, No?
Conclusion
In this post, you learned how you can create graphql based queries for your application. I have only discussed the basics of it. In case you are interested to learn in-depth, you should explore the documentation of GraphQL and graphene. Like always, the code is available on Github.
Have some interesting idea that you would like to be done in FastAPI or otherwise? Let’s discuss at kadnan at gmail dot com