π Introduction to Frontend Architecture
Frontend architecture defines how you structure, organize, and scale your application. The right architecture depends on your team, complexity, and requirements.
Key Decision Factors
π₯ Team Size
How many developers work on the codebase? More developers often need more structure.
π Complexity
Simple landing page vs enterprise dashboard? Complexity drives architecture choices.
π Scalability
Will the app grow significantly? Plan for future scale without over-engineering.
π Deployment
How often do different parts change? Frequent deploys may need independence.
π’ Monolithic Architecture
A single, unified codebase where all features live together. The simplest and most common starting point.
Structure
// Typical monolithic folder structure
my-app/
βββ src/
β βββ components/ // All components together
β β βββ Header.jsx
β β βββ UserCard.jsx
β β βββ ProductCard.jsx
β βββ pages/ // All pages
β β βββ Home.jsx
β β βββ Dashboard.jsx
β β βββ Settings.jsx
β βββ services/ // API calls
β βββ hooks/ // Custom hooks
β βββ store/ // Global state
β βββ App.jsx // Single entry point
βββ package.json // One package
βββ webpack.config.js // One build
Characteristics
| Aspect | Description |
|---|---|
| Single Build | One build process, one deployment |
| Shared Dependencies | All features use same library versions |
| Unified State | Global state management across entire app |
| Single Routing | One router handles all navigation |
| Coupled Features | Changes in one area can affect others |
Pros & Cons
β PROS
- Simple setup and debugging
- No integration overhead
- Consistent UX
- Easy refactoring
- Shared code reuse
β CONS
- Build times grow
- Hard to scale teams
- All-or-nothing deploy
- Technical debt accumulates
- Large bundle sizes
When to Use
- Small to medium apps - Up to ~100k lines of code
- Small teams - 1-5 developers
- Startups/MVPs - Need to move fast
- Simple domains - CRUD applications
Code Example
// src/App.jsx - Single entry point with unified routing
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { Provider } from 'react-redux';
import store from './store';
// All pages imported in one place
import Home from './pages/Home';
import Dashboard from './pages/Dashboard';
import Settings from './pages/Settings';
function App() {
return (
// Single global store wraps everything
<Provider store={store}>
<BrowserRouter>
<Layout>
// All routes defined together
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Layout>
</BrowserRouter>
</Provider>
);
}
π§± Component-Based Architecture
Building UIs from reusable, self-contained components with clear hierarchies. This is fundamental to modern frontend development.
Container vs Presentational Pattern
Container Component - Handles Logic
// Container: Manages state, fetching, business logic
function UserListContainer() {
// State management
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState('');
// Data fetching
useEffect(() => {
fetchUsers()
.then(setUsers)
.finally(() => setLoading(false));
}, []);
// Business logic - filtering
const filteredUsers = useMemo(() =>
users.filter(u => u.name.includes(filter)),
[users, filter]
);
// Pass data to presentational component
return (
<UserList
users={filteredUsers}
loading={loading}
filter={filter}
onFilterChange={setFilter}
/>
);
}
Presentational Component - Handles UI
// Presentational: Only handles rendering
function UserList({ users, loading, filter, onFilterChange }) {
// Only UI logic - no business logic!
if (loading) return <Spinner />;
return (
<div className="user-list">
<SearchInput
value={filter}
onChange={onFilterChange}
placeholder="Search users..."
/>
<div className="user-grid">
{users.map(user => (
<UserCard key={user.id} user={user} />
))}
</div>
{users.length === 0 && (
<EmptyState message="No users found" />
)}
</div>
);
}
Compound Components Pattern
// Compound components - Flexible composition with shared state
const Tabs = ({ children, defaultValue }) => {
const [activeTab, setActiveTab] = useState(defaultValue);
return (
// Shared context for child components
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
};
// Tab trigger button
Tabs.Trigger = function TabTrigger({ value, children }) {
const { activeTab, setActiveTab } = useContext(TabsContext);
return (
<button
className={activeTab === value ? 'active' : ''}
onClick={() => setActiveTab(value)}
>
{children}
</button>
);
};
// Usage - Clean, declarative API
<Tabs defaultValue="general">
<Tabs.List>
<Tabs.Trigger value="general">General</Tabs.Trigger>
<Tabs.Trigger value="security">Security</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="general">
<GeneralSettings />
</Tabs.Content>
</Tabs>
π Micro-Frontend Architecture
Breaking a frontend into independent, deployable micro-applications. Each team owns and deploys their part independently.
Integration Approaches
| Approach | When to Use | Pros | Cons |
|---|---|---|---|
| Build-Time | NPM packages | Simple, Type-safe | Can't deploy independently |
| Server-Side | Edge/SSI composition | SEO friendly, Fast | Complex infrastructure |
| Run-Time (iFrame) | Complete isolation | Easy, Secure | Limited communication |
| Run-Time (JS) | Module Federation | Best DX, Flexible | Complex setup |
Communication Between Micro-Frontends
// Event Bus Pattern - Loosely coupled communication
class EventBus {
listeners = new Map();
on(event, callback) {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event).add(callback);
// Return unsubscribe function
return () => this.listeners.get(event).delete(callback);
}
emit(event, data) {
if (this.listeners.has(event)) {
this.listeners.get(event).forEach(cb => cb(data));
}
}
}
// Product Catalog (Team A) - Emits event
function ProductCard({ product }) {
const addToCart = () => {
window.__EVENT_BUS__.emit('cart:add', { productId: product.id });
};
return <button onClick={addToCart}>Add to Cart</button>;
}
// Checkout (Team B) - Listens for event
function CartIcon() {
const [count, setCount] = useState(0);
useEffect(() => {
return window.__EVENT_BUS__.on('cart:add', (item) => {
setCount(prev => prev + 1);
});
}, []);
return <span>π {count}</span>;
}
π¦ Module Federation
Webpack 5 feature for sharing modules between applications at runtime. The most flexible approach for micro-frontends.
Configuration
Remote App Configuration
// Remote App webpack.config.js (Products micro-frontend)
const ModuleFederationPlugin = require('webpack').container.ModuleFederationPlugin;
module.exports = {
output: {
publicPath: 'http://localhost:3001/',
},
plugins: [
new ModuleFederationPlugin({
name: 'products', // Unique name
filename: 'remoteEntry.js', // Entry file
// Modules this app EXPOSES to others
exposes: {
'./ProductList': './src/components/ProductList',
'./ProductCard': './src/components/ProductCard',
},
// Dependencies shared with host
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
}),
],
};
Host App Configuration
// Host App webpack.config.js (Shell application)
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'shell',
// Remote apps this host CONSUMES
remotes: {
products: 'products@http://localhost:3001/remoteEntry.js',
checkout: 'checkout@http://localhost:3002/remoteEntry.js',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
}),
],
};
Using Remote Modules
// Host App - Dynamic import with React.lazy
const ProductList = React.lazy(() => import('products/ProductList'));
function ProductsPage() {
return (
<Suspense fallback={<ProductsSkeleton />}>
<ProductList />
</Suspense>
);
}
ποΈ Islands Architecture
Partial hydration - only interactive parts (islands) are JavaScript, rest is static HTML. Perfect for content-heavy sites.
Hydration Strategies (Astro)
| Directive | When It Hydrates | Use Case |
|---|---|---|
client:load |
Immediately on page load | Critical interactive elements |
client:idle |
When browser is idle | Non-critical interactivity |
client:visible |
When in viewport | Below-the-fold content |
client:media |
When media query matches | Mobile/desktop only features |
client:only |
Skip SSR, client only | Components that can't SSR |
Astro Example
// BlogPost.astro - Static by default
---
import Header from '../components/Header.astro'; // Static
import Comments from '../components/Comments.jsx'; // Interactive
import ShareButtons from '../components/ShareButtons.jsx';
---
<Layout>
<!-- Static header - no JavaScript -->
<Header />
<!-- Static content -->
<article>
<h1>{post.title}</h1>
<Content />
</article>
<!-- Interactive Island - hydrate when visible -->
<ShareButtons client:visible />
<!-- Another island -->
<Comments postId={post.id} client:visible />
</Layout>
- Faster page loads (less JavaScript)
- Better SEO (static HTML)
- Progressive enhancement
- Smaller bundles
π¨ Rendering Patterns
Different strategies for when and where to render your application.
Comparison
| Pattern | First Paint | TTI | SEO | Dynamic Data |
|---|---|---|---|---|
| CSR | Slow | Slow | β Poor | β Real-time |
| SSR | β Fast | Medium | β Good | β Fresh |
| SSG | β Fastest | β Fast | β Good | β Stale |
| ISR | β Fast | β Fast | β Good | β‘ Balanced |
SSR Example (Next.js)
// pages/products.jsx - Server-Side Rendering
export default function ProductsPage({ products }) {
// Products already available - server fetched them
return (
<div>
<h1>Products</h1>
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
// Runs on EVERY request on the server
export async function getServerSideProps(context) {
const res = await fetch('https://api.example.com/products');
const products = await res.json();
return { props: { products } };
}
ISR Example (Next.js)
// pages/products/[id].jsx - Incremental Static Regeneration
export async function getStaticProps({ params }) {
const product = await fetchProduct(params.id);
return {
props: { product },
// Revalidate every 60 seconds
revalidate: 60
};
}
export async function getStaticPaths() {
const topProducts = await fetchTopProducts(100);
return {
paths: topProducts.map(p => ({ params: { id: p.id } })),
fallback: 'blocking' // Generate others on-demand
};
}
π State Management Patterns
Different approaches to managing application state based on your needs.
Types of State
π Local State
Component-specific state that doesn't need sharing.
useState, useReducer
π Shared State
Multiple components need same data.
Context, Redux, Zustand
π Server State
Data from APIs, needs caching/sync.
React Query, SWR
π URL State
State reflected in URL for sharing.
useParams, useSearchParams
Pattern Comparison
| Pattern | Complexity | Best For |
|---|---|---|
| useState | Low | Component UI state |
| Context | Medium | Theme, Auth |
| Redux/Zustand | Medium-High | Complex apps |
| Jotai/Recoil | Medium | Fine-grained reactivity |
| React Query | Medium | API data caching |
Redux Toolkit Example
// Modern Redux with Redux Toolkit
import { createSlice, configureStore } from '@reduxjs/toolkit';
const cartSlice = createSlice({
name: 'cart',
initialState: { items: [], total: 0 },
reducers: {
addItem(state, action) {
const existing = state.items.find(i => i.id === action.payload.id);
if (existing) {
existing.quantity += 1;
} else {
state.items.push({ ...action.payload, quantity: 1 });
}
state.total = state.items.reduce(
(sum, item) => sum + item.price * item.quantity, 0
);
},
},
});
// Usage in component
function ProductCard({ product }) {
const dispatch = useDispatch();
return (
<button onClick={() => dispatch(addItem(product))}>
Add to Cart
</button>
);
}
React Query Example
// Server state management with React Query
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function useProducts(category) {
return useQuery({
queryKey: ['products', category],
queryFn: () => fetchProducts(category),
staleTime: 5 * 60 * 1000, // Fresh for 5 minutes
refetchOnWindowFocus: true, // Refetch on tab focus
});
}
// Usage
function ProductsPage() {
const { data: products, isLoading, error } = useProducts('electronics');
if (isLoading) return <Spinner />;
if (error) return <Error message={error.message} />;
return products.map(p => <ProductCard key={p.id} product={p} />);
}
π Feature-Based Architecture
Organizing code by business feature/domain instead of technical type. Groups related code together.
β Traditional (by type)
src/
βββ components/
βββ hooks/
βββ services/
βββ store/
βββ pages/
// Problem: Related code
// scattered everywhere
β Feature-Based (by domain)
src/
βββ features/
β βββ auth/
β βββ products/
β βββ cart/
βββ shared/
βββ pages/
// Benefit: Related code
// grouped together
Feature Module Structure
src/features/products/
βββ components/ // Product-specific components
β βββ ProductList.jsx
β βββ ProductCard.jsx
β βββ ProductFilters.jsx
βββ hooks/ // Product-specific hooks
β βββ useProducts.js
β βββ useProductFilters.js
βββ services/ // Product API calls
β βββ productService.js
βββ store/ // Product state
β βββ productSlice.js
βββ types/ // Product types
β βββ product.types.ts
βββ index.js // Public API exports
Public API Pattern
// features/products/index.js - Only export what others need
// Components
export { ProductList } from './components/ProductList';
export { ProductCard } from './components/ProductCard';
// Hooks
export { useProducts } from './hooks/useProducts';
// Types
export type { Product, ProductFilters } from './types/product.types';
// Internal components are NOT exported
// Creates clear boundaries between features
// Usage in pages
import { ProductList, useProducts } from '@/features/products';
import { CartButton } from '@/features/cart';
function ProductsPage() {
const { products, isLoading } = useProducts();
return (
<Layout>
<CartButton />
<ProductList products={products} loading={isLoading} />
</Layout>
);
}
π§Ή Clean Architecture
Separating business logic from UI and infrastructure concerns. Dependencies point inward only.
Layer Responsibilities
| Layer | Contains | Depends On |
|---|---|---|
| UI | React components, hooks | Application |
| Application | Use cases, DTOs | Domain |
| Domain | Entities, business rules | Nothing! |
| Infrastructure | API clients, storage | Domain (interfaces) |
Example
// DOMAIN LAYER - Pure business logic
export class Product {
constructor(
public readonly id: string,
public readonly name: string,
public readonly price: number,
public readonly stock: number
) {}
// Business rule
canOrder(quantity: number): boolean {
return this.stock >= quantity;
}
}
// APPLICATION LAYER - Use cases
export class GetProductsUseCase {
constructor(private productRepository: IProductRepository) {}
async execute(filters?: ProductFilters): Promise<ProductDTO[]> {
const products = await this.productRepository.getAll();
return products.map(p => ({ id: p.id, name: p.name }));
}
}
// UI LAYER - React hook
export function useProducts() {
const [products, setProducts] = useState([]);
const useCase = useMemo(() => {
const repository = new ProductApiRepository();
return new GetProductsUseCase(repository);
}, []);
useEffect(() => {
useCase.execute().then(setProducts);
}, []);
return { products };
}
βοΈ Atomic Design
Building UI from smallest atoms to complete pages. Creates a systematic component hierarchy.
Component Hierarchy
| Level | Description | Examples |
|---|---|---|
| Atoms | Basic building blocks, cannot be broken down further | Button, Input, Icon, Label |
| Molecules | Combinations of atoms, simple groups | SearchBar, FormField, UserCard |
| Organisms | Complex sections composed of molecules/atoms | Header, ProductGrid, Footer |
| Templates | Page structure without real data | MainLayout, DashboardLayout |
| Pages | Complete pages with real content | HomePage, ProductsPage |
Example
// ATOMS - Basic building blocks
function Button({ variant = 'primary', children, ...props }) {
return <button className={`btn btn-${variant}`} {...props}>{children}</button>;
}
// MOLECULES - Combinations of atoms
function SearchBar({ value, onChange, onSearch }) {
return (
<div className="search-bar">
<Icon name="search" />
<Input value={value} onChange={onChange} />
<Button onClick={onSearch}>Search</Button>
</div>
);
}
// ORGANISMS - Complex sections
function Header({ user, onSearch }) {
return (
<header className="header">
<Logo />
<Navigation />
<SearchBar onSearch={onSearch} />
<UserMenu user={user} />
</header>
);
}
// TEMPLATES - Page structure
function MainLayout({ children, user }) {
return (
<div className="main-layout">
<Header user={user} />
<main>{children}</main>
<Footer />
</div>
);
}
// PAGES - Complete pages
function ProductsPage() {
const { user } = useAuth();
const { products } = useProducts();
return (
<MainLayout user={user}>
<h1>Products</h1>
<ProductGrid products={products} />
</MainLayout>
);
}
π Summary & Decision Guide
Quick Reference
| Architecture | Best For | Key Benefit | Main Drawback |
|---|---|---|---|
| Monolithic | Small teams/apps | Simplicity | Scaling issues |
| Component-Based | UI organization | Reusability | None (always use) |
| Feature-Based | Medium teams | Clear ownership | Requires discipline |
| Micro-Frontends | Large orgs | Independence | Complexity |
| Islands | Content sites | Performance | Limited interactivity |
| Clean Architecture | Complex logic | Testability | Boilerplate |
| Atomic Design | Design systems | Consistency | Can be rigid |
Decision Matrix
Common Combinations
| Scenario | Architecture | Rendering | State |
|---|---|---|---|
| Startup MVP | Monolithic | CSR | Context + React Query |
| E-commerce | Feature-Based | SSR/ISR | Redux + React Query |
| Content Site | Islands | SSG | Minimal (Astro) |
| Dashboard | Feature-Based | CSR | Zustand + React Query |
| Enterprise | Micro-Frontends | SSR | Per-app (isolated) |
- β Start simple, evolve as needed
- β Choose rendering strategy based on content type
- β Separate concerns (UI, logic, data)
- β Define clear module boundaries
- β Use TypeScript for large codebases
- β Document architectural decisions
- β Monitor bundle size and performance
- Merge conflicts becoming frequent
- Hard to find where code lives
- Changes have unexpected side effects
- Build times > 10 minutes
- Teams stepping on each other's toes