Skip to content

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:

features/orders/components/
  OrderStatusBadge.tsx
  OrderStatusBadge.test.tsx

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/.

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./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