Unlock GraphQL Power: FastAPI and Strawberry for High-Performance APIs

FastAPI and Strawberry combine to create efficient GraphQL APIs. Key features include schema definition, queries, mutations, pagination, error handling, code organization, authentication, and performance optimization using DataLoader for resolving nested fields efficiently.

Unlock GraphQL Power: FastAPI and Strawberry for High-Performance APIs

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