FastAPI is a high-performance framework for building web APIs with Python. Its simple and intuitive nature makes it easy to quickly develop robust web APIs using very little boilerplate code. In this article, we’ll introduce FastAPI and how to set up a GraphQL server with it.
From the official docs, building web applications with FastAPI reduces about 40 percent of developer-induced errors, and this is made possible through the use of Python 3.6 type declarations. With all its features, including the automatic generation of interactive API documentation, building web apps with Python has never been easier.
Before we get started, let’s confirm that we have Python 3.6+ installed by running the following command on our terminal:
python --version
If this returns an error, click here to download and install Python on your local machine. The Python 3.4+ installation comes with pip by default, however, we’ll need Python 3.6+ to be able to run FastAPI. Pip is the preferred Python package manager and is what we’ll be using to install the supporting modules for our FasAPI app.
With Python and pip installed, let’s add FastAPI to our machine by running the following command on our terminal:
pip install fastapi
We’ll also need Uvicorn, an ASGI (Asynchronous Server Gateway Interface) server to serve our app. Let’s run the following command on our terminal to install it:
pip install uvicorn
With that done, we can go ahead and create a new directory for our app. Let’s name it fastapi-graphql
. Inside our new directory, we’ll create a new file named main.py
. This will be the index file for our server.
In GraphQL, queries are used to fetch data, just like GET requests in the REST API architecture. However, with GraphQL queries, we have the choice of requesting exactly what we want. For example, let’s assume that we have an API for educational courses. A query to our API will look like this:
{ getCourse { id title instructor publishDate } }
When we send this query, we should receive a response with courses from our API and their properties, in this order: id
, title
, instructor
, and publishDate
:
{ "data": { "getCourse": [ { "id": "1", "title": "Python variables explained", "instructor": "Tracy Williams", "publishDate": "12th May 2020" }, { "id": "2", "title": "How to use functions in Python", "instructor": "Jane Black", "publishDate": "9th April 2018" } ] } }
Optionally, we can ask our API to return a list of courses, but this time with just the title
property:
{ getCourse { title } }
We should get a response that looks like this:
{ "data": { "getCourse": [ { "title": "Python variables explained" }, { "title": "How to use functions in Python" } ] } }
This flexibility is one of the things that make applications built with GraphQL highly extensible, and it is made possible through type declaration in GraphQL.
The schema describes our GraphQL service, what data it contains, and the format for that data. From our query, we’ve seen that we can specify what data will be sent to us and how we want that data presented. This is because our GraphQL API already contains a schema for all the data and that schema includes every available field, subfield, and its data type.
To demonstrate this, we’ll create a schemas.py
file in our root directory, which we’ll use to house all the data fields. Let’s start with the course type. This should contain all the information for a particular course, and from our example query above, a course includes the fields id
, title
, instructor
, and publish_date
. We’ll go ahead and update our schemas.py
file with this:
from graphene import String, ObjectType class CourseType(ObjectType): id = String(required=True) title = String(required=True) instructor = String(required=True) publish_date = String()
In our schemas.py
file, we started with importing the types String
and ObjectType
from graphene
. Graphene is a Python library for building GraphQL schemas and types. Let’s run the following command on our terminal to install it:
pip install graphene
After we successfully imported the String
and ObjectType
from graphene
, we went on to define a class CourseType
with the imported ObjectType
in parentheses. We’ll declare almost all our GraphQL types as object types.
The next thing we did was create the different fields for our CourseType
, and we used the String
type for each field. Some other types from graphene
include Int
, Enum
, Date
, List
, and Boolean
.
Notice that we also added a required
argument in the String
type for id
, title
, and instructor
. This means that we won’t be able to add a course to our GraphQL service without including those fields, although we can still exclude any of them when making queries.
Now that we have our schema, we’ll also need a place to store and retrieve our course data. for this demo, we’ll use a JSON database. However, FastAPI supports both relational and non-relational databases like PostgreSQL, MySQL, MongoDB, ElasticSearch, etc.
Let’s create a courses.json
file in our root directory and paste the following block of code in it:
[ { "id": "1", "title": "Python variables explained", "instructor": "Tracy Williams", "publish_date": "12th May 2020" }, { "id": "2", "title": "How to use functions in Python", "instructor": "Jane Black", "publish_date": "9th April 2018" }, { "id": "3", "title": "Asynchronous Python", "instructor": "Matthew Rivers", "publish_date": "10th July 2020" }, { "id": "4", "title": "Build a REST API", "instructor": "Babatunde Mayowa", "publish_date": "3rd March 2016" } ]
We’ll be able to use our GraphQL API to modify and retrieve data from this file.
Resolvers are what our GraphQL service will use to interact with our schema and data source. To create a query resolver for fetching courses, we’ll start by importing fastapi
in our main.py
file:
from fastapi import FastAPI
Next, let’s import the relevant types that we’ll be using in our query resolver just like we did in the schemas.py
file:
... from graphene import ObjectType, List, String, Schema
Notice that we’ve added two new types: List
, which we’ll be using as a wrapper for our CourseType
; and Schema
, which we’ll use to execute the operation.
We’ll also import the AsyncioExecutor
from graphql.execution.executors.asyncio
, which will enable us to make asynchronous calls in our GraphQL service; GraphQLApp
from starlette.graphql
; and the CourseType
from our schemas.py
file:
... from graphql.execution.executors.asyncio import AsyncioExecutor from starlette.graphql import GraphQLApp from schemas import CourseType
Finally, let’s import the built-in json
package for working with our courses.json
file:
... import json
Our final import block should look like this:
from fastapi import FastAPI from graphene import ObjectType, List, String, Schema from graphql.execution.executors.asyncio import AsyncioExecutor from starlette.graphql import GraphQLApp from schemas import CourseType import json
Next, let’s create our query by adding the following block of code to our main.py
file:
... class Query(ObjectType): course_list = None get_course = List(CourseType) async def resolve_get_course(self, info): with open("./courses.json") as courses: course_list = json.load(courses) return course_list
In the above block, we started with creating a class Query
from the object type, then we initialized our course_list
variable on line 3
. This is where we’ll store the course data.
Since we’ll be returning the different course objects in a list, we’ve used the List
type we imported from graphene
to wrap our CourseType
and then assigned it to a get_course
variable on line 4
. This will be the name of our query.
It’s important to note that when making the query in a GraphQL client, we’ll need to provide the name in camel case, i.e., getCourse
instead of get_course
.
Next, we created a resolver method on line 5
for the get_course
query. The name of the resolver method should start with the prefix resolve
followed by the query name — in this case, get_course
— and then separated with an underscore.
The resolver method also expects two positional arguments, which we’ve included in the method definition. On lines
6-8
, we load the data from our courses.json
file, assign it to course_list
, and then return the variable.
Now that we’ve created our GraphQL query, let’s initialize FastAPI and assign our GraphQL service to the index route.
... app = FastAPI() app.add_route("/", GraphQLApp( schema=Schema(query=Query), executor_class=AsyncioExecutor) )
Next, let’s run the following command on our terminal to start our FastAPI app:
uvicorn main:app --reload
We should get a success message similar to this:
Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) Started reloader process Started server process Waiting for application startup. Application startup complete.
We can go ahead and test our GraphQL server by navigating to http://127.0.0.1:8000
. We should see a page that looks like this:
Let’s paste the following query on the left pane and make our API call by clicking on the run button:
{ getCourse { id title instructor publishDate } }
We should get a response that looks like this:
Let’s explore how we can make a query to get the data for only one course. We’ll add an id
param to our query to let our GraphQL server know that we want just the course that matches that id
. To make this work, let’s replace our Query
class with the code below:
class Query(ObjectType): course_list = None get_course = Field(List(CourseType), id=String()) async def resolve_get_course(self, info, id=None): with open("./courses.json") as courses: course_list = json.load(courses) if (id): for course in course_list: if course['id'] == id: return [course] return course_list
On line 3
of our new Query
class, we wrapped the value of get_course
in a Field
type, which allows us to add our query parameters. Here, we added an id
param of type String
. We also included the id
parameter in the resolve_get_course()
method and gave it a default value of None
to make it optional.
On lines
7-9
, we added a conditional that will return only the course that matches an id
if provided. We’ll also need to add the Field
type to our graphene
imports before we move on:
from graphene import ObjectType, List, String, Schema, Field
We can now go ahead and fetch only one course that matches an id
with the following query:
{ getCourse(id: "2") { id title instructor publishDate } }
We should get this as our response:
{ "data": { "getCourse": [ { "id": "2", "title": "How to use functions in Python", "instructor": "Jane Black", "publishDate": "9th April 2018" } ] } }
We’ve seen how to set up our GraphQL server with FastAPI and fetch data from it. Now let’s see how we can use GraphQL mutations to add new courses to our data store or update existing courses. Let’s start by adding the Mutation
type to our graphene
imports:
from graphene import ObjectType, List, String, Schema, Field, Mutation
Now we can create a class named CreateCourse
from the Mutation
type:
class CreateCourse(Mutation): course = Field(CourseType) class Arguments: id = String(required=True) title = String(required=True) instructor = String(required=True)
In our CreateCourse
class, we started with creating a variable for our course, which we wrapped in the Field
class. This is what we’ll be returning to the user on successful creation of the course.
We then went on to create a class for our mutation arguments. Our arguments here are id
, title
, and instructor
. We’ll need to provide this information when making our CreateCourse
mutation.
Next, we’ll need a mutate
method for creating the courses. This is where we’ll use the arguments supplied by the user to create a new course in our data store. In this case, we’ll be mutating our courses.json
file. However, in a production app, you’d probably need a database instead, and as mentioned earlier, FastAPI has support for both relational and non-relational databases.
Let’s create our mutate
method. Note that it has to be named mutate
, as this is what our GraphQL service expects:
class CreateCourse(Mutation): ... async def mutate(self, info, id, title, instructor): with open("./courses.json", "r+") as courses: course_list = json.load(courses) course_list.append({"id": id, "title": title, "instructor": instructor}) courses.seek(0) json.dump(course_list, courses, indent=2) return CreateCourse(course=course_list[-1])
In our mutate
method’s return statement, we called the CreateCourse
class and, in parentheses, assigned the newly created course to the course
variable we declared earlier. This is what our GraphQL API will return to the user as a response to the mutation request.
Now that we have our CreateCourse
class, let’s create a new class from the ObjectType
named Mutation
. We’ll store all our mutations here:
class Mutation(ObjectType): create_course = CreateCourse.Field()
With that done, we can add our Mutation
class to the Schema
in our app.add_route()
function call:
app.add_route("/", GraphQLApp( schema=Schema(query=Query, mutation=Mutation), executor_class=AsyncioExecutor) )
We can now test this by running the following query in our GraphQL client:
mutation { createCourse( id: "11" title: "Python Lists" instructor: "Jane Melody" ) { course { id title instructor } } }
And we should receive this response:
{ "data": { "createCourse": { "course": { "id": "11", "title": "Python Lists", "instructor": "Jane Melody" } } } }
Let’s see how we can handle errors in our app by adding validation for pre-existing IDs. If a user tries to create a course with an ID that already exists in our data store, our GraphQL server should respond with the error message: Course with provided id already exists!
Just before we add the new course to our data store in the CreateCourse
mutate function, let’s paste the following code:
... for course in course_list: if course['id'] == id: raise Exception('Course with provided id already exists!')
Above, we looped through our data store, course_list
, and checked whether there’s an existing course with the same id
as the one from the incoming course. If we have a match, then an exception should be thrown.
Depending on the database and ORM we choose for our application, the process for checking whether there’s a pre-existing value might be different. However, when an exception is raised, GraphQL will always return an error. With the new change in our code, here’s what our CreateCourse
mutation class should look like:
class CreateCourse(Mutation): course = Field(CourseType) class Arguments: id = String(required=True) title = String(required=True) instructor = String(required=True) publish_date = String() async def mutate(self, info, id, title, instructor): with open("./courses.json", "r+") as courses: course_list = json.load(courses) for course in course_list: if course['id'] == id: raise Exception('Course with provided id already exists!') course_list.append({"id": id, "title": title, "instructor": instructor}) courses.seek(0) json.dump(course_list, courses, indent=2) return CreateCourse(course=course_list[-1])
Now we can test this by trying to create a new course with a pre-existing id
. Let’s run the exact mutation from the last CreateCourse
request in our GraphQL client:
mutation { createCourse( id: "11" title: "Python Lists" instructor: "Jane Melody" ) { course { id title instructor } } }
We should receive this as our response:
{ "data": { "createCourse": null }, "errors": [ { "message": "Course with provided id already exists!", "locations": [ { "line": 2, "column": 3 } ], "path": [ "createCourse" ] } ] }
In this article, we’ve learned the basics of FastAPI and how we can use it to set up a GraphQL server. The combination of these two technologies brings a really exciting experience in web development.
With GraphQL, we’re able to make complex queries relatively easy to write while giving web clients the power to ask for exactly what they want. And with FastAPI, we can build robust, high-performance GraphQL servers with very little boilerplate code.
Here’s a link to the repo for our demo course, and feel free to reach out to me on LinkedIn if you need any help.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your site. Instead of guessing why problems happen, you can aggregate and report on problematic GraphQL requests to quickly understand the root cause. In addition, you can track Apollo client state and inspect GraphQL queries' key-value pairs.
LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]
One Reply to "Building a GraphQL server with FastAPI"
hello, I’ve tried this but it’s return some errors like this
Import “graphql.execution.executors.asyncio” could not be resolved
Import “starlette.graphql” could not be resolved
Import “schemas” could not be resolved
how can I fix this? thank you