Code Organization
Code structure patterns, module organization, and maintainable code architecture strategies.
Code Organization
This section covers comprehensive code organization strategies, module patterns, and architectural approaches that ensure maintainable, scalable, and readable codebases.
Code Architecture Philosophy
1. Clean Architecture Principles
The codebase follows clean architecture principles with clear separation of concerns:
┌─────────────────────────────────────────────────────────────┐
│ Presentation Layer │
│ • React Components │
│ • UI State Management │
│ • User Interaction Handlers │
└─────────────────────┬───────────────────────────────────────┘
│
┌─────────────────────▼───────────────────────────────────────┐
│ Application Layer │
│ • Use Cases / Business Logic │
│ • Application Services │
│ • Data Transformation │
└─────────────────────┬───────────────────────────────────────┘
│
┌─────────────────────▼───────────────────────────────────────┐
│ Domain Layer │
│ • Entities and Value Objects │
│ • Domain Services │
│ • Business Rules │
└─────────────────────┬───────────────────────────────────────┘
│
┌─────────────────────▼───────────────────────────────────────┐
│ Infrastructure Layer │
│ • Database Access │
│ • External APIs │
│ • File System │
└─────────────────────────────────────────────────────────────┘
2. Module Organization Strategy
lib/
├── auth/ # Authentication module
│ ├── index.ts # Public API exports
│ ├── types.ts # Type definitions
│ ├── services/ # Business logic
│ │ ├── auth.service.ts
│ │ ├── token.service.ts
│ │ └── session.service.ts
│ ├── repositories/ # Data access
│ │ ├── user.repository.ts
│ │ └── session.repository.ts
│ ├── utils/ # Utility functions
│ │ ├── validators.ts
│ │ ├── formatters.ts
│ │ └── helpers.ts
│ ├── hooks/ # React hooks
│ │ ├── use-auth.ts
│ │ ├── use-session.ts
│ │ └── use-permissions.ts
│ └── constants.ts # Module constants
│
├── products/ # Product management module
│ ├── index.ts
│ ├── types.ts
│ ├── services/
│ │ ├── product.service.ts
│ │ ├── category.service.ts
│ │ └── pricing.service.ts
│ ├── repositories/
│ │ ├── product.repository.ts
│ │ └── category.repository.ts
│ ├── utils/
│ │ ├── sku-generator.ts
│ │ ├── price-calculator.ts
│ │ └── validators.ts
│ ├── hooks/
│ │ ├── use-products.ts
│ │ ├── use-product-form.ts
│ │ └── use-categories.ts
│ └── constants.ts
│
├── inventory/ # Inventory management module
│ ├── index.ts
│ ├── types.ts
│ ├── services/
│ │ ├── inventory.service.ts
│ │ ├── movement.service.ts
│ │ └── adjustment.service.ts
│ ├── repositories/
│ │ ├── inventory.repository.ts
│ │ └── movement.repository.ts
│ ├── utils/
│ │ ├── stock-calculator.ts
│ │ ├── movement-tracker.ts
│ │ └── validators.ts
│ ├── hooks/
│ │ ├── use-inventory.ts
│ │ ├── use-movements.ts
│ │ └── use-adjustments.ts
│ └── constants.ts
│
├── shared/ # Shared utilities
│ ├── api/ # API utilities
│ │ ├── client.ts
│ │ ├── types.ts
│ │ └── error-handler.ts
│ ├── utils/ # General utilities
│ │ ├── date.ts
│ │ ├── format.ts
│ │ ├── validation.ts
│ │ └── helpers.ts
│ ├── hooks/ # Shared hooks
│ │ ├── use-debounce.ts
│ │ ├── use-local-storage.ts
│ │ └── use-async.ts
│ ├── types/ # Global types
│ │ ├── api.ts
│ │ ├── common.ts
│ │ └── database.ts
│ └── constants/ # Global constants
│ ├── app.ts
│ ├── api.ts
│ └── ui.ts
│
└── config/ # Configuration
├── database.ts
├── api.ts
├── env.ts
└── app.ts
Service Layer Architecture
1. Service Pattern Implementation
// lib/products/services/product.service.ts
import { ProductRepository } from '../repositories/product.repository'
import { CategoryRepository } from '../repositories/category.repository'
import { InventoryService } from '../../inventory/services/inventory.service'
import { AuditService } from '../../shared/services/audit.service'
import type { Product, CreateProductData, UpdateProductData } from '../types'
export class ProductService {
constructor(
private productRepository: ProductRepository,
private categoryRepository: CategoryRepository,
private inventoryService: InventoryService,
private auditService: AuditService
) {}
async createProduct(data: CreateProductData): Promise<Product> {
// Validate business rules
await this.validateProductData(data)
// Check SKU uniqueness
const existingProduct = await this.productRepository.findBySku(data.sku)
if (existingProduct) {
throw new Error(`Product with SKU ${data.sku} already exists`)
}
// Create product
const product = await this.productRepository.create(data)
// Initialize inventory
await this.inventoryService.initializeProduct(product.id, {
initial_quantity: data.initial_quantity || 0
})
// Audit log
await this.auditService.log('product_created', {
product_id: product.id,
sku: product.sku
})
return product
}
async updateProduct(id: string, data: UpdateProductData): Promise<Product> {
const existingProduct = await this.productRepository.findById(id)
if (!existingProduct) {
throw new Error('Product not found')
}
// Validate business rules
await this.validateProductData(data, id)
// Check SKU uniqueness if changed
if (data.sku && data.sku !== existingProduct.sku) {
const existingSku = await this.productRepository.findBySku(data.sku)
if (existingSku) {
throw new Error(`Product with SKU ${data.sku} already exists`)
}
}
// Update product
const updatedProduct = await this.productRepository.update(id, data)
// Handle price changes
if (data.price && data.price !== existingProduct.price) {
await this.handlePriceChange(id, existingProduct.price, data.price)
}
// Audit log
await this.auditService.log('product_updated', {
product_id: id,
changes: this.getChanges(existingProduct, data)
})
return updatedProduct
}
async deleteProduct(id: string): Promise<void> {
const product = await this.productRepository.findById(id)
if (!product) {
throw new Error('Product not found')
}
// Check if product has inventory
const inventory = await this.inventoryService.getByProductId(id)
if (inventory && inventory.quantity > 0) {
throw new Error('Cannot delete product with existing inventory')
}
// Soft delete
await this.productRepository.softDelete(id)
// Audit log
await this.auditService.log('product_deleted', {
product_id: id,
sku: product.sku
})
}
async getProducts(filters: ProductFilters): Promise<PaginatedResult<Product>> {
return this.productRepository.findMany(filters)
}
async getProductById(id: string): Promise<Product | null> {
return this.productRepository.findById(id)
}
async searchProducts(query: string, filters?: ProductFilters): Promise<Product[]> {
return this.productRepository.search(query, filters)
}
private async validateProductData(data: Partial<CreateProductData>, excludeId?: string): Promise<void> {
// Validate category exists
if (data.category_id) {
const category = await this.categoryRepository.findById(data.category_id)
if (!category) {
throw new Error('Invalid category')
}
}
// Validate price
if (data.price !== undefined && data.price < 0) {
throw new Error('Price must be non-negative')
}
// Validate cost vs price
if (data.cost !== undefined && data.price !== undefined && data.cost > data.price) {
throw new Error('Cost cannot be greater than price')
}
}
private async handlePriceChange(productId: string, oldPrice: number, newPrice: number): Promise<void> {
// Notify inventory service about price change
await this.inventoryService.updateProductPrice(productId, newPrice)
// Log price history
await this.auditService.log('price_changed', {
product_id: productId,
old_price: oldPrice,
new_price: newPrice
})
}
private getChanges(original: Product, updates: UpdateProductData): Record<string, any> {
const changes: Record<string, any> = {}
Object.keys(updates).forEach(key => {
const originalValue = original[key as keyof Product]
const newValue = updates[key as keyof UpdateProductData]
if (originalValue !== newValue) {
changes[key] = { from: originalValue, to: newValue }
}
})
return changes
}
}
2. Repository Pattern Implementation
// lib/products/repositories/product.repository.ts
import { supabase } from '@/lib/supabase/client'
import type { Product, CreateProductData, UpdateProductData, ProductFilters } from '../types'
export class ProductRepository {
private readonly table = 'products'
async create(data: CreateProductData): Promise<Product> {
const { data: product, error } = await supabase
.from(this.table)
.insert([data])
.select()
.single()
if (error) {
throw new Error(`Failed to create product: ${error.message}`)
}
return product
}
async findById(id: string): Promise<Product | null> {
const { data: product, error } = await supabase
.from(this.table)
.select(`
*,
category:categories(id, name),
inventory:inventory(quantity, allocated, available)
`)
.eq('id', id)
.eq('deleted_at', null)
.single()
if (error) {
if (error.code === 'PGRST116') return null // Not found
throw new Error(`Failed to find product: ${error.message}`)
}
return product
}
async findBySku(sku: string): Promise<Product | null> {
const { data: product, error } = await supabase
.from(this.table)
.select()
.eq('sku', sku)
.eq('deleted_at', null)
.single()
if (error) {
if (error.code === 'PGRST116') return null
throw new Error(`Failed to find product by SKU: ${error.message}`)
}
return product
}
async findMany(filters: ProductFilters): Promise<PaginatedResult<Product>> {
let query = supabase
.from(this.table)
.select(`
*,
category:categories(id, name),
inventory:inventory(quantity, allocated, available)
`, { count: 'exact' })
.eq('deleted_at', null)
// Apply filters
if (filters.category_id) {
query = query.eq('category_id', filters.category_id)
}
if (filters.search) {
query = query.or(`name.ilike.%${filters.search}%,sku.ilike.%${filters.search}%`)
}
if (filters.min_price !== undefined) {
query = query.gte('price', filters.min_price)
}
if (filters.max_price !== undefined) {
query = query.lte('price', filters.max_price)
}
if (filters.in_stock !== undefined) {
if (filters.in_stock) {
query = query.gt('inventory.quantity', 0)
} else {
query = query.eq('inventory.quantity', 0)
}
}
// Apply sorting
const sortField = filters.sort || 'created_at'
const sortOrder = filters.order || 'desc'
query = query.order(sortField, { ascending: sortOrder === 'asc' })
// Apply pagination
const page = filters.page || 1
const limit = filters.limit || 20
const offset = (page - 1) * limit
query = query.range(offset, offset + limit - 1)
const { data: products, error, count } = await query
if (error) {
throw new Error(`Failed to fetch products: ${error.message}`)
}
return {
data: products || [],
pagination: {
page,
limit,
total: count || 0,
totalPages: Math.ceil((count || 0) / limit),
hasNext: offset + limit < (count || 0),
hasPrev: page > 1
}
}
}
async update(id: string, data: UpdateProductData): Promise<Product> {
const { data: product, error } = await supabase
.from(this.table)
.update({
...data,
updated_at: new Date().toISOString()
})
.eq('id', id)
.eq('deleted_at', null)
.select()
.single()
if (error) {
throw new Error(`Failed to update product: ${error.message}`)
}
return product
}
async softDelete(id: string): Promise<void> {
const { error } = await supabase
.from(this.table)
.update({ deleted_at: new Date().toISOString() })
.eq('id', id)
if (error) {
throw new Error(`Failed to delete product: ${error.message}`)
}
}
async search(query: string, filters?: ProductFilters): Promise<Product[]> {
let searchQuery = supabase
.from(this.table)
.select(`
*,
category:categories(id, name),
inventory:inventory(quantity, allocated, available)
`)
.eq('deleted_at', null)
.or(`name.ilike.%${query}%,sku.ilike.%${query}%,barcode.eq.${query}`)
// Apply additional filters
if (filters?.category_id) {
searchQuery = searchQuery.eq('category_id', filters.category_id)
}
if (filters?.in_stock) {
searchQuery = searchQuery.gt('inventory.quantity', 0)
}
searchQuery = searchQuery
.order('name')
.limit(50) // Limit search results
const { data: products, error } = await searchQuery
if (error) {
throw new Error(`Failed to search products: ${error.message}`)
}
return products || []
}
}
3. Custom Hooks Pattern
// lib/products/hooks/use-products.ts
import { useState, useEffect, useCallback } from 'react'
import { ProductService } from '../services/product.service'
import { useDebounce } from '@/lib/shared/hooks/use-debounce'
import type { Product, ProductFilters } from '../types'
interface UseProductsProps {
initialFilters?: ProductFilters
enabled?: boolean
}
export function useProducts({ initialFilters = {}, enabled = true }: UseProductsProps = {}) {
const [products, setProducts] = useState<Product[]>([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState<Error | null>(null)
const [filters, setFilters] = useState<ProductFilters>(initialFilters)
const [pagination, setPagination] = useState({
page: 1,
limit: 20,
total: 0,
totalPages: 0,
hasNext: false,
hasPrev: false
})
// Debounce search to avoid excessive API calls
const debouncedFilters = useDebounce(filters, 300)
const fetchProducts = useCallback(async (searchFilters: ProductFilters) => {
if (!enabled) return
try {
setLoading(true)
setError(null)
const result = await ProductService.getProducts(searchFilters)
setProducts(result.data)
setPagination(result.pagination)
} catch (err) {
setError(err as Error)
setProducts([])
} finally {
setLoading(false)
}
}, [enabled])
// Fetch products when filters change
useEffect(() => {
fetchProducts(debouncedFilters)
}, [debouncedFilters, fetchProducts])
const updateFilters = useCallback((newFilters: Partial<ProductFilters>) => {
setFilters(prev => ({
...prev,
...newFilters,
page: newFilters.page !== undefined ? newFilters.page : 1 // Reset to page 1 for new filters
}))
}, [])
const refetch = useCallback(() => {
fetchProducts(filters)
}, [fetchProducts, filters])
const nextPage = useCallback(() => {
if (pagination.hasNext) {
updateFilters({ page: pagination.page + 1 })
}
}, [pagination.hasNext, pagination.page, updateFilters])
const prevPage = useCallback(() => {
if (pagination.hasPrev) {
updateFilters({ page: pagination.page - 1 })
}
}, [pagination.hasPrev, pagination.page, updateFilters])
const goToPage = useCallback((page: number) => {
updateFilters({ page })
}, [updateFilters])
return {
products,
loading,
error,
filters,
pagination,
updateFilters,
refetch,
nextPage,
prevPage,
goToPage
}
}
// Usage in component
export function ProductList() {
const {
products,
loading,
error,
filters,
pagination,
updateFilters,
nextPage,
prevPage
} = useProducts({
initialFilters: { page: 1, limit: 20 }
})
if (loading) return <Spinner />
if (error) return <ErrorMessage error={error} />
return (
<div>
<ProductFilters
filters={filters}
onFiltersChange={updateFilters}
/>
<ProductGrid products={products} />
<Pagination
current={pagination.page}
total={pagination.totalPages}
onNext={nextPage}
onPrev={prevPage}
hasNext={pagination.hasNext}
hasPrev={pagination.hasPrev}
/>
</div>
)
}
Error Handling Strategy
1. Error Classification
// lib/shared/errors/index.ts
export abstract class AppError extends Error {
abstract readonly code: string
abstract readonly statusCode: number
constructor(message: string, public readonly context?: Record<string, any>) {
super(message)
this.name = this.constructor.name
}
}
export class ValidationError extends AppError {
readonly code = 'VALIDATION_ERROR'
readonly statusCode = 400
constructor(
message: string,
public readonly fieldErrors: Record<string, string[]> = {}
) {
super(message)
}
}
export class NotFoundError extends AppError {
readonly code = 'NOT_FOUND'
readonly statusCode = 404
}
export class UnauthorizedError extends AppError {
readonly code = 'UNAUTHORIZED'
readonly statusCode = 401
}
export class ForbiddenError extends AppError {
readonly code = 'FORBIDDEN'
readonly statusCode = 403
}
export class ConflictError extends AppError {
readonly code = 'CONFLICT'
readonly statusCode = 409
}
export class InternalServerError extends AppError {
readonly code = 'INTERNAL_SERVER_ERROR'
readonly statusCode = 500
}
// Error handling utilities
export function isAppError(error: unknown): error is AppError {
return error instanceof AppError
}
export function getErrorMessage(error: unknown): string {
if (isAppError(error)) {
return error.message
}
if (error instanceof Error) {
return error.message
}
return 'An unexpected error occurred'
}
export function getErrorCode(error: unknown): string {
if (isAppError(error)) {
return error.code
}
return 'UNKNOWN_ERROR'
}
2. Global Error Handler
// lib/shared/utils/error-handler.ts
import { isAppError, InternalServerError } from '../errors'
import { logger } from './logger'
export function handleServiceError(error: unknown, context?: Record<string, any>): never {
// Log error for debugging
logger.error('Service error occurred', {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
context
})
// Re-throw app errors as-is
if (isAppError(error)) {
throw error
}
// Handle specific error types
if (error instanceof Error) {
// Database constraint violations
if (error.message.includes('unique constraint')) {
throw new ConflictError('Resource already exists')
}
// Foreign key violations
if (error.message.includes('foreign key constraint')) {
throw new ValidationError('Invalid reference to related resource')
}
}
// Default to internal server error
throw new InternalServerError('An unexpected error occurred')
}
Best Practices
1. Module Design
- Single Responsibility: Each module has one clear purpose
- Dependency Injection: Use constructor injection for testability
- Interface Segregation: Create focused interfaces
- Barrel Exports: Use index files for clean imports
2. Service Layer
- Stateless Services: Services should not maintain state
- Transaction Management: Handle database transactions properly
- Error Propagation: Let errors bubble up with context
- Logging: Log important business events
3. Repository Pattern
- Data Access Abstraction: Hide database implementation details
- Query Optimization: Use efficient database queries
- Error Handling: Provide meaningful error messages
- Type Safety: Use TypeScript for all data operations
4. Custom Hooks
- Single Concern: Each hook should have one responsibility
- Memoization: Use useMemo and useCallback appropriately
- Cleanup: Handle cleanup in useEffect
- Testing: Make hooks testable in isolation
This code organization strategy provides a solid foundation for building maintainable, scalable applications with clear separation of concerns and consistent patterns throughout the codebase.