Skip to content

🌐 API Management

Overview

Use Dio, Retrofit and Retrofit Generator for API handling.

The API management is located in lib/shared/data/network/ directory, which contains:

  • api_endpoints.dart: Contains all API endpoints
  • api_response.dart: Generic response wrapper with error handling
  • cancellable_repository.dart: Mixin for handling cancellable requests
  • error_response.dart: API error response model with utility methods
  • response_status.dart: Constants for status codes and error identifiers
  • interceptor.dart: Custom Dio interceptor for authentication and error handling

Request/Response Format

Standard Response Structure

The API uses a generic ApiResponse<T> class where:

  • T represents the main data type
dart
class ApiResponse<T> {
  Status? _status;
  T? data;
  String? errorMessage;
  ErrorResponse? errorData;
}

Status Types

dart
enum Status { 
  completed,      // Successful response
  error,          // Error occurred
  sessionExpired  // Authentication token expired
  // ...etc.
}

Code Standards

1. Adding New API Endpoints

Step 1: Add endpoint constant Add your endpoint to api_endpoints.dart:

dart
class ApiEndpoints {
  static const String myNewFeature = '/api/my-new-feature';
}

Step 2: Create service method In your feature's service file, add the API method:

dart
@GET(ApiEndpoints.myNewFeature)
Future<ApiResponse<MyModel, MyExtraData>> getMyData(
  @Query('param') String param,
);

Step 3: Generate code Run the build runner command:

bash
flutter pub run build_runner build --delete-conflicting-outputs

Step 4: Implement repository method In your feature's repository, handle the response:

dart
Future<Either<Failure, MyEntity>> getMyData(String param) async {
  final response = await _service.getMyData(param);
  
  if (response.hasSucceeded) {
    return Right(response.data!.toEntity());
  } else {
    return response.onFail();
  }
}

2. Error Handling Guidelines

  • Always use the onFail() extension method for error handling
  • Never manually handle errors - use the built-in error handling
  • Return Left with appropriate Failure type - the extension handles this automatically
  • Never use ! operator on API responses without checking hasSucceeded first

Correct approach:

dart
Future<Either<Failure, MyEntity>> getData() async {
  final response = await _service.getData();
  if (response.hasSucceeded) {
    return Right(response.data!.toEntity());
  } else {
    return response.onFail();
  }
}

Wrong approach:

dart
// ❌ Don't do this
final data = response.data!;  // May crash if response failed

3. Authentication

  • Authentication tokens are automatically added by the interceptor
  • Don't manually add authorization headers in service methods
  • The interceptor handles token refresh and session management
  • If you need custom headers, add them in interceptor.dart

4. HTTP Methods

Use the appropriate HTTP method annotations:

dart
@GET(ApiEndpoints.endpoint)           // For GET requests
@POST(ApiEndpoints.endpoint)           // For POST requests
@PUT(ApiEndpoints.endpoint)           // For PUT requests
@DELETE(ApiEndpoints.endpoint)        // For DELETE requests
@PATCH(ApiEndpoints.endpoint)          // For PATCH requests

5. Request Parameters

Use appropriate parameter annotations:

dart
@Query('param') String param           // Query parameters
@Path('id') String id                  // Path parameters
@Body() Map<String, dynamic> body      // Request body
@Header('Custom-Header') String header // Custom headers

6. Cancellable Requests

  • Always extend CancellableRepository mixin when creating repositories
  • Use CancelToken for requests that can be cancelled
  • Cancel requests when navigating away from a screen to avoid memory leaks

Example:

dart
class MyRepository with CancellableRepository {
  late CancelToken _cancelToken;
  
  Future<Either<Failure, MyEntity>> getData() async {
    _cancelToken = CancelToken();
    final response = await _service.getData(cancelToken: _cancelToken);
    // ... handle response
  }
}

7. Response Data Transformation

  • Always convert models to entities using toEntity() method
  • Never expose models directly to the domain/presentation layer
  • Create entity classes in the domain layer for each model

Example:

dart
// In repository
if (response.hasSucceeded) {
  return Right(response.data!.toEntity());
}

// In domain layer
abstract class MyEntity {
  final String id;
  final String name;
  
  MyEntity({required this.id, required this.name});
}

Best Practices

Do's ✅

  • Use generic ApiResponse<T, U> for all API responses
  • Always check hasSucceeded before accessing data
  • Use onFail() extension for error handling
  • Convert models to entities in the repository layer
  • Add endpoints to api_endpoints.dart for consistency
  • Run build_runner after adding new service methods
  • Use appropriate HTTP method annotations
  • Document complex API calls with comments

Don'ts ❌

  • Don't use ! operator without checking response status
  • Don't manually handle errors when onFail() is available
  • Don't expose models directly to other layers
  • Don't add authorization headers manually in services
  • Don't create API methods without annotations
  • Don't skip running build_runner
  • Don't ignore the status enum values
  • Don't forget to cancel requests when navigating away

Error Types

The system handles these error types automatically:

  • ConnectionFailure: No internet connection
  • ServerFailure: Server-side errors
  • DevelopmentFailure: Development environment errors
  • SessionExpired: Authentication token expired
  • CancelledRequests: Request was cancelled

Use the onFail() extension method which handles all these cases automatically.

Testing API Calls

When writing tests for API calls:

dart
test('should return entity when API call is successful', () async {
  // Arrange
  when(() => mockService.getData()).thenAnswer(
    (_) async => ApiResponse<MyModel, dynamic>(
      data: MyModel(id: '1', name: 'Test'),
      hasSucceeded: true,
    ),
  );
  
  // Act
  final result = await repository.getData();
  
  // Assert
  expect(result, isA<Right>());
  final entity = result.getOrElse(() => throw Exception('Expected Right'));
  expect(entity.name, 'Test');
});