This is the initial commit for the CODEX_ADK project, a full-stack AI agent management platform featuring: BACKEND (ASP.NET Core 8.0): - CQRS architecture with 6 commands and 7 queries - 16 API endpoints (all working and tested) - PostgreSQL database with 5 entities - AES-256 encryption for API keys - FluentValidation on all commands - Rate limiting and CORS configured - OpenAPI/Swagger documentation - Docker Compose setup (PostgreSQL + Ollama) FRONTEND (Flutter 3.x): - Dark theme with Svrnty branding - Collapsible sidebar navigation - CQRS API client with Result<T> error handling - Type-safe endpoints from OpenAPI schema - Multi-platform support (Web, iOS, Android, macOS, Linux, Windows) DOCUMENTATION: - Comprehensive API reference - Architecture documentation - Development guidelines for Claude Code - API integration guides - context-claude.md project overview Status: Backend ready (Grade A-), Frontend integration pending 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
22 KiB
Flutter Frontend: Complete API Integration Guide
Objective
Integrate your Flutter app with the Codex CQRS-based ASP.NET Core API using type-safe Dart clients generated from OpenAPI specification.
Prerequisites
- Flutter SDK 3.0+ installed
- Dart 3.0+
- Git access to backend repository
- VS Code with Flutter/Dart extensions or Android Studio
- Basic understanding of REST APIs and Dart
Architecture Overview
The Codex API uses CQRS pattern with OpenHarbor.CQRS framework:
- Queries (Read):
POST /api/query/{queryName}orGET /api/query/{queryName} - Commands (Write):
POST /api/command/{commandName} - Dynamic Queries (Paginated):
POST /api/dynamicquery/{itemType}
All requests use JSON payloads (even for empty requests).
Step 1: Access OpenAPI Specification
Get the Spec File
# Option A: Clone backend repository
git clone <backend-repo-url>
cd backend
# The spec is located at: docs/openapi.json
# Option B: Download from running API
curl http://localhost:5246/swagger/v1/swagger.json -o openapi.json
Review Documentation
Before coding, read:
backend/docs/ARCHITECTURE.md- Understand CQRS patternsbackend/docs/CHANGELOG.md- Check for breaking changesbackend/docs/openapi.json- Your API contract
Step 2: Generate Dart API Client
Install OpenAPI Generator
# Install via Homebrew (macOS)
brew install openapi-generator
# Or via npm
npm install -g @openapitools/openapi-generator-cli
# Or use Docker
docker pull openapitools/openapi-generator-cli
Generate Dart Client
# Navigate to your Flutter project
cd /path/to/your/flutter-app
# Create API directory
mkdir -p lib/api
# Generate Dart client
openapi-generator-cli generate \
-i ../backend/docs/openapi.json \
-g dart \
-o lib/api/generated \
--additional-properties=pubName=codex_api_client,pubLibrary=codex_api_client
# If using Docker:
docker run --rm \
-v ${PWD}:/local openapitools/openapi-generator-cli generate \
-i /local/../backend/docs/openapi.json \
-g dart \
-o /local/lib/api/generated \
--additional-properties=pubName=codex_api_client,pubLibrary=codex_api_client
This generates:
lib/api/generated/lib/api.dart- API clientlib/api/generated/lib/model/- DTOs and modelslib/api/generated/doc/- API documentation
Step 3: Configure Flutter Project
Update pubspec.yaml
Add required dependencies:
name: your_flutter_app
description: Your Flutter app description
environment:
sdk: '>=3.0.0 <4.0.0'
dependencies:
flutter:
sdk: flutter
# HTTP client
http: ^1.1.0
# JSON serialization
json_annotation: ^4.8.1
# State management (choose one)
flutter_riverpod: ^2.4.9 # Recommended
# provider: ^6.1.1
# bloc: ^8.1.3
# Secure storage for tokens
flutter_secure_storage: ^9.0.0
# Environment configuration
flutter_dotenv: ^5.1.0
dev_dependencies:
flutter_test:
sdk: flutter
build_runner: ^2.4.7
json_serializable: ^6.7.1
flutter_lints: ^3.0.1
mockito: ^5.4.4
flutter:
uses-material-design: true
# Add environment files
assets:
- .env
- .env.development
- .env.production
Run pub get
flutter pub get
cd lib/api/generated
flutter pub get
cd ../../..
Step 4: Setup Environment Configuration
Create Environment Files
.env.development:
API_BASE_URL=http://localhost:5246
API_TIMEOUT=30000
.env.production:
API_BASE_URL=https://api.yourapp.com
API_TIMEOUT=30000
.env (default/local):
API_BASE_URL=http://localhost:5246
API_TIMEOUT=30000
Create Config Class
lib/config/api_config.dart:
import 'package:flutter_dotenv/flutter_dotenv.dart';
class ApiConfig {
static String get baseUrl => dotenv.env['API_BASE_URL'] ?? 'http://localhost:5246';
static int get timeout => int.parse(dotenv.env['API_TIMEOUT'] ?? '30000');
static const String apiVersion = 'v1';
}
Initialize in main.dart
lib/main.dart:
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// Load environment variables
await dotenv.load(fileName: ".env");
runApp(
const ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Codex App',
theme: ThemeData(
primarySwatch: Colors.blue,
useMaterial3: true,
),
home: const HomePage(),
);
}
}
Step 5: Create API Client Service
Create HTTP Client with Interceptors
lib/services/api_client.dart:
import 'package:http/http.dart' as http;
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import '../config/api_config.dart';
class ApiClient extends http.BaseClient {
final http.Client _client = http.Client();
final FlutterSecureStorage _storage = const FlutterSecureStorage();
@override
Future<http.StreamedResponse> send(http.BaseRequest request) async {
// Add base URL if not already present
if (!request.url.toString().startsWith('http')) {
request = _updateRequestUrl(request);
}
// Add authentication token
final token = await _storage.read(key: 'auth_token');
if (token != null && token.isNotEmpty) {
request.headers['Authorization'] = 'Bearer $token';
}
// Add default headers
request.headers['Content-Type'] = 'application/json';
request.headers['Accept'] = 'application/json';
print('🌐 ${request.method} ${request.url}');
try {
final response = await _client.send(request);
print('✅ ${response.statusCode} ${request.url}');
return response;
} catch (e) {
print('❌ Request failed: $e');
rethrow;
}
}
http.BaseRequest _updateRequestUrl(http.BaseRequest request) {
final newUri = Uri.parse('${ApiConfig.baseUrl}${request.url.path}');
if (request is http.Request) {
final newRequest = http.Request(request.method, newUri)
..headers.addAll(request.headers)
..body = request.body;
return newRequest;
}
throw UnsupportedError('Unsupported request type');
}
Future<void> setAuthToken(String token) async {
await _storage.write(key: 'auth_token', value: token);
}
Future<void> clearAuthToken() async {
await _storage.delete(key: 'auth_token');
}
Future<String?> getAuthToken() async {
return await _storage.read(key: 'auth_token');
}
}
Step 6: Create Service Layer (Repository Pattern)
Health Query Service Example
lib/services/health_service.dart:
import 'package:codex_api_client/api.dart';
import '../models/api_result.dart';
import 'api_client.dart';
class HealthService {
final ApiClient _apiClient;
late final DefaultApi _api;
HealthService(this._apiClient) {
_api = DefaultApi(_apiClient, ApiConfig.baseUrl);
}
/// Check if API is healthy
Future<ApiResult<bool>> checkHealth() async {
try {
// Call POST /api/query/health
final response = await _api.apiQueryHealthPost(
healthQuery: HealthQuery(), // Empty query object
);
return ApiResult.success(response ?? false);
} on ApiException catch (e) {
return ApiResult.failure(
message: e.message ?? 'Health check failed',
statusCode: e.code,
);
} catch (e) {
return ApiResult.failure(
message: 'Network error: $e',
);
}
}
}
Create ApiResult Model
lib/models/api_result.dart:
class ApiResult<T> {
final T? data;
final String? errorMessage;
final int? statusCode;
final Map<String, List<String>>? validationErrors;
bool get isSuccess => errorMessage == null;
bool get isFailure => !isSuccess;
ApiResult.success(this.data)
: errorMessage = null,
statusCode = null,
validationErrors = null;
ApiResult.failure({
required String message,
this.statusCode,
this.validationErrors,
}) : data = null,
errorMessage = message;
/// Handle the result with callbacks
R when<R>({
required R Function(T data) success,
required R Function(String message) failure,
}) {
if (isSuccess && data != null) {
return success(data as T);
} else {
return failure(errorMessage ?? 'Unknown error');
}
}
}
Step 7: Integrate with Riverpod (State Management)
Create Providers
lib/providers/api_providers.dart:
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../services/api_client.dart';
import '../services/health_service.dart';
// API Client Provider
final apiClientProvider = Provider<ApiClient>((ref) {
return ApiClient();
});
// Health Service Provider
final healthServiceProvider = Provider<HealthService>((ref) {
final apiClient = ref.watch(apiClientProvider);
return HealthService(apiClient);
});
// Health Check Provider (auto-fetches)
final healthCheckProvider = FutureProvider<bool>((ref) async {
final healthService = ref.watch(healthServiceProvider);
final result = await healthService.checkHealth();
return result.when(
success: (isHealthy) => isHealthy,
failure: (_) => false,
);
});
Use in UI
lib/screens/home_screen.dart:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/api_providers.dart';
class HomeScreen extends ConsumerWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final healthCheck = ref.watch(healthCheckProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Codex App'),
actions: [
// API Health Indicator
healthCheck.when(
data: (isHealthy) => Icon(
isHealthy ? Icons.check_circle : Icons.error,
color: isHealthy ? Colors.green : Colors.red,
),
loading: () => const CircularProgressIndicator(),
error: (_, __) => const Icon(Icons.error, color: Colors.red),
),
const SizedBox(width: 16),
],
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
healthCheck.when(
data: (isHealthy) => Text(
isHealthy ? 'API Connected ✅' : 'API Disconnected ❌',
style: Theme.of(context).textTheme.headlineSmall,
),
loading: () => const CircularProgressIndicator(),
error: (error, _) => Text('Error: $error'),
),
],
),
),
);
}
}
Step 8: Handle CQRS Commands and Queries
Example: User Command Service
lib/services/user_service.dart:
import 'package:codex_api_client/api.dart';
import '../models/api_result.dart';
import 'api_client.dart';
class UserService {
final ApiClient _apiClient;
late final DefaultApi _api;
UserService(this._apiClient) {
_api = DefaultApi(_apiClient, ApiConfig.baseUrl);
}
/// Create a new user (Command)
Future<ApiResult<void>> createUser({
required String username,
required String email,
required String password,
}) async {
try {
await _api.apiCommandCreateuserPost(
createUserCommand: CreateUserCommand(
username: username,
email: email,
password: password,
),
);
return ApiResult.success(null);
} on ApiException catch (e) {
return _handleApiException(e);
} catch (e) {
return ApiResult.failure(message: 'Network error: $e');
}
}
/// Get paginated users (Dynamic Query)
Future<ApiResult<PagedResult<UserDto>>> getUsers({
int page = 1,
int pageSize = 20,
String? searchTerm,
}) async {
try {
final filters = searchTerm != null && searchTerm.isNotEmpty
? [
DynamicQueryFilter(
path: 'username',
type: FilterType.contains,
value: searchTerm,
),
]
: <DynamicQueryFilter>[];
final response = await _api.apiDynamicqueryUserPost(
dynamicQuery: DynamicQuery(
page: page,
pageSize: pageSize,
filters: filters,
sorts: [
DynamicQuerySort(
path: 'createdAt',
descending: true,
),
],
),
);
return ApiResult.success(response);
} on ApiException catch (e) {
return _handleApiException(e);
} catch (e) {
return ApiResult.failure(message: 'Network error: $e');
}
}
ApiResult<T> _handleApiException<T>(ApiException e) {
switch (e.code) {
case 400:
// Validation errors from FluentValidation
return ApiResult.failure(
message: 'Validation failed',
statusCode: 400,
validationErrors: _parseValidationErrors(e.message),
);
case 401:
return ApiResult.failure(
message: 'Unauthorized. Please log in.',
statusCode: 401,
);
case 403:
return ApiResult.failure(
message: 'Permission denied',
statusCode: 403,
);
case 404:
return ApiResult.failure(
message: 'Resource not found',
statusCode: 404,
);
case 500:
return ApiResult.failure(
message: 'Server error. Please try again.',
statusCode: 500,
);
default:
return ApiResult.failure(
message: e.message ?? 'Unknown error',
statusCode: e.code,
);
}
}
Map<String, List<String>>? _parseValidationErrors(String? message) {
// Parse validation errors from response
// Format depends on backend's error response structure
return null;
}
}
Step 9: Testing
Unit Tests
test/services/health_service_test.dart:
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';
import 'package:your_app/services/health_service.dart';
import 'package:your_app/services/api_client.dart';
@GenerateMocks([ApiClient])
import 'health_service_test.mocks.dart';
void main() {
late HealthService healthService;
late MockApiClient mockApiClient;
setUp(() {
mockApiClient = MockApiClient();
healthService = HealthService(mockApiClient);
});
group('HealthService', () {
test('checkHealth returns true when API is healthy', () async {
// Arrange
when(mockApiClient.send(any)).thenAnswer(
(_) async => http.StreamedResponse(
Stream.value(utf8.encode('true')),
200,
),
);
// Act
final result = await healthService.checkHealth();
// Assert
expect(result.isSuccess, true);
expect(result.data, true);
});
test('checkHealth returns failure on error', () async {
// Arrange
when(mockApiClient.send(any)).thenThrow(Exception('Network error'));
// Act
final result = await healthService.checkHealth();
// Assert
expect(result.isFailure, true);
expect(result.errorMessage, contains('Network error'));
});
});
}
Integration Tests
integration_test/api_integration_test.dart:
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:your_app/services/api_client.dart';
import 'package:your_app/services/health_service.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('API Integration Tests', () {
late ApiClient apiClient;
late HealthService healthService;
setUp(() {
apiClient = ApiClient();
healthService = HealthService(apiClient);
});
testWidgets('Health check endpoint works', (tester) async {
// Ensure backend is running on localhost:5246
final result = await healthService.checkHealth();
expect(result.isSuccess, true);
expect(result.data, true);
});
});
}
Run Tests
# Unit tests
flutter test
# Integration tests (requires backend running)
flutter test integration_test/
Step 10: Error Handling & User Feedback
Create Error Display Widget
lib/widgets/error_snackbar.dart:
import 'package:flutter/material.dart';
import '../models/api_result.dart';
class ErrorSnackbar {
static void show(BuildContext context, ApiResult result) {
if (result.isFailure) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(result.errorMessage ?? 'An error occurred'),
backgroundColor: Colors.red,
action: SnackBarAction(
label: 'Dismiss',
textColor: Colors.white,
onPressed: () {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
},
),
),
);
}
}
static void showValidationErrors(
BuildContext context,
Map<String, List<String>>? errors,
) {
if (errors == null || errors.isEmpty) return;
final errorMessages = errors.entries
.map((e) => '${e.key}: ${e.value.join(', ')}')
.join('\n');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(errorMessages),
backgroundColor: Colors.orange,
duration: const Duration(seconds: 5),
),
);
}
}
Step 11: Monitoring API Changes
Create Update Script
scripts/update_api_client.sh:
#!/bin/bash
echo "🔄 Updating API client from backend..."
# Pull latest backend changes
cd ../backend
git pull origin main
# Regenerate Dart client
cd ../flutter-app
openapi-generator-cli generate \
-i ../backend/docs/openapi.json \
-g dart \
-o lib/api/generated \
--additional-properties=pubName=codex_api_client,pubLibrary=codex_api_client
echo "✅ API client updated!"
echo ""
echo "📝 Next steps:"
echo "1. Check backend/docs/CHANGELOG.md for breaking changes"
echo "2. Run: flutter pub get"
echo "3. Run: flutter test"
echo "4. Fix any compilation errors"
Make executable:
chmod +x scripts/update_api_client.sh
Step 12: CI/CD Integration
GitHub Actions Workflow
.github/workflows/flutter_ci.yml:
name: Flutter CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout Flutter app
uses: actions/checkout@v3
- name: Checkout backend
uses: actions/checkout@v3
with:
repository: your-org/backend
path: backend
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.16.0'
channel: 'stable'
- name: Install OpenAPI Generator
run: npm install -g @openapitools/openapi-generator-cli
- name: Check for API changes
run: |
if ! diff -q lib/api/openapi.json backend/docs/openapi.json; then
echo "⚠️ API changes detected!"
./scripts/update_api_client.sh
fi
- name: Get dependencies
run: flutter pub get
- name: Analyze code
run: flutter analyze
- name: Run tests
run: flutter test
- name: Build app
run: flutter build apk --debug
Complete Checklist for Flutter Team
Initial Setup
- Clone backend repository or access
docs/openapi.json - Review
docs/CHANGELOG.mdfor breaking changes - Review
docs/ARCHITECTURE.mdfor CQRS patterns - Install OpenAPI Generator CLI
- Generate Dart API client
- Update
pubspec.yamlwith dependencies - Run
flutter pub get - Create environment configuration files
- Setup API client with interceptors
Service Layer
- Create
ApiClientwith authentication - Create
ApiResultmodel for error handling - Create service classes (Health, User, etc.)
- Setup Riverpod providers
- Implement error handling
Testing
- Write unit tests for services
- Write widget tests for UI
- Setup integration tests
- Test authentication flow
- Test error scenarios (401, 400, 500)
UI Integration
- Create error display widgets
- Implement loading states
- Add API health indicator
- Handle validation errors in forms
DevOps
- Create API update script
- Setup CI/CD pipeline
- Configure environment variables
- Document team workflow
Common Issues & Solutions
Issue: "Failed to load .env file"
Solution: Ensure .env file exists and is listed in pubspec.yaml assets.
Issue: "401 Unauthorized on all requests"
Solution: Check that ApiClient is properly adding Bearer token to headers.
Issue: "Connection refused to localhost"
Solution:
- On Android emulator: use
http://10.0.2.2:5246 - On iOS simulator: use
http://localhost:5246 - Update
API_BASE_URLin.envaccordingly
Issue: "Type errors after regenerating client"
Solution: Breaking changes occurred. Review backend/docs/CHANGELOG.md and update service layer.
Issue: "OpenAPI generator fails"
Solution: Ensure openapi.json is valid JSON. Validate with: cat openapi.json | python3 -m json.tool
Android Network Configuration
android/app/src/main/AndroidManifest.xml:
<manifest>
<application
android:usesCleartextTraffic="true">
<!-- Your app config -->
</application>
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
iOS Network Configuration
ios/Runner/Info.plist:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
Support & Resources
- OpenAPI Spec:
backend/docs/openapi.json - Breaking Changes:
backend/docs/CHANGELOG.md - Architecture:
backend/docs/ARCHITECTURE.md - Backend API (Dev):
http://localhost:5246 - Swagger UI (Dev):
http://localhost:5246/swagger
Last Updated: 2025-01-26
Backend API Version: v1
OpenAPI Version: 3.0.1
Recommended Dart Client Generator: openapi-generator-cli with dart generator