Skip to main content
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.

Architectural Overview

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.

Core Principles

  1. Feature-First Organization: Code is organized by feature domains rather than technical layers
  2. Type Safety First: TypeScript is mandatory for all new code
  3. Composition Over Inheritance: Use composables and the Composition API exclusively
  4. Direct API Communication: No abstraction layers over fetch() calls
  5. Component-Driven Development: Build from small, reusable components up to complex features

Directory Architecture

Views Layer (/views)

Views represent page-level components that map directly to routes. They orchestrate the overall page functionality and data flow.

Organization Rules

  1. Route Mapping: Each view corresponds to a specific route in the application
  2. Nested Structure: Mirror the URL structure in the directory hierarchy
  3. 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

View Responsibilities

  • 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

What Views Should NOT Do

  • 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 Layer (/components)

Components are reusable UI building blocks that encapsulate specific functionality and presentation logic.

Component Organization Structure

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

Real-World Example: MCP Server Components

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

Barrel Export Pattern (Mandatory)

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'

Usage in Views

<script setup lang="ts">
// Clean, single-line imports for all related components
import {
  InstallationInfo,
  InstallationTabs,
  DangerZone
} from '@/components/mcp-server/installation'
</script>

Component Categories

  1. 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
  2. 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
  3. Shared Components (/components): Cross-feature components
    • Used across multiple features
    • Examples: AppSidebar, NavbarLayout
    • Only place components here if they truly span features

Component Organization Rules

  1. Mirror View Structure: Component organization should parallel view hierarchy
  2. Feature Isolation: Keep feature components within their feature directory
  3. Mandatory Barrel Exports: Every feature directory must export via index.ts
  4. No Deep Nesting: Maximum 3 levels deep (feature/sub-feature/component)
  5. Colocation: Related components stay together

When to Create a New Feature Directory

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

Component Design Rules

  1. Single Responsibility: Each component has one clear purpose
  2. Props Down, Events Up: Maintain unidirectional data flow
  3. Composition Pattern: Break complex components into smaller parts
  4. Self-Contained: Components should work in isolation
  5. Empty States: Always use the shadcn-vue Empty component for no-data states. Never create custom empty state markup with manual styling.

Exceptions to the Structure

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 /ui follow their own structure
  • Extremely small features: Features with only 1-2 simple components

Services Layer (/services)

Services handle all external communication and business logic processing. They act as the bridge between the frontend and backend APIs.

Service Architecture Patterns

  1. Static Class Pattern: All service methods must be static
  2. Direct Fetch Usage: Use native fetch() API exclusively
  3. Type-Safe Contracts: Define interfaces for all API requests/responses
  4. Error Transformation: Convert API errors to user-friendly messages

Service Responsibilities

  • API endpoint communication
  • Request/response transformation
  • Error handling and normalization
  • Cache management (when applicable)
  • Business logic that spans multiple components

Composables Layer (/composables)

Composables are reusable logic units that leverage Vue’s Composition API to share stateful logic across components.

Composable Organization Structure

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

Real-World Example: MCP Server Composables

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

Barrel Export Pattern (Mandatory)

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'

Usage in Components

<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>

Composable Organization Rules

  1. Mirror Component Structure: Composable organization should parallel component hierarchy
  2. Feature Isolation: Keep feature composables within their feature directory
  3. Mandatory Barrel Exports: Every feature directory must export via index.ts
  4. No Deep Nesting: Maximum 3 levels deep (feature/sub-feature/composable)
  5. Colocation: Keep related composables together

When to Create a Feature Composable Directory

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

Composable Design Patterns

  1. Naming Convention: Always prefix with use (e.g., useAuth, useEventBus, useInstallationCache)
  2. Single Purpose: Each composable solves one specific problem
  3. Return Interface: Clearly define what’s returned (state, methods, computed)
  4. Lifecycle Awareness: Handle setup/cleanup in lifecycle hooks

Common Composable Patterns

  • Data Fetching: useAsyncData, usePagination
  • Form Handling: useForm, useValidation
  • UI State: useModal, useToast
  • Feature Logic: useTeamManagement, useCredentials, useInstallationCache

Exceptions to the Structure

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 Layer (/stores)

Stores manage global application state using Pinia, Vue’s official state management solution.

Store Guidelines

  1. Feature-Based Stores: One store per major feature domain
  2. Composition API Style: Use setup stores, not options API
  3. Readonly State: Export readonly refs to prevent external mutations
  4. Action Pattern: All state changes through defined actions

When to Use Stores

  • User session and authentication state
  • Cross-component shared data
  • Cache for expensive operations
  • Application-wide settings

When NOT to Use Stores

  • Component-specific state
  • Temporary UI state
  • Form data (use local state)

API Integration Architecture

Service Layer Pattern

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.

✅ Required Pattern - Direct Fetch Calls

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()
  }
}

❌ Avoid - API Client Libraries

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')

Service Layer Guidelines

  1. Use Static Classes: All service methods should be static
  2. Direct Fetch: Always use native fetch() API
  3. Error Handling: Throw meaningful errors for failed requests
  4. Type Safety: Define proper TypeScript interfaces for requests/responses
  5. Consistent Naming: Use descriptive method names (e.g., getAllServers, createCategory)
  6. Base URL: Always use environment variables for API endpoints

Backend API Environment Variables

Always use VITE_DEPLOYSTACK_BACKEND_URL for all backend API calls and SSE connections.
# .env.local
VITE_DEPLOYSTACK_BACKEND_URL=http://localhost:3000
Usage: DeployStack has a single backend API endpoint. Use getEnv('VITE_DEPLOYSTACK_BACKEND_URL') for all API calls, SSE connections, and WebSocket connections to the backend.
import { getEnv } from '@/utils/env'

// Service Layer
export class McpServerService {
  private static baseUrl = getEnv('VITE_DEPLOYSTACK_BACKEND_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()
  }
}

// SSE Connections in Composables
const baseUrl = getEnv('VITE_DEPLOYSTACK_BACKEND_URL')
const url = `${baseUrl}/api/teams/${teamId}/status/stream`
const eventSource = new EventSource(url, { withCredentials: true })

Using Services in Components

<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>

Data Flow Architecture

Unidirectional Data Flow

User Interaction View Service API

                Component Store Response
  1. User triggers action in a View or Component
  2. View/Component calls Service method
  3. Service communicates with API using fetch()
  4. Response updates Store (if global state)
  5. Components react to store changes

Event-Driven Updates

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.

Persistent State Management

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.

Component Implementation Standards

Vue Component Structure

Always prefer Vue Single File Components (SFC) with <script setup> and <template> sections over TypeScript files with render functions.

✅ Preferred Approach - Vue SFC:

<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>

❌ Avoid - TypeScript files with render functions:

// 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')
        ])
      }
    }
  ]
}

Why Vue SFC is Preferred

  1. Better Developer Experience: Clear separation of logic, template, and styles
  2. Improved Readability: Template syntax is more intuitive than render functions
  3. Better Tooling Support: Vue DevTools, syntax highlighting, and IntelliSense work better
  4. Easier Maintenance: Future developers can understand and modify components more easily
  5. Vue 3 Best Practices: Aligns with official Vue 3 recommendations

Table Components

For table implementations, use the shadcn-vue Table components as documented in the Table Design System. Never use raw HTML table elements.

Component Communication Patterns

Parent-Child Communication

  1. Props for Data Down: Pass data from parent to child
  2. Events for Actions Up: Emit events from child to parent
  3. v-model for Two-Way: Use for form inputs and controlled components

Sibling Communication

  1. Through Parent: Lift state up to common parent
  2. Event Bus: For loosely coupled components
  3. Shared Store: For persistent shared state

Cross-Feature Communication

  1. Event Bus: Primary method for feature-to-feature updates
  2. Shared Services: Common API operations
  3. Global Store: Application-wide state

Form Architecture

Form Handling Strategy

  1. Local State First: Keep form data in component state
  2. Validation Composables: Reuse validation logic
  3. Service Layer Submission: Process through services
  4. Error Display Pattern: Consistent error messaging

Form Patterns

  • Use VeeValidate with Zod schemas for complex forms
  • Implement field-level validation feedback
  • Show loading states during submission
  • Handle API validation errors gracefully

Routing Architecture

Route Organization

  1. Feature Modules: Group related routes by feature
  2. Lazy Loading: Use dynamic imports for route components
  3. Route Guards: Implement authentication and authorization checks
  4. Breadcrumb Support: Maintain hierarchical navigation

Dynamic Routes

  • Use [id] notation for dynamic segments
  • Handle route parameter validation in views
  • Implement proper 404 handling for invalid IDs

Error Handling Architecture

Error Boundaries

  1. View Level: Catch and display page-level errors
  2. Component Level: Handle component-specific errors
  3. Global Level: Catch unhandled errors

Error Patterns

  • Display user-friendly error messages
  • Log technical details for debugging
  • Provide recovery actions when possible
  • Maintain application stability on errors

Performance Architecture

Code Splitting Strategy

  1. Route-Based Splitting: Each route loads its own bundle
  2. Component Lazy Loading: Heavy components load on demand
  3. Vendor Chunking: Separate third-party libraries

Optimization Patterns

  • Use shallowRef for large objects
  • Implement virtual scrolling for long lists
  • Debounce expensive operations
  • Memoize computed values appropriately

Security Architecture

Frontend Security Principles

  1. Never Trust Client: All validation must happen on backend
  2. Secure Storage: Never store sensitive data (passwords, API keys, tokens) in localStorage. See Frontend Storage System for proper storage patterns
  3. XSS Prevention: Sanitize user input, use Vue’s built-in protections
  4. CSRF Protection: Include tokens in API requests

Authentication Flow

  • Token-based authentication (JWT)
  • Automatic token refresh
  • Secure token storage (httpOnly cookies preferred)
  • Route protection via navigation guards

Testing Architecture

Testing Strategy

  1. Unit Tests: For services, composables, and utilities
  2. Component Tests: For isolated component behavior
  3. Integration Tests: For feature workflows
  4. E2E Tests: For critical user paths

Test Organization

  • Mirror source structure in test directories
  • Co-locate test files with source files
  • Use descriptive test names
  • Follow AAA pattern (Arrange, Act, Assert)

Plugin Architecture

Plugin System Design

The application supports runtime plugin loading for extensibility.

Plugin Structure

  1. Entry Point: Each plugin exports a default configuration
  2. Extension Points: Plugins hook into defined extension points
  3. Isolation: Plugins run in isolated contexts
  4. Version Management: Plugins declare compatible versions

Plugin Guidelines

  • Plugins cannot modify core functionality
  • Use provided APIs and extension points
  • Handle errors gracefully
  • Document dependencies clearly

Development Workflow

Code Organization Rules

  1. Feature Cohesion: Keep related code together
  2. Explicit Imports: No magic globals or auto-imports
  3. Type Definitions: Colocate types with their usage
  4. Consistent Naming: Follow established patterns

File Naming Conventions

  • 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)

Import Order

  1. External dependencies
  2. Vue and framework imports
  3. Internal aliases (@/ imports)
  4. Relative imports
  5. Type imports

Anti-Patterns to Avoid

Component Anti-Patterns

  • ❌ Using Options API in new components
  • ❌ Mixing paradigms (Options + Composition)
  • ❌ Direct DOM manipulation
  • ❌ Inline styles for layout
  • ❌ Business logic in templates

State Management Anti-Patterns

  • ❌ Mutating props directly
  • ❌ Excessive global state
  • ❌ Circular store dependencies
  • ❌ Store logic in components

Service Anti-Patterns

  • ❌ Using Axios or other HTTP libraries
  • ❌ Instance-based service classes
  • ❌ Mixing UI concerns in services
  • ❌ Inconsistent error handling

General Anti-Patterns

  • ❌ Premature optimization
  • ❌ Deep component nesting (>3 levels)
  • ❌ Tight coupling between features
  • ❌ Ignoring TypeScript errors
  • ❌ Copy-paste programming

Architecture Decision Records

Why Static Services?

Static service methods ensure:
  • No instance management complexity
  • Predictable behavior
  • Easy testing and mocking
  • Clear API boundaries

Why Direct Fetch?

Using native fetch() provides:
  • No external dependencies
  • Consistent API across services
  • Full control over request/response
  • Smaller bundle size

Why Feature-Based Structure?

Feature organization offers:
  • Better code locality
  • Easier feature removal/addition
  • Clear ownership boundaries
  • Reduced merge conflicts

Migration Guidelines

When refactoring existing code:
  1. Incremental Migration: Update feature by feature
  2. Test Coverage First: Add tests before refactoring
  3. Preserve Functionality: No behavior changes during refactor
  4. Document Changes: Update relevant documentation
  5. Review Thoroughly: Architecture changes need careful review

Future Considerations

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

Conclusion

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.