Ebenezer Don Full-stack software engineer with a passion for building meaningful products that ease the lives of users.

Building a GraphQL server with FastAPI

9 min read 2660

Building Graphql Server Fastapi Symbol

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.

Setting up our app

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.

GraphQL basics: Queries and schema

GraphQL queries

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:

We made a custom demo for .
No really. Click here to check it out.

{
  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.

GraphQL schema

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.

Setting up a temporary database

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.

Creating our query resolvers

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.

Starting up our FastAPI-powered GraphQL server

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:

A Blank GraphQL Server Page

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:

The GraphQL Server Page Response

Fetching only one course

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"
      }
    ]
  }
}

GraphQL mutations

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"
      }
    }
  }
}

Handling request errors

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"
      ]
    }
  ]
}

Conclusion

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.

Monitor failed and slow GraphQL requests in production

While GraphQL has some features for debugging requests and responses, making sure GraphQL reliably serves resources to your production app is where things get tougher. If you’re interested in ensuring network requests to the backend or third party services are successful, try LogRocket.https://logrocket.com/signup/

LogRocket is like a DVR for web 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. .
Ebenezer Don Full-stack software engineer with a passion for building meaningful products that ease the lives of users.

Leave a Reply