
Declarative API client with typed responses, retries, and interceptors
Add to your pubspec.yaml:
dependencies:
philiprehberger_api_client: ^0.2.0
Then run:
dart pub get
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();
// 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 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');
// GET with typed response
final user = await client.getTyped<User>(
'/users/1',
decoder: (json) => User.fromJson(json),
);
// POST with typed response
final created = await client.postTyped<User>(
'/users',
body: {'name': 'Alice'},
decoder: (json) => User.fromJson(json),
);
// Cache GET responses in memory
final cache = CacheInterceptor(
ttl: const Duration(minutes: 5),
maxEntries: 100,
);
client.addInterceptor(cache);
// Invalidate specific paths
cache.invalidate('/users');
// Clear entire cache
cache.clearAll();
// 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;
}
}
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}');
}
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
| 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 |
getTyped<T>(String path, {required T Function(Map<String, dynamic>) decoder, Map<String, String>? query, Map<String, String>? headers}) |
Send a GET request and deserialize the response |
postTyped<T>(String path, {required T Function(Map<String, dynamic>) decoder, Object? body, Map<String, String>? headers}) |
Send a POST request and deserialize the response |
addInterceptor(Interceptor interceptor) |
Add a request/response interceptor |
removeInterceptor(Interceptor interceptor) |
Remove an interceptor |
close() |
Close the underlying HTTP client |
| 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 |
| 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 |
| 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 |
| 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 |
| Class |
Description |
HeaderInterceptor(Map<String, String> headers) |
Adds headers to every request |
LogInterceptor(void Function(String) log) |
Logs requests and responses to a callback |
CacheInterceptor({Duration ttl, int maxEntries}) |
Caches GET responses in memory with TTL and size limit |
| 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 |
dart pub get
dart analyze --fatal-infos
dart test
If you find this project useful:
⭐ Star the repo
🐛 Report issues
💡 Suggest features
❤️ Sponsor development
🌐 All Open Source Projects
💻 GitHub Profile
🔗 LinkedIn Profile
MIT