React – Folder Structure
A consistent folder structure makes it easy to find code, understand boundaries, and onboard new engineers. All Cygnus Dynamics React projects use feature-based (domain-driven) organisation — code is grouped by what it does, not by its technical type.
Core Principle: Feature-Based Organisation
Group by domain feature, not by file type. All the code for a feature — components, hooks, utilities, types, and tests — lives together in one folder.
# ✅ Feature-based — all order code lives together
src/features/orders/
components/OrderList.tsx
components/OrderCard.tsx
hooks/useOrders.ts
hooks/useCancelOrder.ts
utils/formatOrderStatus.ts
types/order.types.ts
index.ts ← public API barrel
# ❌ Type-based — order code is scattered across the entire project
src/
components/OrderList.tsx
components/OrderCard.tsx
hooks/useOrders.ts
utils/formatOrderStatus.ts
types/order.types.ts
Type-based organisation becomes unworkable at scale. To change anything about orders you must navigate four separate folders. Feature-based keeps everything related in one place.
Standard Project Structure
src/
├── app/ # Next.js App Router pages and layouts
│ ├── (auth)/ # Route group — auth pages
│ │ ├── login/
│ │ │ ├── page.tsx
│ │ │ └── error.tsx
│ │ └── layout.tsx
│ ├── dashboard/
│ │ ├── page.tsx
│ │ └── loading.tsx
│ ├── orders/
│ │ ├── [orderId]/
│ │ │ ├── page.tsx
│ │ │ └── error.tsx
│ │ ├── page.tsx
│ │ └── layout.tsx
│ ├── layout.tsx # Root layout
│ ├── error.tsx # Root error boundary
│ └── not-found.tsx
│
├── features/ # Domain feature modules
│ ├── orders/
│ │ ├── components/
│ │ │ ├── OrderCard/
│ │ │ │ ├── OrderCard.tsx
│ │ │ │ ├── OrderCard.test.tsx
│ │ │ │ └── index.ts
│ │ │ ├── OrderList/
│ │ │ │ ├── OrderList.tsx
│ │ │ │ ├── OrderList.test.tsx
│ │ │ │ └── index.ts
│ │ │ └── OrderStatusBadge.tsx
│ │ ├── hooks/
│ │ │ ├── useOrders.ts
│ │ │ ├── useOrders.test.ts
│ │ │ └── useCancelOrder.ts
│ │ ├── utils/
│ │ │ ├── formatOrderStatus.ts
│ │ │ └── formatOrderStatus.test.ts
│ │ ├── types/
│ │ │ └── order.types.ts
│ │ └── index.ts # ← public API — only export what's needed outside
│ │
│ ├── auth/
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── context/
│ │ │ └── AuthContext.tsx
│ │ └── index.ts
│ │
│ └── payments/
│ ├── components/
│ ├── hooks/
│ └── index.ts
│
├── components/ # Shared UI components (used across features)
│ ├── ui/ # Primitive UI — Button, Input, Modal, Badge
│ │ ├── Button/
│ │ │ ├── Button.tsx
│ │ │ ├── Button.test.tsx
│ │ │ └── index.ts
│ │ ├── Input/
│ │ ├── Modal/
│ │ └── index.ts
│ └── layout/ # Layout components — Header, Sidebar, Footer
│ ├── Header.tsx
│ └── Sidebar.tsx
│
├── lib/ # Third-party library setup and configuration
│ ├── queryClient.ts # TanStack Query client setup
│ ├── axios.ts # Axios instance with interceptors
│ └── analytics.ts
│
├── hooks/ # Shared hooks used across multiple features
│ ├── useDebounce.ts
│ ├── useMediaQuery.ts
│ └── useLocalStorage.ts
│
├── utils/ # Shared pure utility functions
│ ├── currency.ts
│ ├── dates.ts
│ └── validation.ts
│
├── types/ # Shared TypeScript types and interfaces
│ ├── api.types.ts
│ ├── common.types.ts
│ └── env.d.ts
│
├── constants/ # Application-wide constants
│ ├── routes.ts
│ ├── api.ts
│ └── config.ts
│
└── styles/ # Global styles and design tokens
├── globals.css
└── variables.css
File Naming Conventions
| File type | Convention | Example |
|---|---|---|
| React component | PascalCase.tsx |
OrderCard.tsx, UserAvatar.tsx |
| Custom hook | camelCase.ts with use prefix |
useOrders.ts, useMediaQuery.ts |
| Utility / helper | camelCase.ts |
formatCurrency.ts, validateEmail.ts |
| TypeScript types | camelCase.types.ts |
order.types.ts, api.types.ts |
| Context | PascalCase.tsx |
AuthContext.tsx, ThemeContext.tsx |
| Test file | Same as file under test + .test |
OrderCard.test.tsx, useOrders.test.ts |
| Story file (Storybook) | Same as component + .stories |
OrderCard.stories.tsx |
| Barrel export | index.ts |
index.ts |
| Next.js special files | lowercase (Next.js convention) | page.tsx, layout.tsx, error.tsx, loading.tsx |
Component Co-location
Co-locate tests, stories, and styles with the component file they belong to. Do not put all tests in a separate top-level __tests__ folder.
# ✅ Co-located — everything for OrderCard lives together
features/orders/components/OrderCard/
OrderCard.tsx # component
OrderCard.test.tsx # test
OrderCard.stories.tsx # storybook story (if used)
OrderCard.module.css # CSS module (if used)
index.ts # barrel: export { default } from './OrderCard'
# ❌ Not co-located — tests separated from components
__tests__/
features/
orders/
components/
OrderCard.test.tsx # far from the component it tests
For simple, single-file components that don't need a sub-folder, place the file directly in the components/ directory alongside the test:
Barrel Exports and Public APIs
Each feature folder must have an index.ts that defines its public API — the set of things other features and pages are allowed to import. Nothing should be imported from inside a feature's folder directly from outside.
// features/orders/index.ts
// ✅ Explicit public API — only export what other parts of the app should use
export { default as OrderCard } from './components/OrderCard'
export { default as OrderList } from './components/OrderList'
export { default as OrderStatusBadge } from './components/OrderStatusBadge'
export { useOrders } from './hooks/useOrders'
export { useCancelOrder } from './hooks/useCancelOrder'
export type { Order, OrderStatus, OrderItem } from './types/order.types'
// Internal utils and helpers are NOT exported — they are private to the feature
// ❌ Don't: export { formatOrderStatus } from './utils/formatOrderStatus'
// Usage in a page — always import from the feature's public API
// ✅ Correct — clean import from the public API
import { OrderList, useOrders } from '@/features/orders'
// ❌ Incorrect — bypasses the public API, creates tight coupling
import { OrderList } from '@/features/orders/components/OrderList/OrderList'
import { useOrders } from '@/features/orders/hooks/useOrders'
Where API Calls Live
API calls must never be made directly inside a component. They belong in dedicated service or query files in src/lib/ or within the feature's hooks/ using TanStack Query.
// ✅ Correct — API call in a typed service module
// features/orders/api/orderApi.ts
import { apiClient } from '@/lib/axios'
import type { Order, CreateOrderRequest } from '../types/order.types'
export const orderApi = {
getOrders: (userId: string): Promise<Order[]> =>
apiClient.get(`/users/${userId}/orders`).then(res => res.data),
getOrder: (orderId: string): Promise<Order> =>
apiClient.get(`/orders/${orderId}`).then(res => res.data),
cancelOrder: (orderId: string): Promise<void> =>
apiClient.delete(`/orders/${orderId}`),
createOrder: (request: CreateOrderRequest): Promise<Order> =>
apiClient.post('/orders', request).then(res => res.data),
}
// ✅ Correct — TanStack Query hook wraps the API call
// features/orders/hooks/useOrders.ts
import { useQuery } from '@tanstack/react-query'
import { orderApi } from '../api/orderApi'
export const useOrders = (userId: string) =>
useQuery({
queryKey: ['orders', userId],
queryFn: () => orderApi.getOrders(userId),
})
// ❌ Incorrect — fetch call directly inside a component
const OrderList = ({ userId }: { userId: string }) => {
const [orders, setOrders] = useState([])
useEffect(() => {
fetch(`/api/orders?userId=${userId}`) // ← never directly in a component
.then(r => r.json())
.then(setOrders)
}, [userId])
}
Path Aliases
Configure TypeScript path aliases to avoid deep relative imports. Every project must have @/ mapped to src/.
// ✅ Correct — clean absolute import
import { OrderCard } from '@/features/orders'
import { Button } from '@/components/ui'
import { formatCurrency } from '@/utils/currency'
// ❌ Incorrect — brittle relative import
import { OrderCard } from '../../../features/orders'
import { Button } from '../../components/ui'
Rules Summary
| Rule | Reason |
|---|---|
| Feature-based grouping | Related code stays together — easy to find, easy to delete |
| One component per file | Stack traces are readable; imports are explicit |
| Co-locate tests with components | Tests live next to the code they test — easier to maintain |
Public API via index.ts barrel |
Clear boundaries between features; internals can change freely |
| No direct API calls in components | Components are for rendering; data fetching belongs in hooks and services |
Path alias @/ → src/ |
No ../../../ — all imports are readable |
PascalCase.tsx for components |
Instantly distinguishable from hooks, utils, and types |