Frontend architecture gets expensive when boundaries are unclear. Small shortcuts become difficult coupling points later.
I now optimize for one thing first: predictable change.
Separation by Behavior, Not by File Type
I avoid "all hooks in one folder" and "all components in one folder" structures once the app grows.
Instead, I group by domain and behavior:
- Feature routes
- Shared UI primitives
- Shared data/domain utilities
- Cross-cutting infrastructure
That pattern keeps local changes local.
Server-First Mental Model
With App Router, I default to server components and add client boundaries only when needed for interaction.
export default async function DashboardPage() {
const data = await getDashboardData();
return (
<>
<Summary data={data.summary} />
<InteractiveChart seed={data.chart} />
</>
);
}
This made bundle size and hydration behavior easier to reason about.
UI Primitives Need Strong Contracts
Reusable components are only useful when their API is disciplined.
I try to keep primitives:
- Variant-based
- Accessible by default
- Narrow in surface area
- Composable through slots/children
Overly flexible components usually hide design drift.
Build Systems and Guardrails
Architecture quality drops quickly without guardrails. I rely on:
- ESLint rules for import boundaries
- Type-driven API contracts
- Route-level loading/error states
- Visual regression snapshots for core flows
The goal is not perfection. The goal is to make the wrong path harder than the right one.