Feature-Sliced Design
Date: 2025-22-09
Status: accepted
Current Architecture Analysis
The Vega web application is built with Remix (React Router v7) and follows a feature-based routing structure with the following characteristics:
Current Structure
services/remix/
├── app/
│ ├── components/ # Shared components
│ │ ├── ui/ # Design system components (shadcn/ui based)
│ │ ├── chat/ # Chat-related components
│ │ ├── forms/ # Form components
│ │ └── shared/ # Shared business components
│ ├── routes/ # Route-based organization
│ │ ├── alerts+/ # Alerts feature
│ │ ├── dashboards+/ # Dashboards feature
│ │ ├── detections+/ # Detections feature
│ │ ├── assessment+/ # Data assessment feature
│ │ ├── intel+/ # Intelligence feature
│ │ ├── notebooks+/ # Notebooks feature
│ │ ├── query+/ # Query feature
│ │ ├── settings+/ # Settings feature
│ │ └── _auth+/ # Authentication
│ ├── queries/ # GraphQL queries
│ ├── utils/ # Utility functions
│ └── transformations/ # Data transformations
Key Technologies
- Framework: Remix (React Router v7) with Vite
- UI Library: shadcn/ui components + Radix UI primitives
- State Management: Zustand + React Query
- Styling: Tailwind CSS
- Data Fetching: GraphQL with custom hooks
- Testing: Vitest + Playwright
Feature-Sliced Design (FSD) Migration
What is Feature-Sliced Design?
Feature-Sliced Design is a methodology for frontend project architecture that organizes code by features and slices, providing clear rules for structuring applications. It emphasizes:
- Slices: Business logic units (features, entities, widgets)
- Layers: Technical layers (app, pages, widgets, features, entities, shared)
- Segments: Code organization within slices (ui, model, lib, api)
FSD Layer Structure
src/
├── app/ # Application layer
│ ├── providers/ # Global providers
│ ├── router/ # Routing configuration
│ └── styles/ # Global styles
├── pages/ # Pages layer
│ ├── dashboard/ # Dashboard page
│ ├── detections/ # Detections page
│ └── settings/ # Settings page
├── widgets/ # Widgets layer
│ ├── navigation/ # Navigation widget
│ ├── dashboard-grid/ # Dashboard grid widget
│ └── detection-list/ # Detection list widget
├── features/ # Features layer
│ ├── auth/ # Authentication feature
│ ├── create-detection/ # Create detection feature
│ ├── dashboard-edit/ # Dashboard editing feature
│ └── query-execution/ # Query execution feature
├── entities/ # Entities layer
│ ├── user/ # User entity
│ ├── detection/ # Detection entity
│ ├── dashboard/ # Dashboard entity
│ └── connector/ # Connector entity
└── shared/ # Shared layer
├── ui/ # UI components
├── lib/ # Libraries and utilities
├── api/ # API layer
└── config/ # Configuration
Migration Strategy
Phase 1: Setup FSD Infrastructure
Step 1: Create Base FSD Structure
// src/shared/lib/fsd/index.ts
export { createSlice } from './create-slice';
export { createFeature } from './create-feature';
export { createEntity } from './create-entity';
export { createWidget } from './create-widget';
// src/shared/lib/fsd/create-slice.ts
export interface SliceConfig {
name: string;
layer: 'shared' | 'entities' | 'features' | 'widgets' | 'pages' | 'app';
segments: string[];
}
export function createSlice(config: SliceConfig) {
return {
...config,
path: `src/${config.layer}/${config.name}`,
};
}Step 2: Create Segment Templates
// src/shared/lib/fsd/segments/ui.tsx
export interface UISegmentProps {
children: React.ReactNode;
className?: string;
}
// src/shared/lib/fsd/segments/model.ts
export interface ModelSegment {
store: any;
hooks: Record<string, Function>;
selectors: Record<string, Function>;
}
// src/shared/lib/fsd/segments/api.ts
export interface APISegment {
endpoints: Record<string, Function>;
types: Record<string, any>;
}Phase 2: Extract Shared Layer
Step 1: Move UI Components
// src/shared/ui/button/index.ts
export { Button } from './ui/Button';
export type { ButtonProps } from './model/types';
// src/shared/ui/button/ui/Button.tsx
import { cn } from '@/shared/lib/utils';
import type { ButtonProps } from '../model/types';
export function Button({ className, variant, size, ...props }: ButtonProps) {
return (
<button
className={cn(
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors',
className
)}
{...props}
/>
);
}
// src/shared/ui/button/model/types.ts
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
size?: 'default' | 'sm' | 'lg' | 'icon';
}Step 2: Create Shared Libraries
// src/shared/lib/utils/index.ts
export { cn } from './cn';
export { formatDate } from './date';
export { debounce } from './debounce';
// src/shared/lib/cn.ts
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}Step 3: Setup API Layer
// src/shared/api/base.ts
import axios from 'axios';
export const api = axios.create({
baseURL: process.env.API_BASE_URL,
headers: {
'Content-Type': 'application/json',
},
});
// src/shared/api/graphql.ts
import { GraphQLClient } from 'graphql-request';
export const graphqlClient = new GraphQLClient(process.env.GRAPHQL_ENDPOINT);Phase 3: Extract Entities Layer
Step 1: User Entity
// src/entities/user/index.ts
export { UserCard } from './ui/UserCard';
export { useUser } from './model/useUser';
export { userStore } from './model/userStore';
export type { User } from './model/types';
// src/entities/user/model/types.ts
export interface User {
id: string;
email: string;
name: string;
role: string;
permissions: string[];
}
// src/entities/user/model/userStore.ts
import { create } from 'zustand';
import type { User } from './types';
interface UserState {
user: User | null;
isLoading: boolean;
setUser: (user: User | null) => void;
setLoading: (loading: boolean) => void;
}
export const userStore = create<UserState>((set) => ({
user: null,
isLoading: false,
setUser: (user) => set({ user }),
setLoading: (isLoading) => set({ isLoading }),
}));
// src/entities/user/model/useUser.ts
import { useUserStore } from './userStore';
import { userApi } from '../api/userApi';
export function useUser() {
const { user, isLoading, setUser, setLoading } = useUserStore();
const fetchUser = async () => {
setLoading(true);
try {
const userData = await userApi.getCurrentUser();
setUser(userData);
} finally {
setLoading(false);
}
};
return { user, isLoading, fetchUser };
}
// src/entities/user/api/userApi.ts
import { graphqlClient } from '@/shared/api/graphql';
import { GET_CURRENT_USER } from './queries';
import type { User } from '../model/types';
export const userApi = {
getCurrentUser: async (): Promise<User> => {
const data = await graphqlClient.request(GET_CURRENT_USER);
return data.me;
},
};Step 2: Detection Entity
// src/entities/detection/index.ts
export { DetectionCard } from './ui/DetectionCard';
export { DetectionList } from './ui/DetectionList';
export { useDetection } from './model/useDetection';
export { detectionStore } from './model/detectionStore';
export type { Detection } from './model/types';
// src/entities/detection/model/types.ts
export interface Detection {
id: string;
name: string;
description: string;
query: string;
severity: 'low' | 'medium' | 'high' | 'critical';
status: 'active' | 'inactive' | 'draft';
createdAt: string;
updatedAt: string;
mitreTechniques: string[];
}
// src/entities/detection/model/detectionStore.ts
import { create } from 'zustand';
import type { Detection } from './types';
interface DetectionState {
detections: Detection[];
currentDetection: Detection | null;
isLoading: boolean;
setDetections: (detections: Detection[]) => void;
setCurrentDetection: (detection: Detection | null) => void;
setLoading: (loading: boolean) => void;
}
export const detectionStore = create<DetectionState>((set) => ({
detections: [],
currentDetection: null,
isLoading: false,
setDetections: (detections) => set({ detections }),
setCurrentDetection: (currentDetection) => set({ currentDetection }),
setLoading: (isLoading) => set({ isLoading }),
}));Phase 4: Extract Features Layer
Step 1: Authentication Feature
// src/features/auth/index.ts
export { LoginForm } from './ui/LoginForm';
export { LogoutButton } from './ui/LogoutButton';
export { useAuth } from './model/useAuth';
export { authStore } from './model/authStore';
// src/features/auth/model/useAuth.ts
import { useAuthStore } from './authStore';
import { authApi } from '../api/authApi';
import { userStore } from '@/entities/user';
export function useAuth() {
const { isAuthenticated, token, setAuth, clearAuth } = useAuthStore();
const { setUser } = userStore();
const login = async (credentials: LoginCredentials) => {
try {
const { token, user } = await authApi.login(credentials);
setAuth(token);
setUser(user);
} catch (error) {
throw error;
}
};
const logout = async () => {
await authApi.logout();
clearAuth();
setUser(null);
};
return { isAuthenticated, token, login, logout };
}
// src/features/auth/ui/LoginForm.tsx
import { useForm } from 'react-hook-form';
import { Button } from '@/shared/ui/button';
import { Input } from '@/shared/ui/input';
import { useAuth } from '../model/useAuth';
export function LoginForm() {
const { login } = useAuth();
const { register, handleSubmit } = useForm();
const onSubmit = async (data: any) => {
try {
await login(data);
} catch (error) {
// Handle error
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Input {...register('email')} placeholder="Email" />
<Input {...register('password')} type="password" placeholder="Password" />
<Button type="submit">Login</Button>
</form>
);
}Step 2: Create Detection Feature
// src/features/create-detection/index.ts
export { CreateDetectionForm } from './ui/CreateDetectionForm';
export { DetectionWizard } from './ui/DetectionWizard';
export { useCreateDetection } from './model/useCreateDetection';
// src/features/create-detection/model/useCreateDetection.ts
import { detectionStore } from '@/entities/detection';
import { detectionApi } from '@/entities/detection/api';
export function useCreateDetection() {
const { setDetections } = detectionStore();
const createDetection = async (data: CreateDetectionData) => {
try {
const newDetection = await detectionApi.create(data);
setDetections(prev => [...prev, newDetection]);
return newDetection;
} catch (error) {
throw error;
}
};
return { createDetection };
}Phase 5: Extract Widgets Layer
Step 1: Navigation Widget
// src/widgets/navigation/index.ts
export { Navigation } from './ui/Navigation';
export { NavigationItem } from './ui/NavigationItem';
export { useNavigation } from './model/useNavigation';
// src/widgets/navigation/ui/Navigation.tsx
import { NavigationItem } from './NavigationItem';
import { useNavigation } from '../model/useNavigation';
export function Navigation() {
const { navigationItems } = useNavigation();
return (
<nav className="flex flex-col space-y-2">
{navigationItems.map((item) => (
<NavigationItem key={item.path} item={item} />
))}
</nav>
);
}
// src/widgets/navigation/model/useNavigation.ts
import { useMemo } from 'react';
import { useUser } from '@/entities/user';
import { useFeatureFlags } from '@/shared/lib/feature-flags';
export function useNavigation() {
const { user } = useUser();
const { featureFlags } = useFeatureFlags();
const navigationItems = useMemo(() => {
return [
{ path: '/dashboard', label: 'Dashboard', visible: true },
{ path: '/detections', label: 'Detections', visible: true },
{ path: '/query', label: 'Query', visible: featureFlags.FEDERATED_SEARCH },
{ path: '/settings', label: 'Settings', visible: user?.role === 'admin' },
].filter(item => item.visible);
}, [user, featureFlags]);
return { navigationItems };
}Step 2: Dashboard Grid Widget
// src/widgets/dashboard-grid/index.ts
export { DashboardGrid } from './ui/DashboardGrid';
export { DashboardWidget } from './ui/DashboardWidget';
export { useDashboardGrid } from './model/useDashboardGrid';
// src/widgets/dashboard-grid/ui/DashboardGrid.tsx
import { DashboardWidget } from './DashboardWidget';
import { useDashboardGrid } from '../model/useDashboardGrid';
export function DashboardGrid() {
const { widgets, layout, updateLayout } = useDashboardGrid();
return (
<div className="grid grid-cols-12 gap-4">
{widgets.map((widget) => (
<DashboardWidget key={widget.id} widget={widget} />
))}
</div>
);
}Phase 6: Extract Pages Layer
Step 1: Dashboard Page
// src/pages/dashboard/index.ts
export { DashboardPage } from './ui/DashboardPage';
// src/pages/dashboard/ui/DashboardPage.tsx
import { Page } from '@/shared/ui/page';
import { DashboardGrid } from '@/widgets/dashboard-grid';
import { Navigation } from '@/widgets/navigation';
export function DashboardPage() {
return (
<Page>
<div className="flex h-full">
<Navigation />
<main className="flex-1">
<DashboardGrid />
</main>
</div>
</Page>
);
}Step 2: Detections Page
// src/pages/detections/index.ts
export { DetectionsPage } from './ui/DetectionsPage';
// src/pages/detections/ui/DetectionsPage.tsx
import { Page } from '@/shared/ui/page';
import { DetectionList } from '@/entities/detection';
import { CreateDetectionForm } from '@/features/create-detection';
import { Navigation } from '@/widgets/navigation';
export function DetectionsPage() {
return (
<Page>
<div className="flex h-full">
<Navigation />
<main className="flex-1">
<CreateDetectionForm />
<DetectionList />
</main>
</div>
</Page>
);
}Phase 7: Update App Layer
Step 1: App Providers
// src/app/providers/index.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { ThemeProvider } from './ThemeProvider';
import { AuthProvider } from './AuthProvider';
const queryClient = new QueryClient();
export function AppProviders({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
<ThemeProvider>
<AuthProvider>
{children}
<ReactQueryDevtools />
</AuthProvider>
</ThemeProvider>
</QueryClientProvider>
);
}Step 2: App Router
// src/app/router/index.tsx
import { createBrowserRouter } from 'react-router-dom';
import { DashboardPage } from '@/pages/dashboard';
import { DetectionsPage } from '@/pages/detections';
import { SettingsPage } from '@/pages/settings';
export const router = createBrowserRouter([
{
path: '/',
element: <DashboardPage />,
},
{
path: '/detections',
element: <DetectionsPage />,
},
{
path: '/settings',
element: <SettingsPage />,
},
]);FSD Import Rules
Public API Pattern
Each slice should have a clear public API:
// src/entities/user/index.ts - Public API
export { UserCard } from './ui/UserCard';
export { useUser } from './model/useUser';
export type { User } from './model/types';
// Usage in other slices
import { UserCard, useUser, type User } from '@/entities/user';Import Restrictions
// ✅ Allowed imports
import { Button } from '@/shared/ui/button'; // From shared
import { User } from '@/entities/user'; // From entities
import { useAuth } from '@/features/auth'; // From features
import { Navigation } from '@/widgets/navigation'; // From widgets
import { DashboardPage } from '@/pages/dashboard'; // From pages
// ❌ Forbidden imports
import { UserCard } from '@/entities/user/ui/UserCard'; // Direct import
import { authStore } from '@/features/auth/model/store'; // Direct import
Benefits of FSD
- Clear Architecture: Well-defined layers and slices
- Scalability: Easy to add new features and entities
- Maintainability: Clear separation of concerns
- Team Collaboration: Clear boundaries for different teams
- Reusability: Shared components and entities
- Testing: Easy to test individual slices
Migration Timeline
Week 1-2: Setup FSD infrastructure and extract shared layer Week 3-4: Extract entities layer (user, detection, dashboard) Week 5-6: Extract features layer (auth, create-detection, query-execution) Week 7-8: Extract widgets layer (navigation, dashboard-grid, detection-list) Week 9-10: Extract pages layer and update app layer Week 11-12: Testing, optimization, and documentation
Implementation Considerations
- Import Rules: Strictly follow FSD import rules
- Public APIs: Each slice should have a clear public API
- Layer Dependencies: Only allow imports from lower layers
- Testing Strategy: Test each slice independently
- Documentation: Document each slice’s purpose and API
Potential Challenges
- Learning Curve: Team needs to understand FSD principles
- Initial Overhead: Setting up the structure requires significant work
- Import Restrictions: Strict import rules might feel limiting
- Refactoring: Existing code needs significant refactoring
- Tooling: Need tools to enforce FSD rules
Success Metrics
- Code Organization: Percentage of code properly organized by FSD layers
- Import Violations: Number of FSD import rule violations
- Feature Development: Time to implement new features
- Code Reusability: Percentage of shared code across features
- Team Productivity: Features delivered per team per sprint