feature-architecture
Guides feature-based architecture in Next.js applications. Activates when creating new features, organizing domain code, setting up barrel exports, or structuring feature modules with components, schemas, types, queries, mocks, stores, and constants.
SKILL.md
| Name | feature-architecture |
| Description | Guides feature-based architecture in Next.js applications. Activates when creating new features, organizing domain code, setting up barrel exports, or structuring feature modules with components, schemas, types, queries, mocks, stores, and constants. |
name: feature-architecture description: "Guides feature-based architecture in Next.js applications. Activates when creating new features, organizing domain code, setting up barrel exports, or structuring feature modules with components, schemas, types, queries, mocks, stores, and constants."
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.