This document outlines our conventions for implementing features using a clean architecture approach with TypeScript, React, and Zod. The pattern described here promotes maintainability, scalability, and testability.
Features should be organized in a dedicated feature folder with the following structure:
src/features/[feature-name]/
├── components/ # React components specific to the feature
├── hooks/ # Custom React hooks for feature logic
├── interfaces/ # TypeScript interfaces and types
├── repositories/ # Data access layer implementations
├── schemas/ # Zod schemas for validation and type inference
└── services/ # Business logic layer
Start by defining your feature's data models using Zod schemas. This provides:
- Type inference
- Runtime validation
- Clear data structure documentation
Example:
// schemas/[feature].schema.ts
import { z } from "zod";
export const entitySchema = z.object({
id: z.string(),
// ... other fields
createdAt: z.date()
});
export const createEntitySchema = entitySchema.omit({
id: true,
createdAt: true
});
export type Entity = z.infer<typeof entitySchema>;
export type CreateEntityInput = z.infer<typeof createEntitySchema>;Define a repository interface and implementation for data access:
- Create the interface:
// interfaces/IRepository.ts
export interface IRepository<T> {
getAll(): Promise<T[]>;
getById(id: string): Promise<T | null>;
create(data: CreateInput): Promise<T>;
update(id: string, data: Partial<T>): Promise<T>;
delete(id: string): Promise<void>;
}- Implement the repository:
// repositories/Repository.ts
export class Repository implements IRepository<T> {
// Implement methods with actual data access logic
// Could be API calls, localStorage, etc.
}Create a service layer to handle business logic:
- Define the service interface:
// interfaces/IService.ts
export interface IService {
// Define business operations
// These should be high-level use cases
}- Implement the service:
// services/Service.ts
export class Service implements IService {
constructor(private repository: IRepository) {}
// Implement business logic
// Coordinate between repository and other services
// Handle complex operations
}Create React hooks to:
- Connect UI components with services
- Manage state
- Handle side effects
- Provide error handling
// hooks/useFeature.ts
export function useFeature() {
const [data, setData] = useState<Data[]>([]);
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
// Implement feature operations
// Handle errors
// Manage loading states
return {
data,
error,
isLoading,
// ... operations
};
}Create components that:
- Use the custom hooks for data and operations
- Focus purely on UI concerns
- Handle user interactions
- Manage local UI state only
// components/FeatureComponent.tsx
export function FeatureComponent() {
const { data, error, isLoading, operations } = useFeature();
// Implement UI logic
// Handle user interactions
// Render UI elements
}-
Separation of Concerns
- Repositories handle data access
- Services handle business logic
- Hooks handle state management and service coordination
- Components handle UI rendering and user interaction
-
Type Safety
- Use Zod schemas for runtime validation
- Leverage TypeScript for compile-time type checking
- Export types from schemas for consistency
-
Error Handling
- Handle errors at appropriate levels
- Provide meaningful error messages
- Use error boundaries where appropriate
-
State Management
- Keep state as close to where it's needed as possible
- Use custom hooks to encapsulate complex state logic
- Consider performance implications of state updates
-
Testing
- Repository methods should be easily testable
- Services should be pure and testable
- Hooks should be tested with React Testing Library
- Components should focus on UI testing
The Todo feature in this project serves as a reference implementation of these conventions. Key aspects include:
- Zod schemas for todo item validation
- Repository pattern for data persistence
- Service layer for business logic
- Custom hook (useTodos) for state management
- Clean component implementation
This pattern provides:
- Clear separation of concerns
- Type safety throughout the application
- Consistent error handling
- Testable code
- Maintainable and scalable features
- Easy to understand and modify
- Reusable components and logic
Create a new feature when you have:
- A distinct set of related functionality
- Unique data models
- Specific business logic
- Dedicated UI components
Each feature should be self-contained but able to interact with other features through well-defined interfaces.