REST APIs expose fixed endpoints. GraphQL gives clients a single endpoint and lets them request exactly the data they need. Python teams often pair Strawberry (schema-first, type-safe) with FastAPI.

GraphQL vs REST

REST GraphQL
Endpoints Multiple URLs Single /graphql
Data fetching Fixed response shape Client selects fields
Over-fetching Common Avoided
Versioning /v1/, /v2/ Evolve schema additively
Caching HTTP cache friendly Requires client-side cache (Apollo)
Learning curve Lower Higher

Use GraphQL when: mobile apps need flexible queries, multiple clients need different data shapes, or you want a strongly typed API contract.

Stick with REST when: simple CRUD, heavy HTTP caching, or team familiarity matters more.

Setup

  pip install strawberry-graphql[fastapi] uvicorn
  

Basic Schema

  # app/schema.py
import strawberry
from typing import Optional

@strawberry.type
class Book:
    id: int
    title: str
    author: str
    pages: Optional[int] = None

# In-memory store for demo
books_db: list[Book] = [
    Book(id=1, title="1984", author="George Orwell", pages=328),
    Book(id=2, title="Dune", author="Frank Herbert", pages=688),
]

@strawberry.type
class Query:
    @strawberry.field
    def books(self) -> list[Book]:
        return books_db

    @strawberry.field
    def book(self, id: int) -> Optional[Book]:
        return next((b for b in books_db if b.id == id), None)
  

FastAPI Integration

  # app/main.py
from fastapi import FastAPI
from strawberry.fastapi import GraphQLRouter
from app.schema import Query

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

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

Run: uvicorn app.main:app --reload

Open http://localhost:8000/graphql for the built-in GraphiQL IDE.

Queries

Clients request only the fields they need:

  # Fetch title and author only — no pages field transferred
query {
  books {
    title
    author
  }
}
  
  query {
  book(id: 1) {
    title
    pages
  }
}
  

Mutations

Mutations modify data (GraphQL convention — queries are read-only):

  @strawberry.input
class AddBookInput:
    title: str
    author: str
    pages: Optional[int] = None

@strawberry.type
class Mutation:
    @strawberry.mutation
    def add_book(self, info, input: AddBookInput) -> Book:
        new_id = max(b.id for b in books_db) + 1 if books_db else 1
        book = Book(id=new_id, title=input.title, author=input.author, pages=input.pages)
        books_db.append(book)
        return book

    @strawberry.mutation
    def delete_book(self, info, id: int) -> bool:
        global books_db
        before = len(books_db)
        books_db = [b for b in books_db if b.id != id]
        return len(books_db) < before

schema = strawberry.Schema(query=Query, mutation=Mutation)
  
  mutation {
  addBook(input: { title: "Neuromancer", author: "William Gibson", pages: 271 }) {
    id
    title
  }
}
  

Nested Types and Resolvers

GraphQL resolves fields lazily — fetch related data only when requested:

  @strawberry.type
class Author:
    id: int
    name: str

authors_db = {1: Author(id=1, name="George Orwell")}

@strawberry.type
class Book:
    id: int
    title: str
    author_id: int

    @strawberry.field
    def author(self) -> Author:
        return authors_db[self.author_id]
  

Client query:

  query {
  books {
    title
    author {
      name
    }
  }
}
  

Database Integration

Connect resolvers to SQLAlchemy or async sessions:

  from sqlalchemy.orm import Session
from fastapi import Depends

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

@strawberry.type
class Query:
    @strawberry.field
    def books(self, info) -> list[Book]:
        db: Session = info.context["db"]
        return db.query(BookModel).all()

async def get_context(db: Session = Depends(get_db)):
    return {"db": db}

graphql_app = GraphQLRouter(schema, context_getter=get_context)
  

Authentication

Pass the current user through context:

  async def get_context(request: Request, db: Session = Depends(get_db)):
    token = request.headers.get("Authorization", "").removeprefix("Bearer ")
    user = verify_token(token) if token else None
    return {"db": db, "user": user}

@strawberry.type
class Mutation:
    @strawberry.mutation
    def add_book(self, info, input: AddBookInput) -> Book:
        if info.context["user"] is None:
            raise Exception("Authentication required")
        ...
  

Subscriptions (Real-Time)

Strawberry supports WebSocket subscriptions for live updates:

  import asyncio
from typing import AsyncGenerator

@strawberry.type
class Subscription:
    @strawberry.subscription
    async def book_added(self) -> AsyncGenerator[Book, None]:
        queue = asyncio.Queue()
        # Push new books to queue from mutation side
        while True:
            book = await queue.get()
            yield book
  

Subscriptions require a WebSocket-capable server and are more complex to deploy than queries/mutations.

Error Handling

Return structured errors instead of raising unhandled exceptions:

  from strawberry.types import Info

@strawberry.mutation
def add_book(self, info: Info, input: AddBookInput) -> Book:
    if not input.title.strip():
        raise ValueError("Title cannot be empty")
    ...
  

GraphQL always returns HTTP 200 with errors in the response body — clients must check the errors field.

N+1 Problem and DataLoaders

Fetching author for each book in a list causes N+1 queries. Use DataLoader to batch:

  pip install strawberry-dataloader
  
  from strawberry.dataloader import DataLoader

async def load_authors(keys: list[int]) -> list[Author]:
    return [authors_db[k] for k in keys]

author_loader = DataLoader(load_fn=load_authors)

@strawberry.field
async def author(self) -> Author:
    return await author_loader.load(self.author_id)
  

Testing GraphQL

  from fastapi.testclient import TestClient
from app.main import app

client = TestClient(app)

def test_books_query():
    response = client.post("/graphql", json={
        "query": "{ books { title author } }"
    })
    assert response.status_code == 200
    data = response.json()
    assert "errors" not in data
    assert len(data["data"]["books"]) >= 1
  

Production Considerations

  1. Query depth limiting — prevent deeply nested queries that overload the server
  2. Complexity analysis — reject expensive queries before execution
  3. Persisted queries — allow only pre-approved queries in production
  4. Rate limiting — apply at the gateway level
  5. Monitoring — log slow resolvers, track error rates

GraphQL is a powerful complement to REST — not a replacement for every API.