
The month I stop treating “frontend architecture” as component trivia and start treating it as a system: URLs as contracts, state as truth, forms as transactions, and a boring stack you can operate.
Axel Domingues
If May was about React as an architectural tool, June is about the thing React doesn’t give you:
A system.
Because real frontends don’t fail in the component tree. They fail at the boundaries:
So this month I’m going to define the boring frontend stack:
Not boring because it’s simple.
Boring because it’s the part you want to stop debating and start standardizing, so your product can scale teams, features, and reliability without re-litigating fundamentals every sprint.
The goal this month
Turn routing, state, and forms into explicit architecture decisions — with checklists you can teach and repeat.
The output
A “boring stack” reference architecture you can adopt, audit, and evolve — without ideology.
The browser is an unreliable runtime:
So when teams say “the frontend is hard”, what they mean is:
There’s a lot of implicit distributed-systems behavior hiding inside UI interactions.
This article is how you make that behavior explicit.
Here’s the mental model I want you to internalize:
Routing boundary
The URL is the public API of your UI. If it lies, everything else becomes hacks.
State boundary
Your app has multiple “truth sources.” If you don’t classify them, you’ll fight ghosts.
Form boundary
A form is a transaction builder. Treat it like one: validation, idempotency, failure recovery.
Data boundary
Server state isn’t “frontend state.” Cache it, invalidate it, and stop copying it into global stores.
If you do nothing else: write these boundaries down and make them reviewable.
A good URL is not “a path.” It’s a projection of user intent into a stable contract:
If you treat routing as an implementation detail, you’ll end up with:
The URL is great for:
The URL is not great for:
People compare routers like a dependency list.
Architects compare routers by capabilities:
Framework routing (e.g., file-based) tends to win when:
Library routing tends to win when:
Most frontend architecture fights come from one category error:
treating all state as one thing.
A scalable frontend distinguishes at least these types:
UI state (ephemeral)
Open/closed, hover, selected row, local toggles. Lives near components.
URL state (shareable)
Filters, pagination, active tabs, selected entity. Must survive refresh.
Server state (cached)
Data from APIs. Must handle staleness, retries, invalidation, dedupe.
Client domain state (owned)
Drafts, optimistic updates, complex editors. You own the source of truth.
When you’re about to add state, ask these in order:
If the value can be computed from props / existing state, don’t store it. Derived state is a common source of bugs.
If yes, put it in the URL (search params / path params).
If the data comes from the server, treat it as cached:
Only then consider a store (context/store library). Keep the surface area small.
You lose cache semantics and you invent your own invalidation bugs.
This is the pragmatic stack I keep seeing win (and why it’s boring):
That’s it.
Everything else is bikeshedding.
Good candidates
Bad candidates
A form is where user intent becomes a write.
That’s a transactional boundary. Treat it with the same seriousness you treat a backend write path.
When you design forms explicitly, you stop shipping “mystery UX”:
Idle → Editing
Inputs update locally, validation is friendly, UI is responsive.
Editing → Submitting
Disable double submit, show progress, preserve user intent.
Submitting → Success
Route or update cache, show confirmation, clear draft.
Submitting → Failure
Keep inputs, map errors to fields, provide recovery, avoid rage-click loops.
Examples:
Goal: reduce friction and prevent “avoidable errors”.
Use a schema (shared if possible) to define:
Goal: prevent divergent rules across components/pages.
Server must still validate:
Goal: the UI is not your firewall.
If a form triggers anything expensive or irreversible (payments, subscriptions, transfers, order placement), you need a combined UX + systems design pattern:
A simple model that works well:
This is where frontend architecture and backend architecture meet — and why I teach them together.
I’m not prescribing an ideology. I’m prescribing roles.
Here’s a stack composition that tends to be stable:
Routing
Nested routes, layout shells, route-level errors, search params as first-class.
Server state
A query cache with invalidation, retries, dedupe, optimistic updates.
Forms
Uncontrolled where possible, schema-driven validation, server error mapping.
Client store (small)
Only for true shared client-owned state: session summary, drafts, flags.
A practical shape (adjust for your framework):
routes/ or pages/ — route modules + loaders + route-level error UIfeatures/ — domain slices (billing, search, onboarding)components/ — shared presentational componentsui/ — design system wrappers (buttons, inputs, layout primitives)lib/ — api clients, schema, utilitiesstate/ — store + query client configurationforms/ — shared form patterns (field wrappers, error mappers)If your frontend is already messy, don’t rewrite.
Standardize incrementally:
Move filters/tabs/pagination into search params. Make deep links work. Fix back button bugs.
Introduce a query cache layer. Move “fetch + cache + invalidate” into that layer. Delete duplicated selectors and reducers gradually.
Pick one form approach for the app. Adopt schema validation. Standardize server error mapping.
Write down:
Then enforce in PR reviews.
This is the theme of this month:
When you classify routing/state/forms as architecture boundaries, these stop being mysteries.
Not by default.
Most apps can go surprisingly far with:
Add a store only when you have true shared client-owned state that cannot be derived or cached.
If the modal represents a meaningful state a user might want to:
…then yes, make it routable.
If it’s purely ephemeral (tooltip-level), keep it local.
Prefer uncontrolled inputs where possible, localize state, and avoid routing/layout patterns that remount the entire form on minor navigation changes.
Also: keep expensive derived computations out of render paths.
The “best” is the one that fits your constraints and your team.
But the architecture doesn’t start with brand names. It starts with responsibilities:
June was about building a frontend that can scale features and teams without collapsing into folklore.
Next month, I’m taking the same “boring” mindset to the server side: Backends: Frameworks Don’t Matter Until They Do
Because the frontend stack only stays boring if the backend is predictable: auth, errors, contracts, consistency, and rollout safety.
Backends: Frameworks Don’t Matter Until They Do (Node, Java, .NET, Go, Python)
Early on, any backend “works.” Then timeouts, GC pauses, cold starts, and operability show up. This is a practical mental model for choosing runtimes and frameworks based on constraints—not vibes.
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).