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 likewatchdog
for auto-reloading during development.
Create a new main.py
file in your project root and populate it with the following foundational code:
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
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:
Now, try executing this query to verify your setup:
{
user {
name
age
}
}
You should receive a response like:
{
"data": {
"user": {
"name": "Patrick",
"age": 100
}
}
}
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.
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:
...
@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:
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:
{
"data": {
"books": [
{
"id": 1,
"title": "1984",
"publishedYear": 1949,
"pageCount": 328,
"author": null
},
{
"id": 2,
"title": "Fahrenheit 451",
"publishedYear": 1953,
"pageCount": 194,
"author": null
}
]
}
}
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:
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:
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:
...
@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
}
}
{
"data": {
"createAuthor": {
"id": 4,
"name": "Isaac Asimov",
"email": "isaac@asimov.com",
"createdAt": "2025-06-17T13:44:41.006003"
}
}
}
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
}
}
}
{
"data": {
"createBook": {
"id": 3,
"title": "Foundation",
"isbn": "978-0-553-29335-0",
"author": {
"name": "Isaac Asimov"
}
}
}
}
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!
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
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.comor submit a pull request and help us build better products for everyone.
See the full list of amazing projects on github