What is Feature-Sliced Design?
Feature-Sliced Design (FSD) is an architectural methodology for frontend applications that organizes code into layers with clear responsibilities and unidirectional dependencies. It helps teams build scalable, maintainable applications by enforcing separation of concerns and preventing architectural decay.
Core Principles
1. Layered Architecture
Code is organized into 7 standardized layers, each with a specific purpose:
app → Application initialization and global configuration
pages → Complete application routes/views
widgets → Large composite components
features → User scenarios and features
entities → Business entities and domain logic
shared → Reusable infrastructure code
2. Unidirectional Dependencies
Layers can only import from layers below them:
app → pages → widgets → features → entities → shared
Forbidden : A feature cannot import from pages or app. An entity cannot import from features.
3. Public API
Each module exposes a public API through an index.ts file. Internal implementation details remain private.
Trazea’s FSD Structure
Complete Folder Hierarchy
src/
├── app/ # Application Layer
│ ├── constants/ # Global constants
│ ├── lib/ # Core setup (Sentry)
│ │ └── sentry.ts
│ ├── providers/ # Global providers
│ │ ├── AuthProvider.tsx # Supabase auth context
│ │ ├── QueryProvider.tsx # TanStack Query
│ │ └── ThemeProvider.tsx # Dark/light mode
│ ├── styles/ # Global styles
│ │ └── index.css # Tailwind imports
│ └── ui/ # App entry and routing
│ ├── App.tsx # Main app component
│ └── Router.tsx # Route configuration
│
├── pages/ # Pages Layer
│ ├── auth/ # Authentication pages
│ │ └── ui/
│ │ ├── LoginPage.tsx
│ │ ├── RegisterPage.tsx
│ │ └── PendingApprovalPage.tsx
│ ├── inventario/ # Inventory views
│ │ ├── model/ # Page-specific state
│ │ └── ui/
│ │ └── InventoryPage.tsx
│ ├── spares/ # Spare parts catalog
│ │ ├── model/
│ │ └── ui/
│ │ └── SparesPage.tsx
│ ├── orders/ # Scooter orders
│ │ ├── api/
│ │ ├── lib/
│ │ ├── model/
│ │ └── ui/
│ │ └── OrdersPage.tsx
│ ├── records/ # Movement records
│ │ ├── api/
│ │ ├── lib/
│ │ ├── model/
│ │ └── ui/
│ │ └── RecordsPage.tsx
│ ├── count/ # Physical counting
│ │ ├── api/
│ │ ├── lib/
│ │ ├── model/
│ │ └── ui/
│ │ └── CountPage.tsx
│ ├── requests/ # Inter-location requests
│ │ └── ui/
│ │ └── RequestsPage.tsx
│ ├── dynamo/ # Scooter tracking
│ │ ├── lib/
│ │ ├── model/
│ │ └── ui/
│ │ └── DynamoPage.tsx
│ ├── Home.tsx # Dashboard
│ ├── Inventory.tsx # Main inventory
│ ├── Products.tsx # Products view
│ └── NotFound.tsx # 404 page
│
├── widgets/ # Widgets Layer
│ ├── nav/ # Navigation widget
│ │ └── ui/
│ │ ├── Sidebar.tsx # Main sidebar
│ │ └── NavItem.tsx # Navigation item
│ ├── notifications/ # Notification center
│ │ ├── lib/
│ │ ├── model/
│ │ │ └── notificationStore.ts
│ │ └── ui/
│ │ ├── NotificationCenter.tsx
│ │ └── NotificationItem.tsx
│ └── pagination/ # Pagination controls
│ ├── model/
│ └── ui/
│ └── Pagination.tsx
│
├── features/ # Features Layer
│ ├── auth-login/ # Login feature
│ │ ├── lib/
│ │ │ └── authHelpers.ts
│ │ └── ui/
│ │ ├── LoginForm.tsx
│ │ └── GoogleAuthButton.tsx
│ ├── spares-create/ # Create spare parts
│ │ └── ui/
│ │ └── CreateSpareForm.tsx
│ ├── spares-upload/ # Bulk upload
│ │ ├── api/
│ │ ├── model/
│ │ │ └── uploadSchema.ts
│ │ └── ui/
│ │ └── ExcelUploader.tsx
│ ├── spares-request-workshop/ # Request workflow
│ │ ├── api/
│ │ ├── lib/
│ │ ├── model/
│ │ │ ├── cartStore.ts
│ │ │ └── requestSchema.ts
│ │ └── ui/
│ │ ├── RequestCart.tsx
│ │ ├── CartItem.tsx
│ │ └── CreateRequestDialog.tsx
│ ├── guarantees-create/ # Warranty creation
│ │ ├── api/
│ │ ├── lib/
│ │ └── ui/
│ │ └── CreateGuaranteeForm.tsx
│ ├── guarantees-dashboard/ # Warranty management
│ │ ├── lib/
│ │ ├── model/
│ │ └── ui/
│ │ ├── GuaranteesList.tsx
│ │ └── GuaranteeCard.tsx
│ ├── count-spares/ # Physical count
│ │ ├── api/
│ │ ├── model/
│ │ │ └── countSchema.ts
│ │ └── ui/
│ │ ├── CountForm.tsx
│ │ └── CountSummary.tsx
│ ├── inventory-filters/ # Advanced filtering
│ │ ├── api/
│ │ ├── lib/
│ │ ├── model/
│ │ │ └── filterStore.ts
│ │ └── ui/
│ │ └── FilterPanel.tsx
│ ├── inventario-crear-repuesto/ # Create inventory item
│ │ ├── api/
│ │ ├── model/
│ │ │ └── schemas.ts
│ │ └── ui/
│ │ └── CreateInventoryItemForm.tsx
│ ├── record-save-movement/ # Log technician movements
│ │ ├── api/
│ │ ├── constants/
│ │ ├── lib/
│ │ └── ui/
│ │ └── SaveMovementForm.tsx
│ └── requests-sended/ # Sent requests view
│ └── ui/
│ └── SendedRequestsList.tsx
│
├── entities/ # Entities Layer
│ ├── user/ # User entity
│ │ ├── lib/
│ │ │ └── permissions.ts
│ │ ├── model/
│ │ │ ├── types.ts
│ │ │ └── useUserStore.ts
│ │ └── ui/
│ │ ├── UserAvatar.tsx
│ │ └── UserBadge.tsx
│ ├── locations/ # Locations entity
│ │ ├── api/
│ │ │ └── locationsApi.ts
│ │ └── ui/
│ │ └── LocationSelector.tsx
│ ├── inventario/ # Inventory entity
│ │ ├── api/
│ │ │ └── inventoryApi.ts
│ │ ├── constants/
│ │ ├── lib/
│ │ │ └── inventoryHelpers.ts
│ │ ├── model/
│ │ │ ├── types.ts
│ │ │ └── hooks/
│ │ │ └── useInventory.ts
│ │ └── ui/
│ │ ├── InventoryCard.tsx
│ │ └── StockBadge.tsx
│ ├── repuestos/ # Spare parts entity
│ │ ├── api/
│ │ │ └── repuestosApi.ts
│ │ ├── constants/
│ │ ├── lib/
│ │ │ └── spareHelpers.ts
│ │ ├── model/
│ │ │ ├── types.ts
│ │ │ └── hooks/
│ │ │ └── useRepuestos.ts
│ │ └── ui/
│ ├── guarantees/ # Warranties entity
│ │ ├── api/
│ │ │ └── guaranteesApi.ts
│ │ └── model/
│ │ └── types.ts
│ ├── requests/ # Requests entity
│ │ ├── api/
│ │ │ └── requestsApi.ts
│ │ └── model/
│ │ └── types.ts
│ ├── movimientos/ # Movements entity
│ │ ├── api/
│ │ │ └── movimientosApi.ts
│ │ ├── lib/
│ │ ├── model/
│ │ │ └── types.ts
│ │ └── ui/
│ │ └── MovementCard.tsx
│ ├── technical/ # Technician entity
│ │ ├── api/
│ │ │ └── technicalApi.ts
│ │ └── lib/
│ └── records/ # Audit records entity
│ └── model/
│ └── types.ts
│
└── shared/ # Shared Layer
├── api/ # Shared API utilities
│ └── supabase.ts # Supabase client
├── lib/ # Utilities
│ ├── utils.ts # General helpers
│ ├── formatters.ts # Date/number formatting
│ └── constants.ts # Shared constants
├── model/ # Shared types
│ └── types.ts
└── ui/ # Base components
├── button.tsx
├── input.tsx
├── dialog.tsx
├── badge.tsx
├── card.tsx
├── table.tsx
├── select.tsx
├── checkbox.tsx
├── avatar.tsx
├── tabs.tsx
├── tooltip.tsx
└── data-table/
├── DataTable.tsx
└── columns.tsx
Layer Responsibilities
App Layer (src/app/)
Purpose : Application-wide configuration and initialization
Contains :
Global providers (Auth, Query, Theme)
Routing configuration
Supabase client setup
Sentry error tracking setup
Global CSS and styles
Application constants
Example : src/app/providers/AuthProvider.tsx
export function AuthProvider ({ children } : { children : ReactNode }) {
const [ session , setSession ] = useState < Session | null >( null )
useEffect (() => {
supabase . auth . getSession (). then (({ data : { session } }) => {
setSession ( session )
})
}, [])
return (
< AuthContext . Provider value = {{ session }} >
{ children }
</ AuthContext . Provider >
)
}
Pages Layer (src/pages/)
Purpose : Complete application routes that compose features and widgets
Contains :
Full page components
Page-specific routing
Feature composition
Layout structure
Naming : *Page.tsx suffix
Example : src/pages/inventario/ui/InventoryPage.tsx
export function InventoryPage () {
return (
< div className = "container" >
< Header />
< InventoryFilters /> { /* from features */ }
< InventoryList /> { /* from features */ }
< Pagination /> { /* from widgets */ }
</ div >
)
}
Purpose : Large composite components used across multiple pages
Contains :
Navigation components
Notification center
Pagination controls
Complex reusable UI blocks
Example : src/widgets/nav/ui/Sidebar.tsx
export function Sidebar () {
const { user } = useUser () // from entities/user
const { permissions } = useUserStore ()
return (
< aside >
< UserBadge user = { user } /> { /* from entities/user */ }
< nav >
{ menuItems . map ( item => (
permissions [ item . key ] && < NavItem key ={ item . key } { ... item } />
))}
</ nav >
</ aside >
)
}
Features Layer (src/features/)
Purpose : Complete user-facing features and use cases
Contains :
Feature-specific UI components
Business logic for the feature
API calls specific to the feature
Form schemas and validation
Feature state management
Structure :
features/feature-name/
├── api/ # Feature-specific API calls
├── lib/ # Feature logic and helpers
├── model/ # Types, schemas, state
├── ui/ # Feature UI components
└── index.ts # Public API
Example : src/features/spares-request-workshop/
// model/cartStore.ts
export const useCartStore = create < CartState >(( set ) => ({
items: [],
addItem : ( item ) => set (( state ) => ({ items: [ ... state . items , item ] })),
removeItem : ( id ) => set (( state ) => ({
items: state . items . filter ( i => i . id !== id )
})),
}))
// ui/RequestCart.tsx
export function RequestCart () {
const { items , removeItem } = useCartStore ()
const { createRequest } = useRequestsApi () // from entities
return (
< Card >
{ items . map ( item => (
< CartItem key = {item. id } item = { item } onRemove = { removeItem } />
))}
< CreateRequestButton items = { items } onCreate = { createRequest } />
</ Card >
)
}
Entities Layer (src/entities/)
Purpose : Business entities with domain logic and data access
Contains :
Entity type definitions
API functions for entity CRUD
Custom hooks for entity data
Entity-specific UI components
Domain helpers
Structure :
entities/entity-name/
├── api/ # Entity API functions
├── lib/ # Entity helpers
├── model/ # Types, hooks, stores
├── ui/ # Entity UI components
└── index.ts # Public API
Example : src/entities/inventario/
// model/types.ts
export interface InventoryItem {
id_inventario : string
id_localizacion : string
id_repuesto : string
stock_actual : number
posicion : string
cantidad_minima : number
estado_stock : string
}
// api/inventoryApi.ts
export async function getInventoryItems ( params : InventoryParams ) {
const { data , error } = await supabase
. from ( 'inventario' )
. select ( '*' )
. eq ( 'id_localizacion' , params . locationId )
if ( error ) throw error
return data
}
// model/hooks/useInventory.ts
export function useInventory ( locationId : string ) {
return useQuery ({
queryKey: [ 'inventory' , locationId ],
queryFn : () => getInventoryItems ({ locationId }),
})
}
Shared Layer (src/shared/)
Purpose : Reusable infrastructure code with no business logic
Contains :
UI primitives (buttons, inputs, dialogs)
Utility functions
Common types
Formatters and validators
API client setup
Example : src/shared/ui/button.tsx
export function Button ({ children , variant = 'default' , ... props } : ButtonProps ) {
return (
< button
className = { cn (
'px-4 py-2 rounded-md' ,
variant === 'primary' && 'bg-blue-600 text-white' ,
variant === 'secondary' && 'bg-gray-200 text-gray-900'
)}
{ ... props }
>
{ children }
</ button >
)
}
Feature Internal Structure
Each feature follows a consistent internal structure:
/api - API Integration
API calls specific to this feature.
// features/count-spares/api/countApi.ts
export async function submitCount ( countData : CountSubmission ) {
const { data , error } = await supabase
. from ( 'registro_conteo' )
. insert ( countData )
if ( error ) throw error
return data
}
/lib - Business Logic
Feature-specific helpers and utilities.
// features/count-spares/lib/countHelpers.ts
export function calculateDifference (
systemQty : number ,
countedQty : number ,
pqQty : number
) : number {
return ( systemQty + pqQty ) - countedQty
}
/model - Data Models
Types, schemas, and state management.
// features/count-spares/model/countSchema.ts
import { z } from 'zod'
export const countItemSchema = z . object ({
id_repuesto: z . string (). uuid (),
cantidad_sistema: z . number (). int (). min ( 0 ),
cantidad_csa: z . number (). int (). min ( 0 ),
cantidad_pq: z . number (). int (). min ( 0 ),
})
export type CountItem = z . infer < typeof countItemSchema >
/ui - User Interface
React components for the feature.
// features/count-spares/ui/CountForm.tsx
export function CountForm ({ locationId } : CountFormProps ) {
const form = useForm < CountItem >({
resolver: zodResolver ( countItemSchema )
})
const { mutate : submitCount } = useMutation ({
mutationFn: submitCount ,
})
return (
< Form { ... form } >
< FormField name = "cantidad_sistema" />
< FormField name = "cantidad_csa" />
< FormField name = "cantidad_pq" />
< Button type = "submit" > Submit Count </ Button >
</ Form >
)
}
Data Flow Through Layers
Example: Creating a Warranty
Best Practices
1. Keep Layers Pure
Good : Feature imports from entities// features/spares-create/ui/CreateSpareForm.tsx
import { useRepuestos } from '@/entities/repuestos'
import { Button } from '@/shared/ui/button'
Bad : Entity imports from features// entities/repuestos/api/repuestosApi.ts
import { createSpareSchema } from '@/features/spares-create/model' // ❌
2. Use Public APIs
Every segment exports through index.ts:
// features/spares-create/index.ts
export { CreateSpareForm } from './ui/CreateSpareForm'
export { createSpareSchema } from './model/schemas'
export type { CreateSpareData } from './model/types'
Other modules import from the public API:
// pages/spares/ui/SparesPage.tsx
import { CreateSpareForm } from '@/features/spares-create'
Keep types, components, and logic for a feature together:
features/spares-request-workshop/
├── model/
│ ├── types.ts # Request types
│ ├── cartStore.ts # Cart state
│ └── schemas.ts # Validation schemas
├── ui/
│ ├── RequestCart.tsx # Uses cartStore
│ └── CartItem.tsx # Uses types
└── api/
└── requestApi.ts # Uses types
4. Single Responsibility
Each feature should handle one user scenario:
spares-create - Creating spare parts
spares-upload - Bulk uploading spare parts
spares-request-workshop - Requesting spare parts
Don’t mix multiple scenarios in one feature.
Migration Guide
If you need to refactor existing code to FSD:
Step 1: Identify the Layer
Is it:
A complete page? → pages
A reusable composite? → widgets
A user feature? → features
A business entity? → entities
Infrastructure code? → shared
Step 2: Create the Structure
mkdir -p src/features/my-feature/{ui,model,api,lib}
touch src/features/my-feature/index.ts
Step 3: Move Files
Move components to ui/, types to model/, etc.
Step 4: Update Imports
Change absolute imports to use the FSD structure:
// Before
import { MyComponent } from '../../components/MyComponent'
// After
import { MyComponent } from '@/features/my-feature'
Step 5: Export Public API
// src/features/my-feature/index.ts
export { MyComponent } from './ui/MyComponent'
export type { MyData } from './model/types'
Benefits of FSD in Trazea
1. Scalability
Easy to add new features without affecting existing code
Clear boundaries prevent feature entanglement
New team members understand structure immediately
2. Maintainability
Related code is colocated
Dependency direction is enforced
Refactoring is safer with clear boundaries
3. Testability
Each layer can be tested independently
Mock dependencies from lower layers
Unit test features in isolation
4. Collaboration
Multiple developers can work on different features
Merge conflicts reduced
Code reviews are more focused
Architecture Overview High-level system architecture
Tech Stack Technologies and tools used
Database Model Database schema and relationships
Feature Development How to create new features