The Case for Service-to-Service GraphQL

markl422 pts0 comments

The Case for Service-to-Service GraphQL<br>Mark Larah · Jul 7, 2026<br>The Case for Service-to-Service GraphQL

I gave a talk at GraphQL Conf 2026 about using GraphQL for<br>service-to-service communication. This post is a condensed written version of<br>those thoughts.

tl;dr : Using GraphQL for service-to-service communication is not an<br>anti-pattern. This is especially true if your org already uses GraphQL for<br>mobile and web clients — consolidating on GraphQL as a single API surface is a<br>very compelling option.

Specifically, here’s where I think it makes the most sense:

#Use Cases

Here are some good candidates in particular for using GraphQL on the backend:

Server Driven UI: This is a pattern employed at companies such as<br>Netflix, Airbnb and Yelp to enable the server to<br>make decisions about what UI widgets are displayed on the client. In many<br>cases, the SDUI payload sent to the client contains the data already embedded<br>into the component. Which means the SDUI backend service needs data from<br>somewhere…

LLM Driven UI: A special case of the above — pages driven by LLMs<br>(typically conversational UIs). The model needs product and user data in order<br>to generate a response. This could be via MCP, or some prefetched data baked<br>into initial context. Either way - we have a service that needs data!

React Server Components: This one feels like cheating, but still counts!<br>There’s already native client support.

In all cases above, the data we provide may ultimately be displayed to end<br>users. Therefore it needs to have the same business logic and auth checks<br>applied as your existing external API - otherwise we risk leaking private<br>information through the model and back out to the user. So from a safety<br>perspective alone, it would be nice to directly reuse the external GraphQL API<br>which already handles these concerns… on the server :)

#The case against service-to-Service GraphQL

If we’re evaluating the “best” data transport protocol purely for<br>service-to-service use cases, binary protocols offer an excellent developer<br>experience, and win on raw performance alone (e.g. gRPC,<br>cap’n’proto, bebop<br>to name a few). More commonly used however, are old-fashioned REST endpoints<br>(ideally typed with Swagger/OpenAPI or similar).

A “boring” stack would reasonably be:

REST or gRPC for service-to-service communication

GraphQL for client-to-service communication (Yes, I think GraphQL counts as boring now!)

The natural next step is combining these: an external GraphQL API layered over a<br>set of internal endpoints. This is indeed largely how GraphQL evolved and has<br>been deployed at many companies.

More recently however, the industry has shifted towards GraphQL<br>Federation as a way to deploy and orchestrate GraphQL across<br>multiple services — replacing the need to wrap endpoints with a big GraphQL<br>proxy service.

Which leads me to wonder: do we still need multiple APIs? Can we just reuse the<br>GraphQL resolvers for internal service use cases too? Surely this is blasphemy!

#”No! GraphQL is for clients only. Keep separate APIs.”

In a situation where we maintain GraphQL resolvers and existing gRPC/REST<br>endpoints, we have (at least) two APIs over the same sets of data. The trap we<br>might fall into is this — duplicated data definitions over the same underlying<br>data.

Clearly it would be a bad idea to duplicate API handler logic over the same data<br>types each time. A separate business logic layer and/or using<br>code generation tools such as ent or TypeSpec are well<br>established patterns to keep things DRY and consistent. This certainly helps,<br>but some logic cannot easily be normalized into a shared abstraction.

We still end up with:

Per-API schema definitions (e.g. A GraphQL schema and an OpenAPI Contract)

Different error handling patterns per API

Different fields exposed for different use cases

Features that don’t map cleanly to a shared abstraction (e.g. @skip/@include)

For instance, let’s take a closer look at error handling. The internal REST API<br>might simply raise a 404:

@router.get("/business/{id}")<br>def get_business(<br>id: int,<br>) -> BusinessResponse:<br>business = lookup_business(id)

if business is None:<br>raise HTTPException(<br>status_code=404,<br>detail=f"Business {id} not found"

return BusinessResponse(**business)<br>Whereas our GraphQL resolver over the same data might instead return<br>error unions:

@strawberry.type<br>class Query:<br>@strawberry.field<br>def business(self, info, id: strawberry.ID) -> Union[<br>Business,<br>BusinessNotFound,<br>]:<br>business = lookup_business(id)

if business is None:<br>return BusinessNotFound(<br>message=f"Business with id {id} not found"

return Business(**business)<br>This difference would be awkward to bake into the lookup_business(...)<br>business logic layer. Per-API adaptor logic must be done per-API.

Let’s also zoom into the differences between the fields the external (GraphQL) and<br>an internal (REST) service might return for the same domain object:

External GraphQL API

type Business {<br>name: String!<br>phoneNumber: String!<br>isOpen:...

graphql service business data cases logic

Related Articles