GraphQL with Python
Build GraphQL APIs in Python with Strawberry and FastAPI — schemas, queries, mutations, subscriptions, and when to choose GraphQL over REST.
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
- Query depth limiting — prevent deeply nested queries that overload the server
- Complexity analysis — reject expensive queries before execution
- Persisted queries — allow only pre-approved queries in production
- Rate limiting — apply at the gateway level
- Monitoring — log slow resolvers, track error rates
Related
- REST API Project — FastAPI REST foundation
- FastAPI Track — routing, auth, deployment
- Network Programming — HTTP fundamentals
- Advanced Testing — test GraphQL endpoints
GraphQL is a powerful complement to REST — not a replacement for every API.