
Fdd Architecture
- 37 installs
- Updated April 17, 2026
- jenishshrestha/ai-skills
Feature-Driven Development architecture and React coding standards for React + Vite apps: organize code by business capability with self-contained feature modules.
About
Structures React + Vite apps by business capability, with each feature owning its components, hooks, types, and tests and a shared layer under a 3+ feature rule. A developer uses it when organizing features, placing components, or setting up folder architecture during active coding.
- Feature modules with public-API index.ts and a shared/ infrastructure layer
- Applies KISS/SOLID/YAGNI and React patterns like hook extraction during coding
Fdd Architecture by the numbers
- 37 all-time installs (skills.sh)
- +2 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #1,406 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/jenishshrestha/ai-skills --skill fdd-architectureAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 37 |
|---|---|
| Last updated | April 17, 2026 |
| Repository | jenishshrestha/ai-skills ↗ |
What it does
Feature-Driven Development architecture and React coding standards for React + Vite apps: organize code by business capability with self-contained feature modules.
Files
Feature-Driven Architecture (FDD) for React + Vite
Organizes code by business capability rather than technical type. Each feature is a self-contained module with its own components, hooks, types, and tests.
Project Structure
src/
├── features/ # Feature modules (business capabilities)
│ ├── auth/
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── lib/
│ │ ├── types/
│ │ ├── index.ts # Public API
│ │ └── auth.test.tsx
│ └── dashboard/
│ └── ...
├── shared/ # Shared infrastructure (3+ feature rule)
│ ├── components/
│ │ ├── ui/ # Primitive components (Button, Input)
│ │ ├── layouts/ # Layout wrappers (Header, Sidebar)
│ │ └── providers/ # Global providers (ThemeProvider)
│ ├── hooks/
│ ├── lib/
│ └── types/
└── app/ # App entry and routing
├── routes/
└── main.tsxReact Quality Principles
These apply to all code written in this project, not just architecture.
KISS — Keep It Simple
Prefer standard React patterns over clever abstractions. If the logic is hard to follow without comments, simplify it. Don't reach for complex state management when a useState will do.
YAGNI — You Aren't Gonna Need It
Don't add "just in case" props, speculative abstractions, or premature optimizations. Build for what's needed now. If a prop has no current consumer, remove it.
Composition over Configuration
When a component accumulates 10+ boolean props, it's a sign it should be split into composable pieces using the compound component pattern:
// ❌ Configuration overload
<Card showHeader showFooter isCollapsible hasBorder variant="outlined" />
// ✅ Composition
<Card>
<Card.Header>Title</Card.Header>
<Card.Content>Body</Card.Content>
<Card.Footer>Actions</Card.Footer>
</Card>Explicit Predictability
Avoid hidden side-effects in useEffect. Prefer explicit event handlers and stable callback references. When something happens, the reader should be able to trace why without hunting through effect dependency arrays.
// ❌ Hidden side-effect — runs on every query change
useEffect(() => {
trackAnalytics("search", { query });
}, [query]);
// ✅ Explicit — fires on user action
const handleSearch = (query: string) => {
setQuery(query);
trackAnalytics("search", { query });
};Accessibility (a11y)
Every interactive element needs keyboard support, appropriate ARIA attributes, and visible focus indicators. Use semantic HTML elements (button, nav, main) over generic div with role attributes.
FDD Rules — Quick Reference
1. Feature Locality (CRITICAL)
All feature code lives inside src/features/[feature-name]/. Limit nesting to 3 levels max.
src/features/user-profile/
├── components/
│ ├── profile-card.tsx
│ └── avatar-upload.tsx
├── hooks/
│ └── use-profile.ts
├── lib/
│ └── format-name.ts
├── types/
│ └── profile.ts
├── index.ts
└── user-profile.test.tsx2. Public API Boundary (HIGH)
Every feature exports through index.ts. Never import from a feature's internals.
// src/features/auth/index.ts
export { LoginForm } from "./components/login-form";
export { useAuth } from "./hooks/use-auth";
export type { AuthUser, AuthState } from "./types";
// ❌ Importing from internals
import { LoginButton } from "@/features/auth/components/login-button";
// ✅ Importing from public API
import { LoginButton } from "@/features/auth";3. Import Conventions (HIGH)
Within a feature — relative paths:
import { useAuth } from "../hooks/use-auth";Between features — aliases through public API:
import { useAuth } from "@/features/auth";Types — always use import type:
import type { User } from "./types";4. Naming Conventions (MEDIUM)
| Kind | Convention | Example |
|---|---|---|
Component files (.tsx) | PascalCase | ProductCard.tsx, DataTable.tsx |
Hook files (.ts) | camelCase with use prefix | useDataTable.ts, useUserProfile.ts |
| Type files | kebab-case + .types.ts | product.types.ts, user-profile.types.ts |
| Schema files | kebab-case + .schema.ts | auth.schema.ts, product.schema.ts |
| Folders | kebab-case | user-profile/, data-table/ |
| Utilities / lib / api / config | kebab-case | fetch-users.ts, utils.ts |
Named imports only — no import *. See rules/naming-consistency.md for details and edge cases.
5. Shared Infrastructure (MEDIUM)
Rule of Three — only promote to src/shared/ after 3+ features use it.
- 1 feature → keep in feature folder
- 2 features → keep in original, or duplicate if logic might diverge
- 3+ features → move to
src/shared/
6. Hook Extraction
Extract business logic from components when:
- Component has 5+ lines of non-rendering logic
- Logic could be reused within the feature
- You need to test logic independently
// ❌ Logic mixed with UI
function ProfileCard() {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/user').then(res => res.json()).then(setUser);
}, []);
return <div>{user?.name}</div>;
}
// ✅ Logic extracted to hook
function ProfileCard() {
const { user, loading } = useUserProfile();
return <div>{user?.name}</div>;
}Deep-Dive Rules
For detailed explanations, examples, and edge cases, read the relevant rule file from rules/. Load only what you need:
| Rule File | Load When |
|---|---|
locality-co-location.md | Creating a new feature, deciding where a file belongs |
locality-depth.md | Feature folder is getting deep, need to restructure |
api-boundary.md | Setting up index.ts exports, reviewing cross-feature imports |
intra-feature-imports.md | Deciding between relative vs absolute import path |
naming-consistency.md | Naming a new file or folder |
named-imports.md | Import style questions, barrel file setup |
shared-global-move.md | Deciding whether to promote code to shared |
shared-component-organization.md | Organizing src/shared/components/ subdirectories |
hook-extraction.md | Extracting logic from a complex component |
Tooling
Vite Path Aliases (vite.config.ts)
export default defineConfig({
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
"@/features": path.resolve(__dirname, "./src/features"),
"@/shared": path.resolve(__dirname, "./src/shared"),
},
},
});TypeScript Paths (tsconfig.json)
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@/features/*": ["./src/features/*"],
"@/shared/*": ["./src/shared/*"]
}
}
}ESLint — Enforce Public API Boundary
{
"rules": {
"no-restricted-imports": [
"error",
{
"patterns": [
{
"group": ["@/features/*/components/*", "@/features/*/hooks/*"],
"message": "Import from feature's public API (index.ts) only"
}
]
}
]
}
}Feature-Based Code Splitting (TanStack Router)
const dashboardRoute = createRoute({
path: "/dashboard",
component: () => import("@/features/dashboard").then((m) => m.DashboardPage),
});For migration guides (flat structure → FDD, Next.js → Vite), see references/migration.md.
{
"skill_name": "FDD-architecture",
"evals": [
{
"id": 1,
"name": "scaffold-new-feature",
"prompt": "Create a new 'notifications' feature for our React app. It needs a NotificationList component, a useNotifications hook for fetching, and a Notification type.",
"expected_output": "Should create src/features/notifications/ with components/, hooks/, types/ subdirectories, an index.ts with public exports, kebab-case file names, and relative imports within the feature."
},
{
"id": 2,
"name": "fix-cross-feature-import",
"prompt": "My dashboard component imports useAuth directly from src/features/auth/hooks/use-auth.ts. Is this correct?",
"expected_output": "Should flag this as a public API boundary violation. The correct import is from '@/features/auth' (the index.ts). Should explain why: encapsulation, refactorability, and discoverability."
},
{
"id": 3,
"name": "shared-promotion-decision",
"prompt": "I have a DatePicker component in src/features/scheduling/components/date-picker.tsx. The invoicing feature also needs it. Should I move it to shared?",
"expected_output": "Should advise against moving to shared — only 2 features use it, which is below the Rule of Three threshold. Recommend keeping it in scheduling and importing via scheduling's public API, or duplicating if the features might diverge."
},
{
"id": 4,
"name": "god-component-refactor",
"prompt": "My ProductListingPage component has 250 lines: it manages filter state, syncs URL params, fetches products, handles pagination, and renders a complex table with inline editing. How should I refactor?",
"expected_output": "Should recommend hook extraction (useProductFilters, useProductList), splitting into sub-components (FilterBar, ProductTable, Pagination), and applying KISS/SRP principles. Should keep everything co-located within the products feature."
},
{
"id": 5,
"name": "composition-over-config",
"prompt": "My Card component has these props: showHeader, showFooter, isCollapsible, hasBorder, hasHoverEffect, variant, size, padding, headerAction, footerAction, onCollapse. Is this OK?",
"expected_output": "Should flag this as a configuration overload (10+ props) and recommend refactoring to a compound component pattern: Card, Card.Header, Card.Content, Card.Footer. Should reference the Composition over Configuration principle."
}
]
}
{
"version": "2.0.0",
"organization": "Engineering",
"date": "March 2026",
"abstract": "Architectural standards for organizing React + Vite applications using Feature-Driven Design (FDD). Ensures code locality, encapsulation, and modular scalability. Framework-agnostic patterns adapted for modern React development.",
"references": [
"https://feature-sliced.design/docs/get-started/tutorial",
"https://vitejs.dev/guide/",
"https://tanstack.com/router/latest",
"https://react.dev/learn/thinking-in-react"
]
}
Migration Guides
From Flat Structure to FDD
1. Identify business capabilities — each becomes a feature 2. Group related files into feature folders (components/, hooks/, types/, lib/) 3. Create `index.ts` for each feature with public exports 4. Update imports to use public APIs (aliases for cross-feature, relative for intra-feature) 5. Move truly shared code to src/shared/ (only if 3+ features use it) 6. Update aliases in vite.config.ts and tsconfig.json
From Next.js to Vite
1. Remove Server Component markers — delete all 'use client' and 'use server' directives 2. Update imports — replace next/* imports (next/image, next/link, next/router) with Vite-compatible equivalents 3. Replace routing — swap Next.js file-based routing for TanStack Router 4. Move data fetching — replace getServerSideProps / Server Actions with React Query / TanStack Query 5. Update config — replace next.config.js with vite.config.ts 6. Keep FDD structure — the feature organization pattern is framework-agnostic
Public API Boundary
Every feature MUST have an index.ts file that acts as its sole public entry point. All inter-feature communication must pass through this file. Never import directly from a feature's internal directories.
Incorrect (Leaking internals):
// Importing from deep inside another feature
import { InternalHelper } from '@/features/auth/lib/internal-helper';
import { formatAuthDate } from '@/features/auth/lib/format-date';Correct (Public API):
// src/features/auth/index.ts — the single entry point
export { useAuth } from './hooks/use-auth';
export { formatAuthDate } from './lib/format-date';
// src/features/profile/page.tsx — consuming feature
import { useAuth, formatAuthDate } from '@/features/auth';Why This Matters
- Encapsulation: Internal refactors don't break consumers.
- Discoverability:
index.tsdocuments what a feature offers. - Circular deps: Forces features to think about their public surface area.
See also: locality-co-location, shared-global-move.
Hook Extraction Pattern
If a component contains more than trivial business logic (state management, data fetching, form handling, side effects), extract that logic into a custom hook within the feature's hooks/ directory.
When to Extract
- Component has 5+ lines of non-rendering logic (state, effects, callbacks).
- Logic could be reused by another component within the same feature.
- You need to test the logic independently of the UI.
Incorrect (Logic mixed with UI):
// src/features/auth/components/login-form.tsx
'use client';
export function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const router = useRouter();
const onSubmit = async () => {
setIsLoading(true);
try {
await authClient.signIn.email({ email, password });
router.push('/dashboard');
} catch (e) {
setError('Invalid credentials');
} finally {
setIsLoading(false);
}
};
return <form>...</form>;
}Correct (Logic in hook, UI in component):
// src/features/auth/hooks/use-login.ts
export function useLogin() {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const router = useRouter();
const onSubmit = async (email: string, password: string) => {
setIsLoading(true);
try {
await authClient.signIn.email({ email, password });
router.push('/dashboard');
} catch (e) {
setError('Invalid credentials');
} finally {
setIsLoading(false);
}
};
return { onSubmit, isLoading, error };
}
// src/features/auth/components/login-form.tsx
('use client');
import { useLogin } from '../hooks/use-login';
export function LoginForm() {
const { onSubmit, isLoading, error } = useLogin();
return <form>...</form>;
}Benefits
- Testability: Test
useLoginindependently without rendering JSX. - Reusability: Another component in the same feature can reuse the hook.
- Readability: Components stay focused on rendering, hooks on behavior.
See also: locality-co-location, server-client-boundary.
Intra-Feature Relative Imports
When importing files that belong to the _same_ feature (i.e., inside src/features/my-feature), you MUST use explicit relative paths (e.g., import { Button } from './components/button').
You MUST NEVER use absolute aliases (e.g., @/features/my-feature/...) to reference files internal to the feature.
Incorrect (Tightly coupled to filesystem location):
// Inside src/features/auth/hooks/use-auth.ts
import { Button } from '@/features/auth/components/button';
import { AUTH_URL } from '@/features/auth/types';Correct (Decoupled and relocatable):
// Inside src/features/auth/hooks/use-auth.ts
import { Button } from '../components/button';
import { AUTH_URL } from '../types';Why This Matters
- Relocatability: A feature folder should be entirely self-contained. If you rename or move the
authfeature directory, its internal relative imports will not break. - Locality First: Relative imports mentally signify "this lives next to me," whereas absolute imports signify "this is a remote dependency."
See also: locality-co-location, api-boundary.
Feature Co-location
Keep all feature-specific code (components, hooks, API contracts, styles, tests) inside the feature's directory. Treat every feature as a self-contained "mini-app."
Standard feature subfolders
| Folder | Purpose | Example files |
|---|---|---|
api/ | API contracts, endpoint configs, fetch functions, query hooks | users-table.config.ts, get-user.ts |
components/ | React components scoped to this feature | UserCard.tsx, ProfileForm.tsx |
hooks/ | Non-API hooks (UI state, form logic) | useProfileForm.ts |
lib/ | Pure utility helpers, formatters, column definitions | columns.tsx, format-name.ts |
types/ | Feature-specific types (or use feature-name.types.ts at root) | index.ts |
Two-tier API principle
- Shared (
src/shared/lib/api/client.ts) — raw HTTP client, interceptors, auth token injection. This is infrastructure. - Feature (
src/features/X/api/) — endpoint-specific configs, fetch functions, query options. This is feature logic that talks to a server.
Incorrect (Scattered files):
src/
components/
FeedbackList.tsx
hooks/
useFeedback.ts
api/
feedback-api.ts ← don't use a global api/ folder for feature code
tests/
feedback-actions.test.ts ← don't use a global tests/ folder
features/
feedback/
page.tsxCorrect (Co-located):
src/
features/
feedback/
api/
feedback-table.config.ts
components/
FeedbackList.tsx
hooks/
useFeedback.ts
lib/
columns.tsx
feedback.types.ts
FeedbackPage.tsx
feedback.test.tsx ← test lives next to the code it verifies
index.tsSee also: api-boundary, naming-consistency.
Nesting Depth
Limit feature nesting to a maximum of 3 levels. If a feature becomes too complex, break it into smaller sub-features or promote shared logic.
Incorrect (Deeply nested):
src/features/dashboard/user/profile/settings/security/hooks/use-mfa.tsCorrect (Flattened structure):
src/features/dashboard/hooks/use-dashboard.ts
src/features/user-settings/hooks/use-mfa.tsNamed Imports Only
Always use named imports instead of namespace imports (import * as). This makes dependencies explicit, improves tree-shaking, and keeps code scannable.
Incorrect (namespace import):
import * as React from 'react';
const context = React.createContext<Value>(defaultValue);
const [state, setState] = React.useState(false);Correct (named imports):
import { createContext, useState } from 'react';
const context = createContext<Value>(defaultValue);
const [state, setState] = useState(false);For types, use import type:
import { useState, type ReactNode } from 'react';Exception
The only acceptable use of import * is for re-exporting an entire module in an index.ts barrel file:
// src/features/auth/index.ts — acceptable
export * from './hooks/use-auth';Naming Conventions
Component files — PascalCase
React component files (.tsx) use PascalCase, matching the exported component name.
✅ DemoTablePage.tsx
✅ DataTable.tsx
✅ Button.tsxHook files — camelCase
Hook files use camelCase, matching the exported hook name.
✅ useUserProfile.ts
✅ useDataTable.ts
✅ useMobile.tsType files — kebab-case with .types.ts suffix
✅ demo-table.types.ts
✅ user-profile.types.tsSchema files — kebab-case with .schema.ts suffix
✅ demo-table.schema.ts
✅ auth.schema.tsAll folders — kebab-case
Every folder, including those containing PascalCase component files, uses kebab-case.
✅ src/shared/components/ui/button/Button.tsx
✅ src/shared/components/ui/data-table/DataTable.tsx
✅ src/features/demo-table/DemoTablePage.tsx
✅ src/shared/lib/data-table/useDataTable.tsUtilities, API, and other files — kebab-case
✅ fetch-users.ts
✅ utils.ts
✅ client.tsSummary
| Type | Convention | Example |
|---|---|---|
Component files (.tsx) | PascalCase | DemoTablePage.tsx |
| Hook files | camelCase | useDataTable.ts |
| Type files | kebab-case + .types.ts | demo-table.types.ts |
| Schema files | kebab-case + .schema.ts | demo-table.schema.ts |
| All folders | kebab-case | data-table/, demo-table/ |
| Utilities / API / config | kebab-case | fetch-users.ts, utils.ts |
Shared Component Organization
The src/shared/components/ directory should be organized into clear sub-categories. Avoid dumping all shared components as flat files in the root of the directory.
Directory Structure
src/shared/components/
├── ui/ # Primitive UI components (Button, Input, Avatar, etc.)
├── layouts/ # Layout wrappers (Header, Footer, Sidebar, AppLayout)
├── providers/ # Global context providers (SessionProvider, ThemeProvider)
├── form/ # Reusable form components (Form, Field, FieldGroup)
├── data-table/ # Reusable data table components
├── logo.tsx # Truly shared atoms (used across 3+ features)
└── theme-toggle.tsxRules
1. `ui/`: Only primitive, unstyled or design-system components (e.g., Shadcn/Radix primitives). These have zero business logic.
2. `layouts/`: Components that define the structural shell of a page or route group. Examples: header.tsx, footer.tsx, app-sidebar.tsx, app-layout.tsx.
3. `providers/`: React Context providers that wrap the app or a subtree. Examples: session-provider.tsx, theme-provider.tsx. These are infrastructure, not UI.
4. Feature-specific composed components: Components that combine primitives with business logic (e.g., user-account-nav.tsx) should live in their owning feature (src/features/auth/components/), not in shared/. They can be exported via the feature's index.ts.
5. Shared composed components: Components like user-info.tsx or logout-menu-item.tsx that are used across 2+ unrelated features may live as flat files in shared/components/. If they grow into a group, create a sub-directory.
Decision Tree
Is it a primitive UI element (no business logic)?
→ ui/
Is it a layout shell (header, sidebar, footer)?
→ layouts/
Is it a React Context provider?
→ providers/
Is it used by only 1-2 features?
→ Keep in the owning feature (src/features/<name>/components/)
Is it used by 3+ features?
→ shared/components/ (flat file or sub-directory)Incorrect (Flat dumping):
src/shared/components/
├── header.tsx
├── footer.tsx
├── session-provider.tsx
├── theme-provider.tsx
├── user-account-nav.tsx ← auth-specific, shouldn't be here
├── user-info.tsx
└── logout-menu-item.tsxCorrect (Organized):
src/shared/components/
├── ui/
├── layouts/
│ ├── header.tsx
│ ├── footer.tsx
│ ├── app-sidebar.tsx
│ └── app-layout.tsx
├── providers/
│ ├── session-provider.tsx
│ └── theme-provider.tsx
├── user-info.tsx ← shared atom (used in header + sidebar)
└── logout-menu-item.tsx ← shared atom (used in header + sidebar)Promoting to Shared
Do not move feature code to src/shared prematurely. Only promote a component or utility to the shared layer if it is used by at least three different features. This prevents "Shared Bloat" and keeps domain logic encapsulated.
The "Rule of Three":
1. 1 Feature: Keep it inside src/features/[feature-name]/components. 2. 2 Features: Keep it in the feature where it was first created, and export it. Or duplicate it if the logic is simple enough but likely to diverge. 3. 3+ Features: Move it to src/shared/components.
Incorrect (Moving too early):
// Component only used in 'auth' and 'profile', but placed in shared
src / shared / components / custom - avatar.tsx;Correct (Keeping it local/domain-specific):
// Keeping it local to its primary domain
src / features / auth / components / custom - avatar.tsx;See also: locality-co-location, api-boundary.