This project follows Clean Architecture principles, ensuring separation of concerns, testability, and maintainability.
┌─────────────────────────────────────────────────┐
│ Domain Layer │
│ (Entities, Interfaces, Business Logic) │
│ - product.entity.ts │
│ - product.repository.ts │
│ └─ No external dependencies │
└─────────────────────────────────────────────────┘
▲
│
│ depends on
│
┌─────────────────────────────────────────────────┐
│ Use Cases Layer │
│ (Application Business Rules) │
│ - scrapeProduct.usecase.ts │
│ └─ Depends only on Domain │
└─────────────────────────────────────────────────┘
▲
│
│ depends on
│
┌─────────────────────────────────────────────────┐
│ Infrastructure Layer │
│ (External Concerns, Implementations) │
│ - httpClient.ts │
│ - paniniScraper.service.ts │
│ └─ Implements Domain interfaces │
└─────────────────────────────────────────────────┘
Dependencies point inward: Outer layers depend on inner layers, never the reverse.
- Domain has no dependencies (pure business logic)
- Use Cases depend only on Domain
- Infrastructure implements Domain interfaces
Purpose: Core business logic and entities
Components:
Productinterface: Data structure definitionProductEntityclass: Business entity with validation and computed propertiesProductRepositoryinterface: Data access contractHttpConfiginterface: Configuration contract- Custom error types:
ProductScrapingError,ProductNotFoundError,InvalidUrlError
Principles Applied:
- ✅ Single Responsibility: Each entity has one clear purpose
- ✅ Open/Closed: Extensible without modification
- ✅ Liskov Substitution: ProductEntity implements Product correctly
- ✅ Interface Segregation: Small, focused interfaces
- ✅ Dependency Inversion: Depends on abstractions, not implementations
No External Dependencies: Pure TypeScript, no imports from infrastructure
Purpose: Application-specific business rules
Components:
ScrapeProductUseCase: Orchestrates product scraping flow
Responsibilities:
- Input validation
- URL normalization
- Coordination between layers
- Error handling delegation
Principles Applied:
- ✅ Single Responsibility: One use case per operation
- ✅ Dependency Injection: Repository injected via constructor
- ✅ Clean interfaces: Simple, focused methods
Purpose: External concerns and implementations
Components:
HttpClient: HTTP request wrapper (abstracts Axios)PaniniScraperService: ImplementsProductRepositoryinterface
Responsibilities:
- HTTP communication
- HTML parsing (Cheerio)
- Error transformation
- External library integration
Principles Applied:
- ✅ Adapter Pattern: HttpClient adapts Axios to our needs
- ✅ Repository Pattern: PaniniScraperService implements data access
- ✅ Dependency Inversion: Implements domain interfaces
- ✅ Error Handling: Transforms external errors to domain errors
Each class has one reason to change:
ProductEntity: Business validation and computed propertiesHttpClient: HTTP communicationPaniniScraperService: Web scraping logicScrapeProductUseCase: Use case orchestration
Open for extension, closed for modification:
- New scrapers can be added by implementing
ProductRepository - Product entity can be extended without modifying existing code
- New HTTP clients can be created by injecting different configurations
Subtypes are substitutable:
ProductEntityimplementsProductcorrectlyPaniniScraperServicecan be replaced by anyProductRepositoryimplementation- Custom errors extend base error properly
Small, focused interfaces:
ProductRepository: Single method for scrapingHttpConfig: Optional, granular configurationProduct: Only essential product data
Depend on abstractions:
- Use cases depend on
ProductRepositoryinterface, not concrete implementation - Infrastructure implements interfaces defined in domain
- No direct dependencies on external libraries in domain/use cases
// Good: Clear, descriptive names
class ScrapeProductUseCase
async scrapeProduct(url: string): Promise<Product>
const normalizedUrl = this.normalizeUrl(url);
// Bad examples avoided:
// class SPU
// async sp(u: string)
// const nu = this.nu(url);// Each function has a single, clear purpose
private normalizeUrl(url: string): string
private extractTitle($: cheerio.CheerioAPI): string
private extractPrices($: cheerio.CheerioAPI): { fullPrice: number; currentPrice: number }Functions are kept concise and focused:
- Average function size: 10-30 lines
- Complex operations broken into smaller methods
- High cohesion within each function
// Price extraction logic centralized
private extractPriceFromText(text: string): number
// URL validation reused
private isValidUrl(url: string): boolean// Specific error types for different scenarios
throw new InvalidUrlError(url);
throw new ProductNotFoundError(url);
throw new ProductScrapingError(message, url);
// Errors handled at appropriate layers
// Domain errors thrown by entities
// Infrastructure errors transformed to domain errors- JSDoc comments for all public APIs
- Examples in documentation
- Why, not what: Comments explain reasoning, not obvious code
- Self-documenting code: Names explain intent
src/
├── domain/ # Business logic (innermost layer)
│ ├── product.entity.ts
│ └── product.repository.ts
├── usecases/ # Application logic
│ └── scrapeProduct.usecase.ts
└── infrastructure/ # External concerns (outermost layer)
├── httpClient.ts
└── paniniScraper.service.ts
- 95%+ test coverage
- 100 passing tests
- Unit tests for isolated components
- Integration tests for complete flows
- Dependency Injection enables mocking
interface ProductRepository {
scrapeProduct(url: string): Promise<Product>;
}Benefit: Abstracts data access, enabling different implementations
export function createPaniniScraper(config?: HttpConfig) {
// Creates configured instance
}Benefit: Simplifies object creation with configuration
class HttpClient {
// Adapts Axios to our interface
}Benefit: Isolates external library, easy to swap
constructor(private readonly productRepository: ProductRepository)Benefit: Loose coupling, testability, flexibility
export class ProductEntity {
constructor(
public readonly title: string,
public readonly fullPrice: number,
// ...
)
}- Full TypeScript coverage
- Strict mode enabled
- No
anytypes except where necessary - Comprehensive interfaces
- Graceful degradation (fallback values)
- Multiple selectors for robustness
- Detailed error messages
- Flexible HTTP config
- Proxy support
- Custom headers
- Timeout control
- Comprehensive README
- JSDoc for all public APIs
- Usage examples
- Architecture documentation
- Add new scrapers by implementing
ProductRepository - Add new entity properties without breaking existing code
- Add new use cases independently
- All components have unit tests
- Integration tests for complete flows
- Mocking enabled by dependency injection
- 95%+ code coverage
- Clear layer separation
- Meaningful names
- Comprehensive documentation
- Self-documenting code
- Changes isolated to specific layers
- No ripple effects across layers
- Interfaces buffer against changes
- Backward compatibility maintained
This project demonstrates:
- ✅ Clean Architecture with proper layer separation
- ✅ SOLID Principles throughout the codebase
- ✅ Clean Code practices for readability
- ✅ High Test Coverage (95%+)
- ✅ Type Safety with TypeScript
- ✅ Maintainability through good design
- ✅ Professional Documentation
- ✅ Production-Ready quality
The architecture ensures the library is flexible, testable, and maintainable while providing a clean API for users.