GraphQL API in Python
Uvicorn runs a FastAPI app that mounts Strawberry’s GraphQL router; graphql-core parses, validates and executes each query, and resolvers load rows through SQLAlchemy from PostgreSQL.
Click any box to open its page. Open it in the playground to rewire it, change the traffic or take parts down.
A GraphQL API in Python isn’t one library but five, each with one job. Uvicorn speaks HTTP and calls the app over ASGI. FastAPI routes the request and runs dependencies. Strawberry turns type-annotated classes into a GraphQL schema and plugs into FastAPI as a router. graphql-core, the Python port of the GraphQL reference implementation, parses, validates and executes the query. Your resolvers, async methods on the Strawberry classes, load rows through SQLAlchemy from PostgreSQL. A runnable version of this stack, with SQLite standing in for PostgreSQL, is on the FastAPI page.
How it flows
- 1
The client posts one GraphQL document
A client sends
POST /graphqlwith a JSON body holding thequeryand itsvariables, such as{ orders(customer: "ada") { id totalCents } }. Uvicorn, the ASGI server FastAPI recommends, accepts the connection, parses the HTTP request and calls the app asapp(scope, receive, send): thescopedescribes the request, and the body arrives ashttp.requestmessages fromreceive. Its--workersoption runs several processes.ClientsUvicorn - 2
FastAPI routes it to Strawberry
app.include_router(GraphQLRouter(schema), prefix="/graphql")registered Strawberry’s router like any other FastAPI router, so FastAPI’s routing (Starlette’s, underneath) sends the request there.GraphQLRouteris a FastAPIAPIRouter, so itscontext_getteris a FastAPI dependency: it can useDepends()to check a token or open a database session, and whatever it returns becomes theinfo.contextthat every resolver sees, next to the request, the response and background tasks. The same route serves the GraphiQL explorer to browsers and accepts queries byGETunless you turn that off. - 3
Strawberry hands the query to graphql-core
At startup,
strawberry.Schema(Query)read the type hints on the@strawberry.typeclasses and built a graphql-core schema from them, turning the Python attributetotal_centsinto the GraphQL fieldtotalCents. Per request, Strawberry decodes the body and passes the query, variables and context to graphql-core, the Python port of GraphQL.js that does the GraphQL work itself. Strawberry pins it to one minor version,graphql-core>=3.3.0,<3.4.0. - 4
graphql-core parses, validates, then executes
graphql-core parses the text into a syntax tree of nodes, then validates that tree against the schema with the rules of the GraphQL specification. A misspelled field or a missing required argument stops here with an error that names the line and column, and no resolver runs. A valid query is executed field by field: for each field graphql-core calls its resolver with the parent value and an
infoobject that carries the context, awaits resolvers that are coroutines, and gathers the results into a tree shaped like the query. - 5
Resolvers load rows through SQLAlchemy
The
ordersresolver is anasyncmethod on the StrawberryQueryclass. It takes the session frominfo.contextand awaitssession.scalars(select(OrderRow).where(OrderRow.customer == customer)). SQLAlchemy renders that as SQL for PostgreSQL and runs it through an async driver, and the resolver turns each row into a StrawberryOrder. Keep resolversasync: Strawberry runs plaindefresolvers on the event loop too, so one blocking call stalls every request on that worker. Nested fields that each load their own rows cause N+1 queries; a StrawberryDataLoaderbatches them into one call. - 6
The answer travels back
graphql-core returns an
ExecutionResultwithdataanderrors. A resolver that raises doesn’t sink the whole query: its field becomesnull, and the error, with the field’spath, goes intoerrorsnext to the rest of the data. Strawberry turns the result into the JSON response (itsprocess_resulthook can hide internal errors), Starlette sends it through ASGI, and Uvicorn writes the HTTP response. Even a query that failed validation comes back as HTTP200with anerrorslist, so monitor the errors, not the status codes. - 7
REST routes share the app
Ordinary FastAPI path operations live in the same app. A health check or a
POST /orders/importvalidates its body with a Pydantic model, uses the same dependencies and reaches the same SQLAlchemy session factory; only the/graphqlprefix belongs to Strawberry.
Why this shape works
- Each library does one job, so each can be replaced on its own: another ASGI server, another GraphQL library on graphql-core, another database behind SQLAlchemy
- Every query is checked against the schema before any resolver runs, so misspelled or malformed queries never reach the database
- One FastAPI app serves GraphQL and REST, with the same dependencies for authentication and database sessions
- Async from the socket to the database driver, so one worker process can have many queries waiting on PostgreSQL at once
Where it strains
- Strawberry runs even sync resolvers on the event loop: write them as
async def, or move blocking calls off it withrun_in_threadpool - Nested resolvers that each query the database cause N+1 queries; batch them with a
DataLoader - Each table is described twice, as a SQLAlchemy model and as a Strawberry type, and the two have to be kept in step by hand
- Clients write the queries, so a public API needs depth or cost limits, and failures arrive as HTTP
200with anerrorslist - graphql-core doesn’t follow semantic versioning: a minor version can break its API, which is why Strawberry pins it to
3.3.x
Common swaps
Prefer to write the schema as SDL? Ariadne is schema-first: the schema is a string, resolvers are bound to its fields with objects such as
QueryType, andmake_executable_schemajoins them. FastAPI mounts Ariadne’sGraphQLASGI app withapp.mount("/graphql/", GraphQL(schema)). Ariadne runs on graphql-core 3 too, so parsing, validation and execution don’t change.Graphene, the older code-first library, defines types as
ObjectTypesubclasses withresolve_methods and also runs on graphql-core 3. Starlette’s built-inGraphQLAppfor Graphene was removed in 0.17.0, so FastAPI mounts it through thestarlette-graphene3package instead.If clients don’t need to pick their fields, drop GraphQL: FastAPI path operations with Pydantic request and response models give each resource its own URL, reject bad input with
422, and publish an OpenAPI document with Swagger UI at/docs. Uvicorn, SQLAlchemy and PostgreSQL stay as they are.
Built from
Strawberry GraphQLOpen sourceAPI frameworksA code-first Python GraphQL library: you write dataclass-style classes with type hints, and Strawberry turns them into the schema.
GraphQL-coreOpen sourceAPI frameworksThe Python port of GraphQL.js: the parser, validator and executor that Strawberry, Ariadne and Graphene all run on.Architectures with parts in common
Sources
- Strawberry docs: FastAPI integration (GraphQLRouter, context_getter, sync vs async resolvers)
- Strawberry docs: DataLoaders
- Strawberry pyproject.toml (graphql-core>=3.3.0,<3.4.0)
- GraphQL-core 3 docs: Introduction
- GraphQL-core 3 docs: Parsing GraphQL queries and schema notation
- GraphQL-core 3 docs: Validating GraphQL queries
- GraphQL-core 3 docs: Executing queries (errors and partial data)
- GraphQL-core 3 README (resolver signature, versioning, libraries built on it)
- FastAPI docs: GraphQL
- FastAPI docs: Alternatives, Inspiration and Comparisons (Uvicorn)
- ASGI docs: Introduction (scope, receive, send)
- Ariadne docs: Introduction (schema-first)
- Ariadne docs: FastAPI integration
- Graphene README
- starlette-graphene3 README
- Starlette docs: GraphQL (removed in 0.17.0)