FastAPI and Strawberry are a powerful combination for building GraphQL APIs. Let’s dive into how to create a robust, performant API using these tools.
First, we’ll set up our project. Make sure you have Python 3.7+ installed, then create a new directory for your project and set up a virtual environment:
mkdir fastapi-graphql-api
cd fastapi-graphql-api
python -m venv venv
source venv/bin/activate # On Windows, use venv\Scripts\activate
Now, let’s install the necessary packages:
pip install fastapi strawberry-graphql uvicorn
With our environment set up, let’s create our main application file, main.py
:
from fastapi import FastAPI
import strawberry
from strawberry.fastapi import GraphQLRouter
app = FastAPI()
@strawberry.type
class Query:
@strawberry.field
def hello(self) -> str:
return "Hello, GraphQL!"
schema = strawberry.Schema(query=Query)
graphql_app = GraphQLRouter(schema)
app.include_router(graphql_app, prefix="/graphql")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
This sets up a basic FastAPI application with a GraphQL endpoint. We’ve defined a simple Query type with a “hello” field that returns a greeting.
To run the application, use:
python main.py
Now, if you navigate to http://localhost:8000/graphql
, you’ll see the GraphQL playground where you can test your API.
Let’s make things more interesting by adding some data models and more complex queries. We’ll create a simple book library API. First, let’s define our data models:
import strawberry
from typing import List, Optional
@strawberry.type
class Author:
id: int
name: str
@strawberry.type
class Book:
id: int
title: str
author: Author
Now, let’s create some mock data and update our Query type:
authors = [
Author(id=1, name="J.K. Rowling"),
Author(id=2, name="George Orwell"),
]
books = [
Book(id=1, title="Harry Potter and the Philosopher's Stone", author=authors[0]),
Book(id=2, title="1984", author=authors[1]),
]
@strawberry.type
class Query:
@strawberry.field
def books(self) -> List[Book]:
return books
@strawberry.field
def book(self, id: int) -> Optional[Book]:
return next((book for book in books if book.id == id), None)
@strawberry.field
def authors(self) -> List[Author]:
return authors
@strawberry.field
def author(self, id: int) -> Optional[Author]:
return next((author for author in authors if author.id == id), None)
This gives us a more robust API with multiple query options. We can now query for all books, a specific book, all authors, or a specific author.
But what about adding new books or authors? For that, we need mutations. Let’s add some:
@strawberry.type
class Mutation:
@strawberry.mutation
def add_book(self, title: str, author_id: int) -> Book:
author = next((a for a in authors if a.id == author_id), None)
if not author:
raise ValueError("Author not found")
book = Book(id=len(books) + 1, title=title, author=author)
books.append(book)
return book
@strawberry.mutation
def add_author(self, name: str) -> Author:
author = Author(id=len(authors) + 1, name=name)
authors.append(author)
return author
schema = strawberry.Schema(query=Query, mutation=Mutation)
Now we can add new books and authors to our library!
One of the great things about GraphQL is its ability to resolve nested fields efficiently. Let’s add a field to our Author type to get all books by that author:
@strawberry.type
class Author:
id: int
name: str
@strawberry.field
def books(self) -> List[Book]:
return [book for book in books if book.author.id == self.id]
This allows us to query for an author and their books in a single request, which is much more efficient than making separate API calls.
Now, let’s talk about some advanced features we can add to our API. One important aspect of any production-ready API is proper error handling. Strawberry allows us to define custom error types:
@strawberry.type
class Error:
message: str
@strawberry.type
class BookResult:
book: Optional[Book] = None
error: Optional[Error] = None
@strawberry.type
class Query:
@strawberry.field
def book(self, id: int) -> BookResult:
book = next((book for book in books if book.id == id), None)
if book:
return BookResult(book=book)
return BookResult(error=Error(message=f"Book with id {id} not found"))
This gives us more control over the shape of our API responses and allows for better error handling on the client side.
Another important feature for larger applications is pagination. Let’s implement cursor-based pagination for our books query:
from typing import Optional
@strawberry.type
class BookConnection:
edges: List["BookEdge"]
page_info: "PageInfo"
@strawberry.type
class BookEdge:
node: Book
cursor: str
@strawberry.type
class PageInfo:
has_next_page: bool
end_cursor: Optional[str]
@strawberry.type
class Query:
@strawberry.field
def books(self, first: int = 10, after: Optional[str] = None) -> BookConnection:
start = int(after) if after else 0
end = start + first
sliced_books = books[start:end]
edges = [
BookEdge(node=book, cursor=str(i))
for i, book in enumerate(sliced_books, start=start)
]
has_next_page = end < len(books)
end_cursor = str(end - 1) if has_next_page else None
return BookConnection(
edges=edges,
page_info=PageInfo(has_next_page=has_next_page, end_cursor=end_cursor)
)
This implements a simple cursor-based pagination system. Clients can now request a specific number of books and use the returned cursor to fetch the next page.
As our API grows, we might want to split our schema into multiple files for better organization. Strawberry makes this easy with its strawberry.Schema
class. Let’s refactor our code a bit:
# books.py
import strawberry
from typing import List, Optional
@strawberry.type
class Book:
id: int
title: str
author: "Author"
@strawberry.type
class BookQuery:
@strawberry.field
def books(self) -> List[Book]:
return books
@strawberry.field
def book(self, id: int) -> Optional[Book]:
return next((book for book in books if book.id == id), None)
@strawberry.type
class BookMutation:
@strawberry.mutation
def add_book(self, title: str, author_id: int) -> Book:
author = next((a for a in authors if a.id == author_id), None)
if not author:
raise ValueError("Author not found")
book = Book(id=len(books) + 1, title=title, author=author)
books.append(book)
return book
# authors.py
import strawberry
from typing import List, Optional
from .books import Book
@strawberry.type
class Author:
id: int
name: str
@strawberry.field
def books(self) -> List[Book]:
return [book for book in books if book.author.id == self.id]
@strawberry.type
class AuthorQuery:
@strawberry.field
def authors(self) -> List[Author]:
return authors
@strawberry.field
def author(self, id: int) -> Optional[Author]:
return next((author for author in authors if author.id == id), None)
@strawberry.type
class AuthorMutation:
@strawberry.mutation
def add_author(self, name: str) -> Author:
author = Author(id=len(authors) + 1, name=name)
authors.append(author)
return author
# main.py
import strawberry
from strawberry.fastapi import GraphQLRouter
from fastapi import FastAPI
from .books import BookQuery, BookMutation
from .authors import AuthorQuery, AuthorMutation
@strawberry.type
class Query(BookQuery, AuthorQuery):
pass
@strawberry.type
class Mutation(BookMutation, AuthorMutation):
pass
schema = strawberry.Schema(query=Query, mutation=Mutation)
app = FastAPI()
graphql_app = GraphQLRouter(schema)
app.include_router(graphql_app, prefix="/graphql")
This structure allows us to organize our code better and makes it easier to maintain as our API grows.
Now, let’s talk about authentication and authorization. In a real-world application, you’d want to secure your API. FastAPI makes it easy to add authentication middleware, and we can use this in combination with Strawberry’s dependency injection system:
from fastapi import Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer
from strawberry.permission import BasePermission
from strawberry.types import Info
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
async def get_current_user(token: str = Depends(oauth2_scheme)):
# In a real application, you'd validate the token here
# For this example, we'll just return a mock user
return {"username": "testuser"}
class IsAuthenticated(BasePermission):
message = "User is not authenticated"
async def has_permission(self, source: Any, info: Info, **kwargs) -> bool:
request = info.context["request"]
try:
await get_current_user(token=request.headers.get("Authorization"))
return True
except HTTPException:
return False
@strawberry.type
class Query:
@strawberry.field(permission_classes=[IsAuthenticated])
def protected_field(self) -> str:
return "This is a protected field"
This sets up a basic authentication system and protects certain fields from unauthorized access.
Lastly, let’s talk about performance optimization. When dealing with complex nested queries, N+1 query problems can arise. Strawberry integrates well with DataLoader, which can help solve this issue:
from strawberry.dataloader import DataLoader
async def load_books_by_author_ids(keys):
# In a real application, this would be a database query
return [[book for book in books if book.author.id == key] for key in keys]
book_loader = DataLoader(load_fn=load_books_by_author_ids)
@strawberry.type
class Author:
id: int
name: str
@strawberry.field
async def books(self, info: Info) -> List[Book]:
return await book_loader.load(self.id)
This ensures that when querying multiple authors and their books, we only make one query to fetch all the books, rather than a separate query for each author.
In conclusion, FastAPI and Strawberry provide a powerful toolset for building GraphQL APIs. We’ve covered the basics of setting up a schema, adding queries and mutations, implementing pagination, handling errors, structuring our code, adding authentication, and optimizing performance. With these tools and techniques, you’re well-equipped to build robust, efficient GraphQL APIs that can scale with