Architecture
Architecture Overview
Frontend architecture patterns - component design, design patterns, micro-frontends, and monorepos
Frontend Architecture
Good architecture enables teams to build scalable, maintainable applications. This section covers patterns and practices for structuring frontend projects.
Core Topics
Component Design
Composition patterns, prop design, and reusability
Design Patterns
Common patterns for frontend development
Micro-Frontends
Breaking monoliths into independent applications
Monorepo
Managing multiple packages in a single repository
Architecture Principles
Separation of Concerns
Application Layers
├── Presentation Layer
│ ├── UI Components
│ ├── Styling
│ └── User Interactions
├── Business Logic Layer
│ ├── State Management
│ ├── Data Transformation
│ └── Validation
└── Data Layer
├── API Communication
├── Caching
└── PersistenceKey Principles
| Principle | Description |
|---|---|
| Single Responsibility | Each module/component has one job |
| Open/Closed | Open for extension, closed for modification |
| Dependency Inversion | Depend on abstractions, not implementations |
| DRY | Don't Repeat Yourself (but don't over-abstract) |
| KISS | Keep It Simple, Stupid |
| YAGNI | You Aren't Gonna Need It |
Project Structure
Feature-Based Structure (Recommended)
src/
├── features/
│ ├── auth/
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── api/
│ │ ├── store/
│ │ ├── types/
│ │ └── index.ts
│ ├── dashboard/
│ └── settings/
├── shared/
│ ├── components/
│ ├── hooks/
│ ├── utils/
│ └── types/
├── lib/
│ ├── api-client.ts
│ └── storage.ts
├── app/
│ ├── routes.tsx
│ ├── providers.tsx
│ └── App.tsx
└── main.tsxLayer-Based Structure
src/
├── components/
│ ├── common/
│ ├── forms/
│ └── layout/
├── pages/
├── hooks/
├── services/
├── store/
├── utils/
├── types/
└── main.tsxDependency Management
Import Rules
// Good - explicit imports, clear dependencies
import { Button } from '@/shared/components';
import { useAuth } from '@/features/auth';
import { formatDate } from '@/shared/utils';
// Feature module index.ts - explicit exports
export { LoginForm } from './components/LoginForm';
export { useAuth } from './hooks/useAuth';
export type { User } from './types';
// Avoid circular dependencies
// features/auth should not import from features/dashboard
// if dashboard imports from authDependency Direction
Allowed Dependencies:
├── Pages → Features → Shared
├── Features → Shared
├── Features → Lib
└── Shared → Lib
Forbidden:
├── Shared → Features
├── Features → Pages
└── Lib → anything aboveState Architecture
State Categorization
| Type | Scope | Examples | Solution |
|---|---|---|---|
| UI State | Component | Open/close, focus | useState, useReducer |
| Feature State | Feature | Form data, filters | Feature store |
| Global State | App-wide | User, theme | Global store |
| Server State | Remote | API data | React Query, SWR |
| URL State | Navigation | Page, params | Router |
State Colocation
// Start with local state
function SearchBox() {
const [query, setQuery] = useState('');
return <input value={query} onChange={e => setQuery(e.target.value)} />;
}
// Lift when needed by siblings
function SearchFeature() {
const [query, setQuery] = useState('');
return (
<>
<SearchBox query={query} onQueryChange={setQuery} />
<SearchResults query={query} />
</>
);
}
// Use global store only when truly global
// (accessed by unrelated features)Communication Patterns
Component Communication
// Props (parent → child)
<Child data={data} onAction={handleAction} />
// Callback (child → parent)
onAction={(result) => setParentState(result)}
// Context (ancestor → descendants)
<ThemeContext.Provider value={theme}>
<DeepChild /> {/* Can access theme */}
</ThemeContext.Provider>
// Events (siblings, unrelated)
const eventBus = new EventEmitter();
eventBus.emit('notification', { message: 'Hello' });
eventBus.on('notification', handleNotification);Error Boundaries
// Error boundary for graceful degradation
class ErrorBoundary extends Component<Props, State> {
state = { hasError: false, error: null };
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, info: ErrorInfo) {
logErrorToService(error, info);
}
render() {
if (this.state.hasError) {
return <this.props.fallback error={this.state.error} />;
}
return this.props.children;
}
}
// Usage
<ErrorBoundary fallback={ErrorPage}>
<FeatureComponent />
</ErrorBoundary>Architecture Decision Records
Document architectural decisions for team alignment.
# ADR-001: State Management Solution
## Status
Accepted
## Context
Need to manage complex state across the application.
## Decision
Use Zustand for global state and React Query for server state.
## Consequences
### Positive
- Simple API, small bundle size
- Clear separation of client/server state
### Negative
- Less structure than Redux
- Team needs to learn new libraryBest Practices
Architecture Guidelines
- Start simple, evolve as needed
- Colocate code that changes together
- Define clear module boundaries
- Establish dependency rules early
- Document architectural decisions
- Review architecture in code reviews
- Refactor continuously, not in big bangs