Feature-Based Architecture
This project follows a strict feature-based architecture. All domain logic is encapsulated inside src/features/. The src/app/ directory contains only thin route files.
IMPORTANT: Read this entire skill before creating any files. Every decision about where code lives is governed by this architecture.
Feature Structure
Every feature is a self-contained module:
src/features/[feature-name]/
├── index.ts # Barrel export — single entry point
├── schemas/
│ └── [name].schema.ts # Zod schemas (single source of truth)
├── types/
│ └── [name].ts # Types inferred from schemas via z.infer
├── components/
│ ├── [name].tsx # React components (Server or Client)
│ └── [name].test.tsx # Co-located unit tests
├── queries/
│ └── use-[resource].ts # Key factory + client hooks + server prefetch
├── hooks/
│ └── use-[name].ts # Custom hooks (not stores)
├── stores/
│ └── [name]-store.ts # Zustand stores (use[Name]Store)
├── mocks/
│ └── [name].mock.ts # Mock factories (createMock[Entity])
└── constants/
└── [name].ts # UPPER_SNAKE_CASE constants
Decision Tree: Where Does This Code Go?
Use this to decide where any new code belongs:
Is it domain-specific logic? ├── YES → src/features/[feature-name]/ │ ├── Is it a React component? → components/[name].tsx │ ├── Is it a data shape/validation? → schemas/[name].schema.ts │ ├── Is it a type inferred from a schema? → types/[name].ts │ ├── Is it API data fetching (queries/mutations)? → queries/use-[resource].ts │ ├── Is it a custom hook (NOT a store)? → hooks/use-[name].ts │ ├── Is it client-only UI state? → stores/[name]-store.ts (Zustand) │ ├── Is it test mock data? → mocks/[name].mock.ts │ ├── Is it a filter definition? → schemas/[name]-filters.schema.ts (server + client filter schemas) │ └── Is it a constant value? → constants/[name].ts ├── NO → Is it a UI primitive (button, input, dialog)? │ ├── YES → src/components/ui/ (shadcn/ui — do NOT modify) │ └── NO → Is it a shared utility or infrastructure? │ ├── YES → src/lib/ (utils, api-client, query-client, etc.) │ └── NO → Is it an external third-party service? │ ├── YES → src/services/[name]/ (e.g., datadog, launchdarkly) │ └── NO → Is it a cross-cutting provider? │ ├── YES → src/lib/providers/[name]-provider.tsx │ └── NO → Is it a global type? │ ├── YES → src/types/ │ └── NO → Ask: does this really need to exist?
Barrel Export Rules
Every feature exports its public API through a single index.ts. External code NEVER imports from feature internals.
See examples/barrel-export.ts for the exact pattern.
Forbidden:
// ❌ NEVER import from feature internals
import { TaskCard } from "@/features/tasks/components/task-card";
import { taskSchema } from "@/features/tasks/schemas/task.schema";
import { useTasks } from "@/features/tasks/queries/use-tasks";
Correct:
// ✅ Always import from the barrel
import { TaskCard, taskSchema, useTasks } from "@/features/tasks";
What to Export in index.ts
Export everything that external code needs:
// src/features/tasks/index.ts
// Components
export { TaskCard } from "./components/task-card";
export { TaskList } from "./components/task-list";
// Queries (client hooks + server prefetch + key factory)
export { useTasks, useTask, prefetchTasks, prefetchTask, taskKeys } from "./queries/use-tasks";
// Schemas
export { taskSchema, createTaskInputSchema } from "./schemas/task.schema";
// Types (use `export type` for types)
export type { Task, CreateTaskInput } from "./types/task";
// Mocks (for external tests)
export { createMockTask, createMockTaskList } from "./mocks/task.mock";
// Stores
export { useTaskStore } from "./stores/task-store";
// Constants
export { TASK_STATUS } from "./constants/task-status";
Import Rules
| From | Can import from | Cannot import from |
|---|---|---|
app/ pages | @/features/[name] (barrel), @/components/ui/, @/lib/ | Feature internals |
| Feature A | @/lib/, @/components/ui/, @/types/, @/features/B (barrel only) | Feature B internals |
| Feature internals | Sibling files within same feature, @/lib/, @/components/ui/ | Other feature internals |
Thin Pages
Pages in src/app/(dashboard)/ are async Server Components that only:
- •Call
await prefetchXxx()for SSR data - •Wrap children with
<Hydrate>from@/lib/hydrate - •Render feature components
See examples/thin-page.tsx for the pattern.
// ✅ CORRECT — thin page
import { Hydrate } from "@/lib/hydrate";
import { TaskList, prefetchTasks } from "@/features/tasks";
export default async function TasksPage() {
await prefetchTasks();
return (
<Hydrate>
<TaskList />
</Hydrate>
);
}
// ❌ WRONG — page with business logic
import { apiClient } from "@/lib/api-client";
export default async function TasksPage() {
const tasks = await apiClient("/api/tasks"); // ❌ fetching directly in page
const filtered = tasks.filter(t => t.status === "active"); // ❌ business logic in page
return <div>{filtered.map(t => <p key={t.id}>{t.title}</p>)}</div>; // ❌ rendering directly
}
Navigation
When a feature needs a nav entry, add it to src/features/shell/constants/navigation.ts. The shell feature owns all navigation.
Naming Conventions
| Element | Convention | Example |
|---|---|---|
| Feature folders | kebab-case singular | task-management/ |
| Components | PascalCase export, kebab-case file | task-card.tsx → TaskCard |
| Hooks | camelCase with use prefix | use-task-filters.ts → useTaskFilters |
| Stores | [name]-store.ts in stores/ | useTaskStore |
| Schemas | [name].schema.ts in schemas/ | taskSchema |
| Mocks | [name].mock.ts in mocks/ | createMockTask |
| Constants | UPPER_SNAKE_CASE | TASK_STATUS |
| Query files | use-[resource].ts in queries/ | use-tasks.ts |
| Type files | [name].ts in types/ | task.ts |
| Test files | [name].test.ts(x) co-located | task-card.test.tsx |
Creating a New Feature — Step by Step
- •Create the feature folder:
src/features/[feature-name]/ - •Create Zod schemas first:
schemas/[name].schema.ts - •Infer types:
types/[name].tswithz.infer<typeof schema> - •Create components:
components/[name].tsx - •Create query file (if data fetching needed):
queries/use-[resource].ts - •Create mock factories:
mocks/[name].mock.ts - •Create stores (if client-only UI state needed):
stores/[name]-store.ts - •Create barrel export:
index.ts - •Create thin page:
src/app/(dashboard)/[feature]/page.tsx - •Add nav item:
src/features/shell/constants/navigation.ts
DO NOT
- •DO NOT create loose components, hooks, or types outside of a feature — all domain code goes in
src/features/. - •DO NOT import from feature internals — always use the barrel
index.ts. - •DO NOT put business logic in pages — pages are thin (prefetch + Hydrate + render).
- •DO NOT put Zustand stores in
hooks/— stores go instores/[name]-store.ts. - •DO NOT put provider logic directly in layout files — providers go in
src/lib/providers/. - •DO NOT modify shadcn/ui files in
src/components/ui/— use them as-is. - •DO NOT hand-write TypeScript interfaces — use Zod schemas with
z.infer. - •DO NOT create a feature without a barrel
index.ts— every feature needs one. - •DO NOT forget to add feedback states (EmptyState, ErrorState, LoadingState) — never return
nullfor empty data.