
Feature Slicing
- 196 installs
- 57 repo stars
- Updated July 7, 2026
- ccheney/robust-skills
Decompose large product features into thin vertical slices that can be planned, built, reviewed, and shipped incrementally with lower risk.
About
Helps engineers and product owners break complex features into small, end-to-end vertical slices that deliver user value early, simplify reviews, and keep robust software delivery predictable.
- Vertical slice decomposition
- Incremental delivery planning
- Scope reduction without losing value
- Traceable implementation units
- Reduced integration risk
Feature Slicing by the numbers
- 196 all-time installs (skills.sh)
- Ranked #1,063 of 3,282 Productivity & Planning skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/ccheney/robust-skills --skill feature-slicingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 196 |
|---|---|
| repo stars | ★ 57 |
| Last updated | July 7, 2026 |
| Repository | ccheney/robust-skills ↗ |
What it does
Decompose large product features into thin vertical slices that can be planned, built, reviewed, and shipped incrementally with lower risk.
Files
Feature-Sliced Design Architecture
Frontend architecture methodology with strict layer hierarchy and import rules for scalable, maintainable applications. FSD organizes code by business domain rather than technical role.
Official Docs: feature-sliced.design | GitHub: feature-sliced
---
THE IMPORT RULE (Critical)
Modules can ONLY import from layers strictly below them. Never sideways or upward.
app → pages → widgets → features → entities → shared
↓ ↓ ↓ ↓ ↓ ✓
✓ ✓ ✓ ✓ ✓ (external only)| Violation | Example | Fix |
|---|---|---|
| Cross-slice (same layer) | features/auth → features/user | Extract to entities/ or shared/ |
| Upward import | entities/user → features/auth | Move shared code down |
| Shared importing up | shared/ → entities/ | Shared has NO internal deps |
Exception: app/ and shared/ have no slices, so internal cross-imports are allowed within them.
---
Layer Hierarchy
| Layer | Purpose | Has Slices | Required |
|---|---|---|---|
app/ | Initialization, routing, providers, global styles | No | Yes |
pages/ | Route-based screens (one slice per route) | Yes | Yes |
widgets/ | Complex reusable UI blocks (header, sidebar) | Yes | No |
features/ | User interactions with business value (login, checkout) | Yes | No |
entities/ | Business domain models (user, product, order) | Yes | No |
shared/ | Project-agnostic infrastructure (UI kit, API client, utils) | No | Yes |
Minimal setup: app/, pages/, shared/ — add other layers as complexity grows.
---
Quick Decision Trees
"Where does this code go?"
Code Placement:
├─ App-wide config, providers, routing → app/
├─ Full page / route component → pages/
├─ Complex reusable UI block → widgets/
├─ User action with business value → features/
├─ Business domain object (data model) → entities/
└─ Reusable, domain-agnostic code → shared/"Feature or Entity?"
| Entity (noun) | Feature (verb) |
|---|---|
user — user data model | auth — login/logout actions |
product — product info | add-to-cart — adding to cart |
comment — comment data | write-comment — creating comments |
order — order record | checkout — completing purchase |
Rule: Entities represent THINGS with identity. Features represent ACTIONS with side effects.
"Which segment?"
Segments (within a slice):
├─ ui/ → React components, styles
├─ api/ → Backend calls, data fetching, DTOs
├─ model/ → Types, schemas, stores, business logic
├─ lib/ → Slice-specific utilities
└─ config/ → Feature flags, constantsNaming: Use purpose-driven names (api/, model/) not essence-based (hooks/, types/).
---
Directory Structure
src/
├── app/ # App layer (no slices)
│ ├── providers/ # React context, QueryClient, theme
│ ├── routes/ # Router configuration
│ └── styles/ # Global CSS, theme tokens
├── pages/ # Page slices
│ └── {page-name}/
│ ├── ui/ # Page components
│ ├── api/ # Loaders, server actions
│ ├── model/ # Page-specific state
│ └── index.ts # Public API
├── widgets/ # Widget slices
│ └── {widget-name}/
│ ├── ui/ # Composed UI
│ └── index.ts
├── features/ # Feature slices
│ └── {feature-name}/
│ ├── ui/ # Feature UI
│ ├── api/ # Feature API calls
│ ├── model/ # State, schemas
│ └── index.ts
├── entities/ # Entity slices
│ └── {entity-name}/
│ ├── ui/ # Entity UI (Card, Avatar)
│ ├── api/ # CRUD operations
│ ├── model/ # Types, mappers, validation
│ └── index.ts
└── shared/ # Shared layer (no slices)
├── ui/ # Design system components
├── api/ # API client, interceptors
├── lib/ # Utilities (dates, validation)
├── config/ # Environment, constants
├── routes/ # Route path constants
└── i18n/ # Translations---
Public API Pattern
Every slice MUST expose a public API via index.ts. External code imports ONLY from this file.
// entities/user/index.ts
export { UserCard } from './ui/UserCard';
export { UserAvatar } from './ui/UserAvatar';
export { getUser, updateUser } from './api/userApi';
export type { User, UserRole } from './model/types';
export { userSchema } from './model/schema';// ✅ Correct
import { UserCard, type User } from '@/entities/user';
// ❌ Wrong
import { UserCard } from '@/entities/user/ui/UserCard';Avoid wildcard exports — they expose internals and harm tree-shaking:
// ❌
export * from './ui';
// ✅
export { UserCard } from './ui/UserCard';---
Cross-Entity References (@x Notation)
When entities legitimately reference each other, use the @x notation:
entities/
├── product/
│ ├── @x/
│ │ └── order.ts # API specifically for order entity
│ └── index.ts
└── order/
└── model/types.ts # Imports from product/@x/order// entities/product/@x/order.ts
export type { ProductId } from '../model/types';
// entities/order/model/types.ts
import type { ProductId } from '@/entities/product/@x/order';Guidelines: Keep cross-imports minimal. Consider merging entities if references are extensive.
---
Anti-Patterns
| Anti-Pattern | Problem | Fix |
|---|---|---|
| Cross-slice import | features/a → features/b | Extract shared logic down |
| Generic segments | components/, hooks/ | Use ui/, lib/, model/ |
| Wildcard exports | export * from './button' | Explicit named exports |
| Business logic in shared | Domain logic in shared/lib | Move to entities/ |
| Single-use widgets | Widget used by one page | Keep in page slice |
| Skipping public API | Import from internal paths | Always use index.ts |
| Making everything a feature | All interactions as features | Only reused actions |
---
TypeScript Configuration
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}---
Reference Documentation
| File | Purpose |
|---|---|
| references/LAYERS.md | Complete layer specifications, flowcharts |
| references/PUBLIC-API.md | Export patterns, @x notation, tree-shaking |
| references/IMPLEMENTATION.md | Code patterns: entities, features, React Query |
| references/NEXTJS.md | App Router integration, page re-exports |
| references/MIGRATION.md | Incremental migration strategy |
| references/CHEATSHEET.md | Quick reference, import matrix |
Resources
Official Sources
- Official Documentation: https://feature-sliced.design
- GitHub Organization: https://github.com/feature-sliced
- Official Examples: https://github.com/feature-sliced/examples
- Specification: https://feature-sliced.design/docs/reference
Community
- Awesome FSD: https://github.com/feature-sliced/awesome (curated articles, videos, tools)
FSD Quick Reference
Sources: Tutorial | Layers | Slices & Segments
Layer Hierarchy
app/ → Providers, routing, global styles [NO slices, REQUIRED]
pages/ → Route screens, one slice per route [HAS slices, REQUIRED]
widgets/ → Complex reusable UI blocks [HAS slices, optional]
features/ → User interactions with business value [HAS slices, optional]
entities/ → Business domain models [HAS slices, optional]
shared/ → Project-agnostic infrastructure [NO slices, REQUIRED]Import Rule: Only import from layers BELOW. Never sideways or up.
---
Import Matrix
| app | pages | widgets | features | entities | shared | |
|---|---|---|---|---|---|---|
| app | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| pages | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ |
| widgets | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
| features | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| entities | ❌ | ❌ | ❌ | ❌ | @x* | ✅ |
| shared | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
*Use @x notation for cross-entity references
---
Quick Decision Trees
"Where does this code go?"
├─ App-wide config, providers, routing → app/
├─ Full page / route component → pages/
├─ Complex reusable UI block → widgets/
├─ User action with business value → features/
├─ Business domain object (data model) → entities/
└─ Reusable, domain-agnostic code → shared/"Feature or Entity?"
| Entity (noun) | Feature (verb) |
|---|---|
user | auth (login/logout) |
product | add-to-cart |
comment | write-comment |
order | checkout |
Entities: THINGS with identity, displayed in lists Features: ACTIONS with side effects, triggered by user
---
Segments
| Segment | Purpose | Examples |
|---|---|---|
ui/ | Components, styles | UserCard.tsx, Button.tsx |
api/ | Backend calls, DTOs | getUser(), createOrder() |
model/ | Types, schemas, stores | User, userSchema, useUserStore |
lib/ | Slice utilities | formatUserName() |
config/ | Configuration | Feature flags, constants |
Naming: Use purpose-driven names (api/, model/) not essence-based (hooks/, types/).
---
File Structure Templates
Entity
entities/{name}/
├── ui/
│ ├── {Name}Card.tsx
│ └── index.ts
├── api/
│ ├── {name}Api.ts
│ ├── queries.ts
│ └── index.ts
├── model/
│ ├── types.ts
│ ├── schema.ts
│ ├── mapper.ts
│ └── index.ts
└── index.tsFeature
features/{name}/
├── ui/
│ ├── {Name}Form.tsx
│ ├── {Name}Button.tsx
│ └── index.ts
├── api/
│ ├── {name}Api.ts
│ └── index.ts
├── model/
│ ├── types.ts
│ ├── schema.ts
│ ├── store.ts
│ └── index.ts
└── index.tsPage
pages/{name}/
├── ui/
│ ├── {Name}Page.tsx
│ └── index.ts
├── api/
│ └── loader.ts
├── model/
│ └── schema.ts
└── index.ts---
Public API Pattern
// entities/user/index.ts
export { UserCard } from './ui/UserCard';
export { UserAvatar } from './ui/UserAvatar';
export { getUser, updateUser } from './api/userApi';
export { useUser, useUsers } from './api/queries';
export type { User, UserRole } from './model/types';
export { userSchema } from './model/schema';
export { mapUserDTO } from './model/mapper';Import from public API only:
// ✅
import { UserCard, type User } from '@/entities/user';
// ❌
import { UserCard } from '@/entities/user/ui/UserCard';---
Cross-Entity References (@x)
When entities must reference each other:
entities/product/@x/order.ts → API for order to import// entities/product/@x/order.ts
export type { ProductId } from '../model/types';
// entities/order/model/types.ts
import type { ProductId } from '@/entities/product/@x/order';---
TypeScript Path Aliases
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}---
Anti-Patterns
| ❌ Don't | ✅ Do |
|---|---|
| Import from higher layer | Import from lower layers only |
| Cross-slice import (same layer) | Use lower layer or @x |
Generic segments: components/, hooks/ | Purpose segments: ui/, lib/ |
Wildcard exports: export * | Explicit exports |
Business logic in shared/ | Keep shared domain-agnostic |
| Single-use widgets | Keep in page slice |
| Everything is a feature | Only reused interactions |
| Import from internal paths | Always use index.ts |
---
Minimal FSD Setup
Start small, add layers as needed:
src/
├── app/
├── pages/
└── shared/Add entities/, features/, widgets/ when complexity grows.
---
Resources
| Resource | Link |
|---|---|
| Official Docs | feature-sliced.design |
| Examples | feature-sliced/examples |
| Awesome FSD | feature-sliced/awesome |
| v2.1 Notes | Pages Come First! |
FSD Implementation Patterns
Sources: Tutorial | Examples | Awesome FSD
Code patterns for Feature-Sliced Design architecture.
---
Entity Pattern
Complete Entity: User
Model Layer (entities/user/model/):
// entities/user/model/types.ts
export interface User {
id: string;
email: string;
name: string;
avatar?: string;
role: UserRole;
createdAt: Date;
}
export type UserRole = 'admin' | 'user' | 'guest';
export interface UserDTO {
id: number;
email: string;
name: string;
avatar_url: string | null;
role: string;
created_at: string;
}// entities/user/model/mapper.ts
import type { User, UserDTO, UserRole } from './types';
export function mapUserDTO(dto: UserDTO): User {
return {
id: String(dto.id),
email: dto.email,
name: dto.name,
avatar: dto.avatar_url ?? undefined,
role: dto.role as UserRole,
createdAt: new Date(dto.created_at),
};
}// entities/user/model/schema.ts
import { z } from 'zod';
export const userSchema = z.object({
name: z.string().min(2, 'Name must be at least 2 characters'),
email: z.string().email('Invalid email address'),
});
export type UserFormData = z.infer<typeof userSchema>;API Layer (entities/user/api/):
// entities/user/api/userApi.ts
import { apiClient } from '@/shared/api';
import { mapUserDTO } from '../model/mapper';
import type { User, UserDTO } from '../model/types';
export async function getCurrentUser(): Promise<User> {
const { data } = await apiClient.get<UserDTO>('/users/me');
return mapUserDTO(data);
}
export async function getUserById(id: string): Promise<User> {
const { data } = await apiClient.get<UserDTO>(`/users/${id}`);
return mapUserDTO(data);
}// entities/user/api/queries.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { getCurrentUser, getUserById, updateUser } from './userApi';
export const userKeys = {
all: ['users'] as const,
current: () => [...userKeys.all, 'current'] as const,
detail: (id: string) => [...userKeys.all, 'detail', id] as const,
};
export function useCurrentUser() {
return useQuery({
queryKey: userKeys.current(),
queryFn: getCurrentUser,
});
}
export function useUser(id: string) {
return useQuery({
queryKey: userKeys.detail(id),
queryFn: () => getUserById(id),
enabled: !!id,
});
}UI Layer (entities/user/ui/):
// entities/user/ui/UserAvatar.tsx
import type { User } from '../model/types';
interface UserAvatarProps {
user: User;
size?: 'sm' | 'md' | 'lg';
}
export function UserAvatar({ user, size = 'md' }: UserAvatarProps) {
const sizes = { sm: 'w-8 h-8', md: 'w-10 h-10', lg: 'w-14 h-14' };
if (user.avatar) {
return (
<img
src={user.avatar}
alt={user.name}
className={`rounded-full ${sizes[size]}`}
/>
);
}
return (
<div className={`rounded-full bg-gray-200 flex items-center justify-center ${sizes[size]}`}>
{user.name.charAt(0).toUpperCase()}
</div>
);
}// entities/user/ui/UserCard.tsx
import type { User } from '../model/types';
import { UserAvatar } from './UserAvatar';
interface UserCardProps {
user: User;
onClick?: () => void;
}
export function UserCard({ user, onClick }: UserCardProps) {
return (
<div
onClick={onClick}
className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-50 cursor-pointer"
>
<UserAvatar user={user} />
<div>
<p className="font-medium">{user.name}</p>
<p className="text-sm text-gray-500">{user.email}</p>
</div>
</div>
);
}Public API (entities/user/index.ts):
// entities/user/index.ts
export { UserAvatar } from './ui/UserAvatar';
export { UserCard } from './ui/UserCard';
export { getCurrentUser, getUserById } from './api/userApi';
export { useCurrentUser, useUser, userKeys } from './api/queries';
export type { User, UserRole, UserDTO } from './model/types';
export { mapUserDTO } from './model/mapper';
export { userSchema, type UserFormData } from './model/schema';---
Feature Pattern
Complete Feature: Authentication
Model Layer (features/auth/model/):
// features/auth/model/types.ts
export interface LoginCredentials {
email: string;
password: string;
}
export interface RegisterData extends LoginCredentials {
name: string;
}
export interface AuthTokens {
accessToken: string;
refreshToken: string;
}// features/auth/model/store.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import type { User } from '@/entities/user';
import type { AuthTokens } from './types';
interface AuthState {
user: User | null;
tokens: AuthTokens | null;
isAuthenticated: boolean;
setAuth: (user: User, tokens: AuthTokens) => void;
clearAuth: () => void;
}
export const useAuthStore = create<AuthState>()(
persist(
(set) => ({
user: null,
tokens: null,
isAuthenticated: false,
setAuth: (user, tokens) => set({ user, tokens, isAuthenticated: true }),
clearAuth: () => set({ user: null, tokens: null, isAuthenticated: false }),
}),
{ name: 'auth-storage' }
)
);// features/auth/model/schema.ts
import { z } from 'zod';
export const loginSchema = z.object({
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Password must be at least 8 characters'),
});
export const registerSchema = loginSchema.extend({
name: z.string().min(2, 'Name must be at least 2 characters'),
});
export type LoginFormData = z.infer<typeof loginSchema>;
export type RegisterFormData = z.infer<typeof registerSchema>;API Layer (features/auth/api/):
// features/auth/api/authApi.ts
import { apiClient } from '@/shared/api';
import { mapUserDTO, type User, type UserDTO } from '@/entities/user';
import type { LoginCredentials, RegisterData, AuthTokens } from '../model/types';
interface AuthResponse {
user: UserDTO;
access_token: string;
refresh_token: string;
}
export async function login(credentials: LoginCredentials): Promise<{ user: User; tokens: AuthTokens }> {
const { data } = await apiClient.post<AuthResponse>('/auth/login', credentials);
return {
user: mapUserDTO(data.user),
tokens: { accessToken: data.access_token, refreshToken: data.refresh_token },
};
}
export async function register(data: RegisterData): Promise<{ user: User; tokens: AuthTokens }> {
const { data: response } = await apiClient.post<AuthResponse>('/auth/register', data);
return {
user: mapUserDTO(response.user),
tokens: { accessToken: response.access_token, refreshToken: response.refresh_token },
};
}
export async function logout(): Promise<void> {
await apiClient.post('/auth/logout');
}UI Layer (features/auth/ui/):
// features/auth/ui/LoginForm.tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Button, Input } from '@/shared/ui';
import { loginSchema, type LoginFormData } from '../model/schema';
import { login } from '../api/authApi';
import { useAuthStore } from '../model/store';
export function LoginForm() {
const setAuth = useAuthStore((s) => s.setAuth);
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<LoginFormData>({
resolver: zodResolver(loginSchema),
});
const onSubmit = async (data: LoginFormData) => {
const { user, tokens } = await login(data);
setAuth(user, tokens);
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<Input
{...register('email')}
type="email"
placeholder="Email"
error={errors.email?.message}
/>
<Input
{...register('password')}
type="password"
placeholder="Password"
error={errors.password?.message}
/>
<Button type="submit" loading={isSubmitting}>
Sign In
</Button>
</form>
);
}// features/auth/ui/LogoutButton.tsx
import { Button } from '@/shared/ui';
import { logout } from '../api/authApi';
import { useAuthStore } from '../model/store';
export function LogoutButton() {
const clearAuth = useAuthStore((s) => s.clearAuth);
const handleLogout = async () => {
await logout();
clearAuth();
};
return (
<Button variant="ghost" onClick={handleLogout}>
Sign Out
</Button>
);
}Public API (features/auth/index.ts):
// features/auth/index.ts
export { LoginForm } from './ui/LoginForm';
export { LogoutButton } from './ui/LogoutButton';
export { useAuthStore } from './model/store';
export { login, register, logout } from './api/authApi';
export type { LoginCredentials, AuthTokens } from './model/types';
export { loginSchema, registerSchema } from './model/schema';---
Widget Pattern
Header Widget
// widgets/header/ui/Header.tsx
import { Link } from 'react-router-dom';
import { UserAvatar } from '@/entities/user';
import { LogoutButton, useAuthStore } from '@/features/auth';
import { SearchBox } from '@/features/search';
import { Logo } from '@/shared/ui';
export function Header() {
const { user, isAuthenticated } = useAuthStore();
return (
<header className="flex items-center justify-between px-6 py-4 border-b">
<Link to="/">
<Logo />
</Link>
<SearchBox />
<nav className="flex items-center gap-4">
{isAuthenticated ? (
<>
<UserAvatar user={user!} size="sm" />
<LogoutButton />
</>
) : (
<Link to="/login">Sign In</Link>
)}
</nav>
</header>
);
}
// widgets/header/index.ts
export { Header } from './ui/Header';---
Page Pattern
Product Detail Page
// pages/product-detail/api/loader.ts
import { getProductById } from '@/entities/product';
import type { LoaderFunctionArgs } from 'react-router-dom';
export async function productDetailLoader({ params }: LoaderFunctionArgs) {
const product = await getProductById(params.id!);
return { product };
}// pages/product-detail/ui/ProductDetailPage.tsx
import { useLoaderData } from 'react-router-dom';
import { ProductCard, type Product } from '@/entities/product';
import { AddToCartButton } from '@/features/cart';
import { Header } from '@/widgets/header';
export function ProductDetailPage() {
const { product } = useLoaderData() as { product: Product };
return (
<>
<Header />
<main className="max-w-4xl mx-auto py-8">
<ProductCard product={product} />
<AddToCartButton productId={product.id} />
</main>
</>
);
}
// pages/product-detail/index.ts
export { ProductDetailPage } from './ui/ProductDetailPage';
export { productDetailLoader } from './api/loader';---
Shared Layer Pattern
API Client
// shared/api/client.ts
import axios from 'axios';
export const apiClient = axios.create({
baseURL: import.meta.env.VITE_API_URL,
headers: { 'Content-Type': 'application/json' },
});
apiClient.interceptors.request.use((config) => {
const storage = localStorage.getItem('auth-storage');
if (storage) {
const { state } = JSON.parse(storage);
if (state?.tokens?.accessToken) {
config.headers.Authorization = `Bearer ${state.tokens.accessToken}`;
}
}
return config;
});
apiClient.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
localStorage.removeItem('auth-storage');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
// shared/api/index.ts
export { apiClient } from './client';UI Components
// shared/ui/Button.tsx
import { forwardRef, type ButtonHTMLAttributes } from 'react';
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'ghost';
loading?: boolean;
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ variant = 'primary', loading, children, disabled, ...props }, ref) => {
const variants = {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300',
ghost: 'text-gray-600 hover:bg-gray-100',
};
return (
<button
ref={ref}
disabled={disabled || loading}
className={`px-4 py-2 rounded-lg font-medium ${variants[variant]} disabled:opacity-50`}
{...props}
>
{loading ? 'Loading...' : children}
</button>
);
}
);// shared/ui/Input.tsx
import { forwardRef, type InputHTMLAttributes } from 'react';
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
error?: string;
}
export const Input = forwardRef<HTMLInputElement, InputProps>(
({ error, className, ...props }, ref) => (
<div>
<input
ref={ref}
className={`w-full px-3 py-2 border rounded-lg ${
error ? 'border-red-500' : 'border-gray-300'
} ${className}`}
{...props}
/>
{error && <p className="mt-1 text-sm text-red-500">{error}</p>}
</div>
)
);
// shared/ui/index.ts
export { Button } from './Button';
export { Input } from './Input';---
App Layer Pattern
Providers Setup
// app/providers/index.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ThemeProvider } from './ThemeProvider';
const queryClient = new QueryClient({
defaultOptions: {
queries: { staleTime: 1000 * 60 * 5, retry: 1 },
},
});
export function Providers({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
<ThemeProvider>{children}</ThemeProvider>
</QueryClientProvider>
);
}Router Configuration
// app/routes/router.tsx
import { createBrowserRouter } from 'react-router-dom';
import { HomePage } from '@/pages/home';
import { ProductDetailPage, productDetailLoader } from '@/pages/product-detail';
import { LoginPage } from '@/pages/login';
export const router = createBrowserRouter([
{ path: '/', element: <HomePage /> },
{
path: '/products/:id',
element: <ProductDetailPage />,
loader: productDetailLoader,
},
{ path: '/login', element: <LoginPage /> },
]);---
TypeScript Configuration
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}FSD Layers Reference
Source: Layers Reference | FSD Overview
Layer Hierarchy
Arranged from highest to lowest responsibility. Each layer can only import from layers below it.
| Layer | Purpose | Has Slices | Required |
|---|---|---|---|
app/ | Application initialization, providers, routing | No | Yes |
pages/ | Route-based screens | Yes | Yes |
widgets/ | Complex reusable UI blocks | Yes | No |
features/ | User interactions with business value | Yes | No |
entities/ | Business domain models | Yes | No |
shared/ | Reusable infrastructure | No | Yes |
Note: processes/ layer is DEPRECATED. Use pages with composition instead.
---
Import Rule
app/ → can import: pages, widgets, features, entities, shared
pages/ → can import: widgets, features, entities, shared
widgets/ → can import: features, entities, shared
features/ → can import: entities, shared
entities/ → can import: shared (use @x for cross-entity)
shared/ → can import: external packages onlyException: app/ and shared/ have no slices, so internal cross-segment imports are allowed.
---
Layer Details
Shared Layer
Shared Layer Docs
Foundation layer for external connections and utilities. No business domain knowledge.
Segments:
shared/
├── api/ # Backend client, request functions, interceptors
├── ui/ # Business-agnostic UI (buttons, inputs, modals)
├── lib/ # Focused utilities (dates, colors, validation)
├── config/ # Environment variables, feature flags
├── routes/ # Route path constants
├── i18n/ # Translation setup
└── types/ # Global TypeScript types (utility types)Guidelines:
- Avoid generic names:
components/,hooks/,utils/ - Use purpose-driven segment names
- Should be extractable to a separate package
- NO domain logic
TypeScript Types:
- Utility types →
shared/lib/utility-types - DTOs →
shared/apinear request functions - Avoid generic
shared/typesfolder
---
Entities Layer
Entities Layer Docs
Real-world business concepts the application works with.
Structure:
entities/
├── user/
│ ├── ui/ # UserAvatar, UserCard, UserBadge
│ ├── api/ # getUser, updateUser, queries
│ ├── model/ # User types, validation, store
│ ├── lib/ # formatUserName, calculateAge
│ └── index.ts # Public API
├── product/
│ ├── ui/
│ ├── api/
│ ├── model/
│ └── index.ts
└── order/
└── ...What belongs here:
- Data models and TypeScript interfaces
- API functions for CRUD operations
- Reusable UI representations
- Validation schemas (Zod, Yup)
- Entity-specific mappers (DTO → Domain)
What doesn't belong:
- User interactions (→ features)
- Page layouts (→ pages)
- Composed UI blocks (→ widgets)
Cross-Entity References (@x Notation):
Cross-Imports @x Notation
When entities must reference each other:
entities/
├── product/
│ ├── @x/
│ │ └── order.ts # API for order entity only
│ └── index.ts
└── order/
└── model/types.ts # imports from product/@x/order// entities/product/@x/order.ts
export type { ProductId, ProductName } from '../model/types';
// entities/order/model/types.ts
import type { ProductId } from '@/entities/product/@x/order';---
Features Layer
Features Layer Docs
User-facing interactions that provide business value.
Key principle: Not everything is a feature. Per FSD v2.1, keep non-reused interactions in page slices.
Structure:
features/
├── auth/
│ ├── ui/ # LoginForm, LogoutButton
│ ├── api/ # login, logout, register
│ ├── model/ # auth state, session, schemas
│ └── index.ts
├── add-to-cart/
│ ├── ui/ # AddToCartButton, QuantitySelector
│ ├── api/ # addToCart mutation
│ ├── model/ # validation
│ └── index.ts
└── search-products/
├── ui/ # SearchInput, Filters
├── api/ # searchProducts
├── model/ # search state
└── index.tsFeature vs Entity Decision:
| Entity | Feature |
|---|---|
| Represents a THING | Represents an ACTION |
user — user data | auth — login/logout |
product — product info | add-to-cart — adding |
comment — comment data | write-comment — creating |
---
Widgets Layer
Widgets Layer Docs
Large, self-sufficient UI components reused across multiple pages.
When to use widgets:
- Component is reused across multiple pages
- Component is complex with multiple children
- Component delivers a complete use case
Structure:
widgets/
├── header/
│ ├── ui/ # Header, NavMenu, UserDropdown
│ └── index.ts
├── sidebar/
│ ├── ui/ # Sidebar, SidebarItem
│ └── index.ts
└── product-list/
├── ui/ # ProductList, ProductGrid, Filters
└── index.tsWidget vs Feature:
- Widget = composed UI block (visual)
- Feature = user interaction (behavioral)
Widgets often compose multiple features:
// widgets/header/ui/Header.tsx
import { UserAvatar } from '@/entities/user';
import { LogoutButton } from '@/features/auth';
import { SearchBox } from '@/features/search';Don't create widgets for:
- Single-use components (keep in page)
- Simple compositions (compose in page directly)
---
Pages Layer
Pages Layer Docs
Individual screens or routes. One slice per route (generally).
Structure:
pages/
├── home/
│ ├── ui/ # HomePage, HeroSection
│ ├── api/ # loader functions
│ └── index.ts
├── product-detail/
│ ├── ui/ # ProductDetailPage
│ ├── api/ # getProduct loader
│ └── index.ts
└── checkout/
├── ui/ # CheckoutPage, Steps
├── api/ # checkout mutations
├── model/ # form validation
└── index.tsGuidelines:
- One slice per route (generally)
- Similar pages can share a slice (login/register)
- Pages compose widgets, features, entities
- Minimal business logic — delegate to lower layers
- Non-reused interactions stay in page slice (v2.1)
---
App Layer
App Layer Docs
Application-wide configuration and initialization.
Structure:
app/
├── providers/ # React context, store setup
│ ├── ThemeProvider.tsx
│ ├── QueryProvider.tsx
│ └── index.ts
├── routes/ # Router configuration
│ └── router.tsx
├── styles/ # Global CSS, theme tokens
│ ├── globals.css
│ └── theme.ts
└── index.tsx # Entry pointResponsibilities:
- Initialize application state
- Set up routing
- Configure global providers
- Define global styles
- Application-wide error boundaries
---
Layer Selection Flowchart
START: Where does this code go?
│
├─ Reusable infrastructure without business logic?
│ └─ YES → shared/
│
├─ Business domain object/data model?
│ └─ YES → entities/
│
├─ User interaction with business value?
│ ├─ YES, reused across pages → features/
│ └─ YES, single page only → Keep in pages/ slice
│
├─ Complex, reusable UI composition?
│ └─ YES → widgets/
│
├─ Route/screen component?
│ └─ YES → pages/
│
└─ App-wide initialization/config?
└─ YES → app/---
Common Mistakes
1. Features in entities — Entities are data, features are actions 2. Single-use widgets — Keep in pages/ instead (v2.1) 3. Business logic in shared — Shared must be domain-agnostic 4. Too many layers — Start with shared, pages, app; add as needed 5. Importing upward — Strictly forbidden 6. Generic segment names — Use purpose-driven: api/, model/, ui/ 7. Everything is a feature — Only reused interactions qualify
Migrating to Feature-Sliced Design
Source: Migration from Custom Architecture | Migration from v2.0 to v2.1
When to Migrate
Consider migrating to FSD if:
- Project has grown too large and interconnected
- Implementing new features takes longer than expected
- Onboarding new developers is difficult
- Circular dependencies are common
- Code ownership is unclear
Don't migrate if the current architecture works well for your team.
---
Migration Strategy: Incremental Adoption
FSD supports incremental adoption. Don't rewrite everything at once.
Phase 1: Setup FSD structure alongside existing code
Phase 2: Migrate shared utilities
Phase 3: Extract entities
Phase 4: Extract features
Phase 5: Migrate pages
Phase 6: Clean up and enforce rules---
Phase 1: Setup FSD Structure
Create Directory Structure
mkdir -p src/{app,pages,widgets,features,entities,shared}/{ui,api,model,lib}Configure Path Aliases
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@components/*": ["src/components/*"],
"@hooks/*": ["src/hooks/*"]
}
}
}---
Phase 2: Migrate Shared Utilities
Before (Typical Structure)
src/
├── utils/
│ ├── api.ts
│ ├── dates.ts
│ ├── validation.ts
│ └── constants.ts
├── hooks/
│ ├── useLocalStorage.ts
│ └── useDebounce.ts
└── components/
├── Button.tsx
├── Input.tsx
└── Modal.tsxAfter (FSD Shared Layer)
src/shared/
├── api/
│ ├── client.ts # from utils/api.ts
│ └── index.ts
├── lib/
│ ├── dates.ts # from utils/dates.ts
│ ├── validation.ts # from utils/validation.ts
│ ├── useLocalStorage.ts # from hooks/
│ ├── useDebounce.ts # from hooks/
│ └── index.ts
├── config/
│ ├── constants.ts # from utils/constants.ts
│ └── index.ts
└── ui/
├── Button/
│ ├── Button.tsx # from components/
│ └── index.ts
├── Input/
├── Modal/
└── index.tsMigration Script
# Move utils
mv src/utils/api.ts src/shared/api/client.ts
mv src/utils/dates.ts src/shared/lib/dates.ts
mv src/utils/validation.ts src/shared/lib/validation.ts
mv src/utils/constants.ts src/shared/config/constants.ts
# Move hooks to lib
mv src/hooks/*.ts src/shared/lib/
# Move components to ui
for component in src/components/*.tsx; do
name=$(basename "$component" .tsx)
mkdir -p "src/shared/ui/$name"
mv "$component" "src/shared/ui/$name/$name.tsx"
echo "export { $name } from './$name';" > "src/shared/ui/$name/index.ts"
doneUpdate Imports
// Before
import { formatDate } from '@/utils/dates';
import { Button } from '@/components/Button';
// After
import { formatDate } from '@/shared/lib';
import { Button } from '@/shared/ui';---
Phase 3: Extract Entities
Identify Entities
Look for business domain objects:
- Types/interfaces representing domain concepts
- API calls for CRUD operations
- Reusable UI components showing domain data
Before (Scattered)
src/
├── types/
│ └── user.ts
├── api/
│ └── userApi.ts
├── components/
│ ├── UserAvatar.tsx
│ └── UserCard.tsx
└── store/
└── userSlice.tsAfter (FSD Entity)
src/entities/user/
├── ui/
│ ├── UserAvatar.tsx
│ ├── UserCard.tsx
│ └── index.ts
├── api/
│ ├── userApi.ts
│ └── index.ts
├── model/
│ ├── types.ts
│ ├── store.ts
│ └── index.ts
└── index.tsEntity Public API
// entities/user/index.ts
export { UserAvatar } from './ui/UserAvatar';
export { UserCard } from './ui/UserCard';
export { getUser, updateUser, deleteUser } from './api/userApi';
export type { User, UserRole } from './model/types';
export { useUserStore } from './model/store';---
Phase 4: Extract Features
Identify Features
Features are user interactions with business value:
- Login/logout functionality
- Add to cart
- Search
- Submit forms
Before (Mixed Concerns)
src/
├── components/
│ ├── LoginForm.tsx
│ └── LogoutButton.tsx
├── api/
│ └── authApi.ts
└── store/
└── authSlice.tsAfter (FSD Feature)
src/features/auth/
├── ui/
│ ├── LoginForm.tsx
│ ├── LogoutButton.tsx
│ └── index.ts
├── api/
│ ├── authApi.ts
│ └── index.ts
├── model/
│ ├── types.ts
│ ├── schema.ts
│ ├── store.ts
│ └── index.ts
└── index.ts---
Phase 5: Migrate Pages
Before
src/pages/
├── Home.tsx
├── ProductList.tsx
└── ProductDetail.tsxAfter
src/pages/
├── home/
│ ├── ui/
│ │ └── HomePage.tsx
│ └── index.ts
├── products/
│ ├── ui/
│ │ └── ProductsPage.tsx
│ └── index.ts
└── product-detail/
├── ui/
│ └── ProductDetailPage.tsx
├── api/
│ └── loader.ts
└── index.tsRefactor Page Components
// Before: src/pages/ProductDetail.tsx
import { useParams } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { fetchProduct } from '@/api/products';
import { AddToCartButton } from '@/components/AddToCartButton';
export function ProductDetail() {
const { id } = useParams();
const { data: product } = useQuery(['product', id], () => fetchProduct(id));
// ...
}
// After: src/pages/product-detail/ui/ProductDetailPage.tsx
import { useParams } from 'react-router-dom';
import { useProduct } from '@/entities/product';
import { AddToCart } from '@/features/add-to-cart';
import { ProductReviews } from '@/widgets/product-reviews';
export function ProductDetailPage() {
const { id } = useParams();
const { data: product } = useProduct(id!);
// ...
}---
Common Migration Patterns
Handling Circular Dependencies
Problem: Existing code has circular imports.
Solution: Extract shared dependencies to lower layers.
// Before: Circular dependency
// components/UserCard.tsx imports from hooks/useAuth.ts
// hooks/useAuth.ts imports from components/UserCard.tsx
// After: Break the cycle
// entities/user/ui/UserCard.tsx — no auth dependency
// features/auth/model/store.ts — no UserCard dependency
// pages/profile/ui/ProfilePage.tsx — composes bothHandling Global State
Problem: Monolithic store accessed everywhere.
Solution: Split store by domain into entity/feature models.
// Before: Monolithic store
export const store = configureStore({
reducer: {
user: userReducer,
products: productsReducer,
cart: cartReducer,
auth: authReducer,
},
});
// After: Distributed stores (Zustand example)
// entities/user/model/store.ts — user data
// entities/product/model/store.ts — product data
// features/cart/model/store.ts — cart state
// features/auth/model/store.ts — auth stateShared Components with Business Logic
Problem: Component has business logic mixed in.
Solution: Split into entity/feature UI and shared UI.
// Before: ProductCard with add-to-cart logic
export function ProductCard({ product }) {
const addToCart = useAddToCart();
return (
<div>
<img src={product.image} />
<h3>{product.name}</h3>
<button onClick={() => addToCart(product)}>Add to Cart</button>
</div>
);
}
// After: Separated concerns
// entities/product/ui/ProductCard.tsx — display only
export function ProductCard({ product, actions }) {
return (
<div>
<img src={product.image} />
<h3>{product.name}</h3>
{actions}
</div>
);
}
// features/add-to-cart/ui/AddToCartButton.tsx — interaction
export function AddToCartButton({ product }) {
const addToCart = useCartStore((s) => s.addItem);
return <button onClick={() => addToCart(product)}>Add to Cart</button>;
}
// Composed in page/widget
<ProductCard
product={product}
actions={<AddToCartButton product={product} />}
/>---
Migration Checklist
- [ ] Create FSD directory structure
- [ ] Configure path aliases
- [ ] Migrate utilities to
shared/lib/ - [ ] Migrate API client to
shared/api/ - [ ] Migrate UI kit to
shared/ui/ - [ ] Identify and extract entities
- [ ] Create entity public APIs
- [ ] Identify and extract features
- [ ] Create feature public APIs
- [ ] Migrate pages to page slices
- [ ] Extract reusable widgets
- [ ] Setup
app/layer with providers - [ ] Remove old directory structure
- [ ] Update documentation
---
Rollback Strategy
Keep old structure working during migration:
{
"paths": {
"@/*": ["./src/*"],
"@components/*": ["src/components/*"],
"@hooks/*": ["src/hooks/*"]
}
}Use feature flags to gradually switch:
import { UserCard as LegacyUserCard } from '@components/UserCard';
import { UserCard as FSDUserCard } from '@/entities/user';
export const UserCard = process.env.USE_FSD ? FSDUserCard : LegacyUserCard;---
Resources
| Resource | Link |
|---|---|
| Migration Guide | feature-sliced.design/docs/guides/migration |
| v2.1 Changes | Pages Come First |
| Community Article | Migrating a Legacy React Project |
FSD with Next.js Integration
Source: Official Next.js Guide | FSD Pure Next.js Template
The Challenge
FSD conflicts with Next.js's built-in app/ and pages/ folders. Both expect specific file structures for routing. FSD uses flat slice architecture.
Solution Overview
Place the Next.js App Router in src/app/ (Next.js ignores src/app/ if root app/ exists). This directory serves double duty: Next.js routing AND the FSD app layer. Re-export page components from FSD pages/ layer.
---
App Router Setup (Next.js 13+)
Directory Structure
project-root/
├── src/
│ ├── app/ # Next.js App Router + FSD app layer
│ │ ├── layout.tsx # Root layout with providers
│ │ ├── page.tsx # Home → re-exports from pages/
│ │ ├── products/
│ │ │ ├── page.tsx
│ │ │ └── [id]/
│ │ │ └── page.tsx
│ │ ├── login/
│ │ │ └── page.tsx
│ │ ├── api/ # API routes
│ │ ├── providers/ # FSD: React context providers
│ │ │ └── index.tsx
│ │ └── styles/ # FSD: Global styles
│ │ └── globals.css
│ ├── pages/ # FSD pages layer (NOT Next.js)
│ │ ├── home/
│ │ ├── products/
│ │ ├── product-detail/
│ │ └── login/
│ ├── widgets/
│ ├── features/
│ ├── entities/
│ └── shared/
├── middleware.ts # Next.js middleware (root)
└── next.config.jsPage Re-Export Pattern
// src/app/page.tsx
export { HomePage as default } from '@/pages/home';
// src/app/products/page.tsx
export { ProductsPage as default } from '@/pages/products';
// src/app/products/[id]/page.tsx
export { ProductDetailPage as default } from '@/pages/product-detail';FSD Page Implementation
// src/pages/home/ui/HomePage.tsx
import { Header } from '@/widgets/header';
import { FeaturedProducts } from '@/widgets/featured-products';
import { HeroSection } from './HeroSection';
export function HomePage() {
return (
<>
<Header />
<main>
<HeroSection />
<FeaturedProducts />
</main>
</>
);
}
// src/pages/home/index.ts
export { HomePage } from './ui/HomePage';Root Layout with Providers
// src/app/layout.tsx
import { Providers } from './providers';
import './styles/globals.css';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}// src/app/providers/index.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ThemeProvider } from 'next-themes';
const queryClient = new QueryClient();
export function Providers({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
<ThemeProvider attribute="class" defaultTheme="system">
{children}
</ThemeProvider>
</QueryClientProvider>
);
}Server Components with Data Fetching
// src/app/products/[id]/page.tsx
import { ProductDetailPage } from '@/pages/product-detail';
import { getProductById } from '@/entities/product';
interface Props {
params: { id: string };
}
export default async function Page({ params }: Props) {
const product = await getProductById(params.id);
return <ProductDetailPage product={product} />;
}
export async function generateStaticParams() {
const products = await getProducts();
return products.map((product) => ({ id: product.id }));
}Server Actions in Features
// src/features/auth/api/actions.ts
'use server';
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
import { loginSchema } from '../model/schema';
export async function loginAction(formData: FormData) {
const rawData = {
email: formData.get('email'),
password: formData.get('password'),
};
const result = loginSchema.safeParse(rawData);
if (!result.success) {
return { errors: result.error.flatten().fieldErrors };
}
const response = await fetch(`${process.env.API_URL}/auth/login`, {
method: 'POST',
body: JSON.stringify(result.data),
headers: { 'Content-Type': 'application/json' },
});
if (!response.ok) {
return { errors: { form: ['Invalid credentials'] } };
}
const { token } = await response.json();
cookies().set('token', token, { httpOnly: true, secure: true });
redirect('/dashboard');
}---
Pages Router Setup (Next.js 12)
Directory Structure
project-root/
├── pages/ # Next.js Pages Router (root)
│ ├── _app.tsx # Custom App
│ ├── _document.tsx
│ ├── index.tsx # Home → re-exports from src/pages
│ ├── products/
│ │ ├── index.tsx
│ │ └── [id].tsx
│ └── api/
├── src/
│ ├── app/
│ │ ├── custom-app/ # _app component
│ │ └── providers/
│ ├── pages/ # FSD pages layer
│ ├── widgets/
│ ├── features/
│ ├── entities/
│ └── shared/
└── next.config.jsCustom App Component
// pages/_app.tsx
export { CustomApp as default } from '@/app/custom-app';
// src/app/custom-app/CustomApp.tsx
import type { AppProps } from 'next/app';
import { Providers } from '../providers';
import '../styles/globals.css';
export function CustomApp({ Component, pageProps }: AppProps) {
return (
<Providers>
<Component {...pageProps} />
</Providers>
);
}Page with getServerSideProps
// pages/products/[id].tsx
import { ProductDetailPage } from '@/pages/product-detail';
import { getProductById } from '@/entities/product';
import type { GetServerSideProps } from 'next';
export default ProductDetailPage;
export const getServerSideProps: GetServerSideProps = async ({ params }) => {
const product = await getProductById(params?.id as string);
if (!product) {
return { notFound: true };
}
return { props: { product } };
};---
TypeScript Configuration
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}---
API Routes
FSD is frontend-focused. For API routes:
Option 1: Keep in src/app/api/
src/app/
├── api/
│ ├── auth/
│ │ └── route.ts
│ └── products/
│ └── route.tsOption 2: Separate Backend (Monorepo)
packages/
├── frontend/ # Next.js + FSD
│ └── src/
│ ├── app/
│ ├── pages/ # FSD pages
│ └── ...
└── backend/ # Express/Fastify
└── src/
└── routes/---
Database Queries
Keep database logic in shared/db/:
// shared/db/client.ts
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
const client = postgres(process.env.DATABASE_URL!);
export const db = drizzle(client);// shared/db/queries/products.ts
import { db } from '../client';
import { products } from '../schema';
import { eq } from 'drizzle-orm';
export async function getAllProducts() {
return db.select().from(products);
}
export async function getProductById(id: string) {
return db.select().from(products).where(eq(products.id, id)).limit(1);
}// entities/product/api/productApi.ts
import { getAllProducts, getProductById as dbGetProduct } from '@/shared/db/queries/products';
import { mapProductRow } from '../model/mapper';
export async function getProducts() {
const rows = await getAllProducts();
return rows.map(mapProductRow);
}
export async function getProductById(id: string) {
const [row] = await dbGetProduct(id);
return row ? mapProductRow(row) : null;
}---
Middleware
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('token')?.value;
const isAuthPage = request.nextUrl.pathname.startsWith('/login');
const isProtected = request.nextUrl.pathname.startsWith('/dashboard');
if (isProtected && !token) {
return NextResponse.redirect(new URL('/login', request.url));
}
if (isAuthPage && token) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*', '/login'],
};---
Common Patterns
Loading States
// src/app/products/loading.tsx
import { ProductListSkeleton } from '@/widgets/product-list';
export default function Loading() {
return <ProductListSkeleton />;
}Error Boundaries
// src/app/products/error.tsx
'use client';
import { Button } from '@/shared/ui';
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<h2 className="text-xl font-bold mb-4">Something went wrong!</h2>
<p className="text-gray-600 mb-4">{error.message}</p>
<Button onClick={reset}>Try again</Button>
</div>
);
}Not Found
// src/app/products/[id]/not-found.tsx
import Link from 'next/link';
export default function NotFound() {
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<h2 className="text-xl font-bold mb-4">Product Not Found</h2>
<Link href="/products" className="text-blue-600 hover:underline">
Back to Products
</Link>
</div>
);
}---
Best Practices
1. Keep Next.js routes thin — Only re-exports and data fetching 2. All UI logic in FSD layers — Components, state, business logic 3. Use path aliases — Clean imports across layers 4. Server Components default — Add 'use client' only when needed 5. Colocate server actions — In feature's api/ segment with 'use server' 6. Shared DB queries — Keep database logic in shared/db/ 7. Middleware at root — Authentication, redirects, headers
---
Resources
| Resource | Link |
|---|---|
| Official Guide | feature-sliced.design/docs/guides/tech/with-nextjs |
| FSD Pure Template | github.com/yunglocokid/FSD-Pure-Next.js-Template |
| i18n Example | github.com/nikolay-malygin/i18n-Next.js-14-FSD |
| App Router Guide | dev.to/m_midas |
FSD Public API Patterns
Source: Public API Reference
What is a Public API?
A public API is a contract between a slice and consuming code. It controls which objects are accessible and how they can be imported.
Implementation: An index.ts barrel file with explicit re-exports.
---
Three Goals of Quality Public APIs
1. Protection from structural changes — Shield consumers from internal refactoring 2. Behavioral transparency — Significant changes reflect in the API 3. Selective exposure — Only necessary parts exposed
---
Basic Pattern
// entities/user/index.ts
export { UserCard } from './ui/UserCard';
export { UserAvatar } from './ui/UserAvatar';
export { getUser, updateUser } from './api/userApi';
export type { User, UserRole } from './model/types';
export { userSchema } from './model/schema';Usage:
import { UserCard, type User } from '@/entities/user';---
Avoid Wildcard Exports
Don't do this:
export * from './ui';
export * from './api';
export * from './model';Problems:
- Reduces discoverability
- Accidentally exposes internals
- Complicates refactoring
- Harms tree-shaking
---
Segment-Level Public APIs
For large slices, define public APIs per segment:
entities/user/
├── ui/
│ ├── UserCard.tsx
│ ├── UserAvatar.tsx
│ └── index.ts
├── api/
│ ├── userApi.ts
│ └── index.ts
├── model/
│ ├── types.ts
│ ├── schema.ts
│ └── index.ts
└── index.ts// entities/user/ui/index.ts
export { UserCard } from './UserCard';
export { UserAvatar } from './UserAvatar';
// entities/user/index.ts
export * from './ui';
export * from './api';
export * from './model';---
Cross-Imports with @x Notation
Official @x Documentation
When entities legitimately reference each other:
entities/
├── song/
│ ├── @x/
│ │ └── artist.ts
│ ├── model/
│ │ └── types.ts
│ └── index.ts
└── artist/
├── model/
│ └── types.ts
└── index.ts// entities/song/@x/artist.ts
export type { Song, SongId } from '../model/types';
// entities/artist/model/types.ts
import type { Song } from '@/entities/song/@x/artist';
export interface Artist {
name: string;
songs: Song[];
}Guidelines for @x:
- Keep cross-imports minimal
- Document why the cross-reference exists
- Consider merging entities if references are extensive
- Use only on Entities layer
---
Avoiding Circular Imports
Problem: Importing from index within a slice causes circulars.
// ❌
import { UserCard } from '../index';
// ✅
import { UserCard } from '../ui/UserCard';Rule: Within a slice, use relative imports. External consumers use the public API.
---
Tree-Shaking Optimization
For large shared UI libraries, split into component-level indices:
shared/ui/
├── Button/
│ ├── Button.tsx
│ └── index.ts
├── Input/
│ ├── Input.tsx
│ └── index.ts
├── Modal/
│ ├── Modal.tsx
│ └── index.ts
└── index.tsImport patterns:
import { Button, Input } from '@/shared/ui';
import { Button } from '@/shared/ui/Button';---
Index File Challenges
Four major issues:
1. Circular imports — Internal files reimporting from index 2. Tree-shaking failures — Unrelated utilities bundled together 3. Weak enforcement — Nothing prevents direct imports technically 4. Performance degradation — Too many indices slow dev servers
Solutions:
- Use relative imports within slices
- Create separate indices per component in
shared/ - Review imports during code review
- Consider monorepo for very large projects
---
Complete Example
// entities/product/model/types.ts
export interface Product {
id: string;
name: string;
price: number;
imageUrl: string;
category: string;
}
export interface ProductFilters {
category?: string;
minPrice?: number;
maxPrice?: number;
}// entities/product/model/schema.ts
import { z } from 'zod';
export const productSchema = z.object({
id: z.string(),
name: z.string().min(1),
price: z.number().positive(),
imageUrl: z.string().url(),
category: z.string(),
});// entities/product/api/productApi.ts
import { apiClient } from '@/shared/api';
import type { Product, ProductFilters } from '../model/types';
export async function getProducts(filters?: ProductFilters): Promise<Product[]> {
const { data } = await apiClient.get('/products', { params: filters });
return data;
}
export async function getProductById(id: string): Promise<Product> {
const { data } = await apiClient.get(`/products/${id}`);
return data;
}// entities/product/ui/ProductCard.tsx
import type { Product } from '../model/types';
interface ProductCardProps {
product: Product;
onSelect?: (product: Product) => void;
}
export function ProductCard({ product, onSelect }: ProductCardProps) {
return (
<div onClick={() => onSelect?.(product)}>
<img src={product.imageUrl} alt={product.name} />
<h3>{product.name}</h3>
<p>${product.price}</p>
</div>
);
}// entities/product/index.ts
export { ProductCard } from './ui/ProductCard';
export { getProducts, getProductById } from './api/productApi';
export type { Product, ProductFilters } from './model/types';
export { productSchema } from './model/schema';