Batteries-included toolkit for building Strawberry GraphQL APIs backed by SQLAlchemy
Source Code: https://github.com/Alteian/strawberry-alchemy
Documentation: docs/index.md · API reference
strawberry-alchemy turns Strawberry GraphQL selection sets into a single optimized
SQLAlchemy query — automatic selectinload for relationships, column deferral, and
SQL EXISTS/COUNT annotations — and ships the supporting cast: filter inputs,
Relay pagination, CRUD repositories, row-level security, permissions, and mapping
helpers.
- No N+1 queries: nested selections become eager loads automatically
- Unrequested columns are deferred — you only fetch what the client asked for
- Declarative computed fields (
hasComments,coverThumbUrl, counts) resolved by SQL subqueries AND/ORfilters, relationship joins, and custom per-model filters from Strawberry inputs- Relay
Connectionpagination withtotalCount - Async repositories with lifecycle hooks and cascade deletes
- Row-level access filters applied to every query — and to
totalCount
pip install strawberry-alchemy
# or
uv add strawberry-alchemyRequires Python >= 3.13, Strawberry GraphQL >= 0.220, SQLAlchemy, Pydantic v2.
# models.py
import uuid
from sqlalchemy import ForeignKey
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column
from strawberry_alchemy.models import Base
class Post(Base):
user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("user.id"))
title: Mapped[str]
body: Mapped[str]# access_filters.py
from typing import Any
from models import Post
from strawberry_alchemy.filtering import AccessControlFilter
class PostAccessFilter(AccessControlFilter):
model_class = Post
@staticmethod
async def apply_filter(query: Any, model: type[Any], context_user: Any) -> Any:
return query.where(model.user_id == context_user.id)# types.py
import strawberry
from typing import ClassVar
from strawberry_alchemy import BaseNodeType
@strawberry.type
class PostType(BaseNodeType):
access_filter: ClassVar = PostAccessFilter()
title: str | None = strawberry.UNSET
body: str | None = strawberry.UNSET# queries.py
import strawberry
from strawberry.types import Info
from strawberry_alchemy import ListResult
@strawberry.type
class Query:
@strawberry.field
async def posts(self, info: Info, limit: int | None = None) -> ListResult[PostType]:
return await PostType.resolve_list(info=info, limit=limit)# schema.py
import strawberry
from strawberry.schema.config import StrawberryConfig
schema = strawberry.Schema(
query=Query,
config=StrawberryConfig(auto_camel_case=True, relay_max_results=100),
)query {
posts(limit: 10) {
items { id title body }
totalCount
}
}Your request context needs three things: get_session(), an identity/user
attribute, and a db_execution_lock (see
docs/getting-started.md#context-contract).
| Page | Topic |
|---|---|
| docs/index.md | Overview, features, architecture |
| docs/getting-started.md | Full walkthrough: models, types, filters, queries, mutations |
| docs/types-and-models.md | Base, BaseNodeType, ListResult, connections, ordering |
| docs/queries.md | resolve_node / resolve_list / resolve_connection, pagination |
| docs/query-optimizer.md | Load strategies, @optimize_field hints, QueryAnalyzer |
| docs/filtering.md | Filter inputs, operators, AND/OR, custom & access-control filters |
| docs/repository.md | CRUD, relations, deletion handlers |
| docs/mapping-and-schema.md | BaseSchema, SQLAlchemy → Strawberry mapping |
| docs/permissions.md | Permission classes, resource checks |
| docs/api-reference.md | Every public export with signatures |
git clone https://github.com/Alteian/strawberry-alchemy.git
cd strawberry-alchemy
uv sync
# Lint & test
uv run ruff check .
uv run pytest -v
# Build
uv buildSee CONTRIBUTING.md for guidelines.