strawberry-graphql

Strawberry GraphQL Patterns

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "strawberry-graphql" with this command: npx skills add yonatangross/orchestkit/yonatangross-orchestkit-strawberry-graphql

Strawberry GraphQL Patterns

Type-safe GraphQL in Python with code-first schema definition.

Overview

  • Complex data relationships (nested queries, multiple entities)

  • Client-driven data fetching (mobile apps, SPAs)

  • Real-time features (subscriptions for live updates)

  • Federated microservice architecture

When NOT to Use

  • Simple CRUD APIs (REST is simpler)

  • Internal microservice communication (use gRPC)

Schema Definition

import strawberry from datetime import datetime from strawberry import Private

@strawberry.enum class UserStatus: ACTIVE = "active" INACTIVE = "inactive"

@strawberry.type class User: id: strawberry.ID email: str name: str status: UserStatus password_hash: Private[str] # Not exposed in schema

@strawberry.field
def display_name(self) -> str:
    return f"{self.name} ({self.email})"

@strawberry.field
async def posts(self, info: strawberry.Info, limit: int = 10) -> list["Post"]:
    return await info.context.post_loader.load_by_user(self.id, limit)

@strawberry.type class Post: id: strawberry.ID title: str content: str author_id: strawberry.ID

@strawberry.field
async def author(self, info: strawberry.Info) -> User:
    return await info.context.user_loader.load(self.author_id)

@strawberry.input class CreateUserInput: email: str name: str password: str

Query and Mutation

@strawberry.type class Query: @strawberry.field async def user(self, info: strawberry.Info, id: strawberry.ID) -> User | None: return await info.context.user_service.get(id)

@strawberry.field
async def me(self, info: strawberry.Info) -> User | None:
    user_id = info.context.current_user_id
    return await info.context.user_service.get(user_id) if user_id else None

@strawberry.type class Mutation: @strawberry.mutation async def create_user(self, info: strawberry.Info, input: CreateUserInput) -> User: return await info.context.user_service.create( email=input.email, name=input.name, password=input.password )

@strawberry.mutation
async def delete_user(self, info: strawberry.Info, id: strawberry.ID) -> bool:
    await info.context.user_service.delete(id)
    return True

DataLoader (N+1 Prevention)

from strawberry.dataloader import DataLoader

class UserLoader(DataLoader[str, User]): def init(self, user_repo): super().init(load_fn=self.batch_load) self.user_repo = user_repo

async def batch_load(self, keys: list[str]) -> list[User]:
    users = await self.user_repo.get_many(keys)
    user_map = {u.id: u for u in users}
    return [user_map.get(key) for key in keys]

class GraphQLContext: def init(self, request, user_service, user_repo, post_repo): self.request = request self.user_service = user_service self.user_loader = UserLoader(user_repo) self._current_user_id = None

@property
def current_user_id(self) -> str | None:
    if self._current_user_id is None:
        token = self.request.headers.get("authorization", "").replace("Bearer ", "")
        self._current_user_id = decode_token(token) if token else None
    return self._current_user_id

FastAPI Integration

from fastapi import FastAPI, Request, Depends from strawberry.fastapi import GraphQLRouter

schema = strawberry.Schema(query=Query, mutation=Mutation, subscription=Subscription)

async def get_context(request: Request, user_service=Depends(get_user_service)) -> GraphQLContext: return GraphQLContext(request=request, user_service=user_service, ...)

graphql_router = GraphQLRouter(schema, context_getter=get_context, graphiql=True)

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

Subscriptions

from typing import AsyncGenerator

@strawberry.type class Subscription: @strawberry.subscription async def user_updated(self, info: strawberry.Info, user_id: strawberry.ID) -> AsyncGenerator[User, None]: async for message in info.context.pubsub.subscribe(f"user:{user_id}:updated"): yield User(**message)

@strawberry.subscription
async def notifications(self, info: strawberry.Info) -> AsyncGenerator["Notification", None]:
    user_id = info.context.current_user_id
    if not user_id:
        raise PermissionError("Authentication required")
    async for message in info.context.pubsub.subscribe(f"user:{user_id}:notifications"):
        yield Notification(**message)

Authentication and Authorization

from strawberry.permission import BasePermission

class IsAuthenticated(BasePermission): message = "User is not authenticated"

async def has_permission(self, source, info: strawberry.Info, **kwargs) -> bool:
    return info.context.current_user_id is not None

class IsAdmin(BasePermission): message = "Admin access required"

async def has_permission(self, source, info: strawberry.Info, **kwargs) -> bool:
    user_id = info.context.current_user_id
    if not user_id:
        return False
    user = await info.context.user_service.get(user_id)
    return user and user.role == "admin"

Usage

@strawberry.type class Query: @strawberry.field(permission_classes=[IsAuthenticated]) async def me(self, info: strawberry.Info) -> User: return await info.context.user_service.get(info.context.current_user_id)

@strawberry.field(permission_classes=[IsAdmin])
async def all_users(self, info: strawberry.Info) -> list[User]:
    return await info.context.user_service.list_all()

Error Handling with Union Types

@strawberry.type class CreateUserSuccess: user: User

@strawberry.type class UserError: message: str code: str field: str | None = None

@strawberry.type class CreateUserError: errors: list[UserError]

CreateUserResult = strawberry.union("CreateUserResult", [CreateUserSuccess, CreateUserError])

@strawberry.type class Mutation: @strawberry.mutation async def create_user(self, info: strawberry.Info, input: CreateUserInput) -> CreateUserResult: errors = [] if not is_valid_email(input.email): errors.append(UserError(message="Invalid email", code="INVALID_EMAIL", field="email")) if errors: return CreateUserError(errors=errors)

    try:
        user = await info.context.user_service.create(**input.__dict__)
        return CreateUserSuccess(user=user)
    except DuplicateEmailError:
        return CreateUserError(errors=[UserError(message="Email exists", code="DUPLICATE_EMAIL", field="email")])

Key Decisions

Decision Recommendation

Schema approach Code-first with Strawberry types

N+1 prevention DataLoader for all nested resolvers

Pagination Relay-style cursor pagination

Auth Permission classes, context-based

Errors Union types for mutations

Subscriptions Redis PubSub for horizontal scaling

Anti-Patterns (FORBIDDEN)

NEVER make database calls in resolver loops (N+1 queries!)

for post_id in self.post_ids: posts.append(await db.get_post(post_id))

CORRECT: Use DataLoader

return await info.context.post_loader.load_many(self.post_ids)

NEVER expose internal IDs without encoding

id: int # Exposes auto-increment ID!

CORRECT: Use opaque IDs

id: strawberry.ID # base64 encoded

NEVER skip input validation in mutations

Related Skills

  • api-design-framework

  • REST API patterns

  • grpc-python

  • gRPC alternative

  • streaming-api-patterns

  • WebSocket patterns

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

responsive-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
General

domain-driven-design

No summary provided by upstream source.

Repository SourceNeeds Review
General

dashboard-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
General

rag-retrieval

No summary provided by upstream source.

Repository SourceNeeds Review