Back to Scaling Python Applications guides

Using GraphQL with Python and FastAPI

Stanley Ulili
Updated on June 17, 2025

FastAPI is a web framework that has transformed Python API development through its exceptional performance, intuitive design, and comprehensive type safety features.

When combined with GraphQL, FastAPI creates an incredibly powerful platform for building modern, flexible APIs that can adapt to evolving client requirements without breaking existing implementations.

This comprehensive tutorial will guide you through building production-ready GraphQL APIs using FastAPI and Strawberry GraphQL.

Prerequisites

Before we begin, ensure you have Python 3.9 or newer installed. This guide also assumes you're comfortable with Python functions, decorators, and basic web APIs.

Setting up your FastAPI GraphQL service

To get the most out of this tutorial, it’s a good idea to set up a fresh FastAPI project so you can follow along and try things out yourself.

Start by creating a new folder for your project and setting up a virtual environment:

 
mkdir fastapi-graphql-api && cd fastapi-graphql-api
 
python3 -m venv venv
 
source venv/bin/activate

Next, install the main packages you'll need for GraphQL:

 
pip install fastapi "strawberry-graphql[fastapi]" "uvicorn[standard]"

Here’s a quick breakdown of what each package does:

  • fastapi: The main web framework we’ll use to build the API.
  • strawberry-graphql[fastapi]: This adds GraphQL support to FastAPI using Strawberry. It includes tools to define your schema, queries, and mutations.
  • uvicorn[standard]: A fast ASGI server that runs your FastAPI app. The [standard] part includes helpful extras like watchdog for auto-reloading during development.

Create a new main.py file in your project root and populate it with the following foundational code:

main.py
import strawberry
from fastapi import FastAPI
from strawberry.fastapi import GraphQLRouter

@strawberry.type
class User:
    name: str
    age: int

@strawberry.type
class Query:
    @strawberry.field
    def user(self) -> User:
        return User(name="Patrick", age=100)

    @strawberry.field
    def hello(self, name: str = "World") -> str:
        return f"Hello, {name}! Welcome to FastAPI + GraphQL"

schema = strawberry.Schema(query=Query)
graphql_app = GraphQLRouter(schema)

app = FastAPI(title="FastAPI GraphQL API")
app.include_router(graphql_app, prefix="/graphql")

This code sets up a basic GraphQL schema using Strawberry, following the official FastAPI pattern. The Query class defines two fields: hello (which takes an optional name) and user (which returns a user object). The @strawberry.type and @strawberry.field decorators turn regular Python classes and methods into GraphQL types and fields.

The setup uses FastAPI’s preferred method: create a Strawberry schema, wrap it in a GraphQLRouter, and include it in your app with a prefix. This lets you mix GraphQL and regular FastAPI routes in the same project.

We'll explore various ways to customize the schema and add more complex functionality later, but for now, let's test the basic implementation by launching your GraphQL server using the following command:

 
uvicorn main:app --reload
Output

INFO:     Will watch for changes in these directories: ['/Users/stanley/fastapi-graphql-api']
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [71473] using WatchFiles
INFO:     Started server process [71477]
INFO:     Waiting for application startup.
INFO:     Application startup complete.

Navigate to http://localhost:8000/graphql in your browser to access the GraphiQL interface. You should see an interactive GraphQL IDE that allows you to explore and test your API:

Screenshot of the GraphiQL interface

Now, try executing this query to verify your setup:

 
{
  user {
    name
    age
  }
}

You should receive a response like:

Output
{
  "data": {
    "user": {
      "name": "Patrick",
      "age": 100
    }
  }
}

Screenshot of the GraphiQL interface with responses

One of the first things you'll notice about the GraphiQL interface is how developer-friendly it is. It offers features like auto-complete, syntax highlighting, and a built-in way to explore your API's schema.

These tools make it easy to test different queries, experiment with your API, and see exactly what data is available.

Designing GraphQL schemas

A GraphQL schema is the core contract between your API and its users. It defines all available operations, data types, and how they relate. Unlike REST, which relies on multiple endpoints, GraphQL uses a single, well-structured schema to describe the entire API.

Schemas are made up of types, fields, arguments, and operations like queries and mutations. Understanding these pieces is key to designing clean, scalable APIs.

Let’s create a more realistic schema. Start by creating a new file called models.py to define our data types.

models.py
from typing import List, Optional
from datetime import datetime
import strawberry

@strawberry.type
class Author:
    id: int
    name: str
    email: str
    bio: Optional[str] = None
    created_at: datetime
    books: List["Book"] = strawberry.field(default_factory=list)

@strawberry.type
class Book:
    id: int
    title: str
    isbn: str
    published_year: int
    page_count: int
    author_id: int
    author: Optional[Author] = None

These types of definitions highlight key GraphQL concepts. Author and Book show how to create object types with scalar fields (like int and str), optional fields, and relationships to other types. The use of "Book" as a forward reference in the Author type lets us handle circular references between types.

We're also using Python's type hints throughout. Strawberry reads these hints to generate the GraphQL schema automatically. This keeps the code clean and easy to follow, while also providing helpful editor support and type-checking.

Next, let’s define input types for mutations. Input types are used to pass structured arguments into queries and mutations. Add the following to your models.py file:

models.py
...
@strawberry.input
class CreateAuthorInput:
    name: str
    email: str
    bio: Optional[str] = None

@strawberry.input
class CreateBookInput:
    title: str
    isbn: str
    published_year: int
    page_count: int
    author_id: int

@strawberry.input
class UpdateBookInput:
    title: Optional[str] = None
    isbn: Optional[str] = None
    published_year: Optional[int] = None
    page_count: Optional[int] = None

Input types offer several advantages over using individual arguments: they group related parameters together, make mutations more readable, and facilitate easier validation and transformation.

The UpdateBookInput type demonstrates how to create partial update inputs where all fields are optional.

Update your main.py file to include these new types and provide some sample data to work with:

main.py
import strawberry
from fastapi import FastAPI
from strawberry.fastapi import GraphQLRouter
from typing import List, Optional
from models import Author, Book, CreateBookInput, CreateAuthorInput
from datetime import datetime
# Sample data for demonstration
authors_db = [
Author(
id=1,
name="George Orwell",
email="george@orwell.com",
bio="English novelist and critic",
created_at=datetime(2020, 1, 15),
),
Author(
id=2,
name="Ray Bradbury",
email="ray@bradbury.com",
bio="American author and screenwriter",
created_at=datetime(2020, 3, 10),
),
]
books_db = [
Book(
id=1,
title="1984",
isbn="978-0-452-28423-4",
published_year=1949,
page_count=328,
author_id=1,
),
Book(
id=2,
title="Fahrenheit 451",
isbn="978-1-451-67331-9",
published_year=1953,
page_count=194,
author_id=2,
),
]
@strawberry.type class User: name: str age: int @strawberry.type class Query: @strawberry.field def user(self) -> User: return User(name="Patrick", age=100) @strawberry.field def hello(self, name: str = "World") -> str: return f"Hello, {name}! Welcome to FastAPI + GraphQL"
@strawberry.field
def books(self) -> List[Book]:
return books_db
@strawberry.field
def book(self, id: int) -> Optional[Book]:
return next((book for book in books_db if book.id == id), None)
@strawberry.field
def authors(self) -> List[Author]:
return authors_db
@strawberry.field
def author(self, id: int) -> Optional[Author]:
return next((author for author in authors_db if author.id == id), None)
schema = strawberry.Schema(query=Query) graphql_app = GraphQLRouter(schema) app = FastAPI(title="FastAPI GraphQL API") app.include_router(graphql_app, prefix="/graphql")

This enhanced schema demonstrates several important query patterns. The books and authors fields return lists of items, while book and author provide single-item lookups by ID. The use of Optional return types indicates that these fields might return None if no matching item is found.

Test your enhanced schema with this query that demonstrates GraphQL's ability to fetch related data in a single request:

 
{
  books {
    id
    title
    publishedYear
    pageCount
    author {
      name
      bio
    }
  }
}

You will see output like this:

Output
{
  "data": {
    "books": [
      {
        "id": 1,
        "title": "1984",
        "publishedYear": 1949,
        "pageCount": 328,
        "author": null
      },
      {
        "id": 2,
        "title": "Fahrenheit 451",
        "publishedYear": 1953,
        "pageCount": 194,
        "author": null
      }
    ]
  }
}

Screenshot of the GraphQL output

However, you'll notice that the author field in the book results returns null. This is because we haven't implemented the relationship resolution yet. Let's add resolver methods to handle these relationships:

models.py
from typing import List, Optional
from datetime import datetime
import strawberry

@strawberry.type
class Author:
    id: int
    ..
    books: List["Book"] = strawberry.field(default_factory=list)

@strawberry.field
def books(self) -> List["Book"]:
# Import here to avoid circular import
from main import books_db
return [book for book in books_db if book.author_id == self.id]
@strawberry.type class Book: id: int ... author: Optional[Author] = None
@strawberry.field
def author(self) -> Optional[Author]:
# Import here to avoid circular import
from main import authors_db
return next((author for author in authors_db if author.id == self.author_id), None)
@strawberry.input class CreateAuthorInput: ...

Now when you run the same query, you'll see the author information populated for each book:

Screenshot in the GraphQL interface showing the author information populated

This demonstrates GraphQL's power to traverse relationships and return exactly the data requested by the client.

Implementing mutations for data modification

While queries handle data retrieval, mutations manage data modifications such as creating, updating, or deleting resources. GraphQL mutations provide a structured approach to state changes while maintaining the same type safety and flexibility as queries. They follow a predictable pattern: accept input parameters, perform the operation, and return the modified data along with any relevant metadata.

Mutations are particularly powerful because they can return complex objects that include both the modified data and additional context like validation errors, success indicators, or related information that changed as a result of the operation.

Let's add comprehensive mutation capabilities to our API. First, add a Mutation class to your main.py file:

main.py
 ...
@strawberry.type
class Query:
    @strawberry.field
    def user(self) -> User:
        return User(name="Patrick", age=100)

    ...

    @strawberry.field
    def author(self, id: int) -> Optional[Author]:
        return next((author for author in authors_db if author.id == id), None)

@strawberry.type
class Mutation:
@strawberry.mutation
def create_author(self, input: CreateAuthorInput) -> Author:
new_id = max([author.id for author in authors_db], default=0) + 1
new_author = Author(
id=new_id,
name=input.name,
email=input.email,
bio=input.bio,
created_at=datetime.now()
)
authors_db.append(new_author)
return new_author
@strawberry.mutation
def create_book(self, input: CreateBookInput) -> Book:
# Validate that the author exists
author = next((a for a in authors_db if a.id == input.author_id), None)
if not author:
raise Exception(f"Author with ID {input.author_id} not found")
new_id = max([book.id for book in books_db], default=0) + 1
new_book = Book(
id=new_id,
title=input.title,
isbn=input.isbn,
published_year=input.published_year,
page_count=input.page_count,
author_id=input.author_id
)
books_db.append(new_book)
return new_book
schema = strawberry.Schema(query=Query, mutation=Mutation)
graphql_app = GraphQLRouter(schema) app = FastAPI(title="FastAPI GraphQL API") app.include_router(graphql_app, prefix="/graphql")

The mutation resolvers demonstrate several important patterns. First, they accept input objects rather than individual parameters, which keeps the GraphQL schema clean and makes validation easier. Second, they perform validation before making changes - the create_book mutation verifies that the referenced author exists before creating the book.

Notice how we generate new IDs by finding the maximum existing ID and incrementing it. In a real application, you'd typically let your database handle ID generation, but this approach works well for our demonstration.

Test your mutations with these GraphQL operations:

 
mutation {
  createAuthor(input: {
    name: "Isaac Asimov"
    email: "isaac@asimov.com"
    bio: "American science fiction writer"
  }) {
    id
    name
    email
    createdAt
  }
}
Output
{
  "data": {
    "createAuthor": {
      "id": 4,
      "name": "Isaac Asimov",
      "email": "isaac@asimov.com",
      "createdAt": "2025-06-17T13:44:41.006003"
    }
  }
}

Screenshot of `createAuthor` mutation set

Now try this:

 
mutation {
  createBook(input: {
    title: "Foundation"
    isbn: "978-0-553-29335-0"
    publishedYear: 1951
    pageCount: 244
    authorId: 3
  }) {
    id
    title
    isbn
    author {
      name
    }
  }
}
Output
{
  "data": {
    "createBook": {
      "id": 3,
      "title": "Foundation",
      "isbn": "978-0-553-29335-0",
      "author": {
        "name": "Isaac Asimov"
      }
    }
  }
}

Screenshot of the GraphQL output

The mutation system provides a clear interface for data modifications while maintaining the same type safety and introspection capabilities that make GraphQL queries so powerful.

Clients can specify exactly what data they want returned after the mutation completes, which is particularly useful for updating UI components efficiently.

Final thoughts

You’ve learned how to build a GraphQL API using FastAPI and Strawberry, including how to set up your project, define types, and create queries and mutations.

This approach offers a fast, flexible, and type-safe way to develop modern APIs. Next, you can add features like a database, authentication, or pagination.

For more details, visit the Strawberry and FastAPI documentation.

Happy coding!

Got an article suggestion? Let us know
Next article
Get Started with Job Scheduling in Python
Learn how to create and monitor Python scheduled tasks in a production environment
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Make your mark

Join the writer's program

Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.

Write for us
Writer of the month
Marin Bezhanov
Marin is a software engineer and architect with a broad range of experience working...
Build on top of Better Stack

Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.

community@betterstack.com

or submit a pull request and help us build better products for everyone.

See the full list of amazing projects on github