Skip to main content

Web Development Overview

Introduction

Modern frontend development centers on building maintainable, scalable, and user-friendly applications using component-driven architecture. This guide establishes foundational principles applicable across React, Angular, and other modern frameworks. Understanding these concepts is crucial for creating applications that remain maintainable as they grow in complexity and team size.

Core Development Philosophy

Component-Driven Development

Component-driven development (CDD) breaks UIs into isolated, reusable pieces that manage their own structure, behavior, and appearance. This approach emerged as web applications grew more complex, requiring better organization than traditional page-based architectures.

Why Components Matter:

At its core, a component is a self-contained unit that encapsulates a specific piece of functionality. Consider a button: it needs markup (HTML), styling (CSS), behavior (JavaScript), and potentially state management. Rather than scattering these concerns across multiple files organized by technology, components co-locate everything related to that button in one place.

This co-location provides several fundamental benefits:

  1. Isolation: Changes to one component don't accidentally break others. Each component operates independently with well-defined inputs (props) and outputs (events).

  2. Reusability: Once built, components can be used throughout the application. A Button component defined once can be reused hundreds of times with different configurations.

  3. Testability: Isolated components are easier to test. You can test a button component without needing to render an entire page.

  4. Maintainability: When fixing bugs or adding features, developers know exactly where to look. All button-related code lives in the button component.

Separation of Concerns

While components co-locate related code, they should still separate different types of concerns internally. Modern frontend architecture distinguishes between:

Container Components (Smart Components): These components handle business logic, data fetching, state management, and side effects. They know about the application's data layer and often connect to state management solutions or data fetching libraries. Container components typically have minimal presentation logic - they delegate rendering to presentational components.

Presentational Components (Dumb Components): These components focus purely on how things look. They receive data through props and render UI based on that data. They don't know where data comes from or how to modify it. All interactions flow outward through callback props.

This separation emerged from practical experience: mixing data fetching with rendering logic creates components that are hard to test, hard to reuse, and hard to understand. The pattern might seem like extra work initially, but it pays dividends as applications grow.

Consider a payment list: the container component handles fetching payments from an API, managing loading states, and error handling. It passes the loaded data to a presentational component that simply renders the list. If you later need to display the same payment list elsewhere (perhaps in a modal or sidebar), you can reuse the presentational component with different data sources.

Single Responsibility Principle

Each component should have one clear reason to exist and one reason to change. This principle, borrowed from object-oriented design, applies powerfully to frontend components.

A component with multiple responsibilities becomes difficult to name, understand, and modify. For example, a PaymentFormWithListAndDetails component that handles form submission, displays a payment list, and shows individual payment details has at least three responsibilities. Any change to how payments are listed, displayed, or submitted requires modifying this large component.

Breaking it apart yields:

  • PaymentForm: Handles form input and submission
  • PaymentList: Displays a list of payments
  • PaymentDetails: Shows details for a single payment

Each component now has a clear purpose. Changes to list rendering don't risk breaking form submission. New developers can understand each component independently.

Project Structure

Feature-Based Organization

Project structure significantly impacts developer productivity and code maintainability. Two primary approaches exist: organizing by technical layer (components/, services/, utils/) or by feature domain (payments/, accounts/, transfers/).

Feature-based organization groups all files related to a specific feature together, regardless of their technical role. This approach scales better because:

  1. Cognitive Load: When working on payments, all payment-related code lives in one place. You don't jump between directories.

  2. Team Organization: Teams can own specific features without stepping on each other's toes. The payments team works in features/payments/, the accounts team in features/accounts/.

  3. Code Reusability Clarity: If something is used across features, it belongs in shared/. If it's only used within one feature, it stays there. This prevents premature abstraction.

  4. Deletion and Refactoring: Removing a feature means deleting one directory. No hunting across multiple locations for related files.

src/
├── features/
│ ├── payments/
│ │ ├── components/ # Payment-specific components
│ │ │ ├── PaymentForm/
│ │ │ ├── PaymentList/
│ │ │ └── PaymentCard/
│ │ ├── hooks/ # Payment-specific hooks
│ │ │ ├── usePayments.ts
│ │ │ └── usePaymentForm.ts
│ │ ├── services/ # Payment API calls
│ │ │ └── paymentService.ts
│ │ ├── types/ # Payment types/interfaces
│ │ │ └── payment.types.ts
│ │ ├── utils/ # Payment-specific utilities
│ │ │ └── paymentValidation.ts
│ │ └── index.ts # Public API
│ │
│ ├── accounts/
│ ├── transactions/
│ └── auth/

├── shared/ # Code used across features
│ ├── components/ # Reusable UI components
│ │ ├── Button/
│ │ ├── Input/
│ │ ├── Modal/
│ │ ├── ErrorBoundary/
│ │ └── LoadingSpinner/
│ ├── hooks/ # Reusable hooks
│ │ ├── useDebounce.ts
│ │ ├── useLocalStorage.ts
│ │ └── useMediaQuery.ts
│ ├── utils/ # Common utilities
│ │ ├── formatters.ts
│ │ ├── validators.ts
│ │ └── api.ts
│ ├── types/ # Shared types
│ │ └── common.types.ts
│ └── constants/ # Application constants
│ └── config.ts

├── App.tsx
├── main.tsx
└── routes.tsx

Feature Module Structure

Each feature module acts as a mini-application within the larger codebase. The index.ts file defines the feature's public API - what other features can import. This encapsulation prevents tight coupling between features.

Example Feature Module:

// features/payments/index.ts
// Public API - only export what other features need
export { PaymentListContainer } from './components/PaymentList';
export { PaymentForm } from './components/PaymentForm';
export type { Payment, PaymentStatus } from './types/payment.types';

Other features import from the module boundary, not from internal files:

// Good
import { PaymentListContainer, Payment } from '@/features/payments';

// Bad - bypasses module boundary
import { PaymentList } from '@/features/payments/components/PaymentList';
import { usePayments } from '@/features/payments/hooks/usePayments';

This boundary prevents features from depending on each other's internal implementation details. If the payments team refactors their internal structure, they only need to ensure the public API remains stable.

Component File Organization

Each significant component deserves its own directory with related files co-located:

PaymentCard/
├── PaymentCard.tsx # Component implementation
├── PaymentCard.test.tsx # Unit tests
├── PaymentCard.module.css # Component-specific styles
├── PaymentCard.types.ts # Component-specific types
└── index.ts # Re-export for clean imports

This structure keeps everything related to PaymentCard together. When working on this component, all relevant files are visible in the same directory. Tests live next to implementation, making them harder to forget or skip.

The index.ts file simplifies imports:

// index.ts
export { PaymentCard } from './PaymentCard';
export type { PaymentCardProps } from './PaymentCard.types';
// Usage - clean import
import { PaymentCard } from './components/PaymentCard';

// Without index.ts - repetitive
import { PaymentCard } from './components/PaymentCard/PaymentCard';

Development Principles

Progressive Enhancement

Progressive enhancement ensures core functionality works for all users, then enhances the experience for those with modern browsers or capabilities. This principle emerged from the reality that users have different devices, browsers, network conditions, and accessibility needs.

The approach works in layers:

  1. HTML Foundation: Core content and functionality accessible via semantic HTML
  2. CSS Enhancement: Visual design and layout improvements
  3. JavaScript Features: Interactive enhancements and improved UX

For example, a payment form should submit via standard HTML form submission even if JavaScript fails to load. Once JavaScript loads, enhance it with client-side validation, real-time feedback, and better error messages.

This doesn't mean building twice. Modern frameworks support server-side rendering (SSR) or static site generation (SSG) that deliver HTML with core functionality, then hydrate with JavaScript for interactivity.

Mobile-First Design

Mobile-first design starts with constraints - smaller screens, touch input, potentially slower networks - then progressively enhances for larger screens. This approach emerged from mobile usage overtaking desktop and from recognizing that designing for constraints forces better decisions.

Starting mobile-first means:

  1. Content Priority: Limited screen space forces you to identify truly essential content
  2. Performance Focus: Slower mobile networks demand efficient asset delivery
  3. Touch Targets: Designing for finger-sized touch targets makes interfaces more accessible
  4. Progressive Enhancement: Base experience works on constrained devices, enhanced on powerful ones

CSS naturally supports this through mobile-first media queries:

/* Base styles - mobile */
.button {
padding: 12px 16px;
font-size: 14px;
}

/* Enhanced for tablets */
@media (min-width: 768px) {
.button {
padding: 14px 20px;
font-size: 16px;
}
}

/* Enhanced for desktops */
@media (min-width: 1024px) {
.button {
padding: 16px 24px;
font-size: 18px;
}
}

The base styles apply to all devices. Larger screens get enhancements through min-width media queries. This is simpler and more maintainable than desktop-first approaches that require overriding complex desktop styles for mobile.

Performance Budget

A performance budget sets measurable limits on aspects affecting load time and interactivity. These budgets prevent performance degradation as features accumulate.

Common budget metrics:

  • Bundle Size: Maximum JavaScript bundle size (e.g., 200KB gzipped for initial bundle)
  • Load Time: Time to interactive under 3G connection (e.g., < 5 seconds)
  • Image Size: Total image payload per page (e.g., < 500KB)
  • Third-Party Scripts: Maximum number and size of external scripts

Tools like webpack-bundle-analyzer, Lighthouse, and WebPageTest help track these metrics. Automated CI/CD checks can fail builds that exceed budgets, preventing performance regressions from reaching production.

Example CI check:

# Fail build if main bundle exceeds 250KB
if [ $(stat -f%z dist/main.js) -gt 250000 ]; then
echo "Bundle size exceeds 250KB"
exit 1
fi

Accessibility as Default

Accessibility isn't a feature to add later - it must be part of every component from the start. Beyond legal compliance (WCAG 2.1 AA), accessibility improves experiences for all users.

Key accessibility principles:

  1. Semantic HTML: Use appropriate elements (<button>, <nav>, <main>) that convey meaning to assistive technologies
  2. Keyboard Navigation: All interactive elements reachable and operable via keyboard
  3. Visual Focus: Clear focus indicators for keyboard navigation
  4. Screen Reader Support: Proper labels, descriptions, and announcements
  5. Color Contrast: Text readable against backgrounds (4.5:1 for normal text)
  6. Form Labels: All inputs have associated labels

These practices benefit everyone:

  • Keyboard navigation helps power users
  • High contrast helps users in bright sunlight
  • Clear labels reduce confusion for all users
  • Semantic HTML improves SEO

See Frontend Accessibility for comprehensive guidelines and examples.

Build and Development Tools

Module Bundlers

Modern web applications use module bundlers (Vite, webpack, Rollup) to transform and optimize code for production. These tools:

  1. Bundle Code: Combine many modules into fewer files for efficient delivery
  2. Transform Code: Convert TypeScript, JSX, and modern JavaScript to browser-compatible code
  3. Optimize Assets: Minify JavaScript, optimize images, remove dead code
  4. Enable Development: Provide hot module replacement (HMR) for instant updates during development

Vite (recommended for new projects) uses native ES modules during development for near-instant server start and updates. It pre-bundles dependencies with esbuild and uses Rollup for optimized production builds.

webpack offers extensive customization through loaders and plugins. It's mature and widely adopted but requires more configuration and has slower development builds than Vite.

Development Server Features

Modern development servers provide:

Hot Module Replacement (HMR): Updates modules in the running application without full page reload. Change a component's code, and it updates instantly while preserving application state.

Fast Refresh: React-specific HMR that preserves component state across edits. Change a component's render logic, and the UI updates without resetting form inputs or navigation state.

Error Overlay: Displays build errors and runtime errors directly in the browser with source code context and stack traces.

Code Quality Tools

Linters (ESLint, stylelint) analyze code for potential errors, enforce coding standards, and suggest best practices. They catch bugs like undefined variables, unused imports, and accessibility issues.

Formatters (Prettier) automatically format code to consistent style. This eliminates formatting debates and keeps code reviews focused on logic rather than style.

Type Checkers (TypeScript) catch type errors before runtime. TypeScript adds static typing to JavaScript, enabling better IDE support, refactoring confidence, and runtime error prevention.

These tools integrate with editors for real-time feedback and with CI/CD pipelines to enforce standards before code merges.

File Naming Conventions

Consistent naming conventions improve discoverability and reduce cognitive load. These conventions apply across the codebase:

Components: PascalCase matching the component name

PaymentCard.tsx
UserProfile.tsx
TransactionHistory.tsx

Hooks: camelCase starting with "use"

usePayments.ts
useAuthContext.ts
useLocalStorage.ts

Utilities: camelCase describing the utility

formatCurrency.ts
validateEmail.ts
parseDate.ts

Types: PascalCase with ".types" suffix

payment.types.ts
user.types.ts
api.types.ts

Tests: Same name as file being tested with ".test" or ".spec" suffix

PaymentCard.test.tsx
formatCurrency.test.ts

Styles: Match component name with appropriate extension

PaymentCard.module.css
PaymentCard.styles.ts (for styled-components)

Environment Configuration

Applications need different configurations for development, staging, and production environments. Environment variables provide this flexibility without code changes.

Environment Files:

.env.development       # Development-specific config
.env.staging # Staging environment config
.env.production # Production config
.env.local # Local overrides (git-ignored)

Example Configuration:

# .env.production
VITE_API_URL=https://api.example.com
VITE_API_TIMEOUT=10000
VITE_ENABLE_ANALYTICS=true

# .env.development
VITE_API_URL=http://localhost:3000
VITE_API_TIMEOUT=30000
VITE_ENABLE_ANALYTICS=false

Accessing in Code:

const apiUrl = import.meta.env.VITE_API_URL;
const timeout = import.meta.env.VITE_API_TIMEOUT;

Important Security Notes:

  • Never commit .env.local or files containing secrets
  • Environment variables are embedded in the built frontend code - never store secrets in frontend environment variables
  • Use backend services to protect sensitive data like API keys

Summary

Modern frontend development emphasizes component-driven architecture, separation of concerns, and accessibility. Feature-based project organization scales better than technology-based organization. Progressive enhancement and mobile-first design ensure applications work for all users across devices and network conditions. Build tools, linters, and type checkers enforce quality and catch errors early. These principles form the foundation for maintainable, scalable frontend applications.