Frontend Architecture & Components
A frontend becomes tangled surprisingly fast: duplicated UI, state that lives in five places, and components nobody can reuse. A little structure keeps it maintainable: composable components, state kept where it belongs, a clear data-fetching pattern, and the firm rule that the UI is never where security or business rules are enforced.
The same design principles that apply to the backend apply to the frontend: separation of concerns, sensible boundaries, and not repeating yourself. In practice this means building small, reusable components, keeping presentation separate from data-fetching and business logic, and being deliberate about where state lives (local versus shared) instead of scattering it.
The security point matters most for juniors: the frontend improves UX, but it is never the security boundary. Anything that must be true — authorization, validation, pricing, limits — is enforced on the server. The client may mirror it for a nicer experience, never in place of it (see Web & Frontend Security).
Structure for maintainability
- DoBuild small, focused, reusable components with clear inputs and outputs (props and events). Compose them instead of copy-pasting UI (see Separation of Concerns).
- DoKeep state where it belongs: local to a component when only it needs it, lifted or shared only when it is genuinely shared. Do not duplicate the same state in several places.
- DoSeparate concerns: presentation components versus data-fetching and logic, so the UI is testable and reusable (see Gotchas: React).
- DoHave one clear pattern for talking to the API, with loading, error, and empty states handled consistently. Avoid ad-hoc fetches everywhere (see API & Contract Design).
- ConsiderA shared component library or design system for consistency and accessibility (see Accessibility), and lazy-loading by route or feature (see Frontend Performance).
- AvoidGiant components that fetch, hold state, contain business rules, and render all at once. They are the frontend version of a god class.
Keep the boundary honest
- AlwaysEnforce every security, authorization, and validation rule on the server. The client may mirror it for UX, but it must never be the only check (see Authentication & Authorization).
- DoValidate user input in the UI for fast feedback, and again on the server for trust. Do both, for different reasons (see Trust Boundaries).
- DoRequest only the data the view needs and only what the user is entitled to. The server decides entitlement, not the component.
- NeverPut secrets, other users' data, or trust decisions in client-side code. The user can read everything shipped to the browser (see Web & Frontend Security).
{user.role === 'admin' && } // hidden, but...
// the delete API has no server-side role check
Hiding the button is not access control. Anyone can call the API directly. The UI check is fine for UX, but the server must enforce the rule, or it is not enforced at all.
{can('delete') && } // nicer UX
// server: [Authorize(Roles="Admin")] on the delete endpoint <-- the real gate
The UI hides what the user cannot do for a clean experience, but the server-side authorization is what actually protects the action.
Self-review checklist
- AskIs this component focused and reusable, or doing too many jobs?
- AskIs state living in the right place, or duplicated across components?
- AskIs any security/authorization decision being made only in the UI?
- AskIs anything sensitive (secrets, others' data) shipped to the browser?