The React frontend is organised around three separation principles: pages are thin — they compose hooks and components with minimal logic; hooks contain all stateful and side-effect logic; and components are pure rendering functions that receive data as props. This makes every layer independently testable and replaceable. The RTK Query API slice provides a single source of truth for server data; Zustand handles auth state across the app without a Provider wrapper; custom hooks compose these primitives into feature-specific interfaces.
Frontend Directory Structure
frontend/src/
├── main.jsx # App entry point, providers, Sentry init
├── App.jsx # Route tree (public + protected routes)
├── config.js # Environment variable config module
│
├── store/ # Redux store
│ ├── index.js # configureStore (RTK Query + UI slices)
│ ├── apiSlice.js # createApi: all blog API endpoints + hooks
│ ├── authSlice.js # Redux auth slice (if using Redux for auth)
│ └── uiSlice.js # UI state (modals, sidebar)
│
├── stores/ # Zustand stores
│ └── authStore.js # User, accessToken, login, logout, init
│
├── context/ # React Context providers
│ └── ToastContext.jsx # ToastProvider, useToast hook
│
├── hooks/ # Custom hooks library
│ ├── useDebounce.js
│ ├── useLocalStorage.js
│ ├── useMediaQuery.js
│ ├── useOnline.js
│ ├── useForm.js
│ ├── usePagination.js
│ ├── useInfiniteScroll.js
│ ├── useWebSocket.js
│ ├── useNotifications.js
│ ├── useLiveComments.js
│ └── usePostFeed.js # Composed: debounce + RTK Query + URL params
│
├── services/ # Axios instances (non-RTK calls, e.g. auth)
│ └── auth.js # authApi: login, register, me, logout
│
├── utils/ # Pure utility functions
│ ├── apiErrors.js # normaliseApiError, extractFieldErrors
│ ├── dates.js # formatRelativeDate, formatAbsoluteDate
│ └── slugify.js # slugify(text)
│
├── components/ # Reusable UI components
│ ├── ui/ # Primitives
│ │ ├── Button.jsx
│ │ ├── Input.jsx # forwardRef, isInvalid prop
│ │ ├── FormField.jsx # label + input + error wrapper
│ │ ├── TagInput.jsx # multi-value tag selector
│ │ ├── DropZone.jsx # drag-and-drop file upload
│ │ ├── Image.jsx # lazy loading + fallback + skeleton
│ │ └── ToastContainer.jsx
│ ├── auth/
│ │ ├── ProtectedRoute.jsx
│ │ └── LogoutButton.jsx
│ ├── post/
│ │ ├── PostCard.jsx
│ │ ├── PostList.jsx
│ │ └── PostListSkeleton.jsx
│ └── layout/
│ ├── Header.jsx # Nav + notification bell + user menu
│ └── Footer.jsx
│
└── pages/ # Route-level page components (thin!)
├── HomePage.jsx # usePostFeed → PostList + Pagination
├── PostDetailPage.jsx # RTK Query + useLiveComments
├── PostEditorPage.jsx # useForm + CoverImageSection
├── LoginPage.jsx
├── RegisterPage.jsx
├── DashboardPage.jsx # user's own posts
└── ProfilePage.jsx # avatar upload + settings
Note: The
services/ directory holds the Axios-based auth service that makes calls outside of RTK Query — specifically the login, register, and token refresh endpoints. These are not in the RTK Query API slice because they have side effects (storing tokens, navigating) that are better handled in the Zustand store’s action functions. For every other endpoint (posts, comments, users, notifications), RTK Query’s apiSlice is the authoritative client.Tip: The
config.js module is the single place all environment variables are read and validated. No other file should read import.meta.env directly. This means you can see all configuration in one place, add runtime validation (throw if a required variable is missing), and mock the config in tests by replacing the module. Keep it small and focused — it should only export constants, not functions.Warning: Keep page components thin. A page component should ideally be 30–60 lines: one hook call for data, one or two layout elements, and a handful of component renders. If a page grows beyond 100 lines, extract a named section component (
PostEditorFormSection, CommentFormSection) or a custom hook. Page components that contain form handling, API call logic, and complex conditional rendering become impossible to test and understand.App.jsx — Route Tree
// src/App.jsx
import { Routes, Route } from "react-router-dom";
import { useEffect } from "react";
import { useAuthStore } from "@/stores/authStore";
import Header from "@/components/layout/Header";
import ProtectedRoute from "@/components/auth/ProtectedRoute";
import HomePage from "@/pages/HomePage";
import PostDetailPage from "@/pages/PostDetailPage";
import PostEditorPage from "@/pages/PostEditorPage";
import LoginPage from "@/pages/LoginPage";
import RegisterPage from "@/pages/RegisterPage";
import DashboardPage from "@/pages/DashboardPage";
import ProfilePage from "@/pages/ProfilePage";
export default function App() {
const init = useAuthStore((s) => s.init);
useEffect(() => { init(); }, [init]); // validate stored token on mount
return (
<>
<Header />
<main className="max-w-5xl mx-auto px-4 py-8">
<Routes>
{/* Public routes */}
<Route path="/" element={<HomePage />} />
<Route path="/posts/:postId" element={<PostDetailPage />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
{/* Protected routes */}
<Route element={<ProtectedRoute />}>
<Route path="/dashboard" element={<DashboardPage />} />
<Route path="/profile" element={<ProfilePage />} />
<Route path="/posts/new" element={<PostEditorPage />} />
<Route path="/posts/:postId/edit" element={<PostEditorPage />} />
</Route>
</Routes>
</main>
</>
);
}