Subscribers Clean Architecture Implementation
Subscribers Clean Architecture Implementation
Section titled “Subscribers Clean Architecture Implementation”This document provides comprehensive documentation for the subscribers module implementation using clean architecture principles in the Vue.js frontend application.
Table of Contents
Section titled “Table of Contents”- Overview
- Architecture Principles
- Folder Structure (Screaming Architecture)
- Layer Implementation
- Dependency Injection
- Testing Patterns
- Usage Examples
- Best Practices
- Troubleshooting
Overview
Section titled “Overview”The subscribers module implements a clean architecture pattern that separates concerns into four distinct layers:
- Domain Layer: Contains business logic, entities, and interfaces
- Infrastructure Layer: Handles external services and API communication
- Presentation Layer: Manages UI components and user interactions
- Store Layer: Provides state management with dependency injection
This architecture ensures high testability, maintainability, and extensibility while following established patterns in the existing codebase.
Key Benefits
Section titled “Key Benefits”- Testability: Each layer can be tested in isolation with mocked dependencies
- Maintainability: Clear separation of concerns makes code easier to understand and modify
- Extensibility: New features can be added without affecting existing layers
- Flexibility: Data sources can be changed without affecting business logic
Architecture Principles
Section titled “Architecture Principles”1. Dependency Inversion
Section titled “1. Dependency Inversion”Higher-level modules (domain) do not depend on lower-level modules (infrastructure). Both depend on abstractions (interfaces).
// Domain layer defines the interfaceexport interface SubscriberRepository { fetchAll(workspaceId: string, filters?: Record<string, string>): Promise<Subscriber[]>}
// Infrastructure layer implements the interfaceexport class SubscriberApi implements SubscriberRepository { async fetchAll(workspaceId: string, filters?: Record<string, string>): Promise<Subscriber[]> { // HTTP API implementation }}
2. Single Responsibility
Section titled “2. Single Responsibility”Each class and module has a single, well-defined responsibility:
- Use Cases: Handle specific business operations
- Repository: Abstract data access
- API: Handle HTTP communication
- Store: Manage application state
3. Interface Segregation
Section titled “3. Interface Segregation”Interfaces are focused and specific to their use cases, avoiding large, monolithic contracts.
4. Screaming Architecture
Section titled “4. Screaming Architecture”The folder structure reflects business capabilities rather than technical concerns, making the domain purpose immediately clear.
Folder Structure (Screaming Architecture)
Section titled “Folder Structure (Screaming Architecture)”src/subscribers/ # Business domain boundary├── domain/ # Business logic layer│ ├── models/ # Domain entities and value objects│ │ ├── Subscriber.ts # Core subscriber entity│ │ ├── schemas.ts # Zod validation schemas│ │ └── index.ts # Public exports│ ├── repositories/ # Abstract repository interfaces│ │ ├── SubscriberRepository.ts # Repository contract│ │ └── index.ts # Public exports│ └── usecases/ # Business logic use cases│ ├── FetchSubscribers.ts # Fetch subscribers use case│ ├── CountByStatus.ts # Count by status use case│ ├── CountByTags.ts # Count by tags use case│ └── index.ts # Public exports├── infrastructure/ # External services layer│ └── api/ # HTTP API implementations│ ├── SubscriberApi.ts # Concrete repository implementation│ └── index.ts # Public exports├── presentation/ # UI layer│ ├── components/ # Reusable UI components│ │ ├── SubscriberList.vue # Subscriber list component│ │ └── index.ts # Public exports│ └── views/ # Page-level components│ ├── SubscriberPage.vue # Subscriber page view│ └── index.ts # Public exports├── store/ # State management layer│ ├── subscriber.store.ts # Pinia store with DI│ └── index.ts # Public exports├── di/ # Dependency injection│ ├── container.ts # DI container│ ├── initialization.ts # Store initialization│ └── index.ts # Public exports├── composables/ # Vue composables│ ├── useSubscribers.ts # Main composable│ └── index.ts # Public exports└── index.ts # Module public API
Why Screaming Architecture?
Section titled “Why Screaming Architecture?”This structure emphasizes business modularity by keeping all technical layers that serve the subscribers
concern within the same bounded context directory. This approach:
- Highlights Business Purpose: The folder name immediately tells you this is about subscribers
- Enforces Isolation: All subscriber-related code lives in one place
- Promotes Autonomy: The module is self-contained and can evolve independently
- Improves Focus: Developers working on subscriber features don’t need to navigate across technical layers
Layer Implementation
Section titled “Layer Implementation”Domain Layer
Section titled “Domain Layer”The domain layer contains the core business logic and is independent of external concerns.
Models (domain/models/
)
Section titled “Models (domain/models/)”Domain entities represent the core business concepts:
// Subscriber.ts - Core domain entityexport interface Subscriber { readonly id: string readonly email: string readonly name?: string readonly status: SubscriberStatus readonly attributes?: Attributes readonly workspaceId: string readonly createdAt?: Date | string readonly updatedAt?: Date | string}
export enum SubscriberStatus { ENABLED = 'ENABLED', DISABLED = 'DISABLED', BLOCKLISTED = 'BLOCKLISTED'}
Key Features:
- Immutability: All properties are
readonly
to enforce immutability - Type Safety: Strong TypeScript typing throughout
- Validation: Zod schemas for runtime validation
- Utility Functions: Domain-specific helper functions
Repository Interface (domain/repositories/
)
Section titled “Repository Interface (domain/repositories/)”Abstract interfaces define data access contracts:
// SubscriberRepository.ts - Abstract repository interfaceexport interface SubscriberRepository { fetchAll(workspaceId: string, filters?: Record<string, string>): Promise<Subscriber[]> countByStatus(workspaceId: string): Promise<CountByStatusResponse[]> countByTags(workspaceId: string): Promise<CountByTagsResponse[]>}
Key Features:
- Abstraction: No implementation details, only contracts
- Testability: Easy to mock for unit tests
- Flexibility: Multiple implementations possible (API, local storage, etc.)
Use Cases (domain/usecases/
)
Section titled “Use Cases (domain/usecases/)”Use cases encapsulate business logic for specific operations:
// FetchSubscribers.ts - Business logic for fetching subscribersexport class FetchSubscribers { constructor(private readonly repository: SubscriberRepository) {}
async execute(workspaceId: string, filters?: FetchSubscribersFilters): Promise<Subscriber[]> { // Validate workspace ID if (!workspaceId || workspaceId.trim() === '') { throw new Error('Workspace ID is required') }
// Sanitize filters const repositoryFilters = filters ? this.sanitizeFilters(filters) : undefined
// Fetch from repository const subscribers = await this.repository.fetchAll(workspaceId, repositoryFilters)
// Apply business logic filtering return this.applyBusinessLogicFilters(subscribers, filters) }}
Key Features:
- Single Responsibility: Each use case handles one business operation
- Dependency Injection: Depends only on repository interface
- Business Logic: Contains domain-specific rules and validation
- Error Handling: Proper error handling and validation
Infrastructure Layer
Section titled “Infrastructure Layer”The infrastructure layer handles external services and API communication.
API Implementation (infrastructure/api/
)
Section titled “API Implementation (infrastructure/api/)”Concrete implementation of repository interfaces:
// SubscriberApi.ts - HTTP API implementationexport class SubscriberApi implements SubscriberRepository { private readonly baseUrl = "/api"
async fetchAll(workspaceId: string, filters?: Record<string, string>): Promise<Subscriber[]> { this.validateWorkspaceId(workspaceId)
const queryParams = this.buildQueryParams(filters) const url = `${this.baseUrl}/workspaces/${workspaceId}/subscribers${queryParams}`
return this.makeApiRequest( url, this.transformSubscriber.bind(this), subscribersArraySchema.safeParse.bind(subscribersArraySchema), "fetchAll subscribers", "Invalid subscriber data received from API" ) }}
Key Features:
- Interface Implementation: Implements repository interface
- HTTP Integration: Uses existing axios configuration
- Data Transformation: Converts API responses to domain models
- Error Handling: Transforms infrastructure errors to domain errors
- Validation: Uses Zod schemas to validate API responses
Presentation Layer
Section titled “Presentation Layer”The presentation layer manages UI components and user interactions.
Components (presentation/components/
)
Section titled “Components (presentation/components/)”Reusable UI components that receive data through props:
<!-- SubscriberList.vue - Reusable subscriber list component --><script setup lang="ts">import type { Subscriber } from '../../domain/models'
interface Props { subscribers: Subscriber[] loading?: boolean error?: string}
interface Emits { (e: 'edit-subscriber', subscriber: Subscriber): void (e: 'toggle-status', subscriber: Subscriber): void (e: 'delete-subscriber', subscriber: Subscriber): void}
const props = withDefaults(defineProps<Props>(), { loading: false})
const emit = defineEmits<Emits>()</script>
Key Features:
- Props-Based: Receives data through props, not direct API calls
- Type Safety: Proper TypeScript typing for props and emits
- Composition API: Uses
<script setup>
syntax - Event Emission: Communicates with parent through events
Views (presentation/views/
)
Section titled “Views (presentation/views/)”Page-level components that coordinate between store and child components:
<!-- SubscriberPage.vue - Page-level component --><script setup lang="ts">import { onMounted } from 'vue'import { useSubscribers } from '../../composables/useSubscribers'import SubscriberList from '../components/SubscriberList.vue'
const { subscribers, isLoading, hasError, error, fetchAllData, clearError} = useSubscribers()
onMounted(async () => { await fetchAllData('current-workspace-id')})</script>
Key Features:
- Store Integration: Uses composables to access store
- Lifecycle Management: Handles component lifecycle events
- Error Handling: Manages error states and user feedback
Store Layer
Section titled “Store Layer”The store layer provides state management with dependency injection.
Pinia Store (store/
)
Section titled “Pinia Store (store/)”Reactive state management with injected use cases:
// subscriber.store.ts - Pinia store with dependency injectionexport const useSubscriberStore = defineStore('subscriber', () => { // Reactive state const subscribers: Ref<Subscriber[]> = ref([]) const loading: Ref<LoadingStates> = ref({ ...defaultLoadingStates }) const error: Ref<SubscriberError | null> = ref(null)
// Injected use cases let useCases: SubscriberUseCases | null = null
// Store actions with use case injection const fetchSubscribers = async (workspaceId: string, filters?: FetchSubscribersFilters): Promise<void> => { await withAsyncAction( 'fetchingSubscribers', () => useCases!.fetchSubscribers.execute(workspaceId, filters), (result) => { subscribers.value = result }, 'FETCH_SUBSCRIBERS_ERROR', 'Failed to fetch subscribers', workspaceId ) }
return { // State subscribers: readonly(subscribers), loading: readonly(loading), error: readonly(error),
// Actions fetchSubscribers, initializeStore, resetState }})
Key Features:
- Dependency Injection: Use cases are injected, not created directly
- Reactive State: Vue 3 reactivity with proper typing
- Error Handling: Consistent error state management
- Loading States: Granular loading states for different operations
Dependency Injection
Section titled “Dependency Injection”The module uses a dependency injection container to manage dependencies and ensure proper layer isolation.
Container (di/container.ts
)
Section titled “Container (di/container.ts)”// container.ts - Dependency injection containerexport function createContainer(): SubscriberContainer { const repository = createRepository() const useCases = createUseCases()
return { repository, useCases }}
export function createUseCases(): SubscriberUseCases { if (useCasesInstance === null) { const repository = createRepository()
useCasesInstance = { fetchSubscribers: new FetchSubscribers(repository), countByStatus: new CountByStatus(repository), countByTags: new CountByTags(repository) } } return useCasesInstance}
Store Initialization (di/initialization.ts
)
Section titled “Store Initialization (di/initialization.ts)”// initialization.ts - Store initialization with DIexport function initializeSubscriberStore(): void { if (isInitialized) return
const store = useSubscriberStore() const useCases = createUseCases()
store.initializeStore(useCases) isInitialized = true}
Composable Integration (composables/useSubscribers.ts
)
Section titled “Composable Integration (composables/useSubscribers.ts)”// useSubscribers.ts - Main composable with auto-initializationexport function useSubscribers() { // Auto-initialize store if needed initializeSubscriberStore()
const store = useSubscriberStore()
return { // State subscribers: store.subscribers, isLoading: store.isLoading, hasError: store.hasError,
// Actions fetchSubscribers: store.fetchSubscribers, clearError: store.clearError, resetState: store.resetState,
// Store reference store }}
Testing Patterns
Section titled “Testing Patterns”The clean architecture enables comprehensive testing at each layer with proper isolation.
Unit Testing Strategy
Section titled “Unit Testing Strategy”Domain Layer Tests
Section titled “Domain Layer Tests”Use Case Testing with Mocked Repository:
describe('FetchSubscribers', () => { let useCase: FetchSubscribers const mockRepository: SubscriberRepository = { fetchAll: vi.fn(), countByStatus: vi.fn(), countByTags: vi.fn() }
beforeEach(() => { vi.clearAllMocks() useCase = new FetchSubscribers(mockRepository) })
it('should fetch subscribers successfully without filters', async () => { // Arrange const workspaceId = 'd2054881-b8c1-4bfa-93ce-a0e94d003ead23' vi.mocked(mockRepository.fetchAll).mockResolvedValue(mockSubscribers)
// Act const result = await useCase.execute(workspaceId)
// Assert expect(mockRepository.fetchAll).toHaveBeenCalledWith(workspaceId, undefined) expect(result).toEqual(mockSubscribers) })})
Key Testing Patterns:
- Mock Dependencies: Use
vi.fn()
to mock repository methods - Arrange-Act-Assert: Clear test structure
- Edge Cases: Test validation, error handling, and boundary conditions
- Isolation: Each test is independent with proper setup/teardown
Infrastructure Layer Tests
Section titled “Infrastructure Layer Tests”API Testing with Mocked HTTP Responses:
describe('SubscriberApi', () => { let subscriberApi: SubscriberApi
beforeEach(() => { subscriberApi = new SubscriberApi() vi.clearAllMocks() })
it('should fetch subscribers successfully', async () => { // Mock axios response mockedAxios.get.mockResolvedValueOnce({ data: mockApiResponse })
const result = await subscriberApi.fetchAll(mockWorkspaceId)
expect(mockedAxios.get).toHaveBeenCalledWith( '/api/workspaces/d2054881-b8c1-4bfa-93ce-a0e94d003ead23/subscribers', { withCredentials: true } ) expect(result).toHaveLength(1) })})
Key Testing Patterns:
- HTTP Mocking: Mock axios responses for different scenarios
- Error Scenarios: Test various HTTP error codes and network failures
- Data Transformation: Verify API responses are correctly transformed to domain models
- Validation: Test Zod schema validation with invalid data
Presentation Layer Tests
Section titled “Presentation Layer Tests”Component Testing with Vue Testing Library:
describe('SubscriberList', () => { it('renders subscribers table with data', () => { const wrapper = mount(SubscriberList, { props: { subscribers: mockSubscribers } })
expect(wrapper.find('[data-testid="subscribers-list"]').exists()).toBe(true) expect(wrapper.findAll('[data-testid="subscriber-item"]')).toHaveLength(3) })
it('emits edit-subscriber event when edit is clicked', async () => { const wrapper = mount(SubscriberList, { props: { subscribers: [mockSubscribers[0]] } })
const editButton = wrapper.find('[data-testid="edit-button"]') await editButton.trigger('click')
expect(wrapper.emitted('edit-subscriber')).toBeTruthy() expect(wrapper.emitted('edit-subscriber')?.[0]).toEqual([mockSubscribers[0]]) })})
Key Testing Patterns:
- Component Mounting: Use
@vue/test-utils
for component testing - Props Testing: Test component behavior with different prop values
- Event Testing: Verify component emits correct events
- UI State Testing: Test loading, error, and empty states
- Mock UI Components: Mock complex UI library components for focused testing
Store Layer Tests
Section titled “Store Layer Tests”Store Testing with Mocked Use Cases:
describe('useSubscriberStore', () => { let store: SubscriberStore let mockUseCases: SubscriberUseCases
beforeEach(() => { setActivePinia(createPinia()) mockUseCases = { fetchSubscribers: { execute: vi.fn() }, countByStatus: { execute: vi.fn() }, countByTags: { execute: vi.fn() } }
store = useSubscriberStore() store.initializeStore(mockUseCases) })
it('should fetch subscribers and update state', async () => { // Mock use case response vi.mocked(mockUseCases.fetchSubscribers.execute).mockResolvedValue(mockSubscribers)
await store.fetchSubscribers('d2054881-b8c1-4bfa-93ce-a0e94d003ead23')
expect(store.subscribers).toEqual(mockSubscribers) expect(store.isLoading).toBe(false) })})
Key Testing Patterns:
- Pinia Testing: Use
createPinia()
andsetActivePinia()
for store testing - Dependency Injection: Mock use cases and inject them into store
- State Verification: Test reactive state updates
- Async Actions: Test loading states and error handling
Integration Testing
Section titled “Integration Testing”Full Layer Integration:
describe('Subscribers Module Integration', () => { beforeEach(() => { setActivePinia(createPinia()) resetContainer() configureContainer({ customRepository: mockRepository }) })
it('should integrate all layers through dependency injection', async () => { const { subscribers, fetchSubscribers, isLoading } = useSubscribers()
await fetchSubscribers('d2054881-b8c1-4bfa-93ce-a0e94d003ead')
expect(isLoading.value).toBe(false) expect(subscribers.value).toHaveLength(2) expect(mockRepository.fetchAll).toHaveBeenCalledWith('d2054881-b8c1-4bfa-93ce-a0e94d003ead', undefined) })})
Key Integration Testing Patterns:
- End-to-End Flow: Test complete data flow from composable to repository
- Dependency Injection: Verify proper dependency wiring
- State Management: Test state updates across layers
- Error Propagation: Verify errors are properly handled across layers
Mocking Strategy
Section titled “Mocking Strategy”The module uses a layered mocking strategy:
- Unit Tests: Mock immediate dependencies only
- Integration Tests: Mock external services (HTTP, storage)
- Component Tests: Mock complex UI components and services
- Store Tests: Mock use cases and business logic
Note on MSW (Mock Service Worker): While the current implementation uses axios mocking for API tests, the architecture is designed to easily integrate with MSW for more realistic HTTP mocking:
// Example MSW integration (not currently implemented)import { rest } from 'msw'import { setupServer } from 'msw/node'
const server = setupServer( rest.get('/api/workspaces/:workspaceId/subscribers', (req, res, ctx) => { return res(ctx.json(mockSubscribers)) }))
beforeAll(() => server.listen())afterEach(() => server.resetHandlers())afterAll(() => server.close())
Usage Examples
Section titled “Usage Examples”Basic Usage
Section titled “Basic Usage”1. Using the Composable in a Component:
<script setup lang="ts">import { onMounted } from 'vue'import { useSubscribers } from '@/subscribers'
const { subscribers, isLoading, hasError, error, fetchSubscribers, clearError} = useSubscribers()
onMounted(async () => { await fetchSubscribers('d2054881-b8c1-4bfa-93ce-a0e94d003ead23')})
const handleRetry = async () => { clearError() await fetchSubscribers('d2054881-b8c1-4bfa-93ce-a0e94d003ead23')}</script>
<template> <div> <div v-if="isLoading">Loading subscribers...</div> <div v-else-if="hasError" class="error"> Error: {{ error?.message }} <button @click="handleRetry">Retry</button> </div> <div v-else> <h2>Subscribers ({{ subscribers.length }})</h2> <ul> <li v-for="subscriber in subscribers" :key="subscriber.id"> {{ subscriber.name || subscriber.email }} - {{ subscriber.status }} </li> </ul> </div> </div></template>
2. Using with Filters:
// Fetch subscribers with filtersawait fetchSubscribers('d2054881-b8c1-4bfa-93ce-a0e94d003ead23', { status: 'ENABLED', search: 'john@example.com'})
// Fetch all data at onceawait fetchAllData('d2054881-b8c1-4bfa-93ce-a0e94d003ead23', { status: 'ENABLED' })
3. Using Status and Tag Counts:
<script setup lang="ts">const { statusCounts, tagCounts, fetchAllData} = useSubscribers()
onMounted(async () => { await fetchAllData('d2054881-b8c1-4bfa-93ce-a0e94d003ead23')})</script>
<template> <div> <h3>Status Distribution</h3> <ul> <li v-for="status in statusCounts" :key="status.status"> {{ status.status }}: {{ status.count }} </li> </ul>
<h3>Top Tags</h3> <ul> <li v-for="tag in tagCounts" :key="tag.tag"> {{ tag.tag }}: {{ tag.count }} </li> </ul> </div></template>
Advanced Usage
Section titled “Advanced Usage”1. Custom Repository Implementation:
// Create a custom repository for testing or different data sourcesclass LocalStorageSubscriberRepository implements SubscriberRepository { async fetchAll(workspaceId: string, filters?: Record<string, string>): Promise<Subscriber[]> { const data = localStorage.getItem(`subscribers-${workspaceId}`) return data ? JSON.parse(data) : [] }
async countByStatus(workspaceId: string): Promise<CountByStatusResponse[]> { // Implementation for local storage }
async countByTags(workspaceId: string): Promise<CountByTagsResponse[]> { // Implementation for local storage }}
// Configure container with custom repositoryconfigureContainer({ customRepository: new LocalStorageSubscriberRepository()})
2. Direct Store Usage:
// Direct store access for advanced scenariosimport { useSubscriberStore } from '@/subscribers/store'
const store = useSubscriberStore()
// Access specific loading statesif (store.loading.fetchingSubscribers) { console.log('Fetching subscribers...')}
// Access computed valuesconsole.log('Total subscribers:', store.subscriberCount)console.log('Total status count:', store.totalStatusCount)
// Reset store statestore.resetState()
3. Error Handling:
const { fetchSubscribers, hasError, error, clearError } = useSubscribers()
try { await fetchSubscribers('d2054881-b8c1-4bfa-93ce-a0e94d003ead23')} catch (err) { // Error is automatically captured in store state if (hasError.value) { console.error('Error code:', error.value?.code) console.error('Error message:', error.value?.message) console.error('Error timestamp:', error.value?.timestamp) }}
// Clear error manuallyclearError()
Best Practices
Section titled “Best Practices”1. Layer Isolation
Section titled “1. Layer Isolation”DO:
// Domain use case depends only on repository interfaceexport class FetchSubscribers { constructor(private readonly repository: SubscriberRepository) {}}
// Infrastructure implements the interfaceexport class SubscriberApi implements SubscriberRepository { // Implementation details}
DON’T:
// Don't import infrastructure in domain layerimport { SubscriberApi } from '../infrastructure/api/SubscriberApi' // ❌
export class FetchSubscribers { constructor(private readonly api: SubscriberApi) {} // ❌}
2. Immutability
Section titled “2. Immutability”DO:
// Use readonly properties in domain modelsexport interface Subscriber { readonly id: string readonly email: string readonly status: SubscriberStatus}
// Return readonly state from storereturn { subscribers: readonly(subscribers), loading: readonly(loading)}
DON’T:
// Don't expose mutable stateexport interface Subscriber { id: string // ❌ - should be readonly email: string // ❌ - should be readonly}
3. Error Handling
Section titled “3. Error Handling”DO:
// Create domain-specific error typesexport class SubscriberValidationError extends Error { constructor(message: string, public readonly validationErrors: unknown) { super(message) this.name = 'SubscriberValidationError' }}
// Handle errors at appropriate layerstry { const result = await this.repository.fetchAll(workspaceId) return result} catch (error) { if (error instanceof SubscriberValidationError) { // Handle validation errors } throw error // Re-throw if not handled}
DON’T:
// Don't swallow errors silentlytry { await this.repository.fetchAll(workspaceId)} catch (error) { // ❌ - Don't ignore errors}
// Don't expose internal error detailsthrow new Error(`Database connection failed: ${internalError.stack}`) // ❌
4. Testing
Section titled “4. Testing”DO:
// Test each layer in isolationdescribe('FetchSubscribers', () => { const mockRepository: SubscriberRepository = { fetchAll: vi.fn(), countByStatus: vi.fn(), countByTags: vi.fn() }
it('should validate workspace ID', async () => { const useCase = new FetchSubscribers(mockRepository)
await expect(useCase.execute('')).rejects.toThrow('Workspace ID is required') expect(mockRepository.fetchAll).not.toHaveBeenCalled() })})
DON’T:
// Don't test multiple layers together in unit testsdescribe('FetchSubscribers', () => { it('should fetch from real API', async () => { const api = new SubscriberApi() // ❌ - Creates real dependencies const useCase = new FetchSubscribers(api)
const result = await useCase.execute('d2054881-b8c1-4bfa-93ce-a0e94d003ead23') // ❌ - Makes real HTTP calls })})
5. Dependency Injection
Section titled “5. Dependency Injection”DO:
// Use factory functions for dependency creationexport function createUseCases(): SubscriberUseCases { const repository = createRepository()
return { fetchSubscribers: new FetchSubscribers(repository), countByStatus: new CountByStatus(repository), countByTags: new CountByTags(repository) }}
// Initialize store with injected dependenciesconst useCases = createUseCases()store.initializeStore(useCases)
DON’T:
// Don't create dependencies directly in classesexport class FetchSubscribers { private repository = new SubscriberApi() // ❌ - Hard dependency
async execute(workspaceId: string): Promise<Subscriber[]> { return this.repository.fetchAll(workspaceId) }}
6. Component Design
Section titled “6. Component Design”DO:
<!-- Receive data through props --><script setup lang="ts">interface Props { subscribers: Subscriber[] loading?: boolean error?: string}
interface Emits { (e: 'edit-subscriber', subscriber: Subscriber): void (e: 'delete-subscriber', subscriber: Subscriber): void}
const props = defineProps<Props>()const emit = defineEmits<Emits>()</script>
DON’T:
<!-- Don't make API calls directly in components --><script setup lang="ts">import { SubscriberApi } from '../infrastructure/api/SubscriberApi' // ❌
const api = new SubscriberApi() // ❌const subscribers = ref<Subscriber[]>([])
onMounted(async () => { subscribers.value = await api.fetchAll('d2054881-b8c1-4bfa-93ce-a0e94d003ead23') // ❌})</script>
Troubleshooting
Section titled “Troubleshooting”Common Issues
Section titled “Common Issues”1. Store Not Initialized Error
Section titled “1. Store Not Initialized Error”Error: Store must be initialized with use cases before use
Solution:
// Ensure store is initialized before useimport { initializeSubscriberStore } from '@/subscribers/di'
// Initialize before using storeinitializeSubscriberStore()
// Or use the composable which auto-initializesconst { subscribers } = useSubscribers()
2. Circular Dependency Issues
Section titled “2. Circular Dependency Issues”Error: Module import cycles or circular dependencies
Solution:
- Use index.ts files for clean exports
- Avoid importing from parent directories
- Use dependency injection instead of direct imports
// Good - Use index.ts for exportsexport { FetchSubscribers } from './FetchSubscribers'export { CountByStatus } from './CountByStatus'
// Good - Import from indeximport { FetchSubscribers, CountByStatus } from '../usecases'
3. Type Errors with Readonly State
Section titled “3. Type Errors with Readonly State”Error: Cannot assign to readonly property
Solution:
// Don't try to mutate readonly state directlysubscribers.value.push(newSubscriber) // ❌
// Use store actions insteadawait store.fetchSubscribers(workspaceId) // ✅
// Or create new arrayssubscribers.value = [...subscribers.value, newSubscriber] // ✅
4. Testing Mock Issues
Section titled “4. Testing Mock Issues”Error: Mocks not working correctly in tests
Solution:
// Clear mocks between testsbeforeEach(() => { vi.clearAllMocks()})
// Reset container for integration testsbeforeEach(() => { resetContainer() configureContainer({ customRepository: mockRepository })})
// Use proper mock typingconst mockRepository = { fetchAll: vi.fn(), countByStatus: vi.fn(), countByTags: vi.fn()} as SubscriberRepository
5. Validation Errors
Section titled “5. Validation Errors”Error: Zod validation failures with API responses
Solution:
// Check API response format matches domain models// Add logging to see actual vs expected dataconsole.log('API Response:', apiResponse)console.log('Validation Error:', validationResult.error)
// Update schemas if API format changesexport const subscriberSchema = z.object({ id: z.string().uuid(), email: z.string().email(), // Add new fields as needed newField: z.string().optional()})
This comprehensive documentation provides everything needed to understand, use, and maintain the subscribers clean architecture implementation. The modular design ensures that the system remains maintainable and extensible as requirements evolve.