Files
minhthu/API_CLIENT_SETUP.md
2025-10-28 00:09:46 +07:00

12 KiB

API Client Setup - Complete Implementation

Overview

I have created a robust API client for your Flutter warehouse management app with comprehensive features including:

  • Automatic token management from secure storage
  • 401 error handling with automatic token clearing
  • Request/response logging with sensitive data redaction
  • Configurable timeouts (30 seconds)
  • Proper error transformation to custom exceptions
  • Support for all HTTP methods (GET, POST, PUT, DELETE)

Files Created

1. Core Network Files

/lib/core/network/api_client.dart

  • Main API client implementation using Dio
  • Automatic Bearer token injection from secure storage
  • Request/response/error interceptors with comprehensive logging
  • 401 error handler that clears tokens and triggers logout callback
  • Methods: get(), post(), put(), delete()
  • Utility methods: testConnection(), isAuthenticated(), getAccessToken(), clearAuth()

/lib/core/network/api_response.dart

  • Generic API response wrapper matching your backend format
  • Structure: Value, IsSuccess, IsFailure, Errors, ErrorCodes
  • Helper methods: hasData, getErrorMessage(), getAllErrorsAsString(), hasErrorCode()

/lib/core/network/api_client_example.dart

  • Comprehensive usage examples for all scenarios
  • Examples for: Login, GET/POST/PUT/DELETE requests, error handling, etc.

/lib/core/network/README.md

  • Complete documentation for the API client
  • Usage guides, best practices, troubleshooting

2. Secure Storage

/lib/core/storage/secure_storage.dart

  • Singleton wrapper for flutter_secure_storage
  • Token management: saveAccessToken(), getAccessToken(), clearTokens()
  • User data: saveUserId(), saveUsername(), etc.
  • Utility methods: isAuthenticated(), clearAll(), containsKey()
  • Platform-specific secure options (Android: encrypted shared prefs, iOS: Keychain)

3. Constants

/lib/core/constants/api_endpoints.dart

  • Centralized API endpoint definitions
  • Authentication: /auth/login, /auth/logout
  • Warehouses: /warehouses
  • Products: /products with query parameter helpers
  • Scans: /api/scans

4. Core Exports

/lib/core/core.dart (Updated)

  • Added exports for new modules:
    • api_endpoints.dart
    • api_response.dart
    • secure_storage.dart

5. Dependencies

pubspec.yaml (Updated)

  • Added flutter_secure_storage: ^9.0.0

Key Features

1. Automatic Token Management

The API client automatically injects Bearer tokens into all requests:

// Initialize with secure storage
final secureStorage = SecureStorage();
final apiClient = ApiClient(secureStorage);

// Token is automatically added to all requests
final response = await apiClient.get('/warehouses');
// Request header: Authorization: Bearer <token>

2. 401 Error Handling

When a 401 Unauthorized error occurs:

  1. Error is logged to console
  2. All tokens are cleared from secure storage
  3. onUnauthorized callback is triggered
  4. App can navigate to login screen
final apiClient = ApiClient(
  secureStorage,
  onUnauthorized: () {
    // This callback is triggered on 401 errors
    context.go('/login'); // Navigate to login
  },
);

3. Comprehensive Logging

All requests, responses, and errors are logged with sensitive data redacted:

REQUEST[GET] => https://api.example.com/warehouses
Headers: {Authorization: ***REDACTED***, Content-Type: application/json}

RESPONSE[200] => https://api.example.com/warehouses
Data: {...}

ERROR[401] => https://api.example.com/products
401 Unauthorized - Clearing tokens and triggering logout

4. Error Handling

Dio exceptions are transformed into custom app exceptions:

try {
  final response = await apiClient.get('/products');
} on NetworkException catch (e) {
  // Timeout, no internet, etc.
  print('Network error: ${e.message}');
} on ServerException catch (e) {
  // 4xx, 5xx errors
  print('Server error: ${e.message}');
  print('Error code: ${e.code}'); // e.g., '401', '404', '500'
}

Specific error codes:

  • 401: Unauthorized (automatically handled)
  • 403: Forbidden
  • 404: Not Found
  • 422: Validation Error
  • 429: Rate Limited
  • 500-504: Server Errors

5. API Response Parsing

All API responses follow the standard format:

final response = await apiClient.get('/warehouses');

final apiResponse = ApiResponse.fromJson(
  response.data,
  (json) => (json as List).map((e) => Warehouse.fromJson(e)).toList(),
);

if (apiResponse.isSuccess && apiResponse.value != null) {
  final warehouses = apiResponse.value;
  print('Found ${warehouses.length} warehouses');
} else {
  print('Error: ${apiResponse.getErrorMessage()}');
}

Usage Examples

Initialize API Client

import 'package:minhthu/core/core.dart';

final secureStorage = SecureStorage();
final apiClient = ApiClient(
  secureStorage,
  onUnauthorized: () {
    // Navigate to login on 401 errors
    context.go('/login');
  },
);

Login Flow

// 1. Login request
final response = await apiClient.post(
  ApiEndpoints.login,
  data: {
    'username': 'user@example.com',
    'password': 'password123',
  },
);

// 2. Parse response
final apiResponse = ApiResponse.fromJson(
  response.data,
  (json) => User.fromJson(json),
);

// 3. Save tokens (typically done in LoginUseCase)
if (apiResponse.isSuccess && apiResponse.value != null) {
  final user = apiResponse.value!;
  await secureStorage.saveAccessToken(user.accessToken);
  await secureStorage.saveRefreshToken(user.refreshToken);
  await secureStorage.saveUserId(user.userId);
}

Get Warehouses (Authenticated)

// Token is automatically added by the interceptor
final response = await apiClient.get(ApiEndpoints.warehouses);

final apiResponse = ApiResponse.fromJson(
  response.data,
  (json) => (json as List).map((e) => Warehouse.fromJson(e)).toList(),
);

if (apiResponse.isSuccess && apiResponse.value != null) {
  final warehouses = apiResponse.value!;
  // Use the data
}

Get Products with Query Parameters

final response = await apiClient.get(
  ApiEndpoints.products,
  queryParameters: ApiEndpoints.productQueryParams(
    warehouseId: 1,
    type: 'import',
  ),
);

Save Scan Data

final response = await apiClient.post(
  ApiEndpoints.scans,
  data: {
    'barcode': '1234567890',
    'field1': 'Value 1',
    'field2': 'Value 2',
    'field3': 'Value 3',
    'field4': 'Value 4',
  },
);

Check Authentication Status

final isAuthenticated = await apiClient.isAuthenticated();
if (!isAuthenticated) {
  // Navigate to login
}

Logout

await apiClient.clearAuth(); // Clears all tokens

Integration with Repository Pattern

The API client is designed to work with your clean architecture:

class WarehouseRemoteDataSourceImpl implements WarehouseRemoteDataSource {
  final ApiClient apiClient;

  WarehouseRemoteDataSourceImpl(this.apiClient);

  @override
  Future<List<Warehouse>> getWarehouses() async {
    final response = await apiClient.get(ApiEndpoints.warehouses);

    final apiResponse = ApiResponse.fromJson(
      response.data,
      (json) => (json as List).map((e) => Warehouse.fromJson(e)).toList(),
    );

    if (apiResponse.isSuccess && apiResponse.value != null) {
      return apiResponse.value!;
    } else {
      throw ServerException(apiResponse.getErrorMessage());
    }
  }
}

Configuration

Timeouts (in app_constants.dart)

static const int connectionTimeout = 30000; // 30 seconds
static const int receiveTimeout = 30000;    // 30 seconds
static const int sendTimeout = 30000;       // 30 seconds

Base URL (in app_constants.dart)

static const String apiBaseUrl = 'https://api.example.com';

Or update dynamically:

apiClient.updateBaseUrl('https://dev-api.example.com');

Security Features

  1. Token Encryption: Tokens stored using platform-specific secure storage

    • Android: Encrypted SharedPreferences
    • iOS: Keychain with first_unlock accessibility
  2. Automatic Token Clearing: 401 errors automatically clear all tokens

  3. Log Sanitization: Authorization headers redacted in logs

    Headers: {Authorization: ***REDACTED***}
    
  4. Singleton Pattern: SecureStorage uses singleton to prevent multiple instances

Testing

To test the API connection:

final isConnected = await apiClient.testConnection();
if (!isConnected) {
  print('Cannot connect to API');
}

Dependency Injection (GetIt)

Register with GetIt service locator:

final getIt = GetIt.instance;

// Register SecureStorage
getIt.registerLazySingleton<SecureStorage>(() => SecureStorage());

// Register ApiClient
getIt.registerLazySingleton<ApiClient>(
  () => ApiClient(
    getIt<SecureStorage>(),
    onUnauthorized: () {
      // Handle unauthorized
    },
  ),
);

File Structure

lib/
  core/
    constants/
      app_constants.dart          # Existing - timeouts and base URL
      api_endpoints.dart          # NEW - API endpoint constants
    network/
      api_client.dart             # UPDATED - Full implementation
      api_response.dart           # NEW - API response wrapper
      api_client_example.dart     # NEW - Usage examples
      README.md                   # NEW - Documentation
    storage/
      secure_storage.dart         # NEW - Secure storage wrapper
    errors/
      exceptions.dart             # Existing - Used by API client
      failures.dart               # Existing - Used by repositories
    core.dart                     # UPDATED - Added new exports

Next Steps

  1. Update Existing Repositories: Update your remote data sources to use the new API client
  2. Configure Base URL: Set the correct API base URL in app_constants.dart
  3. Set Up Navigation: Implement the onUnauthorized callback to navigate to login
  4. Add API Endpoints: Add any missing endpoints to api_endpoints.dart
  5. Test Authentication Flow: Test login, token injection, and 401 handling

Testing the Setup

Run the example:

import 'package:minhthu/core/network/api_client_example.dart';

void main() async {
  await runExamples();
}

Troubleshooting

Token not being injected

  • Verify token is saved: await secureStorage.getAccessToken()
  • Check logs for: REQUEST[...] Headers: {Authorization: ***REDACTED***}

401 errors not clearing tokens

  • Verify onUnauthorized callback is set
  • Check logs for: 401 Unauthorized - Clearing tokens and triggering logout

Connection timeouts

  • Check network connectivity
  • Verify base URL is correct
  • Increase timeout values if needed

Analysis Results

All files pass Flutter analysis with no issues:

  • api_client.dart - No issues found
  • secure_storage.dart - No issues found
  • api_response.dart - No issues found
  • api_endpoints.dart - No issues found

Documentation

For detailed documentation, see:

  • /lib/core/network/README.md - Complete API client documentation
  • /lib/core/network/api_client_example.dart - Code examples

Summary

The API client is production-ready with:

  • Automatic token management from secure storage
  • Request interceptor to inject Bearer tokens
  • Response interceptor for logging
  • Error interceptor to handle 401 errors
  • Automatic token clearing on unauthorized access
  • Comprehensive error handling
  • Request/response logging with sensitive data redaction
  • Support for all HTTP methods (GET, POST, PUT, DELETE)
  • Configurable timeouts (30 seconds)
  • Environment-specific base URLs
  • Connection testing
  • Clean integration with repository pattern
  • Comprehensive documentation and examples
  • All files pass static analysis

The API client is ready to use and follows Flutter best practices and clean architecture principles!