op_rest_api_client
A lightweight, extensible Dart package for interacting with RESTful APIs.
Provides structured API requests, robust error handling, authentication support, and identity-based token management, while leveraging op_result for structured responses.
- 🔐 Authentication support: Integrates with
Identityfor token-based authentication, including automatic token refresh. - 🚧 Automatic token refresh: Automatically retries requests with a refreshed token upon receiving a
401 Unauthorized. - ❌ Robust error handling: Utilizes
OpResult<T>for structured success and failure responses. - 🎯 Type-safe API endpoints: Encourages defining API endpoints using enums or constants for better maintainability.
- ✅ Custom response validation: Allows fine-grained validation via
customValidator.
📦 Installation
Run the install command to always get the latest version:
dart pub add op_rest_api_client
Or add it manually to your pubspec.yaml:
dependencies:
op_rest_api_client: ^<latest version>
🛠️ Usage
1️⃣ Define API Endpoints as an Enum
enum MyApiEndpoints {
getUser,
updateUser,
deleteUser,
}
OpRestApiEndpoint getEndpoint(MyApiEndpoints endpoint) {
switch (endpoint) {
case MyApiEndpoints.getUser:
return OpRestApiEndpoint('/user', OpRestApiClientMethods.get);
case MyApiEndpoints.updateUser:
return OpRestApiEndpoint('/user/update', OpRestApiClientMethods.put);
case MyApiEndpoints.deleteUser:
return OpRestApiEndpoint('/user/delete', OpRestApiClientMethods.delete);
}
}
2️⃣ Initialize the API Client
import 'package:op_rest_api_client/op_rest_api_client.dart';
import 'package:http/http.dart' as http;
final apiClient = OpRestApiClient<MyApiEndpoints>(
baseUrl: "https://api.example.com",
client: http.Client(),
);
3️⃣ Send API Requests
🔹 GET Request
final result = await apiClient.send(
endpoint: getEndpoint(MyApiEndpoints.getUser),
identity: userIdentity, // Optional authentication
);
if (result.isSuccess) {
print("User data: \${result.data.body}");
} else {
print("Error: \${result.error.getErrorMessage()}");
}
🔹 POST Request (With Body)
final newUser = {
'name': 'John Doe',
'email': 'john.doe@example.com',
};
final result = await apiClient.send(
endpoint: getEndpoint(MyApiEndpoints.updateUser),
identity: userIdentity,
body: newUser,
);
if (result.isSuccess) {
print("User updated successfully!");
} else {
print("Error: \${result.error.getErrorMessage()}");
}
🔹 DELETE Request
final result = await apiClient.send(
endpoint: getEndpoint(MyApiEndpoints.deleteUser),
identity: userIdentity,
);
if (result.isSuccess) {
print("User deleted!");
} else {
print("Error: \${result.error.getErrorMessage()}");
}
📌 Authentication & Token Refresh
The client supports authentication via OpRestApiIdentity, handling token refresh automatically.
class MyApiIdentity extends OpRestApiIdentity<String> {
final String accessToken;
final String refreshToken;
MyApiIdentity({
required this.accessToken,
required this.refreshToken,
});
@override
String get identityData => accessToken;
@override
bool isValid() => accessToken.isNotEmpty;
@override
bool isExpired() {
// Example logic for expiration check (adjust as needed)
return false;
}
@override
Map<String, String>? headers() => {"Authorization": "Bearer $accessToken"};
@override
Future<OpRestApiIdentity<String>?> refresh() async {
// Refresh token logic (example)
return MyApiIdentity(
accessToken: "newAccessToken",
refreshToken: "newRefreshToken",
);
}
}
final userIdentity = MyApiIdentity(
accessToken: "abc123",
refreshToken: "refresh123",
);
If a request fails due to an expired token (401):
- The client automatically attempts token refresh once.
- If the refresh succeeds, the original request is retried automatically using the new token.
- If the refresh fails, an unauthenticated error (
OpError(AuthError.unauthenticated)) is returned.
❌ Error Handling
All API responses are wrapped in OpResult<http.Response>, providing a structured way to handle success and failure.
Handling API Responses
- ✅ Success: Returns OpResult.success(response), containing the full http.Response.
- ❌ Failure: Returns OpResult.failure(error), containing an OpError with details such as status code and error type.
A customValidator function can be provided to define additional validation logic before marking a response as success or failure.
Using customValidator for Response Validation:
OpResult<http.Response, MyCustomError>? myValidator(http.Response response) {
if (response.statusCode == 200 || response.statusCode == 202) {
return OpResult.success(response);
}
if (response.statusCode == 404) {
return OpResult.failure(OpError(type: MyCustomError.notFound));
}
return null; // Fallback to default handling
}
final result = await apiClient.send(
endpoint: getEndpoint(MyApiEndpoints.getUser),
customValidator: myValidator,
);
🛠 Advanced Configuration
🌐 Custom Headers
await apiClient.send(
endpoint: getEndPoint(MyApiEndpoints.getUser),
headers: {
"X-Custom-Header": "value",
},
);
📜 Enforcing Response Type
If an API is expected to return a certain content type (e.g. JSON) but sometimes it doesn't, then the optional responseMapper parameter can be used to enforce that format:
final apiClient = OpRestApiClient<MyApiEndpoints>(
baseUrl: "https://api.example.com",
client: http.Client(),
responseMapper: (response) {
// Example: Ensure response is always JSON
if (!response.headers['content-type']?.contains('application/json') ?? true) {
throw FormatException('Invalid response format');
}
return response;
},
);
** Handling true offline detection at app level **
By default, OpRestApiClient distinguishes between:
- networkUnreachable: the device is connected but cannot reach the server (e.g., DNS failure, timeout, socket exception).
- gatewayError: server responded with 502 or 504 (e.g., proxy or upstream error).
However, it does not determine whether the device is completely offline (i.e., no internet connection).
To provide a better user experience, you can probe for actual connectivity at the app level and override the error type accordingly:
if (error.type == OpRestApiErrorType.networkUnreachable) {
final isOffline = await myConnectivityChecker.isOffline();
final adjustedError = isOffline
? OpError(type: OpRestApiErrorType.networkOffline)
: error;
showErrorMessage(adjustedError.getErrorMessage());
}
🚀 Why Use op_rest_api_client?
✔ Structured API calls → No need to manage HTTP manually.
✔ Authentication & token refresh → Handles 401 errors automatically.
✔ Strong typing with enums → Prevents invalid API endpoints.
✔ Full OpResult support → Clear success/failure responses.
✔ Custom error handling → Map HTTP codes to app-specific errors.
📌 Versioning & Changelog
For release history and breaking changes, check the CHANGELOG.
⚖ License
This project is licensed under the BSD 3-Clause License.