DEV Community

郑沛沛
郑沛沛

Posted on

GraphQL with Python: Build Flexible APIs Using Strawberry

REST is great, but sometimes clients need more flexibility. GraphQL lets clients request exactly the data they need. Here's how to build a GraphQL API in Python.

Why GraphQL?

  • No over-fetching: client gets only requested fields
  • No under-fetching: get related data in one request
  • Strongly typed schema serves as documentation
  • Great for mobile apps and complex frontends

Setup with Strawberry

pip install strawberry-graphql[fastapi]
Enter fullscreen mode Exit fullscreen mode

Basic Schema

import strawberry
from strawberry.fastapi import GraphQLRouter
from fastapi import FastAPI

@strawberry.type
class User:
    id: int
    name: str
    email: str

@strawberry.type
class Post:
    id: int
    title: str
    content: str
    author: User

# Sample data
users_db = [
    {"id": 1, "name": "Alice", "email": "alice@example.com"},
    {"id": 2, "name": "Bob", "email": "bob@example.com"},
]
posts_db = [
    {"id": 1, "title": "Hello GraphQL", "content": "GraphQL is awesome", "author_id": 1},
    {"id": 2, "title": "Python Tips", "content": "Use type hints", "author_id": 1},
    {"id": 3, "title": "Docker Guide", "content": "Containers 101", "author_id": 2},
]

@strawberry.type
class Query:
    @strawberry.field
    def users(self) -> list[User]:
        return [User(**u) for u in users_db]

    @strawberry.field
    def user(self, id: int) -> User | None:
        for u in users_db:
            if u["id"] == id:
                return User(**u)
        return None

    @strawberry.field
    def posts(self, limit: int = 10) -> list[Post]:
        results = []
        for p in posts_db[:limit]:
            author = next(u for u in users_db if u["id"] == p["author_id"])
            results.append(Post(id=p["id"], title=p["title"], content=p["content"], author=User(**author)))
        return results

schema = strawberry.Schema(query=Query)
app = FastAPI()
app.include_router(GraphQLRouter(schema), prefix="/graphql")
Enter fullscreen mode Exit fullscreen mode

Querying

# Get only what you need
query {
  users {
    name
    email
  }
}

# Nested data in one request
query {
  posts(limit: 5) {
    title
    author {
      name
    }
  }
}

# Single user
query {
  user(id: 1) {
    name
    email
  }
}
Enter fullscreen mode Exit fullscreen mode

Mutations

@strawberry.input
class CreatePostInput:
    title: str
    content: str
    author_id: int

@strawberry.type
class Mutation:
    @strawberry.mutation
    def create_post(self, input: CreatePostInput) -> Post:
        new_id = max(p["id"] for p in posts_db) + 1
        author = next(u for u in users_db if u["id"] == input.author_id)
        post_data = {
            "id": new_id,
            "title": input.title,
            "content": input.content,
            "author_id": input.author_id,
        }
        posts_db.append(post_data)
        return Post(id=new_id, title=input.title, content=input.content, author=User(**author))

    @strawberry.mutation
    def delete_post(self, id: int) -> bool:
        for i, p in enumerate(posts_db):
            if p["id"] == id:
                posts_db.pop(i)
                return True
        return False

schema = strawberry.Schema(query=Query, mutation=Mutation)
Enter fullscreen mode Exit fullscreen mode
mutation {
  createPost(input: { title: "New Post", content: "Hello!", authorId: 1 }) {
    id
    title
    author { name }
  }
}
Enter fullscreen mode Exit fullscreen mode

DataLoader: Solving N+1 Queries

from strawberry.dataloader import DataLoader

async def load_users(ids: list[int]) -> list[User]:
    # Single batch query instead of N queries
    users = await db.fetch_all("SELECT * FROM users WHERE id = ANY($1)", ids)
    user_map = {u["id"]: User(**u) for u in users}
    return [user_map.get(id) for id in ids]

user_loader = DataLoader(load_fn=load_users)

@strawberry.type
class Post:
    id: int
    title: str
    author_id: strawberry.Private[int]

    @strawberry.field
    async def author(self) -> User:
        return await user_loader.load(self.author_id)
Enter fullscreen mode Exit fullscreen mode

Authentication in GraphQL

from strawberry.types import Info
from fastapi import Depends

async def get_context(current_user=Depends(get_current_user)):
    return {"user": current_user}

@strawberry.type
class Query:
    @strawberry.field
    def me(self, info: Info) -> User:
        user = info.context["user"]
        if not user:
            raise Exception("Not authenticated")
        return user

app.include_router(
    GraphQLRouter(schema, context_getter=get_context),
    prefix="/graphql"
)
Enter fullscreen mode Exit fullscreen mode

Key Takeaways

  1. GraphQL shines when clients need flexible data fetching
  2. Strawberry provides a Pythonic, type-safe GraphQL experience
  3. Use DataLoaders to prevent N+1 query problems
  4. Mutations handle writes, queries handle reads
  5. Authentication works through context injection

6. GraphQL playground at /graphql gives you interactive docs for free

🚀 Level up your AI workflow! Check out my AI Developer Mega Prompt Pack — 80 battle-tested prompts for developers. $9.99

Top comments (0)