| title | Frontend Architecture |
|---|---|
| description | Comprehensive guide to DeployStack frontend application architecture, design patterns, and development principles |
| sidebarTitle | Architecture |
This document defines the architectural principles, patterns, and conventions that govern the DeployStack frontend application. All developers must understand and follow these guidelines to maintain consistency and quality across the codebase.
The DeployStack frontend follows a feature-based modular architecture with clear separation of concerns. The application is built on Vue 3's Composition API, emphasizing type safety, reusability, and maintainability.
- Feature-First Organization: Code is organized by feature domains rather than technical layers
- Type Safety First: TypeScript is mandatory for all new code
- Composition Over Inheritance: Use composables and the Composition API exclusively
- Direct API Communication: No abstraction layers over fetch() calls
- Component-Driven Development: Build from small, reusable components up to complex features
Views represent page-level components that map directly to routes. They orchestrate the overall page functionality and data flow.
- Route Mapping: Each view corresponds to a specific route in the application
- Nested Structure: Mirror the URL structure in the directory hierarchy
- Feature Grouping: Group related views in subdirectories
views/
├── admin/ # Admin-only views
│ ├── mcp-categories/ # Category management feature
│ │ └── index.vue # Main listing page
│ └── mcp-server-catalog/ # Catalog management feature
│ ├── index.vue # Listing
│ ├── add.vue # Creation
│ └── edit/[id].vue # Dynamic editing
├── teams/ # Team management feature
│ ├── index.vue # Teams listing
│ └── manage/[id].vue # Team management page
└── Dashboard.vue # Top-level dashboard
- Route handling: Process route parameters and query strings
- Data orchestration: Coordinate multiple service calls
- Layout selection: Choose appropriate layout wrapper
- Permission checks: Verify user access rights
- Error boundaries: Handle page-level errors
- Contain complex business logic (use services)
- Implement reusable UI patterns (use components)
- Directly manage global state (use stores)
- Include detailed form validation (use composables)
Components are reusable UI building blocks that encapsulate specific functionality and presentation logic.
MANDATORY: All feature-specific components must follow a hierarchical directory structure that mirrors the view organization. This creates clear ownership boundaries and improves code discoverability.
components/
├── ui/ # Design system components (shadcn-vue)
│ ├── button/
│ ├── card/
│ └── input/
├── [feature]/ # Feature-specific components
│ ├── [sub-feature]/ # Nested feature organization
│ │ ├── ComponentName.vue # Individual components
│ │ ├── AnotherComponent.vue
│ │ └── index.ts # Barrel exports (mandatory)
│ └── index.ts
├── AppSidebar.vue # Top-level shared components
└── NavbarLayout.vue
components/
├── mcp-server/
│ ├── installation/
│ │ ├── InstallationInfo.vue
│ │ ├── InstallationTabs.vue
│ │ ├── McpToolsTab.vue
│ │ ├── TeamConfiguration.vue
│ │ ├── UserConfiguration.vue
│ │ ├── DangerZone.vue
│ │ └── index.ts # Export all installation components
│ └── catalog/
│ ├── ServerCard.vue
│ ├── ServerFilters.vue
│ └── index.ts
Every feature component directory must include an index.ts file that exports all components. This creates a clean import API and makes refactoring easier.
// components/mcp-server/installation/index.ts
export { default as InstallationInfo } from './InstallationInfo.vue'
export { default as InstallationTabs } from './InstallationTabs.vue'
export { default as McpToolsTab } from './McpToolsTab.vue'
export { default as TeamConfiguration } from './TeamConfiguration.vue'
export { default as UserConfiguration } from './UserConfiguration.vue'
export { default as DangerZone } from './DangerZone.vue'<script setup lang="ts">
// Clean, single-line imports for all related components
import {
InstallationInfo,
InstallationTabs,
DangerZone
} from '@/components/mcp-server/installation'
</script>-
UI Components (
/ui): Generic, design-system components - read the UI Design System- Examples: Buttons, Modals, Inputs
- Stateless and focused on presentation
- Use shadcn-vue components where applicable
- Follow shadcn-vue patterns
- No business logic
- Highly reusable across features
-
Feature Components (
/components/[feature]/[sub-feature]): Domain-specific components- Must follow hierarchical organization matching views
- Contain feature-specific logic
- Reusable within their domain
- Must include barrel exports via
index.ts - May compose UI components
-
Shared Components (
/components): Cross-feature components- Used across multiple features
- Examples:
AppSidebar,NavbarLayout - Only place components here if they truly span features
- Mirror View Structure: Component organization should parallel view hierarchy
- Feature Isolation: Keep feature components within their feature directory
- Mandatory Barrel Exports: Every feature directory must export via
index.ts - No Deep Nesting: Maximum 3 levels deep (feature/sub-feature/component)
- Colocation: Related components stay together
Create a new feature component directory when:
- You have 3+ components related to the same feature
- The components are reused across multiple views within the feature
- The feature has clear boundaries and ownership
- The components share common types or logic
- Single Responsibility: Each component has one clear purpose
- Props Down, Events Up: Maintain unidirectional data flow
- Composition Pattern: Break complex components into smaller parts
- Self-Contained: Components should work in isolation
You may deviate from the structure for:
- One-off components: Components used in a single view can stay in the view file
- UI library components: shadcn-vue components in
/uifollow their own structure - Extremely small features: Features with only 1-2 simple components
Services handle all external communication and business logic processing. They act as the bridge between the frontend and backend APIs.
- Static Class Pattern: All service methods must be static
- Direct Fetch Usage: Use native fetch() API exclusively
- Type-Safe Contracts: Define interfaces for all API requests/responses
- Error Transformation: Convert API errors to user-friendly messages
- API endpoint communication
- Request/response transformation
- Error handling and normalization
- Cache management (when applicable)
- Business logic that spans multiple components
Composables are reusable logic units that leverage Vue's Composition API to share stateful logic across components.
MANDATORY: Feature-specific composables must follow a hierarchical directory structure that mirrors the component and view organization. This creates consistency across the codebase and makes related logic easy to find.
composables/
├── [feature]/ # Feature-specific composables
│ ├── [sub-feature]/ # Nested feature organization
│ │ ├── useFeatureLogic.ts # Individual composables
│ │ ├── useAnotherLogic.ts
│ │ └── index.ts # Barrel exports (mandatory)
│ └── index.ts
├── useAuth.ts # Top-level shared composables
├── useEventBus.ts
└── useBreadcrumbs.ts
composables/
├── mcp-server/
│ ├── installation/
│ │ ├── useInstallationCache.ts
│ │ ├── useInstallationForm.ts
│ │ └── index.ts # Export all installation composables
│ └── catalog/
│ ├── useCatalogFilters.ts
│ ├── useCatalogSearch.ts
│ └── index.ts
├── useAuth.ts
└── useEventBus.ts
Every feature composable directory must include an index.ts file that exports all composables. This mirrors the component structure and provides a clean import API.
// composables/mcp-server/installation/index.ts
export { useMcpInstallationCache } from './useInstallationCache'
export { useInstallationForm } from './useInstallationForm'<script setup lang="ts">
// Clean imports matching the component import pattern
import { useMcpInstallationCache } from '@/composables/mcp-server/installation'
import { InstallationTabs } from '@/components/mcp-server/installation'
const {
installation,
isLoading,
loadInstallation
} = useMcpInstallationCache()
</script>- Mirror Component Structure: Composable organization should parallel component hierarchy
- Feature Isolation: Keep feature composables within their feature directory
- Mandatory Barrel Exports: Every feature directory must export via
index.ts - No Deep Nesting: Maximum 3 levels deep (feature/sub-feature/composable)
- Colocation: Keep related composables together
Create a new feature composable directory when:
- You have 2+ composables related to the same feature
- The composables are reused across multiple components within the feature
- The feature has clear boundaries and ownership
- The composables share common types or state
- Naming Convention: Always prefix with
use(e.g.,useAuth,useEventBus,useInstallationCache) - Single Purpose: Each composable solves one specific problem
- Return Interface: Clearly define what's returned (state, methods, computed)
- Lifecycle Awareness: Handle setup/cleanup in lifecycle hooks
- Data Fetching:
useAsyncData,usePagination - Form Handling:
useForm,useValidation - UI State:
useModal,useToast - Feature Logic:
useTeamManagement,useCredentials,useInstallationCache
You may deviate from the structure for:
- Global utilities: Composables used across all features (e.g.,
useAuth,useEventBus) - Single composable features: Features with only one composable can stay at the root
- Third-party integrations: External library wrappers may have their own structure
Stores manage global application state using Pinia, Vue's official state management solution.
- Feature-Based Stores: One store per major feature domain
- Composition API Style: Use setup stores, not options API
- Readonly State: Export readonly refs to prevent external mutations
- Action Pattern: All state changes through defined actions
- User session and authentication state
- Cross-component shared data
- Cache for expensive operations
- Application-wide settings
- Component-specific state
- Temporary UI state
- Form data (use local state)
IMPORTANT: The frontend uses a service layer pattern with direct fetch() calls for API communication. This is the established pattern and must be followed for consistency.
All API services must use direct fetch() calls instead of API client libraries. This ensures consistency across the codebase and simplifies maintenance.
// services/mcpServerService.ts
export class McpServerService {
private static baseUrl = getEnv('VITE_API_URL')
static async getAllServers(): Promise<McpServer[]> {
const response = await fetch(`${this.baseUrl}/api/mcp-servers`)
if (!response.ok) {
throw new Error('Failed to fetch MCP servers')
}
return response.json()
}
static async deployServer(serverId: string, config: DeployConfig): Promise<Deployment> {
const response = await fetch(`${this.baseUrl}/api/mcp-servers/${serverId}/deploy`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(config),
})
if (!response.ok) {
throw new Error('Failed to deploy MCP server')
}
return response.json()
}
}Do not use API client libraries like Axios, or custom API client wrappers:
// DON'T DO THIS
import axios from 'axios'
import { apiClient } from '@/utils/apiClient'
// Avoid these patterns
const response = await axios.get('/api/servers')
const data = await apiClient.get('/api/servers')- Use Static Classes: All service methods should be static
- Direct Fetch: Always use native
fetch()API - Error Handling: Throw meaningful errors for failed requests
- Type Safety: Define proper TypeScript interfaces for requests/responses
- Consistent Naming: Use descriptive method names (e.g.,
getAllServers,createCategory) - Base URL: Always use environment variables for API endpoints
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { McpServerService } from '@/services/mcpServerService'
import type { McpServer } from '@/types/mcp'
const servers = ref<McpServer[]>([])
const isLoading = ref(false)
const error = ref<string | null>(null)
async function fetchServers() {
isLoading.value = true
error.value = null
try {
servers.value = await McpServerService.getAllServers()
} catch (err) {
error.value = err instanceof Error ? err.message : 'Unknown error'
console.error('Failed to fetch servers:', err)
} finally {
isLoading.value = false
}
}
onMounted(() => {
fetchServers()
})
</script>User Interaction → View → Service → API
↓
Component ← Store ← Response- User triggers action in a View or Component
- View/Component calls Service method
- Service communicates with API using fetch()
- Response updates Store (if global state)
- Components react to store changes
The application uses an event bus for cross-component communication without direct coupling. This enables real-time updates across unrelated components, cache invalidation signals, global notifications, and feature-to-feature communication.
For complete details on the event bus system, including usage patterns, naming conventions, and implementation examples, see the Event Bus Documentation.
The application includes a storage system built into the event bus for managing persistent state across route changes and browser sessions. This system provides type-safe localStorage access with automatic event emission for reactive updates.
For complete details on the storage system, including usage patterns, naming conventions, and best practices, see the Frontend Storage System.
Always prefer Vue Single File Components (SFC) with <script setup> and <template> sections over TypeScript files with render functions.
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { Button } from '@/components/ui/button'
import { Settings } from 'lucide-vue-next'
// Props with TypeScript
interface Props {
title: string
count?: number
onAction?: (id: string) => void
}
const props = withDefaults(defineProps<Props>(), {
count: 0
})
// Composables
const { t } = useI18n()
// Reactive state
const isVisible = ref(true)
// Computed properties
const displayTitle = computed(() =>
`${props.title} (${props.count})`
)
// Methods
function toggleVisibility() {
isVisible.value = !isVisible.value
}
function handleAction(id: string) {
props.onAction?.(id)
}
</script>
<template>
<div v-if="isVisible" class="component-container">
<h2 class="text-xl font-semibold">{{ displayTitle }}</h2>
<Button
@click="toggleVisibility"
class="mt-2"
variant="outline"
>
<Settings class="h-4 w-4 mr-2" />
{{ t('common.toggle') }}
</Button>
</div>
</template>// Don't create files like this for UI components
import { h } from 'vue'
import type { ColumnDef } from '@tanstack/vue-table'
export function createColumns(): ColumnDef[] {
return [
{
id: 'actions',
cell: ({ row }) => {
return h('div', { class: 'flex justify-end' }, [
h(Button, {
onClick: () => handleAction(row.original.id)
}, () => 'Action')
])
}
}
]
}- Better Developer Experience: Clear separation of logic, template, and styles
- Improved Readability: Template syntax is more intuitive than render functions
- Better Tooling Support: Vue DevTools, syntax highlighting, and IntelliSense work better
- Easier Maintenance: Future developers can understand and modify components more easily
- Vue 3 Best Practices: Aligns with official Vue 3 recommendations
For table implementations, use the shadcn-vue Table components as documented in the Table Design System. Never use raw HTML table elements.
- Props for Data Down: Pass data from parent to child
- Events for Actions Up: Emit events from child to parent
- v-model for Two-Way: Use for form inputs and controlled components
- Through Parent: Lift state up to common parent
- Event Bus: For loosely coupled components
- Shared Store: For persistent shared state
- Event Bus: Primary method for feature-to-feature updates
- Shared Services: Common API operations
- Global Store: Application-wide state
- Local State First: Keep form data in component state
- Validation Composables: Reuse validation logic
- Service Layer Submission: Process through services
- Error Display Pattern: Consistent error messaging
- Use VeeValidate with Zod schemas for complex forms
- Implement field-level validation feedback
- Show loading states during submission
- Handle API validation errors gracefully
- Feature Modules: Group related routes by feature
- Lazy Loading: Use dynamic imports for route components
- Route Guards: Implement authentication and authorization checks
- Breadcrumb Support: Maintain hierarchical navigation
- Use
[id]notation for dynamic segments - Handle route parameter validation in views
- Implement proper 404 handling for invalid IDs
- View Level: Catch and display page-level errors
- Component Level: Handle component-specific errors
- Global Level: Catch unhandled errors
- Display user-friendly error messages
- Log technical details for debugging
- Provide recovery actions when possible
- Maintain application stability on errors
- Route-Based Splitting: Each route loads its own bundle
- Component Lazy Loading: Heavy components load on demand
- Vendor Chunking: Separate third-party libraries
- Use
shallowReffor large objects - Implement virtual scrolling for long lists
- Debounce expensive operations
- Memoize computed values appropriately
- Never Trust Client: All validation must happen on backend
- Secure Storage: Never store sensitive data (passwords, API keys, tokens) in localStorage. See Frontend Storage System for proper storage patterns
- XSS Prevention: Sanitize user input, use Vue's built-in protections
- CSRF Protection: Include tokens in API requests
- Token-based authentication (JWT)
- Automatic token refresh
- Secure token storage (httpOnly cookies preferred)
- Route protection via navigation guards
- Unit Tests: For services, composables, and utilities
- Component Tests: For isolated component behavior
- Integration Tests: For feature workflows
- E2E Tests: For critical user paths
- Mirror source structure in test directories
- Co-locate test files with source files
- Use descriptive test names
- Follow AAA pattern (Arrange, Act, Assert)
The application supports runtime plugin loading for extensibility.
- Entry Point: Each plugin exports a default configuration
- Extension Points: Plugins hook into defined extension points
- Isolation: Plugins run in isolated contexts
- Version Management: Plugins declare compatible versions
- Plugins cannot modify core functionality
- Use provided APIs and extension points
- Handle errors gracefully
- Document dependencies clearly
- Feature Cohesion: Keep related code together
- Explicit Imports: No magic globals or auto-imports
- Type Definitions: Colocate types with their usage
- Consistent Naming: Follow established patterns
- Components: PascalCase (e.g.,
UserProfile.vue) - Composables: camelCase with 'use' prefix (e.g.,
useAuth.ts) - Services: camelCase with 'Service' suffix (e.g.,
userService.ts) - Types: PascalCase for interfaces/types (e.g.,
UserCredentials) - Views: Match route names (e.g.,
index.vue,[id].vue)
- External dependencies
- Vue and framework imports
- Internal aliases (@/ imports)
- Relative imports
- Type imports
- ❌ Using Options API in new components
- ❌ Mixing paradigms (Options + Composition)
- ❌ Direct DOM manipulation
- ❌ Inline styles for layout
- ❌ Business logic in templates
- ❌ Mutating props directly
- ❌ Excessive global state
- ❌ Circular store dependencies
- ❌ Store logic in components
- ❌ Using Axios or other HTTP libraries
- ❌ Instance-based service classes
- ❌ Mixing UI concerns in services
- ❌ Inconsistent error handling
- ❌ Premature optimization
- ❌ Deep component nesting (>3 levels)
- ❌ Tight coupling between features
- ❌ Ignoring TypeScript errors
- ❌ Copy-paste programming
Static service methods ensure:
- No instance management complexity
- Predictable behavior
- Easy testing and mocking
- Clear API boundaries
Using native fetch() provides:
- No external dependencies
- Consistent API across services
- Full control over request/response
- Smaller bundle size
Feature organization offers:
- Better code locality
- Easier feature removal/addition
- Clear ownership boundaries
- Reduced merge conflicts
When refactoring existing code:
- Incremental Migration: Update feature by feature
- Test Coverage First: Add tests before refactoring
- Preserve Functionality: No behavior changes during refactor
- Document Changes: Update relevant documentation
- Review Thoroughly: Architecture changes need careful review
As the application grows, consider:
- Micro-frontend architecture for team autonomy
- Module federation for dynamic feature loading
- GraphQL adoption for efficient data fetching
- Server-side rendering for performance
- Progressive Web App capabilities
This architecture provides a scalable, maintainable foundation for the DeployStack frontend. Following these patterns ensures consistency, reduces bugs, and improves developer productivity. When in doubt, prioritize clarity and simplicity over clever solutions.
Remember: Architecture is a team effort. Propose improvements, discuss trade-offs, and evolve these patterns as the application grows.