
Your data boundary is not “how we call the API.” It’s a coupling contract between teams, runtimes, and failure modes. This post gives you a practical decision framework for REST, GraphQL, and RPC-style boundaries (including tRPC) — with the tradeoffs that show up in production.
Axel Domingues
January was the macro view: why modern web architecture looks like it does.
February was the wire contract: HTTP is already a distributed systems API.
March was the runtime reality: the browser is a scheduler with a frame budget, and “random UI bugs” are often deterministic timing problems.
April is where these threads meet:
Your data boundary decides how product requirements become network calls, how teams coordinate change, and how failures show up in the UI.
And it’s one of the most expensive things to “just refactor later.”
The boundary is the full contract surface:Not the transport (it’s usually HTTP).
Teams often choose a boundary by vibe:
That’s not architecture.
Architecture is choosing where coupling belongs and what kind of coupling your org can afford.
REST-style
Coupling lives in resource design, endpoint semantics, and compatibility rules.
GraphQL-style
Coupling lives in schema governance and resolver behavior.
RPC / tRPC-style
Coupling lives in shared types, shared runtime assumptions, and release cadence.
“Custom fetch + endpoints”
Coupling lives in your team’s heads… until production forces you to formalize it.
Your job isn’t to pick a “best” boundary.
It’s to pick the boundary whose failure modes your team can operate.
The web didn’t evolve because engineers got bored.
It evolved because product requirements demanded new shapes of coupling.
AJAX made the browser a partial application runtime. It also created the first modern failure pattern:
Fetch improved ergonomics and modern flows.
But fetch does not answer:
Fetch is a primitive. Boundaries are policies.
GraphQL moved response shaping into a query language:
It solves a real pain (many clients + volatile UI surfaces).
It also introduces new operational problems:
tRPC is the “shared type system boundary”:
Tradeoff:
When teams ask “REST or GraphQL or tRPC?”, I translate it into five real decisions.
This is not cosmetic — it determines how fast UIs can change without backend work.
If edge caching matters, this choice is heavy.
This post is about making those choices explicit.
GraphQL can reduce over/under fetching.
But the bigger gain is usually organizational:
GraphQL shines when UI surfaces change faster than backend teams can keep up.
The deeper truth:
Overfetching is usually a symptom of bad boundary granularity, not a missing query language.
Choose GraphQL for change velocity across many clients, not for “bandwidth vibes.”
Here’s a framework that avoids ideology and survives architecture reviews.
Whatever you choose, you need:
They choose a tool and skip the governance that makes it safe.
If you force me to choose a default, it’s not a tool — it’s a posture:
Default posture
Start with typed REST + an explicit schema contract (OpenAPI/JSON Schema) and add a BFF where needed. Graduate to GraphQL only when your client landscape and UI volatility justify the governance cost.
Why this tends to work:
But “default” is not “always.” So let’s get concrete.
REST isn’t “old.” REST is discipline.
REST works well when:
/users/{id}, /orders/{id}, /catalog/items/doThing, /execute, /getDashboardDataREST makes the server choose shapes.
That’s a feature when you want:
It’s friction when:
That’s where GraphQL and/or a BFF often enters.
GraphQL shines when:
But you pay for it.
Schema governance cost
Deprecation rules, nullability discipline, ownership, review process.
Resolver performance cost
N+1 avoidance, batching, caching, query planning discipline.
Security + abuse cost
Depth/complexity limits, persisted queries, field-level auth.
Observability cost
Tracing must see resolver hotspots, not just “/graphql was called.”
GraphQL is not “hard.”
GraphQL is a governance system disguised as an API protocol.
That’s a performance and security liability.
GraphQL doesn’t eliminate backend architecture.
It moves it.
tRPC is compelling when:
tRPC becomes painful when:
tRPC is basically:
“Our boundary is the TypeScript compiler.”
That’s powerful. It’s also a coupling choice.
A common lifecycle:
That’s not failure. That’s maturity.
BFF isn’t a protocol.
It’s an architectural placement: a server layer allowed to be UI-shaped.
A BFF can sit on top of:
Its purpose is to:
BFF is good when
UI is volatile, domain services are stable, and you want independent evolution.
BFF is risky when
It becomes “the real backend” with no ownership, no tests, and no performance budgets.
BFF is often the pragmatic middle ground:
Use this as a starting point, not a ideology.
Choose REST-style when:
Choose GraphQL when:
Choose tRPC/RPC when:
Add a BFF when:
Most production pain comes from missing guardrails, not the boundary choice.
These are non-negotiable in serious teams:
Standard error contract
One error shape, one tracing story, one retry story.
Versioning + deprecation rules
Additive change by default. Breaking change requires an explicit plan.
Observability
Trace IDs, per-endpoint/per-resolver timings, client-side correlation.
Performance budgets
Response size limits, query depth limits, timeouts aligned to user expectations.
Install these guardrails early and you can survive almost any boundary decision.
Skip them and even the “best” boundary will eventually fail under load and change velocity.
Search boxes and filters are concurrency machines. Make “latest wins” explicit.
// Pseudocode: AbortController + request versioning
let current = 0
let abort: AbortController | null = null
async function load(query: string) {
current += 1
const version = current
abort?.abort()
abort = new AbortController()
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`, { signal: abort.signal })
const data = await res.json()
if (version !== current) return // ignore stale responses
render(data)
}
This applies to REST, GraphQL, and tRPC alike.
If the action can create side effects, the boundary must support idempotency.
That’s not “backend stuff.”
That’s the boundary doing its job.
Boundaries aren’t DX preferences.
They’re organizational commitments:
Pick the one you can operate.
April takeaway
A data boundary is a coupling contract. Choose it by org model + change velocity + operability, not by hype.
MDN — Fetch API
Fetch is the primitive. Understanding it helps regardless of the boundary you choose.
No. GraphQL is a different boundary with a different governance model.
Some orgs thrive with it (many clients + volatile UI). Some orgs drown in it (no limits, weak tracing, unclear ownership).
End-to-end types are fantastic for DX.
But “better” depends on coupling tolerance:
They treat REST as “routes + JSON” and skip discipline:
Good REST is a stability tool.
They ship a schema without an operating model:
GraphQL without governance becomes a performance and security problem.
Now that we’ve chosen where data contracts live, we need to confront the layer that consumes them:
React.
Not as a UI library — as an architectural tool with cost models.
React as an Architecture Tool: Components, Hooks, and the Cost of Re-rendering
React is not a UI library — it’s a runtime for composing change. This post teaches the mental model that makes large React codebases predictable: component boundaries, hooks as architecture, and the real cost model of re-rendering (so you optimize the right thing).
Browser Reality: The Event Loop, Rendering, and Why UX Bugs Look Like Backend Bugs
The browser is a constrained runtime with a scheduling problem: one main thread, many responsibilities, and users who notice missed frames. This post gives you the mental model to debug “random” UX failures as deterministic timing and contention issues.