How to Build Out a GraphQL Endpoint in FastAPI
What is GraphQL
GraphQL is a powerful, expressive query language for APIs, as well as a runtime for using existing data to respond to these queries. Often viewed as the successor to REST APIs, it provides a concise and easily human-readable interface for querying your API for data and tasks. GraphQL offers a number of significant advantages to developers when they use it in their applications.
Why use GraphQL with FastAPI
FastAPI is already a powerful framework for building REST APIs. So, why bother adding GraphQL into the mix? GraphQL offers a number of significant benefits to applications that implement it. Too many to mention here, in fact. Click here to read more about all about the benefits GraphQL can bring to your API.
Strawberry
In order to use GraphQL with FastAPI, we need an intermediary that allows us to define GraphQL queries in Python. Strawberry bridges the gap between GraphQL and FastAPI. It is a python library that allows us to define the schema for responding to GraphQL queries and mutations using simple python classes using the power of decorators and type hinting.
Building the Endpoint
So, now that we know all the reasons to use GraphQL with out FastAPI project, and we have a way to define GraphQL schema in Python, we need to create the endpoint that allows our api to receive and respond to GraphQL queries. Luckily, a direct integration between FastAPI and Strawberry exists:
import strawberry
from strawberry.fastapi import GraphQLRouter
# TODO: Define Query and Mutation here
schema = strawberry.Schema(query=Query, mutation=Mutation)
graphql_router = GraphQLRouter(
schema
)
Looking at the snippet above, we see how simple it is to create a FastAPI router that understands GraphQL. We simply create an instance of the GraphQL router class, and pass a strawberry schema to it. That, in turn, is made up of two components, query and mutation, which we will define later. Add the router to your app like so:
# include graphql router
app.include_router(graphql_router, prefix="/graphql")
Defining the Schema
Now that the GraphQL route is up and running, its time to define the schema for interacting with it using GraphQL. We will set up two sets of schema: Query schema, used to read data, and Mutation schema, used to modify data. But first, we need to define schema for the types of record
Entity Schema
In order to interact with records from our database, we need to define an accompanying strawberry schema to go with the model. This schema will help strawberry to tell which fields should be able to be queried in GraphQL, and will help it filter out only the data the client requested. Here is a basic strawberry schema for something simple: a post.
@strawberry.type
class GQL_Post:
id: strawberry.ID = strawberry.field(description="The id of the post.")
title: str = strawberry.field(description="The title of the post.")
content: str = strawberry.field(description="The content of the post.")
author_id: strawberry.ID = strawberry.field(description="The id of the post's author")
created_at: str = strawberry.field(
description="The datetime the post was created."
)
updated_at: str = strawberry.field(
description="The datetime the post was last updated."
)
This schema is relatively easy to understand. Strawberry uses decorators to recognize values it is supposed to use for GrapQL queries. And, it uses Python type hinting to track the types that each field should have. The strawberry.ID type is used to represent any field that represents the ID of another record. This can be used for string or integer ids alike.
Query
Now that we have our basic post schema, it is time to add the entry point for queries, or requests to read existing data, to our api. Begin by declaring a base Query class. This class will serve as the entry point for all GraphQL mutations. Then, we will add strawberry fields to it like so:
class Query:
@strawberry.field
async def me(self, info: strawberry.Info) -> GQL_User:
"""Get the current user, using strawberry authentication extension"""
user = await info.context["get_user"]()
if user:
return user
return None
@strawberry.field
async def post(
self, id: strawberry.ID, info: strawberry.Info
) -> Optional[GQL_Post]:
"""Get a post by it's id."""
db: Session = next(get_db()) #database session object
post = get_post(db, id) #function to get post by id
return post
Each function of this class serves a bit like a route handler in a REST API. It defines a function that takes certain inputs, defined as arguments to the function, and returns the corresponding type to the requester. As you can see, type hints for all arguments and return types are enforced by strawberry to ensure a valid response to the query.
Here is a query that uses the post GraphQL handler to get the title, content, and author_id of a post with an id of 38.
query Post {
post(id: 38) {
id
title
content
author_id
}
}
This query hits the second handler function in the previous code snippet, and the return result looks like this:
{
"data": {
"post": {
"id": "38",
"title": "How to Tie Your Shoe",
"content": "So you want to learn to tie your shoe? Well, le...",
"author_id": "23"
}
}
}
As you can see, the response structure closely matches the format of the request. A response entry is included for each requested attribute of the post. No more, no less. If we had only wanted the title (say in a query loop or post listing, we could have used this query to minimize waste:
query Post {
post(id: 38) {
title
}
}
Which would have returned…
{
"data": {
"post": {
"title": "How to Tie Your Shoe",
}
}
}
Pretty neat! The strawberry query schema allows us to easily and dynamically select which operations the API should perform, minimizing waste and boosting performance. This simple, modular structure can be nested many layers deep, allowing interesting dynamic relationships between different pieces of data without needing a complicated database schema. Now that we know how to implement a Query schema in strawberry for our GraphQL API, let’s move on to modifying data using Mutations!
Mutation
Mutations are defined in a very similar way to Queries in strawberry. Declare a base mutation class, then, add function handlers annotated with @strawberry.mutations like this:
@strawberry.type
class Mutation:
@strawberry.mutation
async def create_post(
self, title: str, content: str, author_id: int, info: strawberry.Info
) -> GQL_Post:
"""Create a new post."""
user = await info.context["get_user"]()
create = RaffleCreate(title=title, content=content, author_id=author_id)
db: Session = next(get_db()) #database session object
return create_post(db, create, user) #function to create post
@strawberry.mutation()
async def update_post(
self,
id: strawberry.ID,
info: strawberry.Info,
title: Optional[str],
content: Optional[str],
author_id: Optional[int]
) -> GQL_Post:
"""Update a post."""
user = await info.context["get_user"]()
update = RaffleUpdate(title=title, content=content, author_id=author_id)
db: Session = next(get_db()) #database session object
return update_post(db, update, user) #function to update post
Once again, each function in the mutation object behaves a bit like a route handler, calling a python function to handle the result. Parameters that are used to execute are passed to the handler function as arguments, which are then used to create ORM models, which in turn are used to create and update posts in the database. Then, once again, the mutations return the type specified in the type hinting intelligently based on the information the user requested. This request hits the create_post handler, creates a post record in our database, and returns the listed fields:
mutation CreatePost {
createPost(title: "How to Tie Your Shoe", body: "Just do it!", author_id: 1) {
id
title
body
authorId
}
}
Here is the response:
{
"data": {
"createPost": {
"id": 39,
"title": "How to Tie Your Shoe",
"body": "Just do it!",
"authorId": 1
}
}
}
Bringing It Together
So, now we have all our queries and mutations defined, all that is left to do is to import and attach them to the strawberry GraphQL router we built previously. Then, we will be able to use our GraphQL schema to interact with out API.
import strawberry
from strawberry.fastapi import GraphQLRouter
from query import Query
from mutation import Mutation
schema = strawberry.Schema(query=Query, mutation=Mutation)
graphql_router = GraphQLRouter(
schema
)
Note that all I did was import the classes we defined, and pass them in as keyword arguments to strawberry.Schema. And that’s it. The GraphQLRouter instance can be attached to the FastAPI app just like any other router, and just like that, your FastAPI API can now handle and respond to GraphQL queries. Pretty nifty, right?
Getting the Current User
Throughout this article, you have probably noticed the somewhat mysterious info: strawberry.Info argument to many of the handler functions, and the line
user = await info.context["get_user"]()
in all my handlers. What’s the big idea with this? Well, the full explanation is a bit outside the scope of this article. But basically, strawberry will pass an argument to any handler function annotated with @strawberry.field that has an argument of the type strawberyy.Info containing all kinds of metadata about the request. In my API, I want to pass the authenticated user to my database function for logging reasons, so I grab the authenticated API user from the request (authentication is a separate topic that is not addressed here).
Conclusion
Congrats! You made it! By now, you should have learned all the basics of building a simple GraphQL API on top of FastAPI using strawberry. This includes setting up the GraphQL router and schema, declaring queries, defining mutations, and putting it all back together to get the project up and running. I hope this guide has proved useful to you, and if you have any questions, feel free to leave a comment! Happy coding!
Leave a Reply