This skill should be used when the user asks to "implement FSD", "use Feature-Sliced Design", "organize architecture", "structure project folders", "set up FSD layers", "create feature slices", "refactor to FSD", "add FSD structure", or mentions "feature slices", "layered architecture", "FSD methodology", "architectural organization", "views layer", "entities layer", "shared layer", "Next.js with FSD", or "Turborepo FSD structure". Provides comprehensive guidance for implementing Feature-Sliced Design methodology in Next.js applications with custom 'views' layer naming.
Implements Feature-Sliced Design architecture in Next.js projects with custom 'views' layer structure.
/plugin marketplace add constellos/claude-code-plugins/plugin install project-context@constellos-localThis skill inherits all available tools. When active, it can use any tool Claude has access to.
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}/Build widgets
widgets/{name}/Organize views
/app to /src/views/app, business logic in /src/viewsConfigure app layer
app/providers/app/styles/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',
},
],
},
],
},
};