-
-
Notifications
You must be signed in to change notification settings - Fork 0
Add support for Subscribers in the frontend app #161
Description
Requirements Document
Introduction
This feature implements a clean architecture pattern for the subscribers module in the frontend Vue.js application. The goal is to separate concerns by organizing code into distinct layers: domain (business logic), infrastructure (external services), presentation (UI components), and store (state management). This architecture will provide better testability, maintainability, and extensibility for subscriber-related functionality.
Requirements
Requirement 1
User Story: As a developer, I want a clean architecture structure for the subscribers module, so that the codebase is maintainable, testable, and follows separation of concerns principles.
Acceptance Criteria - Requirement 1
- WHEN the subscribers module is implemented THEN the system SHALL organize code into domain, infrastructure, presentation, and store layers
- WHEN implementing the domain layer THEN the system SHALL define abstract interfaces for repositories and concrete use cases for business logic
- WHEN implementing the infrastructure layer THEN the system SHALL provide concrete implementations of repository interfaces using HTTP API calls
- WHEN implementing the presentation layer THEN the system SHALL contain Vue components that are decoupled from direct API calls
- WHEN implementing the store layer THEN the system SHALL use dependency injection to connect use cases with repository implementations
- WHEN designing layer interactions THEN the system SHALL ensure that each layer is agnostic of the others (e.g., domain must not import infrastructure)
- WHEN implementing use cases THEN the system SHALL avoid including UI logic or HTTP/network concerns
Requirement 2
User Story: As a developer, I want domain models and interfaces defined, so that business logic is independent of external dependencies.
Acceptance Criteria - Requirement 2
- WHEN defining domain models THEN the system SHALL create TypeScript interfaces for Subscriber, SubscriberStatus, and related types
- WHEN defining repository interfaces THEN the system SHALL create abstract SubscriberRepository interface with methods for fetching, counting by status, and counting by tags
- WHEN implementing use cases THEN the system SHALL create FetchSubscribers, CountByStatus, and CountByTags classes that depend only on repository interfaces
- WHEN validating data THEN the system SHALL use Zod schemas for type validation in the domain layer
- WHEN defining domain models THEN the system SHALL avoid serialization logic and only represent pure data structures
- WHEN creating types THEN the system SHALL prefer using 'readonly' modifiers to enforce immutability
Requirement 3
User Story: As a developer, I want HTTP API integration separated from business logic, so that the data source can be changed without affecting the domain layer.
Acceptance Criteria - Requirement 3
- WHEN implementing API integration THEN the system SHALL create SubscriberApi class that implements SubscriberRepository interface
- WHEN making HTTP requests THEN the system SHALL use the existing axios wrapper with authentication and interceptors
- WHEN handling API responses THEN the system SHALL transform API data to domain models
- WHEN constructing API URLs THEN the system SHALL properly format workspace IDs and query parameters
- WHEN handling API errors THEN the system SHALL provide appropriate error handling and transformation
- WHEN handling API errors THEN the system SHALL transform them into domain-level error objects
- WHEN returning data from API implementations THEN the system SHALL return domain models, not raw API payloads
Requirement 4
User Story: As a developer, I want Vue components that are decoupled from direct API calls, so that components are easier to test and maintain.
Acceptance Criteria - Requirement 4
- WHEN creating presentation components THEN the system SHALL separate views from reusable components
- WHEN implementing SubscriberPage view THEN the system SHALL coordinate between store and child components
- WHEN implementing SubscriberList component THEN the system SHALL receive data through props rather than direct API calls
- WHEN using TypeScript THEN the system SHALL properly type all component props and emits
- WHEN following Vue conventions THEN the system SHALL use Composition API with script setup syntax
- WHEN designing components THEN the system SHALL avoid any awareness of API or repository dependencies
- WHEN building components THEN the system SHALL use typed props and slots for data and UI composition
Requirement 5
User Story: As a developer, I want a Pinia store that uses dependency injection, so that the store is testable and follows clean architecture principles.
Acceptance Criteria - Requirement 5
- WHEN implementing the store THEN the system SHALL inject use case dependencies rather than making direct API calls
- WHEN managing state THEN the system SHALL provide reactive state for subscribers, loading states, and error handling
- WHEN implementing store actions THEN the system SHALL delegate business logic to use case classes
- WHEN handling async operations THEN the system SHALL properly manage loading states and error conditions
- WHEN structuring the store THEN the system SHALL follow Pinia best practices with proper TypeScript typing
- WHEN instantiating dependencies THEN the system SHALL avoid creating repository or HTTP clients directly within the store
- WHEN implementing store behavior THEN the system SHALL avoid coupling with routing logic or browser storage APIs
Requirement 6
User Story: As a developer, I want comprehensive test coverage for all layers, so that the architecture is reliable and maintainable.
Acceptance Criteria - Requirement 6
- WHEN testing use cases THEN the system SHALL provide unit tests with mocked repository dependencies
- WHEN testing API integration THEN the system SHALL provide integration tests with mocked HTTP responses
- WHEN testing components THEN the system SHALL provide unit tests using Vue Testing Library
- WHEN testing the store THEN the system SHALL provide tests with mocked use case dependencies
- WHEN running tests THEN the system SHALL achieve at least 90% code coverage for the subscribers module
- WHEN writing unit tests THEN the system SHALL avoid relying on global state or shared store instances
- WHEN mocking HTTP responses THEN the system SHALL use tools like MSW or equivalent to ensure realistic test conditions
Requirement 7
User Story: As a developer, I want proper file organization following the project's folder structure conventions, so that the code is discoverable and follows established patterns.
Acceptance Criteria - Requirement 7
- WHEN organizing files THEN the system SHALL place the subscribers module under
client/apps/web/src/subscribers/ - WHEN structuring domain layer THEN the system SHALL organize files into
domain/models/,domain/repositories/, anddomain/usecases/folders - WHEN structuring infrastructure THEN the system SHALL place API implementations in
infrastructure/api/folder - WHEN structuring presentation THEN the system SHALL separate components and views into
presentation/components/andpresentation/views/folders - WHEN implementing the store THEN the system SHALL place store files in
store/folder with proper naming conventions - WHEN choosing file naming THEN the system SHALL follow consistent conventions (e.g., PascalCase for components, kebab-case for files)
- WHEN organizing feature folders THEN the system SHALL maintain consistency with existing module layout conventions (e.g., under 'modules/subscribers')
Design Document
Overview
This design implements a clean architecture pattern for the subscribers module in the Vue.js frontend application. The architecture separates concerns into four distinct layers: Domain (business logic), Infrastructure (external services), Presentation (UI components), and Store (state management). This approach ensures high testability, maintainability, and extensibility while following established patterns in the existing codebase.
This design follows the Screaming Architecture principle: the folder structure reflects the business capability (
subscribers) rather than technical concerns. All layers—domain, infrastructure, presentation, and store—are encapsulated inside thesubscribers/directory, highlighting its role as a core business function of the application.
The design leverages dependency injection principles to decouple layers, making the system more flexible and easier to test. Each layer has specific responsibilities and communicates through well-defined interfaces.
Architecture
Folder Structure (Screaming Architecture)
src/subscribers/
├── domain/
│ ├── models/ # Domain entities and value objects
│ ├── repositories/ # Abstract repository interfaces
│ └── usecases/ # Business logic use cases
├── infrastructure/
│ └── api/ # HTTP API implementations
├── presentation/
│ ├── components/ # Reusable UI components
│ └── views/ # Page-level components
└── store/ # Pinia state management
This structure emphasizes business modularity: all technical layers that serve the subscribers concern live inside the same bounded context directory. This helps enforce isolation, autonomy, and focus within business capabilities.
Dependency Flow
graph TD
A[Presentation Layer] --> B[Store Layer]
B --> C[Domain Use Cases]
C --> D[Domain Repository Interface]
E[Infrastructure Layer] --> D
subgraph "Domain Layer"
C
D
F[Domain Models]
end
subgraph "External Dependencies"
G[HTTP API]
H[Axios Client]
end
E --> G
E --> H
Components and Interfaces
Domain Layer
Models (domain/models/)
Subscriber.ts
- Defines core domain entities:
Subscriber,SubscriberStatus,Attributes - Includes Zod schemas for validation:
subscriberSchema - Defines response types:
CountByStatusResponse,CountByTagsResponse - Provides type-safe interfaces independent of external APIs
Repository Interface (domain/repositories/)
SubscriberRepository.ts
export interface SubscriberRepository {
fetchAll(workspaceId: string, filters?: Record<string, string>): Promise<Subscriber[]>
countByStatus(workspaceId: string): Promise<CountByStatusResponse[]>
countByTags(workspaceId: string): Promise<CountByTagsResponse[]>
}Use Cases (domain/usecases/)
FetchSubscribers.ts
- Encapsulates business logic for retrieving subscribers
- Depends only on
SubscriberRepositoryinterface - Handles filtering and data transformation logic
CountByStatus.ts
- Manages subscriber counting by status
- Provides aggregated status information
CountByTags.ts
- Handles tag-based subscriber counting
- Manages tag aggregation logic
Infrastructure Layer
API Implementation (infrastructure/api/)
SubscriberApi.ts
- Implements
SubscriberRepositoryinterface - Uses existing axios instance with interceptors
- Handles HTTP request/response transformation
- Manages API endpoint construction and query parameters
- Transforms API responses to domain models
Key features:
- Leverages existing XSRF token handling from axios interceptors
- Uses workspace-based API endpoints following existing patterns
- Handles authentication through existing interceptor setup
- Provides proper error handling and transformation
Presentation Layer
Components (presentation/components/)
SubscriberList.vue
- Reusable component for displaying subscriber data
- Receives data through props (no direct API calls)
- Implements proper TypeScript typing for props
- Uses Composition API with
<script setup lang="ts">syntax - Follows existing Vue conventions and styling patterns
Views (presentation/views/)
SubscriberPage.vue
- Page-level component that coordinates data flow
- Integrates with Pinia store for state management
- Handles user interactions and navigation
- Manages loading states and error handling
Store Layer
Note on Reusability and Isolation
All components and views in the subscribers folder are intended to be tightly coupled to this domain. Avoid reusing them across unrelated modules to preserve cohesion and domain boundaries. Shared logic or UI patterns should be abstracted into @/components/ui or a designated shared layer, if absolutely necessary.
Pinia Store (store/)
subscriber.store.ts
- Uses dependency injection for use case instances
- Manages reactive state for subscribers, loading, and errors
- Delegates business logic to domain use cases
- Follows existing Pinia patterns from auth store
- Provides proper TypeScript typing for all state and actions
Data Models
Core Domain Models
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
}
enum SubscriberStatus {
ENABLED = 'ENABLED',
DISABLED = 'DISABLED',
BLOCKLISTED = 'BLOCKLISTED'
}
interface Attributes {
[key: string]: string | string[] | number | boolean
}API Response Models
interface CountByStatusResponse {
count: number
status: string
}
interface CountByTagsResponse {
count: number
tag: string
}Error Handling
Domain Layer Error Handling
- Use cases throw domain-specific errors
- Repository interface defines expected error types
- Business logic errors are separate from infrastructure errors
Infrastructure Layer Error Handling
- API implementation catches HTTP errors
- Transforms infrastructure errors to domain errors
- Leverages existing axios interceptor error handling
- Provides meaningful error messages for business logic
Presentation Layer Error Handling
- Components receive error state through props or store
- Views handle error display and user feedback
- Loading states managed through store actions
Testing Strategy
Unit Testing Approach
Domain Layer Tests
- Use Cases: Mock repository dependencies using Jest/Vitest mocks
- Models: Test validation schemas and type transformations
- Repository Interface: Test contract compliance
Infrastructure Layer Tests
- API Implementation: Mock axios responses using axios-mock-adapter
- Integration Tests: Test actual HTTP request/response cycles
- Error Handling: Test various HTTP error scenarios
Presentation Layer Tests
- Components: Use Vue Testing Library for component testing
- Views: Test user interactions and state management integration
- Props/Events: Test component communication patterns
Store Layer Tests
- Actions: Mock use case dependencies
- State Management: Test reactive state updates
- Error States: Test error handling and loading states
Test Structure
tests/
├── unit/
│ ├── domain/
│ │ ├── usecases/
│ │ └── models/
│ ├── infrastructure/
│ │ └── api/
│ ├── presentation/
│ │ ├── components/
│ │ └── views/
│ └── store/
└── integration/
└── api/
Coverage Requirements
- Minimum 90% code coverage for all layers
- 100% coverage for critical business logic (use cases)
- Integration tests for API endpoints
- Component interaction tests for presentation layer
Implementation Considerations
Dependency Injection Setup
- Use factory functions to create use case instances with injected dependencies
- Store initialization includes dependency injection configuration
- Maintain singleton pattern for repository implementations
TypeScript Integration
- Strict typing throughout all layers
- Use of utility types for API transformations
- Proper generic typing for repository interfaces
Vue.js Integration
- Follows existing Composition API patterns
- Integrates with current Pinia store structure
- Uses existing component styling and conventions
Performance Considerations
- Lazy loading of use case instances
- Efficient state management with Pinia
- Proper reactive data handling
- Optimized component re-rendering
Extensibility
- Easy addition of new use cases
- Simple repository implementation swapping
- Modular component structure
- Scalable store organization