REST Was an Implementation Detail. Prove It.
We said the frontend depended on backend capabilities, not REST. Then we replaced REST with GraphQL.
Transport independence should be demonstrated, not merely claimed.
The architecture already looked independent from REST. Application use cases depended on capabilities. The Orders feature did not know about URLs or HTTP verbs. Angular composition selected the concrete transport at runtime.
Structurally, everything looked right.
But an interface and an architecture diagram are not proof that something is replaceable.
The Claim
The claim was deliberately specific:
The frontend Application depends on the backend capabilities it needs, not on REST.
If that was true, another transport should be able to satisfy the same capabilities without forcing the higher-level frontend architecture to change.
RestOrderApi ───────┐
│
├── same capability boundary
│
GraphqlOrderApi ────┘
↓
same Application
↓
same DomainDefine the Proof Before the Change
Before introducing GraphQL, I defined what success meant.
Application changes = 0
Domain changes = 0
use-case changes = 0
capability contracts = unchanged
Orders feature changes = 0
browser journey changes = 0The controlled variable was narrow:
RestOrderApi + REST server
↓
GraphqlOrderApi + GraphQL serverThis was not a migration project, and it was not an attempt to prove that GraphQL was better than REST.
GraphQL was an architectural probe.
First, Make the Choice Explicit
Before introducing a second transport, the runtime selection of the existing REST adapter needed clearer ownership.
The running application had to choose a concrete implementation somewhere.
app.config.ts
↓
ORDER_API_PROVIDER
↓
RestOrderApiExtracting that selection did not change product behavior. REST still powered the existing application and the already proven cancellation journey.
It simply made the substitution point explicit before another implementation was introduced.
GraphQL Had to Fit the Existing Capability
The new GraphqlOrderApi grew capability by capability until it could satisfy the same backend responsibilities already expected by the Application:
getOrder()
→ Promise<Order>
listOrders()
→ Promise<OrderSummary[]>
placeOrder()
→ Promise<void>
cancelOrder()
→ Promise<void>Then GraphQL challenged the boundary in an interesting way.
A GraphQL mutation could return an updated Order. But the existing Application contract did not ask for one.
await orderApi.cancelOrder(order.id);
order.cancel();The Application already owned that transition after the remote operation succeeded.
Changing the capability contract just because GraphQL offered a different response shape would have allowed the new transport to reshape higher-level behavior.
So the adapter changed to fit the capability. The capability did not change to fit GraphQL.
GraphqlOrderApi
implements OrderApiA Second Transport Had to Be Real
Adapter tests alone were not enough.
They could prove that GraphqlOrderApi sent a request to /graphql and mapped a GraphQL-shaped response into the existing frontend model.
But a mocked response could not prove that a real GraphQL boundary parsed a document, validated a schema, executed resolvers, changed state, and returned the expected result.
So the experiment included a real development GraphQL server.
GraphqlOrderApi
↓
HTTP POST /graphql
↓
GraphQL schema + resolvers
↓
shared development state
↓
GraphQL responseThe mutations were verified through persisted state, not merely successful responses.
ORD-1001
Draft → Submitted
ORD-1002
Submitted → CancelledSubsequent GraphQL queries returned those new states. The second transport was now exercising real behavior.
Run the Same Journey Twice
Even that was not the decisive proof. We now had two adapters and two real development backends, but the architectural claim concerned the whole frontend journey.
The strongest test already existed:
e2e/features/orders/cancel-order.spec.tsThe test described what the customer did. It contained no knowledge of whether REST or GraphQL was behind the capability boundary.
So the same browser journey was executed against both transports:
npm run e2e:rest
→ 1 passed
npm run e2e:graphql
→ 1 passedThe execution path changed:
REST
ORDER_API
↓
RestOrderApi
↓
REST serverGRAPHQL
ORDER_API
↓
GraphqlOrderApi
↓
GraphQL serverBut the higher-level journey remained the same:
same customer journey
│
┌──────┴──────┐
│ │
REST GraphQL
│ │
└──────┬──────┘
│
persisted state
│
authoritative reload
│
CancelledWhat Changed — and What Didn't
CHANGED
UNCHANGED
What It Proved — and What It Didn't
The result does not mean that REST and GraphQL are equivalent.
It does not prove that GraphQL is better.
And it does not prove that every transport in every system can be exchanged without cost.
For one verified Orders journey, REST could be replaced with GraphQL while the higher-level frontend architecture remained unchanged.
That is narrower than claiming complete transport independence across the entire application.
It is also stronger, because the claim is supported by an actual substitution experiment.
The Lesson
Calling REST an implementation detail is easy. Hiding it behind an interface is also easy.
The more useful question is whether another transport can take its place without forcing the higher-level architecture to follow it.
Transport independence should be demonstrated, not merely claimed.
CONTINUE THE ARCHITECTURE JOURNEY
The substitution experiment is only part of the story.
This experiment comes from my book Building Software Around the Business — Not Around the Framework — Frontend Architecture with TypeScript, Angular, and React.
The book follows the architecture from the first frontend behavior through Domain and Application boundaries, state ownership, capability segregation, runtime composition, and the final REST-to-GraphQL substitution proof.
Explore the frontend architecture book →