philiprehberger_api_client

Declarative API client with typed responses, retries, and interceptors
Requirements
Installation
Add to your pubspec.yaml:
dependencies:
philiprehberger_api_client: ^0.1.0
Then run:
dart pub get
Usage
import 'package:philiprehberger_api_client/api_client.dart';
final client = ApiClient(baseUrl: 'https://api.example.com');
final response = await client.get('/users');
print(response.jsonList);
client.close();
GET Requests
// Simple GET
final response = await client.get('/users');
// GET with query parameters
final filtered = await client.get('/users', query: {'role': 'admin'});
// Access typed response data
final users = response.jsonList;
final user = response.jsonMap;
POST, PUT, PATCH Requests
// POST with JSON body (automatically serialized)
final created = await client.post('/users', body: {
'name': 'Alice',
'email': 'alice@example.com',
});
// PUT request
await client.put('/users/1', body: {'name': 'Updated'});
// PATCH request
await client.patch('/users/1', body: {'email': 'new@example.com'});
// DELETE request
await client.delete('/users/1');
Interceptors
// Add headers to every request
client.addInterceptor(HeaderInterceptor({
'Authorization': 'Bearer token',
'Accept': 'application/json',
}));
// Log requests and responses
client.addInterceptor(LogInterceptor(print));
// Custom interceptor
class AuthRefreshInterceptor extends Interceptor {
@override
ApiRequest onRequest(ApiRequest request) {
return request.withHeaders({'Authorization': 'Bearer $token'});
}
@override
void onError(Object error) {
if (error is HttpError && error.statusCode == 401) {
// Handle token refresh
}
throw error;
}
}
Retry Configuration
final client = ApiClient(
baseUrl: 'https://api.example.com',
retryConfig: const RetryConfig(
maxAttempts: 3,
initialDelay: Duration(milliseconds: 500),
backoffMultiplier: 2.0,
retryableStatuses: {408, 429, 500, 502, 503, 504},
),
);
Error Handling
try {
final response = await client.get('/users');
} on HttpError catch (e) {
print('HTTP ${e.statusCode}');
} on TimeoutError catch (e) {
print('Request timed out: ${e.message}');
} on RetryExhaustedError catch (e) {
print('Failed after ${e.attempts} attempts');
} on ApiError catch (e) {
print('API error: ${e.message}');
}
Response Inspection
final response = await client.get('/users/1');
// Status helpers
response.isSuccess; // true for 2xx
response.isClientError; // true for 4xx
response.isServerError; // true for 5xx
// Typed JSON access
response.json; // dynamic
response.jsonMap; // Map<String, dynamic>
response.jsonList; // List<dynamic>
// Metadata
response.statusCode; // 200
response.headers; // Map<String, String>
response.duration; // Duration
API
ApiClient
| Method / Property | Description |
|---|
ApiClient({required String baseUrl, Duration timeout, RetryConfig? retryConfig, http.Client? httpClient}) | Create an API client |
get(String path, {Map<String, String>? query, Map<String, String>? headers}) | Send a GET request |
post(String path, {Object? body, Map<String, String>? headers}) | Send a POST request |
put(String path, {Object? body, Map<String, String>? headers}) | Send a PUT request |
patch(String path, {Object? body, Map<String, String>? headers}) | Send a PATCH request |
delete(String path, {Map<String, String>? headers}) | Send a DELETE request |
addInterceptor(Interceptor interceptor) | Add a request/response interceptor |
removeInterceptor(Interceptor interceptor) | Remove an interceptor |
close() | Close the underlying HTTP client |
ApiRequest
| Method / Property | Description |
|---|
ApiRequest({required String method, required Uri uri, Map<String, String> headers, String? body}) | Create an API request |
method | The HTTP method |
uri | The fully resolved URI |
headers | The request headers |
body | The request body |
withHeaders(Map<String, String> extra) | Create a copy with merged headers |
ApiResponse
| Method / Property | Description |
|---|
statusCode | The HTTP status code |
headers | The response headers |
body | The response body as a string |
duration | The time taken to complete the request |
isSuccess | Whether the status code is 2xx |
isClientError | Whether the status code is 4xx |
isServerError | Whether the status code is 5xx |
json | Decode the body as JSON (dynamic) |
jsonMap | Decode the body as a JSON map |
jsonList | Decode the body as a JSON list |
Error Types
| Class | Description |
|---|
ApiError | Base error for API operations |
HttpError | Thrown for non-2xx status codes; exposes statusCode |
TimeoutError | Thrown when a request exceeds the timeout |
RetryExhaustedError | Thrown when all retry attempts fail; exposes attempts and lastError |
Interceptor
| Method | Description |
|---|
onRequest(ApiRequest request) | Called before a request is sent; return a modified or same request |
onResponse(ApiResponse response) | Called after a response is received; return a modified or same response |
onError(Object error) | Called on error; can throw or handle |
Built-in Interceptors
| Class | Description |
|---|
HeaderInterceptor(Map<String, String> headers) | Adds headers to every request |
LogInterceptor(void Function(String) log) | Logs requests and responses to a callback |
RetryConfig
| Method / Property | Description |
|---|
RetryConfig({int maxAttempts, Duration initialDelay, double backoffMultiplier, Set<int> retryableStatuses}) | Create retry configuration |
maxAttempts | Maximum number of retry attempts (default: 3) |
initialDelay | Initial delay before the first retry (default: 500ms) |
backoffMultiplier | Multiplier for each subsequent retry (default: 2.0) |
retryableStatuses | Status codes that trigger a retry (default: 408, 429, 500, 502, 503, 504) |
delayForAttempt(int attempt) | Calculate delay for a given attempt number |
shouldRetry(int statusCode) | Whether a status code should trigger a retry |
Development
dart pub get
dart analyze --fatal-infos
dart test
Support
If you find this project useful:
⭐ Star the repo
🐛 Report issues
💡 Suggest features
❤️ Sponsor development
🌐 All Open Source Projects
💻 GitHub Profile
🔗 LinkedIn Profile
License
MIT