Feature-Sliced Design by constellos/claude-code-plugins
npx skills add https://github.com/constellos/claude-code-plugins --skill 'Feature-Sliced Design'Feature-Sliced Design (FSD) 是一种用于将前端应用程序组织成标准化、可扩展结构的架构方法。它通过分层层次结构提供了清晰的关注点分离,防止循环依赖并提升可维护性。
为何使用 FSD:
自定义 'views' 层: 此技能使用 'views' 而非标准的 FSD 'pages' 层,以避免与 Next.js App Router 的 /app 目录混淆。/app 目录仅处理路由(最小逻辑),而 /src/views 包含您实际的页面业务逻辑。
Next.js 集成: FSD 通过将路由关注点(在 /app 中)与业务逻辑(在 /src/views 和其他 FSD 层中)分离,与 Next.js App Router 无缝协作。这保持了路由配置的简洁,同时保留了 FSD 的架构优势。
在以下情况应用 Feature-Sliced Design:
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
FSD 将代码组织成 7 个标准化层级(从高到低):
导入规则: 模块只能从层级结构中严格位于其下方的层导入。
┌─────────────────┐
│ app │ ← 可以从所有下方层导入
├─────────────────┤
│ views │ ← 可以导入:widgets, features, entities, shared
├─────────────────┤
│ widgets │ ← 可以导入:features, entities, shared
├─────────────────┤
│ features │ ← 可以导入:entities, shared
├─────────────────┤
│ entities │ ← 只能导入:shared
├─────────────────┤
│ shared │ ← 不能从任何 FSD 层导入
└─────────────────┘
此层次结构防止循环依赖并确保清晰的架构边界。
为何使用 'views' 而非 'pages':
/app 目录进行路由(App Router)/app) 和业务逻辑 (/src/views) 之间的混淆关注点分离:
/app 目录(根层级):仅 Next.js 路由,最小逻辑
page.tsx、layout.tsx、路由组/src/views 导入并渲染/src/views 层(FSD):页面业务逻辑、组件组合
这种分离保持了路由配置的简洁,同时遵循了 FSD 架构原则。
切片 是层内基于领域的划分(app 和 shared 除外,它们没有切片)。
示例:
views/dashboard - 仪表板页面切片widgets/header - 头部 widget 切片features/auth - 身份验证功能切片entities/user - 用户实体切片公共 API 模式: 每个切片通过 index.ts 导出以控制其公共接口:
// src/features/auth/index.ts
export { LoginForm } from './ui/LoginForm';
export { useAuth } from './model/useAuth';
export type { AuthState } from './model/types';
// 不导出内部实现细节
这防止了深层导入并保持了封装性。
片段 是切片内基于用途的分组:
示例结构:
features/
└── auth/
├── ui/
│ ├── LoginForm.tsx
│ └── SignupForm.tsx
├── model/
│ ├── useAuth.ts
│ └── types.ts
├── api/
│ └── authApi.ts
└── index.ts
Next.js App Router 使用 /app 目录进行路由。FSD 层位于 /src 目录中。
文件组织:
my-nextjs-app/
├── app/ # Next.js 路由(最小逻辑)
│ ├── layout.tsx # 根布局
│ ├── page.tsx # 主页路由
│ ├── dashboard/
│ │ └── page.tsx # 仪表板路由
│ └── settings/
│ └── page.tsx # 设置路由
│
├── src/ # FSD 层
│ └── views/ # 页面业务逻辑
│ ├── home/
│ │ ├── ui/
│ │ │ └── HomeView.tsx
│ │ └── index.ts
│ ├── dashboard/
│ │ ├── ui/
│ │ │ └── DashboardView.tsx
│ │ ├── model/
│ │ │ └── useDashboard.ts
│ │ └── index.ts
│ └── settings/
│ ├── ui/
│ │ └── SettingsView.tsx
│ └── index.ts
路由页面从 views 导入:
// app/dashboard/page.tsx - 仅路由
import { DashboardView } from '@/views/dashboard';
export default function DashboardPage() {
return <DashboardView />;
}
// src/views/dashboard/ui/DashboardView.tsx - 业务逻辑
import { Header } from '@/widgets/header';
import { StatsCard } from '@/features/analytics';
export function DashboardView() {
return (
<div>
<Header />
<StatsCard />
</div>
);
}
独立 Next.js 应用程序的完整 FSD 结构:
my-nextjs-app/
├── app/ # Next.js App Router
│ ├── layout.tsx # 根布局
│ ├── page.tsx # 主页路由
│ ├── (auth)/ # 路由组
│ │ ├── login/
│ │ │ └── page.tsx
│ │ └── signup/
│ │ └── page.tsx
│ ├── dashboard/
│ │ └── page.tsx
│ ├── api/ # API 路由
│ │ └── users/
│ │ └── route.ts
│ └── not-found.tsx
│
├── src/
│ ├── app/ # App 层(无切片)
│ │ ├── providers/
│ │ │ ├── AuthProvider.tsx
│ │ │ └── QueryProvider.tsx
│ │ ├── styles/
│ │ │ └── globals.css
│ │ └── config/
│ │ └── constants.ts
│ │
│ ├── views/ # Views 层(页面逻辑)
│ │ ├── home/
│ │ ├── dashboard/
│ │ ├── login/
│ │ └── signup/
│ │
│ ├── widgets/ # Widgets 层
│ │ ├── header/
│ │ ├── sidebar/
│ │ ├── footer/
│ │ └── notification-panel/
│ │
│ ├── features/ # Features 层
│ │ ├── auth/
│ │ ├── search/
│ │ ├── theme-toggle/
│ │ └── user-profile/
│ │
│ ├── entities/ # Entities 层
│ │ ├── user/
│ │ ├── post/
│ │ ├── comment/
│ │ └── session/
│ │
│ └── shared/ # Shared 层(无切片)
│ ├── ui/ # UI 组件
│ │ ├── button/
│ │ ├── input/
│ │ └── card/
│ ├── lib/ # 工具函数
│ │ ├── format.ts
│ │ └── validation.ts
│ ├── api/ # API 客户端
│ │ └── client.ts
│ └── config/
│ └── env.ts
│
├── public/
│ ├── images/
│ └── fonts/
│
└── package.json
Turborepo 单体仓库内的 FSD 结构,其中每个应用都有独立的 FSD 组织:
turborepo-root/
├── apps/
│ ├── web/ # 面向消费者的应用
│ │ ├── app/ # Next.js 路由
│ │ │ ├── layout.tsx
│ │ │ ├── page.tsx
│ │ │ └── shop/
│ │ │ └── page.tsx
│ │ ├── src/ # 独立的 FSD 结构
│ │ │ ├── app/
│ │ │ ├── views/
│ │ │ │ ├── home/
│ │ │ │ └── shop/
│ │ │ ├── widgets/
│ │ │ │ ├── product-grid/
│ │ │ │ └── shopping-cart/
│ │ │ ├── features/
│ │ │ │ ├── add-to-cart/
│ │ │ │ └── checkout/
│ │ │ ├── entities/
│ │ │ │ ├── product/
│ │ │ │ └── order/
│ │ │ └── shared/
│ │ └── package.json
│ │
│ └── admin/ # 管理仪表板应用
│ ├── app/ # Next.js 路由
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ └── products/
│ │ └── page.tsx
│ ├── src/ # 独立的 FSD 结构
│ │ ├── app/
│ │ ├── views/
│ │ │ ├── dashboard/
│ │ │ └── products/
│ │ ├── widgets/
│ │ │ ├── admin-header/
│ │ │ └── stats-panel/
│ │ ├── features/
│ │ │ ├── product-editor/
│ │ │ └── user-management/
│ │ ├── entities/
│ │ │ ├── product/
│ │ │ └── admin/
│ │ └── shared/
│ └── package.json
│
├── packages/ # 可选的共享包
│ ├── ui/ # 共享 UI 组件(可镜像 shared/ui)
│ │ ├── button/
│ │ └── input/
│ ├── utils/ # 共享工具函数
│ │ └── validation.ts
│ └── types/ # 共享 TypeScript 类型
│ └── common.ts
│
├── turbo.json
└── package.json
Turborepo 关键原则:
web、admin)都有自己的完整 FSD 结构packages/ 目录中(可选)workspace:* 协议处理包依赖关系目的: 应用范围的设置、初始化和全局配置。
职责:
导入规则: 可以从所有下方层导入(views、widgets、features、entities、shared)。
无切片: app 层直接包含片段(providers/、styles/、config/)。
示例:
// src/app/providers/Providers.tsx
'use client';
import { QueryClientProvider } from '@tanstack/react-query';
import { queryClient } from '@/shared/api/queryClient';
import { AuthProvider } from '@/features/auth';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
<AuthProvider>
{children}
</AuthProvider>
</QueryClientProvider>
);
}
// app/layout.tsx
import { Providers } from '@/app/providers/Providers';
import '@/app/styles/globals.css';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
目的: 页面级业务逻辑和组件组合。
职责:
导入规则: 可以从 widgets、features、entities、shared 导入。
有切片: 每个页面都有自己的切片(例如 views/dashboard、views/settings)。
示例:
// src/views/dashboard/ui/DashboardView.tsx
import { Header } from '@/widgets/header';
import { Sidebar } from '@/widgets/sidebar';
import { StatsCard } from '@/features/analytics';
import { RecentActivity } from '@/features/activity';
import { User } from '@/entities/user';
interface DashboardViewProps {
user: User;
}
export function DashboardView({ user }: DashboardViewProps) {
return (
<div className="dashboard">
<Header user={user} />
<div className="dashboard-content">
<Sidebar />
<main>
<StatsCard userId={user.id} />
<RecentActivity userId={user.id} />
</main>
</div>
</div>
);
}
// src/views/dashboard/index.ts
export { DashboardView } from './ui/DashboardView';
// app/dashboard/page.tsx
import { DashboardView } from '@/views/dashboard';
import { getCurrentUser } from '@/entities/user';
export default async function DashboardPage() {
const user = await getCurrentUser();
return <DashboardView user={user} />;
}
目的: 大型、自包含的复合 UI 块,组合了多个功能。
职责:
导入规则: 可以从 features、entities、shared 导入。
有切片: 每个 widget 都有自己的切片(例如 widgets/header、widgets/sidebar)。
示例:
// src/widgets/header/ui/Header.tsx
import { SearchBar } from '@/features/search';
import { UserMenu } from '@/features/user-menu';
import { NotificationBell } from '@/features/notifications';
import { User } from '@/entities/user';
import { Logo } from '@/shared/ui/logo';
interface HeaderProps {
user: User;
}
export function Header({ user }: HeaderProps) {
return (
<header className="header">
<Logo />
<SearchBar />
<div className="header-actions">
<NotificationBell userId={user.id} />
<UserMenu user={user} />
</div>
</header>
);
}
// src/widgets/header/index.ts
export { Header } from './ui/Header';
目的: 面向用户的交互和具有明确业务价值的业务逻辑。
职责:
导入规则: 可以从 entities、shared 导入。
有切片: 每个功能都有自己的切片(例如 features/auth、features/search)。
示例:
// src/features/auth/model/types.ts
export interface LoginCredentials {
email: string;
password: string;
}
// src/features/auth/api/login.ts
import { User } from '@/entities/user';
import { apiClient } from '@/shared/api/client';
import type { LoginCredentials } from '../model/types';
export async function login(credentials: LoginCredentials): Promise<User> {
const response = await apiClient.post('/auth/login', credentials);
return response.data;
}
// src/features/auth/ui/LoginForm.tsx
'use client';
import { useState } from 'react';
import { login } from '../api/login';
import { Button } from '@/shared/ui/button';
import { Input } from '@/shared/ui/input';
export function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
await login({ email, password });
};
return (
<form onSubmit={handleSubmit}>
<Input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<Input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
/>
<Button type="submit">Login</Button>
</form>
);
}
// src/features/auth/index.ts
export { LoginForm } from './ui/LoginForm';
export { login } from './api/login';
export type { LoginCredentials } from './model/types';
目的: 业务领域对象和核心数据模型。
职责:
导入规则: 只能从 shared 导入。
有切片: 每个实体都有自己的切片(例如 entities/user、entities/post)。
示例:
// src/entities/user/model/types.ts
export interface User {
id: string;
name: string;
email: string;
avatar?: string;
role: 'admin' | 'user';
}
// src/entities/user/api/getUser.ts
import { apiClient } from '@/shared/api/client';
import type { User } from '../model/types';
export async function getUser(id: string): Promise<User> {
const response = await apiClient.get(`/users/${id}`);
return response.data;
}
export async function getCurrentUser(): Promise<User> {
const response = await apiClient.get('/users/me');
return response.data;
}
// src/entities/user/ui/UserCard.tsx
import type { User } from '../model/types';
import { Avatar } from '@/shared/ui/avatar';
interface UserCardProps {
user: User;
}
export function UserCard({ user }: UserCardProps) {
return (
<div className="user-card">
<Avatar src={user.avatar} alt={user.name} />
<div>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
</div>
);
}
// src/entities/user/index.ts
export { UserCard } from './ui/UserCard';
export { getUser, getCurrentUser } from './api/getUser';
export type { User } from './model/types';
目的: 可复用的工具函数、UI 组件和第三方集成。
职责:
导入规则: 不能从任何 FSD 层导入(只能导入外部包)。
无切片: 直接包含片段(ui/、lib/、api/、config/)。
示例:
// src/shared/ui/button/Button.tsx
import { type ButtonHTMLAttributes } from 'react';
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
}
export function Button({
variant = 'primary',
size = 'md',
className,
children,
...props
}: ButtonProps) {
return (
<button
className={`button button--${variant} button--${size} ${className}`}
{...props}
>
{children}
</button>
);
}
// src/shared/lib/format.ts
export function formatDate(date: Date): string {
return new Intl.DateTimeFormat('en-US').format(date);
}
export function formatCurrency(amount: number): string {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(amount);
}
// src/shared/api/client.ts
import axios from 'axios';
export const apiClient = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
headers: {
'Content-Type': 'application/json',
},
});
// src/shared/config/env.ts
export const env = {
apiUrl: process.env.NEXT_PUBLIC_API_URL!,
nodeEnv: process.env.NODE_ENV,
} as const;
创建 FSD 文件夹结构:
mkdir -p src/{app,views,widgets,features,entities,shared}
mkdir -p src/app/{providers,styles,config}
mkdir -p src/shared/{ui,lib,api,config}
从实体(底层)开始。定义您的核心业务模型:
// src/entities/user/model/types.ts
export interface User {
id: string;
name: string;
email: string;
}
// src/entities/user/api/getUser.ts
export async function getUser(id: string): Promise<User> {
// API 实现
}
// src/entities/user/index.ts
export type { User } from './model/types';
export { getUser } from './api/getUser';
创建使用实体的功能:
// src/features/user-profile/ui/UserProfile.tsx
import { User } from '@/entities/user'; // ✅ 功能导入实体
export function UserProfile({ user }: { user: User }) {
return <div>{user.name}</div>;
}
// src/features/user-profile/index.ts
export { UserProfile } from './ui/UserProfile';
构建复合 widgets:
// src/widgets/header/ui/Header.tsx
import { UserProfile } from '@/features/user-profile'; // ✅ widget 导入功能
import { SearchBar } from '@/features/search';
export function Header({ user }) {
return (
<header>
<SearchBar />
<UserProfile user={user} />
</header>
);
}
创建页面级 views:
// src/views/dashboard/ui/DashboardView.tsx
import { Header } from '@/widgets/header'; // ✅ view 导入 widget
export function DashboardView() {
return (
<div>
<Header />
{/* 更多内容 */}
</div>
);
}
// src/views/dashboard/index.ts
export { DashboardView } from './ui/DashboardView';
将 views 连接到 Next.js 路由:
// app/dashboard/page.tsx
import { DashboardView } from '@/views/dashboard';
export default function DashboardPage() {
return <DashboardView />;
}
// ✅ 层从下方层导入
import { User } from '@/entities/user'; // Feature → Entity
import { LoginForm } from '@/features/auth'; // Widget → Feature
import { Header } from '@/widgets/header'; // View → Widget
// ✅ 任何层从 shared 导入
import { Button } from '@/shared/ui/button';
import { formatDate } from '@/shared/lib/format';
// ✅ 切片从较低层的不同切片导入
import { User } from '@/entities/user'; // features/auth → entities/user
import { Post } from '@/entities/post'; // features/like → entities/post
// ❌ 层从相同或更高层导入
import { DashboardView } from '@/views/dashboard'; // Feature → View(向上)
import { Header } from '@/widgets/header'; // Feature → Widget(向上)
import { LoginForm } from '@/features/login'; // features/auth → features/login(同层)
// ❌ 同层内的跨切片导入
import { SearchBar } from '@/features/search'; // features/auth → features/search
// ❌ shared 从 FSD 层导入
import { User } from '@/entities/user'; // shared/lib → entities/user
无效(跨功能导入):
// ❌ src/features/search/ui/SearchBar.tsx
import { LoginForm } from '@/features/auth'; // 同层导入
有效(提取到 widget):
// ✅ src/widgets/navbar/ui/Navbar.tsx
import { SearchBar } from '@/features/search';
import { LoginForm } from '@/features/auth';
export function Navbar() {
return (
<nav>
<SearchBar />
<LoginForm />
</nav>
);
}
问题:
// features/auth 导入 features/user-settings
// features/user-settings 导入 features/auth
// ❌ 循环依赖
解决方案 1:提取到实体
// 将共享逻辑移动到 entities/user
// 两个功能都从 entities/user 导入
// ✅ 无循环依赖
解决方案 2:提取到 widget
// 创建导入两个功能的 widgets/user-panel
// ✅ widget 层可以从 features 导入
始终使用 index.ts 控制导出:
// src/features/auth/index.ts
export { LoginForm } from './ui/LoginForm';
export { useAuth } from './model/useAuth';
export type { AuthState } from './model/types';
// ❌ 不要导出内部辅助函数
// export { validatePassword } from './lib/validation'; // 保持内部
仅从公共 API 导入:
// ✅ 正确
import { LoginForm } from '@/features/auth';
// ❌ 错误(深层导入)
import { LoginForm } from '@/features/auth/ui/LoginForm';
目的: React 组件和视觉元素。
何时使用:
示例:
// src/entities/user/ui/UserCard.tsx
import type { User } from '../model/types';
export function UserCard({ user }: { user: User }) {
return (
<div className="user-card">
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
}
目的: 业务逻辑、状态管理和类型定义。
何时使用:
示例:
// src/features/auth/model/useAuth.ts
'use client';
import { create } from 'zustand';
import type { User } from '@/entities/user';
interface AuthState {
user: User | null;
isAuthenticated: boolean;
login: (user: User) => void;
logout: () => void;
}
export const useAuth = create<AuthState>((set) => ({
user: null,
isAuthenticated: false,
login: (user) => set({ user, isAuthenticated: true }),
logout: () => set({ user: null, isAuthenticated: false }),
}));
目的: API 客户端、数据获取和外部集成。
何时使用:
示例:
// src/entities/user/api/userApi.ts
'use server';
import { apiClient } from '@/shared/api/client';
import type { User } from '../model/types';
export async function fetchUsers(): Promise<User[]> {
const response = await apiClient.get('/users');
return response.data;
}
export async function createUser(data: Omit<User, 'id'>): Promise<User> {
const response = await apiClient.post('/users', data);
return response.data;
}
目的: 特定于切片的工具函数和辅助函数。
何时使用:
示例:
// src/features/auth/lib/validation.ts
import { z } from 'zod';
export const loginSchema = z.object({
email: z.string().email('无效的电子邮件地址'),
password: z.string().min(8, '密码必须至少8个字符'),
});
export function validateLogin(data: unknown) {
return loginSchema.parse(data);
}
目的: 配置常量和功能开关。
何时使用:
示例:
// src/app/config/theme.ts
export const theme = {
colors: {
primary: '#0070f3',
secondary: '#ff4081',
},
breakpoints: {
sm: '640px',
md: '768px',
lg: '1024px',
},
} as const;
自底向上方法(推荐):
从 shared 层开始
shared/ui/shared/lib/shared/api/ 中配置 API 客户端定义实体
entities/{name}/model/ 中创建实体类型entities/{name}/api/提取功能
features/{name}/ 中创建功能切片构建 widgets
widgets/{name}/ 中创建 widget 切片组织 views
/app 移动到 /src/views/app,业务逻辑保留在 /src/views配置 app 层
app/providers/app/styles/增量迁移:
全程测试:
保持切片隔离:
使用公共 API 模式:
index.ts 导出测试与实现放在一起:
features/
└── auth/
├── ui/
│ ├── LoginForm.tsx
│ └── LoginForm.test.tsx # 测试放在实现旁边
└── index.ts
避免"上帝切片":
按领域而非技术命名:
Feature-Sliced Design (FSD) is an architectural methodology for organizing frontend applications into a standardized, scalable structure. It provides clear separation of concerns through a layered hierarchy that prevents circular dependencies and promotes maintainability.
Why use FSD:
Custom 'views' layer: This skill uses 'views' instead of the standard FSD 'pages' layer to avoid confusion with Next.js App Router's /app directory. The /app directory handles routing only (minimal logic), while /src/views contains your actual page business logic.
Next.js integration: FSD works seamlessly with Next.js App Router by separating routing concerns (in /app) from business logic (in /src/views and other FSD layers). This keeps your routing configuration clean while maintaining FSD's architectural benefits.
Apply Feature-Sliced Design when:
FSD organizes code into 7 standardized layers (from highest to lowest):
Import rule: A module can only import from layers strictly below it in the hierarchy.
┌─────────────────┐
│ app │ ← Can import from all layers below
├─────────────────┤
│ views │ ← Can import: widgets, features, entities, shared
├─────────────────┤
│ widgets │ ← Can import: features, entities, shared
├─────────────────┤
│ features │ ← Can import: entities, shared
├─────────────────┤
│ entities │ ← Can import: shared only
├─────────────────┤
│ shared │ ← Cannot import from any FSD layer
└─────────────────┘
This hierarchy prevents circular dependencies and ensures clear architectural boundaries.
Why 'views' instead of 'pages':
/app directory for routing (App Router)/app) and business logic (/src/views)Separation of concerns:
/app directory (root level): Next.js routing only, minimal logic
page.tsx, layout.tsx, route groups/src/views/src/views layer (FSD): Page business logic, component composition
This separation keeps routing configuration clean while maintaining FSD architectural principles.
Slices are domain-based partitions within layers (except app and shared, which have no slices).
Examples:
views/dashboard - Dashboard page slicewidgets/header - Header widget slicefeatures/auth - Authentication feature sliceentities/user - User entity slicePublic API pattern: Each slice exports through index.ts to control its public interface:
// src/features/auth/index.ts
export { LoginForm } from './ui/LoginForm';
export { useAuth } from './model/useAuth';
export type { AuthState } from './model/types';
// Internal implementation details NOT exported
This prevents deep imports and maintains encapsulation.
Segments are purpose-based groupings within slices:
Example structure:
features/
└── auth/
├── ui/
│ ├── LoginForm.tsx
│ └── SignupForm.tsx
├── model/
│ ├── useAuth.ts
│ └── types.ts
├── api/
│ └── authApi.ts
└── index.ts
Next.js App Router uses /app directory for routing. FSD layers live in /src directory.
File organization:
my-nextjs-app/
├── app/ # Next.js routing (minimal logic)
│ ├── layout.tsx # Root layout
│ ├── page.tsx # Home route
│ ├── dashboard/
│ │ └── page.tsx # Dashboard route
│ └── settings/
│ └── page.tsx # Settings route
│
├── src/ # FSD layers
│ └── views/ # Page business logic
│ ├── home/
│ │ ├── ui/
│ │ │ └── HomeView.tsx
│ │ └── index.ts
│ ├── dashboard/
│ │ ├── ui/
│ │ │ └── DashboardView.tsx
│ │ ├── model/
│ │ │ └── useDashboard.ts
│ │ └── index.ts
│ └── settings/
│ ├── ui/
│ │ └── SettingsView.tsx
│ └── index.ts
Routing pages import from views:
// app/dashboard/page.tsx - Routing only
import { DashboardView } from '@/views/dashboard';
export default function DashboardPage() {
return <DashboardView />;
}
// src/views/dashboard/ui/DashboardView.tsx - Business logic
import { Header } from '@/widgets/header';
import { StatsCard } from '@/features/analytics';
export function DashboardView() {
return (
<div>
<Header />
<StatsCard />
</div>
);
}
Complete FSD structure for a standalone Next.js application:
my-nextjs-app/
├── app/ # Next.js App Router
│ ├── layout.tsx # Root layout
│ ├── page.tsx # Home route
│ ├── (auth)/ # Route group
│ │ ├── login/
│ │ │ └── page.tsx
│ │ └── signup/
│ │ └── page.tsx
│ ├── dashboard/
│ │ └── page.tsx
│ ├── api/ # API routes
│ │ └── users/
│ │ └── route.ts
│ └── not-found.tsx
│
├── src/
│ ├── app/ # App layer (no slices)
│ │ ├── providers/
│ │ │ ├── AuthProvider.tsx
│ │ │ └── QueryProvider.tsx
│ │ ├── styles/
│ │ │ └── globals.css
│ │ └── config/
│ │ └── constants.ts
│ │
│ ├── views/ # Views layer (page logic)
│ │ ├── home/
│ │ ├── dashboard/
│ │ ├── login/
│ │ └── signup/
│ │
│ ├── widgets/ # Widgets layer
│ │ ├── header/
│ │ ├── sidebar/
│ │ ├── footer/
│ │ └── notification-panel/
│ │
│ ├── features/ # Features layer
│ │ ├── auth/
│ │ ├── search/
│ │ ├── theme-toggle/
│ │ └── user-profile/
│ │
│ ├── entities/ # Entities layer
│ │ ├── user/
│ │ ├── post/
│ │ ├── comment/
│ │ └── session/
│ │
│ └── shared/ # Shared layer (no slices)
│ ├── ui/ # UI components
│ │ ├── button/
│ │ ├── input/
│ │ └── card/
│ ├── lib/ # Utilities
│ │ ├── format.ts
│ │ └── validation.ts
│ ├── api/ # API client
│ │ └── client.ts
│ └── config/
│ └── env.ts
│
├── public/
│ ├── images/
│ └── fonts/
│
└── package.json
FSD structure within a Turborepo monorepo where each app has independent FSD organization:
turborepo-root/
├── apps/
│ ├── web/ # Consumer-facing app
│ │ ├── app/ # Next.js routing
│ │ │ ├── layout.tsx
│ │ │ ├── page.tsx
│ │ │ └── shop/
│ │ │ └── page.tsx
│ │ ├── src/ # Independent FSD structure
│ │ │ ├── app/
│ │ │ ├── views/
│ │ │ │ ├── home/
│ │ │ │ └── shop/
│ │ │ ├── widgets/
│ │ │ │ ├── product-grid/
│ │ │ │ └── shopping-cart/
│ │ │ ├── features/
│ │ │ │ ├── add-to-cart/
│ │ │ │ └── checkout/
│ │ │ ├── entities/
│ │ │ │ ├── product/
│ │ │ │ └── order/
│ │ │ └── shared/
│ │ └── package.json
│ │
│ └── admin/ # Admin dashboard app
│ ├── app/ # Next.js routing
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ └── products/
│ │ └── page.tsx
│ ├── src/ # Independent FSD structure
│ │ ├── app/
│ │ ├── views/
│ │ │ ├── dashboard/
│ │ │ └── products/
│ │ ├── widgets/
│ │ │ ├── admin-header/
│ │ │ └── stats-panel/
│ │ ├── features/
│ │ │ ├── product-editor/
│ │ │ └── user-management/
│ │ ├── entities/
│ │ │ ├── product/
│ │ │ └── admin/
│ │ └── shared/
│ └── package.json
│
├── packages/ # Optional shared packages
│ ├── ui/ # Shared UI components (can mirror shared/ui)
│ │ ├── button/
│ │ └── input/
│ ├── utils/ # Shared utilities
│ │ └── validation.ts
│ └── types/ # Shared TypeScript types
│ └── common.ts
│
├── turbo.json
└── package.json
Key Turborepo principles:
web, admin) has its own complete FSD structurepackages/ directory (optional)workspace:* protocol for package dependenciesPurpose: Application-wide setup, initialization, and global configuration.
Responsibilities:
Import rules: Can import from all layers below (views, widgets, features, entities, shared).
No slices: The app layer contains segments directly (providers/, styles/, config/).
Example:
// src/app/providers/Providers.tsx
'use client';
import { QueryClientProvider } from '@tanstack/react-query';
import { queryClient } from '@/shared/api/queryClient';
import { AuthProvider } from '@/features/auth';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
<AuthProvider>
{children}
</AuthProvider>
</QueryClientProvider>
);
}
// app/layout.tsx
import { Providers } from '@/app/providers/Providers';
import '@/app/styles/globals.css';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
Purpose: Page-level business logic and component composition.
Responsibilities:
Import rules: Can import from widgets, features, entities, shared.
Has slices: Each page gets its own slice (e.g., views/dashboard, views/settings).
Example:
// src/views/dashboard/ui/DashboardView.tsx
import { Header } from '@/widgets/header';
import { Sidebar } from '@/widgets/sidebar';
import { StatsCard } from '@/features/analytics';
import { RecentActivity } from '@/features/activity';
import { User } from '@/entities/user';
interface DashboardViewProps {
user: User;
}
export function DashboardView({ user }: DashboardViewProps) {
return (
<div className="dashboard">
<Header user={user} />
<div className="dashboard-content">
<Sidebar />
<main>
<StatsCard userId={user.id} />
<RecentActivity userId={user.id} />
</main>
</div>
</div>
);
}
// src/views/dashboard/index.ts
export { DashboardView } from './ui/DashboardView';
// app/dashboard/page.tsx
import { DashboardView } from '@/views/dashboard';
import { getCurrentUser } from '@/entities/user';
export default async function DashboardPage() {
const user = await getCurrentUser();
return <DashboardView user={user} />;
}
Purpose: Large, self-contained composite UI blocks that combine multiple features.
Responsibilities:
Import rules: Can import from features, entities, shared.
Has slices: Each widget gets its own slice (e.g., widgets/header, widgets/sidebar).
Example:
// src/widgets/header/ui/Header.tsx
import { SearchBar } from '@/features/search';
import { UserMenu } from '@/features/user-menu';
import { NotificationBell } from '@/features/notifications';
import { User } from '@/entities/user';
import { Logo } from '@/shared/ui/logo';
interface HeaderProps {
user: User;
}
export function Header({ user }: HeaderProps) {
return (
<header className="header">
<Logo />
<SearchBar />
<div className="header-actions">
<NotificationBell userId={user.id} />
<UserMenu user={user} />
</div>
</header>
);
}
// src/widgets/header/index.ts
export { Header } from './ui/Header';
Purpose: User-facing interactions and business logic with clear business value.
Responsibilities:
Import rules: Can import from entities, shared.
Has slices: Each feature gets its own slice (e.g., features/auth, features/search).
Example:
// src/features/auth/model/types.ts
export interface LoginCredentials {
email: string;
password: string;
}
// src/features/auth/api/login.ts
import { User } from '@/entities/user';
import { apiClient } from '@/shared/api/client';
import type { LoginCredentials } from '../model/types';
export async function login(credentials: LoginCredentials): Promise<User> {
const response = await apiClient.post('/auth/login', credentials);
return response.data;
}
// src/features/auth/ui/LoginForm.tsx
'use client';
import { useState } from 'react';
import { login } from '../api/login';
import { Button } from '@/shared/ui/button';
import { Input } from '@/shared/ui/input';
export function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
await login({ email, password });
};
return (
<form onSubmit={handleSubmit}>
<Input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<Input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
/>
<Button type="submit">Login</Button>
</form>
);
}
// src/features/auth/index.ts
export { LoginForm } from './ui/LoginForm';
export { login } from './api/login';
export type { LoginCredentials } from './model/types';
Purpose: Business domain objects and core data models.
Responsibilities:
Import rules: Can import from shared only.
Has slices: Each entity gets its own slice (e.g., entities/user, entities/post).
Example:
// src/entities/user/model/types.ts
export interface User {
id: string;
name: string;
email: string;
avatar?: string;
role: 'admin' | 'user';
}
// src/entities/user/api/getUser.ts
import { apiClient } from '@/shared/api/client';
import type { User } from '../model/types';
export async function getUser(id: string): Promise<User> {
const response = await apiClient.get(`/users/${id}`);
return response.data;
}
export async function getCurrentUser(): Promise<User> {
const response = await apiClient.get('/users/me');
return response.data;
}
// src/entities/user/ui/UserCard.tsx
import type { User } from '../model/types';
import { Avatar } from '@/shared/ui/avatar';
interface UserCardProps {
user: User;
}
export function UserCard({ user }: UserCardProps) {
return (
<div className="user-card">
<Avatar src={user.avatar} alt={user.name} />
<div>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
</div>
);
}
// src/entities/user/index.ts
export { UserCard } from './ui/UserCard';
export { getUser, getCurrentUser } from './api/getUser';
export type { User } from './model/types';
Purpose: Reusable utilities, UI components, and third-party integrations.
Responsibilities:
Import rules: Cannot import from any FSD layer (only external packages).
No slices: Contains segments directly (ui/, lib/, api/, config/).
Example:
// src/shared/ui/button/Button.tsx
import { type ButtonHTMLAttributes } from 'react';
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
}
export function Button({
variant = 'primary',
size = 'md',
className,
children,
...props
}: ButtonProps) {
return (
<button
className={`button button--${variant} button--${size} ${className}`}
{...props}
>
{children}
</button>
);
}
// src/shared/lib/format.ts
export function formatDate(date: Date): string {
return new Intl.DateTimeFormat('en-US').format(date);
}
export function formatCurrency(amount: number): string {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(amount);
}
// src/shared/api/client.ts
import axios from 'axios';
export const apiClient = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
headers: {
'Content-Type': 'application/json',
},
});
// src/shared/config/env.ts
export const env = {
apiUrl: process.env.NEXT_PUBLIC_API_URL!,
nodeEnv: process.env.NODE_ENV,
} as const;
Create the FSD folder structure:
mkdir -p src/{app,views,widgets,features,entities,shared}
mkdir -p src/app/{providers,styles,config}
mkdir -p src/shared/{ui,lib,api,config}
Start with entities (bottom layer). Define your core business models:
// src/entities/user/model/types.ts
export interface User {
id: string;
name: string;
email: string;
}
// src/entities/user/api/getUser.ts
export async function getUser(id: string): Promise<User> {
// API implementation
}
// src/entities/user/index.ts
export type { User } from './model/types';
export { getUser } from './api/getUser';
Create features that use entities:
// src/features/user-profile/ui/UserProfile.tsx
import { User } from '@/entities/user'; // ✅ Feature imports entity
export function UserProfile({ user }: { user: User }) {
return <div>{user.name}</div>;
}
// src/features/user-profile/index.ts
export { UserProfile } from './ui/UserProfile';
Build composite widgets:
// src/widgets/header/ui/Header.tsx
import { UserProfile } from '@/features/user-profile'; // ✅ Widget imports feature
import { SearchBar } from '@/features/search';
export function Header({ user }) {
return (
<header>
<SearchBar />
<UserProfile user={user} />
</header>
);
}
Create page-level views:
// src/views/dashboard/ui/DashboardView.tsx
import { Header } from '@/widgets/header'; // ✅ View imports widget
export function DashboardView() {
return (
<div>
<Header />
{/* More content */}
</div>
);
}
// src/views/dashboard/index.ts
export { DashboardView } from './ui/DashboardView';
Wire views to Next.js routing:
// app/dashboard/page.tsx
import { DashboardView } from '@/views/dashboard';
export default function DashboardPage() {
return <DashboardView />;
}
// ✅ Layer importing from layer below
import { User } from '@/entities/user'; // Feature → Entity
import { LoginForm } from '@/features/auth'; // Widget → Feature
import { Header } from '@/widgets/header'; // View → Widget
// ✅ Any layer importing from shared
import { Button } from '@/shared/ui/button';
import { formatDate } from '@/shared/lib/format';
// ✅ Slice importing from different slice in lower layer
import { User } from '@/entities/user'; // features/auth → entities/user
import { Post } from '@/entities/post'; // features/like → entities/post
// ❌ Layer importing from same or higher layer
import { DashboardView } from '@/views/dashboard'; // Feature → View (upward)
import { Header } from '@/widgets/header'; // Feature → Widget (upward)
import { LoginForm } from '@/features/login'; // features/auth → features/login (same layer)
// ❌ Cross-slice imports within same layer
import { SearchBar } from '@/features/search'; // features/auth → features/search
// ❌ Shared importing from FSD layers
import { User } from '@/entities/user'; // shared/lib → entities/user
Invalid (cross-feature import):
// ❌ src/features/search/ui/SearchBar.tsx
import { LoginForm } from '@/features/auth'; // Same layer import
Valid (extract to widget):
// ✅ src/widgets/navbar/ui/Navbar.tsx
import { SearchBar } from '@/features/search';
import { LoginForm } from '@/features/auth';
export function Navbar() {
return (
<nav>
<SearchBar />
<LoginForm />
</nav>
);
}
Problem:
// features/auth imports features/user-settings
// features/user-settings imports features/auth
// ❌ Circular dependency
Solution 1: Extract to entity
// Move shared logic to entities/user
// Both features import from entities/user
// ✅ No circular dependency
Solution 2: Extract to widget
// Create widgets/user-panel that imports both features
// ✅ Widget layer can import from features
Always use index.ts to control exports:
// src/features/auth/index.ts
export { LoginForm } from './ui/LoginForm';
export { useAuth } from './model/useAuth';
export type { AuthState } from './model/types';
// ❌ Do NOT export internal helpers
// export { validatePassword } from './lib/validation'; // Keep internal
Import from public API only:
// ✅ Correct
import { LoginForm } from '@/features/auth';
// ❌ Wrong (deep import)
import { LoginForm } from '@/features/auth/ui/LoginForm';
Purpose: React components and visual elements.
When to use:
Example:
// src/entities/user/ui/UserCard.tsx
import type { User } from '../model/types';
export function UserCard({ user }: { user: User }) {
return (
<div className="user-card">
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
}
Purpose: Business logic, state management, and type definitions.
When to use:
Example:
// src/features/auth/model/useAuth.ts
'use client';
import { create } from 'zustand';
import type { User } from '@/entities/user';
interface AuthState {
user: User | null;
isAuthenticated: boolean;
login: (user: User) => void;
logout: () => void;
}
export const useAuth = create<AuthState>((set) => ({
user: null,
isAuthenticated: false,
login: (user) => set({ user, isAuthenticated: true }),
logout: () => set({ user: null, isAuthenticated: false }),
}));
Purpose: API clients, data fetching, and external integrations.
When to use:
Example:
// src/entities/user/api/userApi.ts
'use server';
import { apiClient } from '@/shared/api/client';
import type { User } from '../model/types';
export async function fetchUsers(): Promise<User[]> {
const response = await apiClient.get('/users');
return response.data;
}
export async function createUser(data: Omit<User, 'id'>): Promise<User> {
const response = await apiClient.post('/users', data);
return response.data;
}
Purpose: Utility functions and helpers specific to the slice.
When to use:
Example:
// src/features/auth/lib/validation.ts
import { z } from 'zod';
export const loginSchema = z.object({
email: z.string().email('Invalid email address'),
password: z.string().min(8, 'Password must be at least 8 characters'),
});
export function validateLogin(data: unknown) {
return loginSchema.parse(data);
}
Purpose: Configuration constants and feature flags.
When to use:
Example:
// src/app/config/theme.ts
export const theme = {
colors: {
primary: '#0070f3',
secondary: '#ff4081',
},
breakpoints: {
sm: '640px',
md: '768px',
lg: '1024px',
},
} as const;
Bottom-up approach (recommended):
Start with shared layer
shared/ui/shared/lib/shared/api/Define entities
entities/{name}/model/entities/{name}/api/Extract features
features/{name}/Incremental migration:
Testing throughout:
Keep slices isolated:
Use Public API pattern:
index.tsColocate tests:
features/
└── auth/
├── ui/
│ ├── LoginForm.tsx
│ └── LoginForm.test.tsx # Test next to implementation
└── index.ts
Avoid "god slices":
Name by domain, not tech:
features/product-searchfeatures/search-bar-componentUse TypeScript strict mode:
{
"compilerOptions": {
"strict": true
}
}
Document architecture decisions:
// src/shared/ui/button/Button.tsx
export function Button({ children, ...props }) {
return <button {...props}>{children}</button>;
}
// Usage in feature
import { Button } from '@/shared/ui/button';
// src/shared/api/client.ts
import axios from 'axios';
export const apiClient = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
});
// Usage in entity
import { apiClient } from '@/shared/api/client';
// src/features/product-form/ui/ProductForm.tsx
'use client';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { productSchema } from '../lib/validation';
import { createProduct } from '../api/createProduct';
export function ProductForm() {
const { register, handleSubmit } = useForm({
resolver: zodResolver(productSchema),
});
return <form onSubmit={handleSubmit(createProduct)}>...</form>;
}
// src/features/auth/model/useAuth.ts
export const useAuth = create<AuthState>((set) => ({...}));
// src/widgets/header/ui/Header.tsx
import { useAuth } from '@/features/auth';
export function Header() {
const { user } = useAuth();
return <header>Welcome, {user?.name}</header>;
}
// app/dashboard/page.tsx
import { DashboardView } from '@/views/dashboard';
import { getUser } from '@/entities/user';
export default async function DashboardPage() {
const user = await getUser('current');
return <DashboardView user={user} />;
}
// src/views/dashboard/ui/DashboardView.tsx
import type { User } from '@/entities/user';
export function DashboardView({ user }: { user: User }) {
return <div>Welcome, {user.name}</div>;
}
Problem: Two slices import from each other.
Solution:
Problem: TypeScript cannot resolve @/ imports.
Solution: Configure path aliases in tsconfig.json:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/app/*": ["src/app/*"],
"@/views/*": ["src/views/*"],
"@/widgets/*": ["src/widgets/*"],
"@/features/*": ["src/features/*"],
"@/entities/*": ["src/entities/*"],
"@/shared/*": ["src/shared/*"]
}
}
}
Problem: Next.js cannot find modules after restructuring.
Solution:
.next directory: rm -rf .nextnpm installnpm run dev{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/app/*": ["src/app/*"],
"@/views/*": ["src/views/*"],
"@/widgets/*": ["src/widgets/*"],
"@/features/*": ["src/features/*"],
"@/entities/*": ["src/entities/*"],
"@/shared/*": ["src/shared/*"]
}
},
"include": ["src", "app"]
}
// .eslintrc.js
module.exports = {
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
{
group: ['@/views/*', '@/widgets/*'],
message: 'Features cannot import from views or widgets',
},
],
},
],
},
};
Weekly Installs
0
Repository
GitHub Stars
5
First Seen
Jan 1, 1970
Security Audits
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
106,200 周安装
Build widgets
widgets/{name}/Organize views
/app to /src/views/app, business logic in /src/viewsConfigure app layer
app/providers/app/styles/