Code Organization Options for arda-frontend-app
Analysis of directory structure options that express the functional boundaries identified in the codebase audit — SPA vs. BFF vs. shared, client state vs. server state, domain features vs. infrastructure — and can be enforced with ESLint rules.
1. Current State
Section titled “1. Current State”Directory structure
Section titled “Directory structure”src/├── app/ # Next.js App Router (pages + API routes mixed)│ ├── api/arda/ # BFF proxy routes (55 route.ts files)│ ├── items/ # Page components (colocated with pages)│ ├── kanban/ # Page components│ └── ... # 15+ page directories├── components/ # React components (119 files, flat by feature)│ ├── common/ # Shared layout components│ ├── items/ # Item-specific components (ItemFormPanel, etc.)│ ├── scan/ # Scan-specific components│ ├── settings/ # Settings components│ ├── table/ # ArdaGrid, column presets│ ├── ui/ # ShadCN primitives│ └── ...├── lib/ # Mixed utilities (37 files, flat)│ ├── ardaClient.ts # SPA-only: API calls to BFF│ ├── jwt.ts # Shared: server verification + client decode│ ├── env.ts # BFF-only: server secrets│ ├── api-helpers.ts # BFF-only: request parsing│ ├── mappers/ # SPA-only: type mapping│ └── validators/ # SPA-only: form validation├── store/ # Redux Toolkit (33 files)│ ├── slices/ # State slices│ ├── thunks/ # Async actions│ ├── hooks/ # Redux hooks│ ├── selectors/ # Memoized selectors│ └── middleware/ # Token refresh middleware├── contexts/ # React contexts (4 files)├── hooks/ # Custom hooks (14 files, flat)├── types/ # TypeScript types (flat)├── constants/ # Constants and enums├── mocks/ # MSW setup + handlers + data├── utils/ # Helper functions├── lambda/ # Cognito Lambda functions (separate concern)├── test-utils/ # Test render helpers└── tests/ # Integration testsProblems
Section titled “Problems”-
lib/is a flat bag — server-only (env.ts), client-only (ardaClient.ts), and shared (jwt.ts) modules coexist with no structural distinction. An ESLint rule cannot target “server files in lib” because there’s no directory boundary. -
No SPA API functions layer — client-side API calls live in
lib/ardaClient.ts(a single 500+ line file mixing all domains). The proposedsrc/api/layer from this project has no precedent. -
Components are flat by feature —
components/items/contains 20+ files mixing page-specific components, cell editors, form panels, and typeahead editors. No distinction between “used on one page” vs. “reusable across features.” -
Hooks are flat —
hooks/has 14 files with no grouping by concern (auth hooks, form hooks, data hooks mixed). -
Store mixes server and client state —
store/slices/itemsSlice.tsholds both server data (fetched items) and client UI state (column visibility, active tab, drafts). TanStack Query adoption will split this, but the store structure doesn’t express the boundary. -
app/mixes pages and BFF — Next.js App Router forcesapi/underapp/, but this means page routes and server routes are siblings. No ESLint rule can distinguish “this is a server-only directory” vs. “this is a client page.”
2. ESLint Enforcement Tools
Section titled “2. ESLint Enforcement Tools”eslint-plugin-boundaries
Section titled “eslint-plugin-boundaries”The primary tool. Defines zones (directory-based boundary regions) and rules restricting imports between zones.
// Example: zones definitionsettings: { 'boundaries/elements': [ { type: 'bff', pattern: 'src/server/**' }, { type: 'spa-api', pattern: 'src/api/**' }, { type: 'hooks', pattern: 'src/hooks/**' }, { type: 'store', pattern: 'src/store/**' }, { type: 'ui', pattern: 'src/components/**' }, { type: 'types', pattern: 'src/types/**' }, { type: 'shared', pattern: 'src/shared/**' }, ],},rules: { 'boundaries/element-types': ['error', { default: 'disallow', rules: [ // BFF can import from shared and types, nothing else { from: 'bff', allow: ['shared', 'types'] }, // SPA API can import from shared and types { from: 'spa-api', allow: ['shared', 'types'] }, // Hooks can import from spa-api, store, shared, types { from: 'hooks', allow: ['spa-api', 'store', 'shared', 'types'] }, // UI can import from hooks, store, shared, types — NOT from bff or spa-api { from: 'ui', allow: ['hooks', 'store', 'shared', 'types'] }, // Shared can only import from types { from: 'shared', allow: ['types'] }, ], }],}Requirement: zones must map to directories. This is why the directory structure matters — you can’t define a zone for “server-only files in lib” when they’re mixed with client files.
@typescript-eslint/no-restricted-imports
Section titled “@typescript-eslint/no-restricted-imports”Blocks specific import patterns anywhere, regardless of directory:
'@typescript-eslint/no-restricted-imports': ['error', { patterns: [{ group: ['@tanstack/react-query'], importNamePattern: '^use', message: 'Import TanStack hooks from @/hooks/, not directly', }],}]import/no-restricted-paths
Section titled “import/no-restricted-paths”Restricts specific source → target path combinations:
'import/no-restricted-paths': ['error', { zones: [{ target: './src/components/**', from: './src/server/**', message: 'Components cannot import server-only code', }],}]server-only package (Next.js)
Section titled “server-only package (Next.js)”Not ESLint — build-time enforcement. Any file that import 'server-only'
causes a build error if bundled into client code.
3. Options
Section titled “3. Options”Option A: Layer-Based (Horizontal Slices)
Section titled “Option A: Layer-Based (Horizontal Slices)”Organize by technical concern at the top level. Each directory is a zone with clear import rules.
src/├── app/ # Next.js App Router — pages only│ ├── items/│ ├── kanban/│ └── ...│├── server/ # BFF-only code (Zone: bff)│ ├── routes/ # API route handlers│ │ ├── arda/items/ # mirrors app/api/arda/items/│ │ ├── arda/kanban/│ │ └── storage/ # image upload BFF routes│ ├── lib/ # Server-only utilities│ │ ├── env.ts│ │ ├── jwt-verify.ts│ │ └── api-helpers.ts│ └── index.ts # Barrel (import 'server-only')│├── api/ # SPA API functions (Zone: spa-api)│ ├── image-upload.ts # Plain async fetch functions│ ├── items.ts # (future: extract from ardaClient)│ └── index.ts│├── hooks/ # React hooks (Zone: hooks)│ ├── queries/ # TanStack useQuery wrappers│ ├── mutations/ # TanStack useMutation wrappers│ ├── providers/ # FD-01 typed provider implementations│ └── auth/ # Auth-related hooks│├── store/ # Redux — client state only (Zone: store)│ ├── slices/│ ├── hooks/│ └── middleware/│├── components/ # React components (Zone: ui)│ ├── common/│ ├── items/│ ├── scan/│ └── ui/ # ShadCN primitives│├── shared/ # Code used by both SPA and BFF (Zone: shared)│ ├── jwt-decode.ts│ ├── types/ # Shared TypeScript interfaces│ └── utils.ts│├── providers/ # React context providers│ ├── query-provider.tsx│ ├── cdn-cookie-provider.tsx│ └── redux-provider.tsx│├── mocks/ # MSW setup + handlers├── test-utils/ # Test helpers└── constants/ # Constants and enumsESLint zones:
| Zone | Directory | Can import from |
|---|---|---|
bff | src/server/ | shared, types |
spa-api | src/api/ | shared, types |
hooks | src/hooks/ | spa-api, store, shared, types |
store | src/store/ | shared, types |
ui | src/components/, src/providers/ | hooks, store, shared, types |
shared | src/shared/ | types only |
pages | src/app/ (non-api) | ui, hooks, providers, store, shared, types |
Next.js constraint: App Router requires API routes under src/app/api/.
The src/server/routes/ directory would hold the handler logic, but the
actual route.ts files in src/app/api/ would be thin re-exports:
// src/app/api/image-upload/route.ts — thin re-exportexport { POST } from '@/server/routes/storage/image-upload';Pros:
- Clear zones map 1:1 to directories — ESLint rules are straightforward
- BFF code is physically separated from SPA code
- New layer (
src/api/) has a dedicated home import 'server-only'insrc/server/index.tscatches leaks at build time
Cons:
- Next.js forces
app/api/for routing, so server routes need thin re-exports - Feature code is spread across directories (items logic in
components/items/,hooks/providers/,api/items.ts,server/routes/arda/items/) - Moderate migration effort (~65 files moved + ~200 import updates)
Option B: Domain-Based (Vertical Slices)
Section titled “Option B: Domain-Based (Vertical Slices)”Organize by domain feature. Each feature contains its own components, hooks, API functions, and types. Shared infrastructure stays horizontal.
src/├── app/ # Next.js App Router — pages + API routes│ ├── api/arda/ # BFF routes (thin, delegate to features)│ └── items/ # Page routes│├── features/ # Domain features (Zone per feature)│ ├── items/│ │ ├── components/ # ItemFormPanel, cell editors, etc.│ │ ├── hooks/ # useItemQuery, useItemMutation, providers│ │ ├── api/ # SPA API functions for items│ │ ├── types/ # Item-specific types│ │ └── index.ts # Feature barrel│ ├── image-upload/│ │ ├── components/ # (wiring hooks, not design system components)│ │ ├── hooks/ # useImageUpload, useCheckReachability│ │ ├── api/ # SPA API functions for image upload│ │ └── types/│ ├── kanban/│ ├── scan/│ └── auth/│├── server/ # BFF infrastructure (Zone: bff)│ ├── lib/│ │ ├── env.ts│ │ ├── jwt-verify.ts│ │ └── api-helpers.ts│ └── middleware/│├── shared/ # Cross-feature shared code (Zone: shared)│ ├── lib/│ │ ├── jwt-decode.ts│ │ └── utils.ts│ ├── types/│ ├── hooks/ # Generic hooks (useDebounce, etc.)│ └── components/│ └── ui/ # ShadCN primitives│├── store/ # Redux — client state (Zone: store)│ ├── slices/│ └── hooks/│├── providers/ # React context providers├── mocks/├── test-utils/└── constants/ESLint zones:
| Zone | Directory | Can import from |
|---|---|---|
bff | src/server/ | shared |
feature-* | src/features/*/ | shared, store, other features (opt-in) |
store | src/store/ | shared |
shared | src/shared/ | nothing (leaf) |
pages | src/app/ (non-api) | features/*, shared, store, providers |
Cross-feature imports can be controlled: by default, features cannot import from each other. When needed, the ESLint rule allows explicit exceptions:
// items feature can import from image-upload feature (for ImageFormField wiring){ from: 'feature-items', allow: ['feature-image-upload', 'shared', 'store'] }Pros:
- Feature code is colocated — all items logic in one directory
- New features don’t touch shared directories
- Cross-feature dependencies are explicit and auditable
- Natural code-splitting boundaries
Cons:
- Shared components (used by multiple features) need a decision: which feature
owns them, or do they go in
shared/components/? - BFF route handlers may serve multiple features (e.g., a single item route used by both items and kanban features)
- Deep nesting:
src/features/items/hooks/providers/useItemImageUpload.ts - Feature boundary decisions can be contentious (is “image upload” a feature or part of “items”?)
Option C: Hybrid — Layers with Feature Grouping
Section titled “Option C: Hybrid — Layers with Feature Grouping”Top-level layers (like Option A) but with feature grouping within the component and hook layers.
src/├── app/ # Next.js App Router│ ├── api/ # Route files (thin, delegate to server/)│ └── (pages)/ # Page routes│├── server/ # BFF-only (Zone: bff)│ ├── routes/│ ├── lib/│ └── index.ts # import 'server-only'│├── api/ # SPA API functions (Zone: spa-api)│ ├── items.ts│ ├── image-upload.ts│ └── kanban.ts│├── hooks/ # React hooks (Zone: hooks)│ ├── items/ # Item-specific hooks│ │ ├── queries.ts│ │ ├── mutations.ts│ │ └── providers.ts # FD-01 typed provider implementations│ ├── image-upload/ # Image upload hooks│ │ ├── mutations.ts│ │ └── providers.ts│ ├── cdn/│ │ └── queries.ts│ └── auth/│ └── useAuth.ts│├── components/ # React components (Zone: ui)│ ├── items/ # Feature-grouped│ ├── scan/│ ├── common/ # Cross-feature shared│ └── ui/ # ShadCN primitives│├── store/ # Redux — client state (Zone: store)│ ├── slices/│ └── hooks/│├── shared/ # SPA + BFF shared code (Zone: shared)│ ├── jwt-decode.ts│ ├── types/│ └── utils.ts│├── providers/ # React context providers├── mocks/├── test-utils/└── constants/ESLint zones — same as Option A, with additional granularity possible within hooks:
// hooks/items/ can import from api/items.ts but not api/kanban.ts'boundaries/element-types': ['error', { rules: [ { from: 'hooks-items', allow: ['spa-api-items', 'store', 'shared'] }, { from: 'hooks-image-upload', allow: ['spa-api-image-upload', 'shared'] }, ],}]Pros:
- Layers are clear at the top level (easy to understand the architecture)
- Feature grouping within layers keeps related code close
- ESLint zones are straightforward — top-level directories are zones
- Less nesting than Option B
- Natural migration path from current structure (move files into new top-level dirs)
Cons:
- Feature code is still spread across directories (components/items/, hooks/items/, api/items.ts) — but they’re grouped within each layer
- Per-feature ESLint rules within a zone require more configuration
4. Comparison
Section titled “4. Comparison”| Criterion | Option A (Layers) | Option B (Features) | Option C (Hybrid) |
|---|---|---|---|
| ESLint zone clarity | Strong — 1:1 with top dirs | Moderate — per-feature zones need enumeration | Strong — 1:1 with top dirs |
| Boundary enforcement | Simple — 7 zones | Complex — N features + shared zones | Simple — 7 zones, optional per-feature |
| Feature colocality | Low — items code in 4 dirs | High — all in one dir | Medium — grouped within layers |
| New feature effort | Touch 4 directories | Create 1 directory | Touch 3 directories |
| Migration from current | Moderate — restructure lib/, add server/, api/, shared/ | High — reorganize everything | Moderate — same as A + feature dirs in hooks/components |
| Next.js compatibility | Good — thin re-exports in app/api/ | Good — same | Good — same |
| Cross-feature visibility | Implicit — hooks/ is flat | Explicit — ESLint controls | Implicit within layer, explicit across |
| Discoverability | Layer → feature (top-down) | Feature → layers (bottom-up) | Layer → feature (top-down) |
5. Recommended ESLint Configuration
Section titled “5. Recommended ESLint Configuration”Regardless of the chosen directory structure, these rules should be adopted:
5.1 eslint-plugin-boundaries — Zone enforcement
Section titled “5.1 eslint-plugin-boundaries — Zone enforcement”import boundaries from 'eslint-plugin-boundaries';
{ plugins: { boundaries }, settings: { 'boundaries/elements': [ { type: 'bff', pattern: 'src/server/**', mode: 'folder' }, { type: 'spa-api', pattern: 'src/api/**', mode: 'folder' }, { type: 'hooks', pattern: 'src/hooks/**', mode: 'folder' }, { type: 'store', pattern: 'src/store/**', mode: 'folder' }, { type: 'ui', pattern: 'src/components/**', mode: 'folder' }, { type: 'providers', pattern: 'src/providers/**', mode: 'folder' }, { type: 'shared', pattern: 'src/shared/**', mode: 'folder' }, { type: 'pages', pattern: 'src/app/**', mode: 'folder', capture: ['app'] }, ], }, rules: { 'boundaries/element-types': ['error', { default: 'allow', rules: [ // BFF cannot import SPA code { from: 'bff', disallow: ['ui', 'hooks', 'store', 'providers', 'spa-api'] }, // SPA API cannot import BFF or UI { from: 'spa-api', disallow: ['bff', 'ui', 'hooks', 'store', 'providers'] }, // UI cannot import BFF or SPA API directly { from: 'ui', disallow: ['bff', 'spa-api'] }, // Shared cannot import anything except types { from: 'shared', disallow: ['bff', 'spa-api', 'ui', 'hooks', 'store', 'providers'] }, // Store cannot import UI or BFF { from: 'store', disallow: ['bff', 'ui', 'providers'] }, ], }], },}5.2 @typescript-eslint/no-restricted-imports — Package boundaries
Section titled “5.2 @typescript-eslint/no-restricted-imports — Package boundaries”{ rules: { '@typescript-eslint/no-restricted-imports': ['error', { patterns: [ // Design system components must not import TanStack Query // (enforced in ux-prototype, but also guard against leaks here) { group: ['@tanstack/react-query'], importNamePattern: '^(useQuery|useMutation|useQueryClient)', message: 'Do not import TanStack Query in components imported from design system. Use typed provider hooks instead (FD-01).', }, // server-only modules must not be imported from client directories { group: ['@/server/*'], message: 'Cannot import server-only code from this location. Use @/api/ for SPA-side API calls.', }, ], }], // Apply different rules to server files }, // Override for BFF files — allow server imports overrides: [{ files: ['src/server/**/*.ts', 'src/app/api/**/*.ts'], rules: { '@typescript-eslint/no-restricted-imports': 'off', // server can import anything }, }],}5.3 Per-directory server-only enforcement
Section titled “5.3 Per-directory server-only enforcement”import 'server-only';// Build fails if any client component transitively imports this5.4 Dependency direction visualization
Section titled “5.4 Dependency direction visualization”The import rules form a directed acyclic graph:
6. Decision (FD-02)
Section titled “6. Decision (FD-02)”Option C (Hybrid) adopted for arda-frontend-app. Recorded as FD-02 in the
project decision log.
Reasons:
-
Matches the existing mental model — the current structure is already layer-based. Adding
server/,api/, andshared/as explicit layers (and restructuringlib/) is evolutionary, not revolutionary. -
ESLint zones are straightforward — top-level directories map 1:1 to boundary zones. No need to enumerate every feature as a zone.
-
Feature grouping within layers provides colocality where it matters (hooks for items are in
hooks/items/, not scattered) without the overhead of full vertical slices. -
Migration path is incremental — can be done in phases:
- Phase 1: Create
server/,api/,shared/; movelib/contents (this is #734) - Phase 2: Add
eslint-plugin-boundarieswith zone rules - Phase 3: Group hooks and components by feature within their layers
- Phase 4: Add per-feature ESLint rules within zones (optional)
- Phase 1: Create
-
Compatible with TanStack Query adoption —
hooks/queries/,hooks/mutations/, andhooks/providers/(or grouped by feature:hooks/items/,hooks/image-upload/) have a clear home.
Copyright: (c) Arda Systems 2025-2026, All rights reserved
Copyright: © Arda Systems 2025-2026, All rights reserved