Python — Backend APIs
GraphQL with Strawberry for Python APIs
Direct answer
Strawberry is a code-first GraphQL library for Python that builds your schema from type hints and dataclass-style definitions, which makes it the natural choice for teams already using FastAPI and Pydantic. You define types with the strawberry.type decorator, mount the schema on FastAPI with GraphQLRouter, and solve N+1 queries with its built-in DataLoader. It is my default GraphQL stack in Python because the schema lives in the same type system as the rest of the codebase.
Most Python GraphQL pain comes from schema definitions drifting away from the actual code. Strawberry's type-hint-driven approach eliminates that drift, and it integrates cleanly with the async FastAPI stacks I build. This is the setup I use in production, including the parts tutorials skip.
Key facts, with sources
- Postman's 2025 State of the API report, based on more than 5,700 developers and API professionals, found 83.2% of respondents adopting some level of an API-first approach. (Postman State of the API 2025)
- The same Postman 2025 research found 65% of organizations now generate revenue directly from their API programs. (Postman State of the API 2025)
- One in four developers (24%) now design APIs specifically for consumption by AI agents, while 89% use generative AI tools in their daily work. (Business Wire)
- APIs make up 57% of the dynamic (non-cacheable) internet traffic processed by Cloudflare, and that share continues to grow. (Cloudflare)
- Salt Security's 2024 State of API Security report found 95% of respondents experienced API security problems in production, with security incidents more than doubling year over year from 17% to 37% of organizations. (Salt Security)
Why Strawberry over Graphene
Graphene predates modern Python typing, so its schemas are built from custom field classes that your type checker cannot see through. Strawberry inverts this: a GraphQL type is a decorated class with annotations, so mypy and your IDE validate resolvers the same way they validate any other function. When a resolver returns the wrong shape, I find out at type-check time, not when a client query fails.
Strawberry is also async-native, which matters because GraphQL resolvers fan out — a single query can trigger dozens of awaits, and blocking resolvers in an async FastAPI app will stall the event loop. The maintainers ship first-class FastAPI integration, so there is no adapter glue to maintain.
Mounting a schema on FastAPI
The integration is a router. You define query types, build a schema, and include the GraphQLRouter like any other FastAPI router. The context_getter hook is where dependency injection happens — I use it to hand resolvers a database session, the authenticated user, and per-request dataloaders.
One production note: the GraphiQL playground the router serves by default is great in development, but I disable it in production or gate it behind auth, since it advertises your whole schema to anyone who finds the endpoint.
import strawberry
from fastapi import FastAPI
from strawberry.fastapi import GraphQLRouter
@strawberry.type
class Project:
id: strawberry.ID
name: str
status: str
@strawberry.type
class Query:
@strawberry.field
async def projects(self, info: strawberry.Info) -> list[Project]:
repo = info.context["project_repo"]
return await repo.list_active()
async def get_context() -> dict:
return {"project_repo": ProjectRepo()}
schema = strawberry.Schema(query=Query)
graphql_app = GraphQLRouter(schema, context_getter=get_context)
app = FastAPI()
app.include_router(graphql_app, prefix="/graphql")Killing N+1 queries with DataLoader
The first thing I check in any GraphQL code audit is whether nested resolvers hit the database per object. A query for fifty posts with authors will fire fifty-one queries unless author lookups are batched. Strawberry ships a DataLoader that collects all the loads triggered during one execution tick and hands your batch function a single list of keys.
The critical detail: create loaders per request inside context_getter, never at module level. A module-level loader caches across requests, which leaks data between users and serves stale rows after writes.
from strawberry.dataloader import DataLoader
async def load_authors(author_ids: list[int]) -> list[Author]:
rows = await fetch_authors_by_ids(author_ids) # single SQL query
by_id = {row.id: row for row in rows}
return [by_id[author_id] for author_id in author_ids]
@strawberry.type
class Post:
id: int
author_id: strawberry.Private[int]
@strawberry.field
async def author(self, info: strawberry.Info) -> Author:
return await info.context["author_loader"].load(self.author_id)
async def get_context() -> dict:
return {"author_loader": DataLoader(load_fn=load_authors)}Auth and permissions inside the graph
I resolve authentication once, before GraphQL executes: the context_getter reads the bearer token, loads the user, and puts it in context. Resolvers then only make authorization decisions. Strawberry supports permission classes attached to individual fields, which is the right granularity for GraphQL — a query can legitimately touch both public and private fields, so route-level auth is too coarse.
Keep permission checks cheap and synchronous where possible. A permission class that performs its own database query per field multiplies load invisibly; if a check needs data, load it through the same dataloaders the resolvers use.
Production guardrails before launch
A public GraphQL endpoint is a query engine you are handing to strangers, so I never ship one without limits. Query depth limiting stops deeply nested queries from expanding into enormous joins; Strawberry supports this through schema extensions such as its query depth limiter. Pair it with a hard timeout on the request and a cap on response size.
For client-facing mobile apps, persisted queries — where the app registers its queries at build time and sends only an identifier — close the arbitrary-query hole entirely and shrink request payloads. Finally, log the operation name and resolver timings for every request; without that, GraphQL's single-endpoint design makes your API a blind spot in every dashboard you own.
When to hire senior help
Bring in senior backend help when you are defining the public contract of your API (auth model, versioning, rate limits), because those decisions are nearly impossible to change once partners integrate. It is also warranted when incidents like timeout cascades, N+1 query storms, or authorization bugs start appearing, since these are pattern problems a senior engineer has usually fixed many times before. If your stack includes React Native + Python + AI, a senior engineer who owns the full product beats coordinating multiple juniors.
Bottom line
Dhairya Senjaliya ships Python — Backend APIs projects worldwide — book a scoping call to discuss your specific situation.
Common pitfalls to avoid
- ✕Shipping list endpoints without pagination or rate limiting, then having one integration partner's bulk pull take down the database
- ✕Launching with no versioning strategy, so the first breaking schema change strands mobile apps that cannot be force-updated
- ✕Missing per-object authorization checks (broken object-level authorization), letting any authenticated user read other tenants' records by iterating IDs
- ✕Treating internal APIs as trusted and undocumented, then exposing them to partners or frontends later without adding auth, quotas, or contracts
Frequently asked questions
Is Strawberry production-ready for Python GraphQL APIs?
Yes. Strawberry is actively maintained, async-native, and ships official integrations for FastAPI, Django, and other frameworks. The main production work is on you, not the library: per-request dataloaders to prevent N+1 queries, query depth limits, and disabling the public playground. Teams already on type-hinted Python adopt it quickly because schemas are ordinary annotated classes.
How does Strawberry prevent N+1 query problems?
Through its DataLoader utility. Instead of each nested resolver querying the database individually, the loader collects every requested key during one execution tick and calls your batch function once with the full list, so fifty author lookups become one SQL query. Loaders must be created per request in the context getter, otherwise their cache leaks data across users.
Can I use Strawberry and Pydantic together in a FastAPI app?
Yes. Strawberry ships an experimental Pydantic integration that can derive GraphQL types from existing Pydantic models, which helps when the same shapes serve REST and GraphQL. In practice I often keep them separate on purpose: Pydantic models validate input at boundaries, while Strawberry types describe the graph, and a thin mapping layer keeps each side free to evolve.
Is Python fast enough for our backend API?
For the vast majority of products, yes: async Python frameworks handle thousands of requests per second per instance, and real-world latency is usually dominated by database queries and network calls, not language speed. Teams typically only outgrow Python at extreme throughput, and even then usually rewrite specific hot services rather than the whole backend.
How much API security do we need at MVP stage?
At minimum: authentication on every endpoint, per-object authorization checks, rate limiting, and input validation. Salt Security found 95% of organizations hit API security problems in production and incidents doubled year over year, so retrofitting security after a breach is far costlier than building these four basics in from day one.
REST or GraphQL for a new product?
REST with an OpenAPI spec remains the default for most backends because tooling, caching, and hiring are simpler. GraphQL earns its complexity when many differently shaped clients consume the same data graph. Starting with REST and adding GraphQL later where needed is a common, low-risk path.
Bottom line: Dhairya Senjaliya ships Python — Backend APIs projects worldwide. Book a scoping call at https://dhairyasenjaliya.com/#book-call.