As a front-end developer and a long-time Jamstacker, I’ve had plenty of time to get frustrated with the way we use apis. The REST protocol seems like a step in the right direction (it is), but I still grumble maliciously about its limitations, despite improvements.
So when I heard GraphQL, I was shocked.
The idea is simple: the API itself defines the kind of data it understands and exposes a single endpoint to the user. The user then provides the endpoint with a query that looks like JSON, without all the pesky values, quotes, and commas.
The API returns a JSON version of the query with all the data you asked for. It’s an incredibly simple idea, but it solves almost every problem I’ve had with the API.
What is an Ariadne?
Normally, the GraphQL APIs are created in JavaScript, but my first love is Python, which is why I looked at Ariadne. Ariadne is a Python library that helps you create the GraphQL API without the extra burden.
In this article, I’ll document making the Ariadne GraphQL API in Python 3.8, which gives users access to a simple array /dictionary structure.
Start using Ariadne
I’m going to assume that you have Python set up on your computer and that you have installed Ariadne using pip3 install Ariadne.
But I want to give you a little tip here: stick with a single data source (such as a database, a layer of business logic, or a Python dict). When I first heard about GraphQL, my first thought was that I could use it to merge all the other apis I was using into one endpoint — THAT I could get rid of all the inconsistencies between the REST and SOAP apis and get all the data and functionality I needed without any difficulty.
It’s possible, but developing it on your own is a big problem. This concept is called API grid, and it was pioneered by the guys at TakeShape. If you’re interested in learning more about TakeShape, feel free to check out their new documentation page, but for the sake of simplicity, I’ll insist on exposing a single data source here.
How does Ariadne work
Now, let’s see how Ariadne works. You can follow their quick start guide, but I’m going to simplify it. It looks like this.
First, you define a type using GraphQL’s special schema definition language. It’s similar to the TypeScript interface, where you define the keys of an object and the type of the value of each key.
Every application in Ariadne needs a type called Query, because it compares to the program’s input, so we’ll do that now. It’s going to look something like this.
type Query {
hello: String!
}
Copy the code
This is a very basic definition. In short, we define a type called Query. It has a key, it’s called hello, and it’s always going to be a string. Here’s a bonus: at the end of the line! This means that if the object matches this type, hello will always be in the object. If you omit the exclamation point, then hello will be optional.
Now, in our Python file (which I’ll call endpoint-py), we’ll stick this type definition into a string and pass it to Ariadne’s GQL function. So far, our file looks like this.
from ariadne import gql
typedefs = """
type Query {
hello: String!
}
"""
typedefs = gql(type_defs)
Copy the code
This will validate our type definition and throw an error if we write it incorrectly.
Next, Ariadne wants us to create an instance of the ObjectType class and pass in the name of our type. In short, this will be the Python representation of the type we’re going to do.
We’ll also add some templates at the end and move our type definitions there. Now endpoint-py looks like this.
from ariadne import ObjectType, gql, make_executable_schema
from ariadne.asgi import GraphQL
basetype = ObjectType("Query") # there is a shortcut for this, but explicit is better than implicit
type_defs = """
type Query {
hello: String!
}
"""
app = GraphQL(
make_executable_schema(
gql(type_defs),
basetype
),
debug=True
)
Copy the code
The main purpose of Ariadne is to scan the input query and, for each key, run a parser function to get the value of that key. It does this through decorators, which is a cool Pythonic way to hand over your functions to Ariadne without needing more templates. This is our endpoint-py, which has a parser function for our hello key.
from ariadne import ObjectType, gql, make
Copy the code
That’s pretty much it. Ariadne has many fascinating and useful features (seriously, scroll through their documentation), but that’s all you need to get started and understand how it works. If you’re interested in testing this, but it needs to be on a server.
You can use Uvicorn to temporarily turn your local machine into a server. In short, you will use PIP install uvicorn, CD to install your endpoint.py is, and run uvicorn endpoint:app. Then, go to 127.0.0.1:8000, where you’ll see Ariadne’s GraphQL interface. It looks cool.
There’s just one caveat: the introductory document page I’m loosely following here makes a good point halfway through. Real-world parsers are rarely that simple: they usually read data from a source (such as a database), process input, or parse values in the context of the parent object (sic).
Translated into simple English?” Our API does absolutely nothing useful. You give it a query, it says Hello World! “Is neither interesting nor helpful. The parser function we create needs to take input, get data from somewhere, and return a result in order to be valuable.”
Ok, now that we have our template, let’s try to make the API worthwhile by accessing a rudimentary database of Python arrays and dictionaries.
Build a sample GraphQL API
HMM… What should we build? Here’s what I think.
- The query you enter should take the name of one of my favorite sitcoms.
- The query will return one
Sitcom
Type, which should have a name (which will be a string),number_of_seasons
(Int) and a character (an array of characters). - Character types will have
first_name
.last_name
, andactor_name
Field, all strings.
That sounds doable! We only have two types (Sitcoms and Character), and the data we expose can easily be stored in a Python dictionary structure. Here is the dictionary I will use.
characters = {
"jeff-winger": {
"first_name": "Jeffrey",
"last_name": "Winger",
"actor_name": "Joel McHale"
},
"michael-scott": {
"first_name": "Michael",
"last_name": "Scott",
"actor_name": "Steve Carell"
},
...
}
sitcoms = {
"office": {
"name": "The Office (US)",
"number_of_seasons": 9, # but let's be real, 7
"characters": [
"michael-scott",
"jim-halpert",
"pam-beesly",
"dwight-schrute",
...
]
},
"community": {
"name": "Community",
"number_of_seasons": 6, #sixseasonsandamovie
"characters": [
"jeff-winger",
"britta-perry",
"abed-nadir",
"ben-chang",
...
]
},
...
}
Copy the code
We will define our type, just as we did earlier with our Query type. Let’s try this.
query = ObjectType("Query") sitcom = ObjectType("Sitcom") character = ObjectType("Character") type_defs = """ type Query { result(name: String!) : Sitcom } type Sitcom { name: String! number_of_seasons: Int! characters: [Character!] ! } type Character { first_name: String! last_name: String! actor_name: String! } """ app = GraphQL( make_executable_schema( gql(type_defs), query, sitcom, character ), debug=True )Copy the code
In parentheses is the query type, which is a parameter. We pass a name (which will always be a string) on the Result key of the query type, which will be sent to our parser. I’ll talk more about that in a moment.
If you want to know [Character!] ! This simply means that the array is required, as well as the characters in the array. In practice, arrays must exist, and they must have characters in them.
In addition, in the final template, we passed all three types to the make_executable_schema function. This tells Ariadne that it can start using them. In fact, we can add as many types there as we want.
So, here’s how this is going to work. The client will send a request that looks like this.
<code>{
result(name:"community")
}</code>
Copy the code
The server will accept this request, send “Community” to the parser of the result field, and not just return any melodrama, but the correct melodrama. Now let’s set up these parsers.
Here is our complete endpoint-py.
from ariadne import ObjectType, gql, make_executable_schema from ariadne.asgi import GraphQL import json with open('sitcoms.json') as sitcom_file: sitcom_list = json.loads(sitcom_file.read()) with open('characters.json') as character_file: character_list = json.loads(character_file.read()) query = ObjectType("Query") sitcom = ObjectType("Sitcom") character = ObjectType("Character") type_defs = """ type Query { result(name: String!) : Sitcom } type Sitcom { name: String! number_of_seasons: Int! characters: [Character!] ! } type Character { first_name: String! last_name: String! actor_name: String! } """ @query.field("result") def getSitcom(*_, name): return sitcom_list[name] if name in sitcom_list else None @sitcom.field("characters") def getCharacters(sitcom, _): characters = [] for name in sitcom["characters"]: characters.append(character_list[name] if name in character_list else None) return characters app = GraphQL( make_executable_schema( gql(type_defs), query, sitcom, character ), debug=True )Copy the code
That’s the whole procedure! We are using the data in the JSON file to fill in the response to the GraphQL query we entered.
Other benefits of using Ariadne
Although we don’t have to finish it! Here are some ideas I’ve come up with about what to do next.
We’re just using a rudimentary JSON data storage structure, which is bad, but makes sense for a sample application like this. For something larger than this toy application, we want to use a more robust data source, such as a suitable database.
We could have a MySQL database with a table for sitcoms and a table for characters, and get that data in a parser function. In addition, the query itself is only half of what we can do with GraphQL and Ariadne. Mutations are the other half. They allow you to update existing records, add new records, or possibly delete rows. Setting these up in Ariadne is fairly easy.
Of course, creating an API to track sitcoms and characters is a bit pointless, but it’s an interesting experiment. All of this could be used more effectively if we built such a GraphQL service around more useful data. Let’s say you’re running an existing REST API– why not provide this data with GraphQL?
Finally, when we create a GraphQL API, it’s tempting to try to pull data from our own database and merge data from external sources, such as some third-party API. You can do this by making requests to these external apis in the parser over HTTP, but this will greatly reduce your program and leave you to worry about edge cases and error handling.
Trust me, it’s more trouble than it’s worth. However, to take this project a step further, you can have your Ariadne application pull data from your internal database, plug the API you just created into an API grid (such as TakeShape), and then combine it with some other third party services.
That way, everything that’s hard to merge is the grid’s problem, not yours. I’ve done this several times, and it’s comforting to see.
conclusion
There’s not much on that. I try to explain as many details as possible in case you want to explore any of them further, but the technique is fairly simple.