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>
646 lines
14 KiB
Markdown
646 lines
14 KiB
Markdown
# 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<UserDto>(
|
|
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<UserDto>(
|
|
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
|
|
/// <summary>Gets user by ID</summary>
|
|
/// <response code="200">Returns user details</response>
|
|
/// <response code="404">User not found</response>
|
|
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<Result<UserDto>> getUser(String userId) async {
|
|
return executeQuery<UserDto>(
|
|
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<String, Object?> 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<String, Object?> 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<String, Object?> toJson();
|
|
}
|
|
|
|
// Example implementation
|
|
class HealthQuery implements Serializable {
|
|
const HealthQuery();
|
|
|
|
@override
|
|
Map<String, Object?> toJson() => {}; // Empty for parameterless queries
|
|
}
|
|
```
|
|
|
|
### Result Type (Functional Error Handling)
|
|
|
|
Never use try-catch for API calls. Use `Result<T>`:
|
|
|
|
```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<T>, 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
|