HTTP Nexus

pub package License: MIT

A lightweight HTTP client wrapper for Flutter that handles the tedious parts of API communication. It automatically retries failed requests, manages errors gracefully, and helps you write cleaner networking code.

Why HTTP Nexus?

Working with APIs in Flutter often means writing the same boilerplate code repeatedly: retry logic, timeout handling, error parsing, and logging. This package takes care of all that so you can focus on building features.

Features

Core functionality:

  • Automatic retry with exponential backoff
  • Clean error types for different failure scenarios
  • Request and response interceptors
  • Timeout management
  • Request cancellation
  • Automatic JSON encoding and decoding

Advanced capabilities:

  • Offline request queue (stores requests when offline, replays when connected)
  • Built-in logging for debugging
  • Rate limiting to prevent API abuse
  • Global configuration with sensible defaults
  • Authentication token management

Installation

Add this to your package's pubspec.yaml file:

dependencies:
  http_nexus: ^1.0.0

Then run:

flutter pub get

Quick Start

import 'package:http_nexus/http_nexus.dart';

// Create a client
final client = SmartApiClient(
  config: ApiClientConfig(
    baseUrl: 'https://api.example.com',
  ),
);

// Make a GET request
final users = await client.get('/users');

// Make a POST request
final newUser = await client.post(
  '/users',
  body: {
    'name': 'John Doe',
    'email': 'john@example.com',
  },
);

Basic Usage

Configuration

final config = ApiClientConfig(
  baseUrl: 'https://api.example.com',
  defaultTimeout: Duration(seconds: 30),
  defaultHeaders: {
    'Accept': 'application/json',
  },
  retryPolicy: RetryPolicy(
    maxRetries: 3,
    initialDelay: Duration(seconds: 1),
    backoffMultiplier: 2.0,
  ),
  enableLogging: true,
);

final client = SmartApiClient(config: config);

Making Requests

GET Request

try {
  final response = await client.get('/posts/1');
  print('Title: ${response['title']}');
} on ServerException catch (e) {
  print('Server error: ${e.statusCode}');
} on NetworkException catch (e) {
  print('Network error: ${e.message}');
}

POST Request

final newPost = await client.post(
  '/posts',
  body: {
    'title': 'My Post',
    'body': 'Post content',
    'userId': 1,
  },
);

PUT Request

final updated = await client.put(
  '/posts/1',
  body: {
    'title': 'Updated Title',
  },
);

DELETE Request

await client.delete('/posts/1');

Query Parameters

final results = await client.get(
  '/search',
  queryParameters: {
    'q': 'flutter',
    'limit': 10,
  },
);

Custom Headers

final response = await client.get(
  '/protected',
  headers: {
    'Authorization': 'Bearer token123',
  },
);

Request Cancellation

// Start request with a cancel token
final cancelToken = 'my-request-1';
final future = client.get('/slow-endpoint', cancelToken: cancelToken);

// Cancel the request
client.cancelRequest(cancelToken);

Using Interceptors

Logging Interceptor

final client = SmartApiClient(config: config);
client.addRequestInterceptor(LoggingInterceptor());

Authentication Interceptor

final authInterceptor = AuthInterceptor(
  tokenProvider: () async {
    // Return your auth token
    return await getAuthToken();
  },
);

client.addRequestInterceptor(authInterceptor);

Custom Interceptor

class CustomInterceptor implements RequestInterceptor {
  @override
  Future<void> onRequest(
    Uri url,
    HttpMethod method,
    Map<String, String> headers,
    dynamic body,
  ) async {
    // Add custom logic
    headers['X-Custom-Header'] = 'value';
  }
}

client.addRequestInterceptor(CustomInterceptor());

Retry Policy

final config = ApiClientConfig(
  baseUrl: 'https://api.example.com',
  retryPolicy: RetryPolicy(
    maxRetries: 5,
    initialDelay: Duration(milliseconds: 500),
    maxDelay: Duration(seconds: 10),
    backoffMultiplier: 2.0,
    shouldRetry: (exception, retryCount) {
      // Custom retry logic
      return exception is NetworkException || 
             exception is TimeoutException;
    },
  ),
);

Rate Limiting

final config = ApiClientConfig(
  baseUrl: 'https://api.example.com',
  maxRateLimitPerMinute: 60, // Max 60 requests per minute
);

Authentication Token Provider

final config = ApiClientConfig(
  baseUrl: 'https://api.example.com',
  authTokenProvider: () async {
    // Fetch token from secure storage
    return await SecureStorage.getToken();
  },
);

Offline Queue

When enabled, requests made while offline are automatically queued and retried when connection is restored:

final config = ApiClientConfig(
  baseUrl: 'https://api.example.com',
  enableOfflineQueue: true,
);

Advanced Features

Custom Timeout Per Request

final response = await client.get(
  '/slow-endpoint',
  timeout: Duration(seconds: 60),
);

Network Status Checking

import 'package:http_nexus/http_nexus.dart';

final networkChecker = NetworkChecker();

// Check if connected
final isConnected = await networkChecker.isConnected();

// Listen to connectivity changes
networkChecker.onConnectivityChanged.listen((status) {
  print('Connectivity changed: $status');
});

// Wait for connection
await networkChecker.waitForConnection(
  timeout: Duration(seconds: 30),
);

🎯 Error Handling

Smart API Client provides typed exceptions for different error scenarios:

try {
  final response = await client.get('/endpoint');
} on NetworkException catch (e) {
  // Network connectivity issues
  print('Network error: ${e.message}');
} on TimeoutException catch (e) {
  // Request timed out
  print('Timeout: ${e.message}');
} on ServerException catch (e) {
  // Server returned error status code
  print('Server error ${e.statusCode}: ${e.message}');
} on AuthenticationException catch (e) {
  // 401 - Authentication failed
  print('Auth failed: ${e.message}');
} on AuthorizationException catch (e) {
  // 403 - Insufficient permissions
  print('Access denied: ${e.message}');
} on RateLimitException catch (e) {
  // 429 - Rate limit exceeded
  print('Rate limit: ${e.message}');
} on ParseException catch (e) {
  // Failed to parse response
  print('Parse error: ${e.message}');
} on CancelledException catch (e) {
  // Request was cancelled
  print('Cancelled: ${e.message}');
} on ApiException catch (e) {
  // Catch-all for other API errors
  print('API error: ${e.message}');
}

API Reference

ApiClientConfig

Property Type Default Description
baseUrl String required Base URL for all requests
defaultTimeout Duration 30s Default request timeout
defaultHeaders Map<String, String> {} Headers added to all requests
retryPolicy RetryPolicy RetryPolicy() Configuration for retry logic
enableLogging bool false Enable request/response logging
enableOfflineQueue bool true Enable offline request queue
maxRateLimitPerMinute int? null Max requests per minute
authTokenProvider Function? null Function to provide auth token

RetryPolicy

Property Type Default Description
maxRetries int 3 Maximum retry attempts
initialDelay Duration 1s Initial delay before retry
maxDelay Duration 30s Maximum delay between retries
backoffMultiplier double 2.0 Exponential backoff multiplier
shouldRetry Function - Custom retry decision logic

SmartApiClient Methods

HTTP Methods

  • get(endpoint, {headers, queryParameters, timeout, cancelToken})
  • post(endpoint, {body, headers, queryParameters, timeout, cancelToken})
  • put(endpoint, {body, headers, queryParameters, timeout, cancelToken})
  • patch(endpoint, {body, headers, queryParameters, timeout, cancelToken})
  • delete(endpoint, {body, headers, queryParameters, timeout, cancelToken})

Interceptors

  • addRequestInterceptor(interceptor)
  • addResponseInterceptor(interceptor)

Cancellation

  • cancelRequest(cancelToken)

Cleanup

  • close() - Close client and cleanup resources

Best Practices

1. Create a single client instance

Avoid creating multiple client instances. Instead, create one and reuse it throughout your app:

// Good: Singleton instance
class ApiService {
  static final ApiService _instance = ApiService._internal();
  factory ApiService() => _instance;
  
  late final SmartApiClient client;
  
  ApiService._internal() {
    client = SmartApiClient(config: ApiClientConfig(
      baseUrl: 'https://api.example.com',
    ));
  }
}

// Usage
final api = ApiService().client;

2. Use specific error types

Handle errors specifically rather than catching all exceptions:

// Good: Handle specific errors differently
try {
  await client.get('/data');
} on AuthenticationException {
  navigateToLogin();
} on NetworkException {
  showOfflineBanner();
}

3. Configure timeouts appropriately

Different endpoints may need different timeouts:

// Good: Adjust timeout based on expected response time
await client.get('/fast', timeout: Duration(seconds: 5));
await client.post('/slow-upload', timeout: Duration(minutes: 2));

4. Always clean up resources

Close the client when you're done with it:

@override
void dispose() {
  client.close();
  super.dispose();
}

Common Use Cases

File Upload (with JSON)

final imageBase64 = base64Encode(imageBytes);
await client.post('/upload', body: {
  'filename': 'photo.jpg',
  'data': imageBase64,
});

Pagination

Future<List<Post>> loadPage(int page) async {
  final response = await client.get(
    '/posts',
    queryParameters: {
      'page': page,
      'limit': 20,
    },
  );
  return (response as List).map((json) => Post.fromJson(json)).toList();
}

Authentication Flow

Future<void> login(String email, String password) async {
  final response = await client.post('/auth/login', body: {
    'email': email,
    'password': password,
  });
  
  await saveToken(response['token']);
}

Testing

The package includes comprehensive unit tests. Run them with:

flutter test

Example App

Check out the example app in the /example folder for a complete working demonstration of all features.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Support

If you find this package helpful, please consider giving it a star on GitHub. If you encounter any issues or have questions, please open an issue on the repository.

Changelog

See CHANGELOG.md for a list of changes in each version.


Built with Flutter