Strawberry is a code-first GraphQL library for Python. pip install strawberry-graphql. Define type: @strawberry.type class User: id: strawberry.ID; name: str; email: str. Query: @strawberry.type class Query: @strawberry.field async def user(self, id: strawberry.ID, info: strawberry.Info) -> User: return await get_user(id). Schema: schema = strawberry.Schema(query=Query). Mutation: @strawberry.type class Mutation: @strawberry.mutation async def create_user(self, name: str, email: str) -> User: .... schema = strawberry.Schema(query=Query, mutation=Mutation). FastAPI: from strawberry.fastapi import GraphQLRouter; router = GraphQLRouter(schema). app.include_router(router, prefix="/graphql"). Context: async def get_context(request): return {"db": db, "user": request.state.user}. GraphQLRouter(schema, context_getter=get_context). Info: def resolver(info: strawberry.Info) -> str: db = info.context["db"]. Input type: @strawberry.input class CreateUserInput: name: str; email: str. Enum: @strawberry.enum class UserRole(Enum): USER="user"; ADMIN="admin". Optional field: email: Optional[str] = None. Nullable: strawberry.annotation(str, is_optional=True). DataLoader: async def load_users(keys): return await db.get_many(keys). loader = DataLoader(load_fn=load_users). user = await info.context["user_loader"].load(id). Permission: class IsAuthenticated(BasePermission): message="Not authenticated"; async def has_permission(self, source, info, **kwargs): return info.context["user"] is not None. @strawberry.field(permission_classes=[IsAuthenticated]). Subscription: @strawberry.subscription async def count(self, target: int) -> AsyncGenerator[int, None]: for i in range(target): yield i; await asyncio.sleep(1). Pydantic integration: @strawberry.experimental.pydantic.type(model=UserModel) class UserType: name: auto; email: auto. schema.execute_sync(query) for tests. Claude Code generates Strawberry schemas, async resolvers, DataLoader implementations, and subscription handlers.
CLAUDE.md for Strawberry
## Strawberry Stack
- Version: strawberry-graphql >= 0.233 | pip install "strawberry-graphql[fastapi]"
- Type: @strawberry.type|input|enum Class decorators with Python type hints
- Schema: strawberry.Schema(query=Query, mutation=Mutation, subscription=Sub)
- FastAPI: GraphQLRouter(schema, context_getter=get_context)
- Context: info.context["db"/"user"/"loader"] in any resolver
- DataLoader: strawberry.dataloader.DataLoader(load_fn=batch_fn) — batches N+1
- Test: result = await schema.execute(query, variable_values={...})
Strawberry GraphQL Pipeline
# app/graphql_schema.py — Strawberry schema definition
from __future__ import annotations
import asyncio
import logging
from datetime import datetime
from enum import Enum
from typing import AsyncGenerator, Optional
import strawberry
from strawberry.dataloader import DataLoader
from strawberry.permission import BasePermission
from strawberry.types import Info
logger = logging.getLogger(__name__)
# ─────────────────────────────────────────────────────────────────────────────
# Enums
# ─────────────────────────────────────────────────────────────────────────────
@strawberry.enum
class UserRole(Enum):
USER = "user"
MODERATOR = "moderator"
ADMIN = "admin"
@strawberry.enum
class OrderStatus(Enum):
PENDING = "pending"
PAID = "paid"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"
# ─────────────────────────────────────────────────────────────────────────────
# Data store (in-memory for demo — replace with real DB calls)
# ─────────────────────────────────────────────────────────────────────────────
_USERS: dict[str, dict] = {
"1": {"id": "1", "name": "Alice Smith", "email": "[email protected]", "role": "user"},
"2": {"id": "2", "name": "Bob Jones", "email": "[email protected]", "role": "admin"},
"3": {"id": "3", "name": "Carol White", "email": "[email protected]", "role": "user"},
}
_ORDERS: dict[str, dict] = {
"101": {"id": "101", "user_id": "1", "status": "paid", "total": 49.99},
"102": {"id": "102", "user_id": "1", "status": "shipped", "total": 129.00},
"103": {"id": "103", "user_id": "2", "status": "delivered","total": 9.99},
}
# ─────────────────────────────────────────────────────────────────────────────
# Permission classes
# ─────────────────────────────────────────────────────────────────────────────
class IsAuthenticated(BasePermission):
message = "Authentication required"
async def has_permission(self, source, info: Info, **kwargs) -> bool:
return info.context.get("user") is not None
class IsAdminUser(BasePermission):
message = "Admin access required"
async def has_permission(self, source, info: Info, **kwargs) -> bool:
user = info.context.get("user")
return user is not None and user.get("role") == "admin"
# ─────────────────────────────────────────────────────────────────────────────
# GraphQL types
# ─────────────────────────────────────────────────────────────────────────────
@strawberry.type
class OrderType:
id: strawberry.ID
status: OrderStatus
total: float
created_at: Optional[datetime] = None
@strawberry.type
class UserType:
id: strawberry.ID
name: str
email: str
role: UserRole
@strawberry.field
async def orders(self, info: Info) -> list[OrderType]:
"""Resolve orders via DataLoader to avoid N+1 queries."""
loader: DataLoader = info.context["orders_by_user_loader"]
user_orders = await loader.load(str(self.id))
return [
OrderType(
id=o["id"],
status=OrderStatus(o["status"]),
total=o["total"],
)
for o in (user_orders or [])
]
@strawberry.field(permission_classes=[IsAdminUser])
def admin_notes(self) -> str:
"""Only admins can see this field."""
return f"Internal notes for user {self.id}"
@strawberry.type
class PaginatedUsers:
items: list[UserType]
total: int
page: int
page_size: int
has_next: bool
# ─────────────────────────────────────────────────────────────────────────────
# Input types
# ─────────────────────────────────────────────────────────────────────────────
@strawberry.input
class CreateUserInput:
name: str
email: str
role: UserRole = UserRole.USER
@strawberry.input
class UpdateUserInput:
name: Optional[str] = None
email: Optional[str] = None
role: Optional[UserRole] = None
@strawberry.input
class PaginationInput:
page: int = 1
page_size: int = 20
# ─────────────────────────────────────────────────────────────────────────────
# DataLoaders — batch and cache DB calls
# ─────────────────────────────────────────────────────────────────────────────
async def load_orders_by_user(user_ids: list[str]) -> list[list[dict]]:
"""
Batch-load orders for multiple users in one query.
Returns a list aligned with user_ids — same length, items may be empty lists.
"""
# In production: SELECT * FROM orders WHERE user_id = ANY($1)
result: dict[str, list[dict]] = {uid: [] for uid in user_ids}
for order in _ORDERS.values():
uid = order["user_id"]
if uid in result:
result[uid].append(order)
return [result[uid] for uid in user_ids]
def build_context_loaders() -> dict:
return {
"orders_by_user_loader": DataLoader(load_fn=load_orders_by_user),
}
# ─────────────────────────────────────────────────────────────────────────────
# Query type
# ─────────────────────────────────────────────────────────────────────────────
@strawberry.type
class Query:
@strawberry.field
async def user(self, id: strawberry.ID, info: Info) -> Optional[UserType]:
data = _USERS.get(str(id))
if data is None:
return None
return UserType(id=data["id"], name=data["name"],
email=data["email"], role=UserRole(data["role"]))
@strawberry.field
async def users(
self,
info: Info,
pagination: Optional[PaginationInput] = None,
role: Optional[UserRole] = None,
) -> PaginatedUsers:
p = pagination or PaginationInput()
items = list(_USERS.values())
if role is not None:
items = [u for u in items if u["role"] == role.value]
total = len(items)
start = (p.page - 1) * p.page_size
page_items = items[start: start + p.page_size]
return PaginatedUsers(
items=[
UserType(id=u["id"], name=u["name"],
email=u["email"], role=UserRole(u["role"]))
for u in page_items
],
total=total,
page=p.page,
page_size=p.page_size,
has_next=(start + p.page_size) < total,
)
@strawberry.field
async def me(self, info: Info) -> Optional[UserType]:
user = info.context.get("user")
if user is None:
return None
data = _USERS.get(str(user["id"]))
return UserType(id=data["id"], name=data["name"],
email=data["email"], role=UserRole(data["role"])) if data else None
# ─────────────────────────────────────────────────────────────────────────────
# Mutation type
# ─────────────────────────────────────────────────────────────────────────────
@strawberry.type
class Mutation:
@strawberry.mutation(permission_classes=[IsAuthenticated])
async def create_user(self, input: CreateUserInput, info: Info) -> UserType:
if any(u["email"] == input.email for u in _USERS.values()):
raise ValueError(f"Email {input.email!r} already in use")
new_id = str(max(int(k) for k in _USERS) + 1)
user = {"id": new_id, "name": input.name,
"email": input.email, "role": input.role.value}
_USERS[new_id] = user
logger.info("Created user id=%s email=%s", new_id, input.email)
return UserType(id=new_id, name=input.name,
email=input.email, role=input.role)
@strawberry.mutation(permission_classes=[IsAuthenticated])
async def update_user(
self,
id: strawberry.ID,
input: UpdateUserInput,
info: Info,
) -> Optional[UserType]:
user = _USERS.get(str(id))
if user is None:
return None
if input.name is not None: user["name"] = input.name
if input.email is not None: user["email"] = input.email
if input.role is not None: user["role"] = input.role.value
return UserType(id=user["id"], name=user["name"],
email=user["email"], role=UserRole(user["role"]))
@strawberry.mutation(permission_classes=[IsAdminUser])
async def delete_user(self, id: strawberry.ID, info: Info) -> bool:
if str(id) in _USERS:
del _USERS[str(id)]
return True
return False
# ─────────────────────────────────────────────────────────────────────────────
# Subscription type
# ─────────────────────────────────────────────────────────────────────────────
@strawberry.type
class Subscription:
@strawberry.subscription
async def user_created(self, info: Info) -> AsyncGenerator[UserType, None]:
"""Streams new users as they are created (demo: simulates with a counter)."""
for i in range(3):
await asyncio.sleep(1)
yield UserType(
id=strawberry.ID(str(1000 + i)),
name=f"New User {i}",
email=f"new{i}@example.com",
role=UserRole.USER,
)
@strawberry.subscription
async def order_status_changed(
self, order_id: strawberry.ID, info: Info
) -> AsyncGenerator[OrderType, None]:
"""Streams status updates for a specific order."""
statuses = [OrderStatus.PAID, OrderStatus.SHIPPED, OrderStatus.DELIVERED]
for status in statuses:
await asyncio.sleep(2)
yield OrderType(id=order_id, status=status, total=49.99)
# ─────────────────────────────────────────────────────────────────────────────
# Schema and FastAPI integration
# ─────────────────────────────────────────────────────────────────────────────
schema = strawberry.Schema(
query=Query,
mutation=Mutation,
subscription=Subscription,
)
# FastAPI integration:
FASTAPI_EXAMPLE = """
from fastapi import FastAPI, Request
from strawberry.fastapi import GraphQLRouter
from app.graphql_schema import schema, build_context_loaders
async def get_context(request: Request) -> dict:
# Inject auth user and DataLoaders into context
user = getattr(request.state, "user", None)
return {
"request": request,
"user": user,
**build_context_loaders(),
}
graphql_router = GraphQLRouter(schema, context_getter=get_context)
app = FastAPI()
app.include_router(graphql_router, prefix="/graphql")
"""
# Test
if __name__ == "__main__":
result = schema.execute_sync("""
query {
users { items { id name email role } total }
}
""")
print("Errors:", result.errors)
print("Users:", result.data["users"]["total"])
For the Graphene alternative — Graphene requires manual graphene.String(), graphene.List(UserType), and graphene.NonNull() declarations separate from Python type hints, while Strawberry reads standard Python type hints: list[UserType] becomes [UserType!]! in the GraphQL schema automatically, Optional[UserType] becomes UserType, and @strawberry.enum converts a plain Python Enum directly — no .graphene.Enum.from_enum() shim. For the Ariadne (schema-first) alternative — Ariadne requires writing SDL type definitions in strings or separate .graphql files and then binding resolver functions by name, while Strawberry generates SDL from Python classes so you cannot have a mismatch between the type definition and its resolver, @strawberry.field(permission_classes=[IsAuthenticated]) decorates the resolver directly rather than wrapping it in middleware, and DataLoader integrates with info.context so each GraphQL request gets its own loader cache without request-scope leakage. The Claude Skills 360 bundle includes Strawberry skill sets covering @strawberry.type/input/enum decorators, async field resolvers, Query and Mutation classes, permission_classes for field authorization, DataLoader batching for N+1 prevention, Subscription async generators, context injection with GraphQLRouter, PaginatedUsers patterns, error handling with strawberry.extensions, and schema.execute for testing. Start with the free tier to try GraphQL API code generation.