Skip to content
All essays
WebMarch 8, 202411 min

Frontend Architecture Best Practices

Essential frontend architecture patterns that scale from small projects to enterprise applications.

Ü
Ümit Uz
Mobile & Full Stack Developer

Good frontend architecture enables teams to move fast without breaking things. Here's how to build scalable applications.

Core Principles

Separation of Concerns

Keep different responsibilities isolated.

src/
├── components/     # UI components
├── pages/          # Page components
├── hooks/          # Custom hooks
├── services/       # API calls
├── store/          # State management
├── utils/          # Helper functions
└── types/          # TypeScript definitions

Single Responsibility

Each module should have one reason to change.

javascript
// Bad: Component does too much
function UserProfile() {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    fetchUser();
    fetchPosts();
  }, []);

  async function fetchUser() {
    const data = await api.get('/user');
    setUser(data);
  }

  async function fetchPosts() {
    const data = await api.get('/posts');
    setPosts(data);
  }

  return <div>{/* ... */}</div>;
}

// Good: Separated concerns
function UserProfile() {
  const { user, posts, loading } = useUserProfile();

  if (loading) return <Spinner />;
  return <UserProfileView user={user} posts={posts} />;
}

Project Structure

Feature-Based Organization

Group by feature, not by type.

src/
├── features/
│   ├── auth/
│   │   ├── components/
│   │   ├── hooks/
│   │   ├── services/
│   │   ├── types/
│   │   └── index.ts
│   └── dashboard/
│       ├── components/
│       ├── hooks/
│       └── index.ts
└── shared/
    ├── components/
    ├── hooks/
    └── utils/

Domain-Driven Design

Align structure with business domains.

javascript
// Domain: User Management
// Feature: Authentication
// Component: LoginForm
// Location: features/auth/components/LoginForm.tsx

State Management Strategy

Types of State

  1. 1Server State: Data from APIs
  2. 2Client State: Form inputs, UI state
  3. 3URL State: Query params, hash
  4. 4Temp State: Modals, dropdowns

Tool Selection

javascript
// Server State - React Query
const { data, isLoading } = useQuery('users', fetchUsers);

// Client State - Zustand
const useStore = create((set) => ({
  theme: 'light',
  setTheme: (theme) => set({ theme }),
}));

// URL State - React Router
const [searchParams] = useSearchParams();
const page = searchParams.get('page');

// Temp State - useState
const [isModalOpen, setIsModalOpen] = useState(false);

Component Architecture

Atomic Design

Break down UI into hierarchy.

atoms/
├── Button.tsx
├── Input.tsx
└── Badge.tsx

molecules/
├── SearchBar.tsx     # Input + Button
├── FormField.tsx     # Label + Input + Error
└── UserAvatar.tsx    # Image + Badge

organisms/
├── Header.tsx        # Logo + Nav + Search
├── Card.tsx          # Image + Title + Actions
└── DataTable.tsx     # Pagination + Filters + Table

Component Composition

Build complex UIs from simple parts.

javascript
// Complex component from simple parts
<Card>
  <CardHeader>
    <CardTitle>User Profile</CardTitle>
    <CardActions>
      <Button>Edit</Button>
      <Button variant="danger">Delete</Button>
    </CardActions>
  </CardHeader>
  <CardBody>
    <UserAvatar user={user} />
    <UserInfo user={user} />
  </CardBody>
</Card>

Data Layer

Repository Pattern

Abstract data access.

javascript
// Repository
class UserRepository {
  async findById(id) {
    const response = await api.get(`/users/${id}`);
    return UserDTO.fromJSON(response.data);
  }

  async findAll(filters) {
    const response = await api.get('/users', { params: filters });
    return response.data.map(data => UserDTO.fromJSON(data));
  }
}

// Usage
const user = await userRepository.findById('123');

Service Layer

Business logic separation.

javascript
class AuthService {
  constructor(private userRepository: UserRepository) {}

  async login(email, password) {
    const user = await this.userRepository.findByEmail(email);
    if (!user || !user.verifyPassword(password)) {
      throw new AuthError('Invalid credentials');
    }
    return this.generateToken(user);
  }
}

Error Handling

Global Error Boundary

Catch React errors.

javascript
class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null };

  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    logError(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return <ErrorFallback error={this.state.error} />;
    }
    return this.props.children;
  }
}

API Error Handling

Centralized error handling.

javascript
api.interceptors.response.use(
  response => response,
  error => {
    if (error.response?.status === 401) {
      redirectToLogin();
    } else if (error.response?.status === 500) {
      showServerError();
    }
    return Promise.reject(error);
  }
);

Code Quality

Linting and Formatting

Enforce consistency.

json
{
  "extends": [
    "eslint:recommended",
    "plugin:react/recommended",
    "plugin:@typescript-eslint/recommended"
  ],
  "rules": {
    "no-console": "warn",
    "prefer-const": "error",
    "@typescript-eslint/explicit-function-return-type": "warn"
  }
}

Code Reviews

Review checklist:

  • Does it follow patterns?
  • Is it tested?
  • Is it performant?
  • Is it accessible?
  • Is it secure?

Testing Strategy

javascript
// Unit tests
describe('formatDate', () => {
  it('formats date correctly', () => {
    expect(formatDate('2024-01-01')).toBe('Jan 1, 2024');
  });
});

// Integration tests
describe('LoginForm', () => {
  it('submits form with credentials', async () => {
    render(<LoginForm />);
    await userEvent.type(screen.getByLabelText('Email'), 'test@example.com');
    await userEvent.click(screen.getByText('Submit'));
    expect(mockLogin).toHaveBeenCalledWith('test@example.com');
  });
});

// E2E tests
test('user can login', async ({ page }) => {
  await page.goto('/login');
  await page.fill('[name="email"]', 'test@example.com');
  await page.click('[type="submit"]');
  await expect(page).toHaveURL('/dashboard');
});

Performance

Code Splitting

javascript
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));

<Suspense fallback={<Spinner />}>
  <Routes>
    <Route path="/dashboard" element={<Dashboard />} />
    <Route path="/settings" element={<Settings />} />
  </Routes>
</Suspense>

Memoization

javascript
// Memoize expensive calculations
const expensiveValue = useMemo(() => {
  return computeExpensiveValue(a, b);
}, [a, b]);

// Memoize callbacks
const handleClick = useCallback(() => {
  doSomething(a, b);
}, [a, b]);

// Memoize components
const ExpensiveComponent = memo(({ data }) => {
  return <ComplexVisualization data={data} />;
});

Documentation

Component Documentation

javascript
/**
 * Button component with variants and sizes
 *
 * @param variant - 'primary' | 'secondary' | 'ghost'
 * @param size - 'sm' | 'md' | 'lg'
 * @param isLoading - Shows loading spinner
 *
 * @example
 * <Button variant="primary" size="lg" onClick={handleClick}>
 *   Submit
 * </Button>
 */
export function Button({ variant, size, isLoading, children, ...props }) {
  // ...
}

Architecture Decision Records

Document major decisions.

markdown
# ADR-001: Adopt React Query

## Status
Accepted

## Context
Need better server state management

## Decision
Use React Query for all server state

## Consequences
- Less boilerplate code
- Better caching
- Team needs to learn new library

Conclusion

Good frontend architecture is about patterns that scale. Separate concerns, organize by feature, choose appropriate state management, build composable components, and maintain code quality. The goal is to enable fast development without sacrificing maintainability.

Related essays

Next essay
Component Library Design Guide