Building Modern Web Apps
Creating production-ready web applications requires more than just knowing a framework. You need solid architecture, clear separation of concerns, and scalable patterns.
Application Architecture Layers
A well-structured application separates concerns into distinct layers:
┌─────────────────────────────────┐
│ Presentation Layer (UI) │ ← React Components
├─────────────────────────────────┤
│ Business Logic Layer │ ← Services, Utilities
├─────────────────────────────────┤
│ Data Access Layer │ ← API clients, DB queries
├─────────────────────────────────┤
│ External Services │ ← APIs, Databases, CDNs
└─────────────────────────────────┘
Folder Structure
Here's a battle-tested folder structure:
src/
├── app/ # Next.js App Router pages
├── components/ # Reusable UI components
│ ├── ui/ # Base components (Button, Input)
│ ├── features/ # Feature-specific components
│ └── layouts/ # Layout components
├── lib/ # Business logic
│ ├── services/ # External API clients
│ ├── utils/ # Helper functions
│ └── hooks/ # Custom React hooks
├── types/ # TypeScript type definitions
└── config/ # Configuration files
This structure scales from small projects to large enterprise applications with minimal changes.
Component Design Principles
1. Single Responsibility
Each component should do one thing well:
// ❌ Bad: Component does too much
function UserDashboard() {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [analytics, setAnalytics] = useState(null);
// Fetching logic, rendering logic, all mixed together
// ...
}
// ✅ Good: Separated concerns
function UserDashboard() {
return (
<>
<UserProfile />
<UserPosts />
<UserAnalytics />
</>
);
}
2. Composition Over Inheritance
Build complex UIs from simple, composable pieces:
// Composable Card component
interface CardProps {
children: React.ReactNode;
variant?: 'default' | 'outlined' | 'elevated';
}
function Card({ children, variant = 'default' }: CardProps) {
return <div className={`card card-${variant}`}>{children}</div>;
}
// Usage - compose cards with other components
<Card variant="elevated">
<CardHeader title="User Stats" />
<CardContent>
<StatsChart data={stats} />
</CardContent>
</Card>
State Management Strategy
Choose your state management solution based on your application's complexity, not popularity!
Local State (useState)
For component-specific state:
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
Server State (React Query / SWR)
For data from external sources:
import useSWR from 'swr';
function UserProfile({ userId }: { userId: string }) {
const { data, error, isLoading } = useSWR(
`/api/users/${userId}`,
fetcher
);
if (isLoading) return <Skeleton />;
if (error) return <ErrorState />;
return <ProfileCard user={data} />;
}
Global State (Context / Zustand)
For app-wide state:
// Zustand store
import create from 'zustand';
const useThemeStore = create((set) => ({
theme: 'light',
toggleTheme: () => set((state) => ({
theme: state.theme === 'light' ? 'dark' : 'light'
})),
}));
Error Handling
Robust applications handle errors gracefully:
// Error Boundary for React errors
function ErrorBoundary({ children }: { children: React.ReactNode }) {
return (
<Suspense fallback={<Loading />}>
{children}
</Suspense>
);
}
// Async error handling
async function fetchWithRetry(url: string, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (error) {
if (i === retries - 1) throw error;
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
}
}
Performance Optimization
Code Splitting
Load code only when needed:
// Dynamic imports
const HeavyComponent = dynamic(() => import('./HeavyComponent'), {
loading: () => <Spinner />,
ssr: false, // Skip server-side rendering if not needed
});
Memoization
Prevent unnecessary re-renders:
const MemoizedComponent = memo(function ExpensiveComponent({ data }) {
// Complex rendering logic
return <div>{/* ... */}</div>;
});
// Memoize expensive calculations
const processedData = useMemo(() => {
return expensiveOperation(rawData);
}, [rawData]);
Testing Strategy
A good test suite gives you confidence to refactor and ship features faster!
Test Pyramid
┌──────────┐
│ E2E │ ← Few, high-value tests
├──────────┤
│Integration│ ← Moderate coverage
├──────────┤
│ Unit │ ← Many, fast tests
└──────────┘
Deployment Checklist
Before deploying to production:
- [ ] Environment variables configured
- [ ] Error tracking setup (Sentry, etc.)
- [ ] Analytics integrated
- [ ] SEO metadata complete
- [ ] Performance audited (Lighthouse)
- [ ] Security headers configured
- [ ] Database migrations tested
- [ ] Rollback plan documented
Conclusion
Building modern web applications is about making intentional architectural decisions:
- Separate concerns into clear layers
- Compose components from small, focused pieces
- Manage state appropriately for each use case
- Handle errors gracefully
- Test strategically
- Optimize based on real metrics
Start with solid foundations, and scale as your application grows!