Clean Architecture in React & Flutter Projects

Clean Architecture with Feature Modules
Organize your Flutter and Next.js/TypeScript apps by feature, each containing its own core, data, domain, and presentation layers for maximum maintainability.
1. Feature-based Folder Structure
Group related code by feature under features/
. Each feature folder contains its own layers: core utilities, data handling, domain logic, and presentation/UI.
lib/ or src/
├── core/
│ └── utilities, constants, global dependencies
│
├── features/
│ ├── authentication/
│ │ ├── core/
│ │ ├── data/
│ │ ├── domain/
│ │ └── presentation/
│ └── address/
│ ├── core/
│ ├── data/
│ ├── domain/
│ └── presentation/
│
├── main.dart (Flutter)
└── pages/ (Next.js/TS entry points)
2. Detailed Layer Structure
Within each feature, use the classic Clean Architecture layers:
features/address/
│
├─ core/ # shared models, error handling, DI setup
│
├─ data/
│ ├─ data_sources/
│ │ └─ address_remote_data_source.dart
│ ├─ models/
│ │ └─ address_model.dart
│ └─ repositories/
│ └─ address_repository_impl.dart
│
├─ domain/
│ ├─ entities/
│ │ └─ address_entity.dart
│ ├─ repositories/
│ │ └─ address_repository.dart
│ └─ use_cases/
│ └─ get_address_use_case.dart
│
└─ presentation/
├─ controllers/ # Bloc or Cubit in Flutter
├─ pages/ # Flutter screens or Next.js pages
└─ widgets/ # Reusable UI components
3. Implementation in Flutter
In Flutter, your main.dart
should initialize dependency injection (injection_container.dart) and navigate to feature pages:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await initInjection(); // register repositories, use cases, datasources
Bloc.observer = AppBlocObserver();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// ...
@override
Widget build(BuildContext context) {
return MaterialApp(
home: AddressPage(), // from features/address/presentation/pages
);
}
}
Each feature's presentation layer can use Bloc or Cubit for state management:
class AddressCubit extends Cubit {
final GetAddressUseCase useCase;
AddressCubit(this.useCase): super(AddressInitial());
Future fetchAddresses() async {
emit(AddressLoading());
final result = await useCase.execute();
result.fold(
(failure) => emit(AddressError(failure.message)),
(list) => emit(AddressLoaded(list)),
);
}
}
4. Implementation in Next.js (TypeScript)
In a Next.js + TypeScript project, mirror the same feature-based structure under src/
:
src/
├── core/
│ └── apiClient.ts # axios setup, error handlers
│
├── features/
│ └── address/
│ ├── data/
│ │ ├── addressApi.ts # fetch functions
│ │ └── addressModel.ts
│ ├── domain/
│ │ ├── addressEntity.ts
│ │ ├── addressRepository.ts
│ │ └── getAddressUseCase.ts
│ └── presentation/
│ ├── AddressPage.tsx # Next.js page under pages/
│ └── AddressCard.tsx
│
└── pages/
└── address.tsx # Next.js route that renders
Example Next.js page using the use case:
import { GetAddressUseCase } from 'features/address/domain/getAddressUseCase';
import { AddressCard } from 'features/address/presentation/AddressCard';
export default function AddressPage() {
const [addresses, setAddresses] = useState([]);
useEffect(() => {
const useCase = new GetAddressUseCase();
useCase.execute().then(data => setAddresses(data));
}, []);
return (
<div className="space-y-4">
{addresses.map(a => <AddressCard key={a.id} address={a} />)}
</div>
);
}
Conclusion
By applying a feature-based folder structure and Clean Architecture layers in both Flutter and Next.js, you encapsulate business logic, data handling, and UI within each feature. This approach improves modularity, testability, and scalability across your applications.