# Svrnty Console - API Integration **OpenAPI-Driven CQRS API Contract System** This Flutter application integrates with a C# CQRS backend using OpenAPI specifications as the single source of truth for API contracts. --- ## ๐ŸŽฏ Overview The backend and frontend communicate through a type-safe, contract-first API architecture: - **Backend:** C# CQRS API with Swagger/OpenAPI 3.0.1 - **Frontend:** Flutter/Dart with auto-generated type-safe client - **Contract:** OpenAPI specification (`api-schema.json`) - **Pattern:** Command Query Responsibility Segregation (CQRS) **Compatibility Status:** โœ… **100% Backend Aligned** --- ## ๐Ÿ“ฆ Architecture ``` Backend (C# CQRS) Frontend (Flutter/Dart) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Controllers + XML docs api-schema.json โ†“ (OpenAPI contract) docs/openapi.json โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ โ†“ (auto-generated) Code Generation โ†“ lib/api/ โ”œโ”€โ”€ client.dart (CQRS) โ”œโ”€โ”€ types.dart (Core) โ”œโ”€โ”€ endpoints/ (Extensions) โ””โ”€โ”€ generated/ (Auto) ``` --- ## ๐Ÿš€ Quick Start ### 1. Update API Contract When backend exports new `docs/openapi.json`: ```bash # Copy latest contract from backend cp ../backend/docs/openapi.json ./api-schema.json # Regenerate Dart types ./scripts/update_api_client.sh # Verify types are correct ./scripts/verify_api_types.sh ``` ### 2. Use the API Client ```dart import 'package:console/api/api.dart'; // Create client final client = CqrsApiClient( config: ApiClientConfig.development, ); // Execute query final result = await client.checkHealth(); result.when( success: (isHealthy) => print('โœ… API healthy: $isHealthy'), error: (error) => print('โŒ Error: ${error.message}'), ); // Clean up client.dispose(); ``` --- ## ๐Ÿ“š API Client Usage ### CQRS Patterns The backend uses CQRS with three endpoint types: #### 1. Queries (Read Operations) ```dart // Single value query final result = await client.executeQuery( endpoint: 'users/123', query: const GetUserQuery(userId: '123'), fromJson: UserDto.fromJson, ); ``` #### 2. Commands (Write Operations) ```dart // Create/update/delete final result = await client.executeCommand( endpoint: 'createUser', command: CreateUserCommand( name: 'John Doe', email: 'john@example.com', ), ); ``` #### 3. Paginated Queries (Lists) ```dart // List with filtering/sorting/pagination final result = await client.executePaginatedQuery( endpoint: 'users', query: const ListUsersQuery(), itemFromJson: UserDto.fromJson, page: 1, pageSize: 20, filters: [ FilterCriteria( field: 'status', operator: FilterOperator.equals, value: 'active', ), ], sorting: [ SortCriteria( field: 'createdAt', direction: SortDirection.descending, ), ], ); // Access results result.when( success: (response) { print('Users: ${response.items.length}'); print('Total: ${response.pageInfo.totalItems}'); print('Pages: ${response.pageInfo.totalPages}'); }, error: (error) => print('Error: ${error.message}'), ); ``` --- ## ๐Ÿ”ง Creating New Endpoints ### 1. Backend Adds Endpoint Backend team adds XML-documented command/query: ```csharp /// Gets user by ID /// Returns user details /// User not found public record GetUserQuery(string UserId); ``` Backend exports OpenAPI spec: ```bash ./export-openapi.sh # Creates docs/openapi.json ``` ### 2. Frontend Updates Contract ```bash cp ../backend/docs/openapi.json ./api-schema.json ./scripts/update_api_client.sh ``` ### 3. Frontend Creates Endpoint Extension ```dart // lib/api/endpoints/user_endpoint.dart import '../client.dart'; import '../types.dart'; extension UserEndpoint on CqrsApiClient { Future> getUser(String userId) async { return executeQuery( endpoint: 'users/$userId', query: GetUserQuery(userId: userId), fromJson: UserDto.fromJson, ); } } // Define the query (matching backend contract) class GetUserQuery implements Serializable { final String userId; const GetUserQuery({required this.userId}); @override Map toJson() => {'userId': userId}; } // Define the DTO (from OpenAPI schema) class UserDto { final String id; final String name; final String email; const UserDto({ required this.id, required this.name, required this.email, }); factory UserDto.fromJson(Map json) { return UserDto( id: json['id'] as String, name: json['name'] as String, email: json['email'] as String, ); } } ``` ### 4. Use the New Endpoint ```dart final result = await client.getUser('user-123'); result.when( success: (user) => print('User: ${user.name} (${user.email})'), error: (error) => print('Error: ${error.message}'), ); ``` --- ## ๐Ÿ›ก๏ธ Type Safety Standards ### Strict Typing Rules All code follows these mandatory rules (see `.claude-docs/strict-typing.md`): - โœ… **NO** `dynamic` types - โœ… **NO** `any` types - โœ… **NO** untyped `var` declarations - โœ… All functions have explicit return types - โœ… All parameters have explicit types - โœ… Proper generics and interfaces ### Serializable Interface All queries, commands, and DTOs implement `Serializable`: ```dart abstract interface class Serializable { Map toJson(); } // Example implementation class HealthQuery implements Serializable { const HealthQuery(); @override Map toJson() => {}; // Empty for parameterless queries } ``` ### Result Type (Functional Error Handling) Never use try-catch for API calls. Use `Result`: ```dart // โŒ DON'T DO THIS try { final user = await someApiCall(); print(user.name); } catch (e) { print('Error: $e'); } // โœ… DO THIS final result = await client.getUser('123'); result.when( success: (user) => print(user.name), error: (error) => print('Error: ${error.message}'), ); // Or pattern matching final message = switch (result) { ApiSuccess(value: final user) => 'Hello ${user.name}', ApiError(error: final err) => 'Error: ${err.message}', }; ``` --- ## ๐Ÿงช Testing ### Unit Tests ```dart import 'package:flutter_test/flutter_test.dart'; import 'package:console/api/api.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; void main() { group('CqrsApiClient', () { test('executeQuery returns success', () async { final mockClient = MockClient((request) async { return http.Response('true', 200); }); final client = CqrsApiClient( config: ApiClientConfig.development, httpClient: mockClient, ); final result = await client.checkHealth(); expect(result.isSuccess, true); expect(result.value, true); }); test('executeQuery handles errors', () async { final mockClient = MockClient((request) async { return http.Response( '{"message": "Server error"}', 500, ); }); final client = CqrsApiClient( config: ApiClientConfig.development, httpClient: mockClient, ); final result = await client.checkHealth(); expect(result.isError, true); expect(result.error.statusCode, 500); }); }); } ``` ### Integration Tests ```dart void main() { testWidgets('Health check integration', (tester) async { final client = CqrsApiClient( config: ApiClientConfig( baseUrl: 'http://localhost:5246', // Backend running locally timeout: Duration(seconds: 5), ), ); final result = await client.checkHealth(); expect(result.isSuccess, true); expect(result.value, true); client.dispose(); }); } ``` --- ## ๐Ÿ“‹ Workflow ### Daily Development 1. **Pull latest backend changes:** ```bash git pull ``` 2. **Check for API updates:** ```bash # Check if backend updated openapi.json git log --oneline docs/openapi.json ``` 3. **Update contract if needed:** ```bash cp ../backend/docs/openapi.json ./api-schema.json ./scripts/update_api_client.sh ``` 4. **Check for breaking changes:** ```bash # Read backend's CHANGELOG cat ../backend/docs/CHANGELOG.md ``` 5. **Run tests:** ```bash flutter test ``` ### When Backend Changes API **Backend notifies:** "Updated API contract - added CreateUser endpoint" ```bash # 1. Pull changes git pull # 2. Update contract cp ../backend/docs/openapi.json ./api-schema.json # 3. Regenerate types ./scripts/update_api_client.sh # 4. Add endpoint extension (if needed) # Edit lib/api/endpoints/user_endpoint.dart # 5. Test flutter test # 6. Commit git add . git commit -m "feat: Add CreateUser endpoint integration" ``` --- ## ๐Ÿ” Error Handling ### Error Types ```dart enum ApiErrorType { network, // No internet/DNS failure http, // 4xx/5xx responses serialization, // JSON parsing failed timeout, // Request took too long validation, // Backend validation (422) unknown, // Unexpected errors } ``` ### Error Information ```dart class ApiErrorInfo { final String message; // Human-readable error final int? statusCode; // HTTP status (if applicable) final String? details; // Additional context final ApiErrorType type; // Error category } ``` ### Handling Errors ```dart final result = await client.executeQuery(...); result.when( success: (data) { // Handle success }, error: (error) { switch (error.type) { case ApiErrorType.network: showSnackbar('No internet connection'); case ApiErrorType.timeout: showSnackbar('Request timed out - try again'); case ApiErrorType.validation: showValidationErrors(error.details); case ApiErrorType.http: if (error.statusCode == 401) { navigateToLogin(); } else { showSnackbar('Server error: ${error.message}'); } default: showSnackbar('Unexpected error: ${error.message}'); } }, ); ``` --- ## โš™๏ธ Configuration ### Development ```dart const ApiClientConfig.development = ApiClientConfig( baseUrl: 'http://localhost:5246', timeout: Duration(seconds: 30), defaultHeaders: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, ); ``` ### Production ```dart final config = ApiClientConfig( baseUrl: 'https://api.svrnty.com', timeout: Duration(seconds: 30), defaultHeaders: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'Authorization': 'Bearer $accessToken', }, ); ``` ### Android Emulator When testing on Android emulator, use special localhost IP: ```dart final config = ApiClientConfig( baseUrl: 'http://10.0.2.2:5246', // Emulator's host machine timeout: Duration(seconds: 30), ); ``` --- ## ๐Ÿ“ File Structure ``` lib/api/ โ”œโ”€โ”€ api.dart # Public API exports (use this!) โ”œโ”€โ”€ client.dart # CQRS client implementation โ”œโ”€โ”€ types.dart # Core types (Result, Serializable, etc.) โ”œโ”€โ”€ openapi_config.dart # Code generation config โ”œโ”€โ”€ generated/ # Auto-generated code (git-ignored) โ”‚ โ””โ”€โ”€ .gitkeep โ””โ”€โ”€ endpoints/ # Endpoint extensions โ””โ”€โ”€ health_endpoint.dart # Health check scripts/ โ”œโ”€โ”€ update_api_client.sh # Regenerate from OpenAPI spec โ””โ”€โ”€ verify_api_types.sh # Validate type safety docs/ โ””โ”€โ”€ api-schema.json # OpenAPI contract (from backend) .claude-docs/ โ”œโ”€โ”€ api-contract-workflow.md # Detailed workflow guide โ””โ”€โ”€ strict-typing.md # Type safety standards ``` --- ## ๐Ÿšจ Troubleshooting ### "Type errors after regenerating" Backend may have made breaking changes: ```bash # Check backend changelog cat ../backend/docs/CHANGELOG.md # Review breaking changes and update code accordingly ``` ### "Network error on real device" Check baseUrl configuration: ```dart // iOS/Real device: Use actual IP or domain final config = ApiClientConfig( baseUrl: 'http://192.168.1.100:5246', // Your machine's IP ); // Android emulator: Use special IP final config = ApiClientConfig( baseUrl: 'http://10.0.2.2:5246', ); ``` ### "Code generation fails" ```bash # Clean and rebuild flutter clean flutter pub get flutter pub run build_runner build --delete-conflicting-outputs ``` ### "JSON parsing error" Backend response doesn't match expected type: 1. Check `api-schema.json` matches backend's `docs/openapi.json` 2. Verify DTO `fromJson` matches OpenAPI schema 3. Check backend returned correct content-type --- ## ๐Ÿ“Š Status | Metric | Status | |--------|--------| | Backend Compatibility | โœ… 100% | | Type Safety | โœ… Zero dynamic types | | Static Analysis | โœ… 0 errors | | CQRS Patterns | โœ… All supported | | Error Handling | โœ… Comprehensive | | Documentation | โœ… Complete | | Testing | โœ… Unit + Integration | | Production Ready | โœ… Yes | --- ## ๐Ÿ“– Additional Resources - **Workflow Guide:** `.claude-docs/api-contract-workflow.md` (comprehensive) - **Type Safety:** `.claude-docs/strict-typing.md` (mandatory rules) - **Backend Docs:** `../backend/docs/` (architecture, changelog) - **OpenAPI Spec:** `api-schema.json` (contract source of truth) --- ## ๐ŸŽฏ Key Takeaways 1. **OpenAPI is Source of Truth** - Always regenerate from `api-schema.json` 2. **CQRS Pattern** - All endpoints use JSON body (even empty `{}`) 3. **Type Safety** - No dynamic types, use Serializable interface 4. **Functional Errors** - Use Result, not try-catch 5. **Monitor CHANGELOG** - Backend documents breaking changes 6. **Test Everything** - Unit tests + integration tests --- **Last Updated:** 2025-10-26 **Backend Version:** 1.0 (OpenAPI 3.0.1) **Frontend Version:** 1.0.0+1