Apiarist

A Dart package for handling API responses in a structured and type-safe way. Apiarist provides a clean abstraction for managing different API response states including loading, success, error, and failure scenarios.

Features

  • Type-safe API response handling - Generic ApiResponse<T> class for any data type
  • Comprehensive state management - Support for loading, data, error, and failure states
  • HTTP status code handling - Built-in support for all standard HTTP status codes
  • Riverpod integration - Seamless integration with Riverpod for state management
  • Composite responses - Combine multiple parallel API responses with intelligent error handling
  • Sequential chaining - Chain dependent API calls with automatic error propagation
  • Graceful error handling - Distinguish between API errors and application failures

Installation

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

dependencies:
  apiarist: ^1.0.7

Then run:

dart pub get

Usage

Basic Usage

import 'package:apiarist/apiarist.dart';

// Create different response states
ApiResponse<String> loadingResponse = ApiResponse.loading();
ApiResponse<String> dataResponse = ApiResponse.data("Hello, World!");
ApiResponse<String> errorResponse = ApiResponse.error(ApiError(/* error details */));
ApiResponse<String> failureResponse = ApiResponse.failure(ApiFailure(/* failure details */));

// Handle responses with pattern matching
String result = response.when(
  data: (data) => "Success: $data",
  loading: () => "Loading...",
  error: (error) => "Error: ${error.message}",
  failure: (failure) => "Failure: ${failure.message}",
);

Advanced Error Handling

Handle specific HTTP status codes:

String handleResponse = response.when(
  data: (data) => "Success: $data",
  loading: () => "Loading...",
  error: (error) => "Generic error: ${error.message}",
  failure: (failure) => "Failure: ${failure.message}",
  
  // Specific HTTP status code handlers
  badRequest: (error) => "Bad request: ${error.message}",
  unauthorized: (error) => "Please log in again",
  forbidden: (error) => "Access denied",
  notfound: (error) => "Resource not found",
  internalServerError: (error) => "Server error, please try again later",
  // ... and many more status codes
);

Composite Responses (Parallel API Calls)

Combine multiple parallel API responses with intelligent error prioritization:

ApiResponse<CombinedData> combinedResponse = ApiResponse.composite([
  userResponse,
  settingsResponse,
  notificationsResponse,
], (responses) {
  // All responses are successful, combine the data
  return CombinedData(
    user: responses[0],
    settings: responses[1],
    notifications: responses[2],
  );
});

Use this when you need to wait for multiple independent API calls to complete before displaying data.

Chaining Responses (Sequential API Calls)

Chain sequential API calls where each call depends on the previous response:

// Fluent chaining syntax
final result = await api
  .getUser(userId)
  .chain((user) => api.getProfile(user.profileId))
  .chain((profile) => api.getSettings(profile.settingsId))
  .convertData((settings) => settings.getDetails());

// Handle the final result
result.when(
  data: (details) => print("Success: $details"),
  loading: () => print("Loading..."),
  error: (error) => print("API error: ${error.statusCode}"),
  failure: (failure) => print("Failed: ${failure.error}"),
);

The chain() method automatically propagates errors and failures through the chain. If any call fails, subsequent calls are skipped and the error/failure is returned.

Data Conversion

Convert response data to different types:

ApiResponse<User> userResponse = ApiResponse.data(userData);
ApiResponse<String> userNameResponse = userResponse.convertData((user) => user.name);

Checking Response State

if (response.isLoading) {
  // Show loading indicator
}

if (response.hasValue) {
  // Use response.value
}

if (response.hasError) {
  // Handle API error
  print("Error: ${response.error?.message}");
}

if (response.hasFailure) {
  // Handle application failure
  print("Failure: ${response.failure?.message}");
}

Integration with Riverpod

import 'package:riverpod/riverpod.dart';

final userProvider = FutureProvider<ApiResponse<User>>((ref) async {
  try {
    final response = await apiService.getUser();
    return ApiResponse.data(response);
  } on ApiError catch (e) {
    return ApiResponse.error(e);
  } catch (e) {
    return ApiResponse.failure(ApiFailure(e.toString()));
  }
});

// In your widget
Consumer(
  builder: (context, ref, child) {
    final userResponse = ref.watch(userProvider);
    
    return userResponse.when(
      data: (response) => response.when(
        data: (user) => UserWidget(user: user),
        loading: () => CircularProgressIndicator(),
        error: (error) => ErrorWidget(error: error),
        failure: (failure) => FailureWidget(failure: failure),
      ),
      loading: () => CircularProgressIndicator(),
      error: (error, stack) => ErrorWidget(error: error),
    );
  },
)

API Reference

ApiResponse

The main class for handling API responses.

Constructors

  • ApiResponse.data(T value) - Create a successful response with data
  • ApiResponse.loading() - Create a loading response
  • ApiResponse.error(ApiError error) - Create an error response
  • ApiResponse.failure(ApiFailure failure) - Create a failure response

Properties

  • bool isLoading - Whether the response is in loading state
  • bool hasValue - Whether the response has data
  • T? value - The response data (null if not in data state)
  • ApiError? error - The API error (null if not in error state)
  • bool hasError - Whether the response has an error
  • ApiFailure? failure - The failure details (null if not in failure state)
  • bool hasFailure - Whether the response has a failure

Methods

  • when<R>({...}) - Pattern match on the response state with optional specific status code handlers
  • convertData<R>(R Function(T) converter) - Convert the response data to a different type
  • chain<R>(Future<ApiResponse<R>> Function(T) fn) - Chain sequential API calls with automatic error propagation
  • static composite<T>(List<ApiResponse> responses, T Function(List) combiner) - Combine multiple parallel responses

ApiError

Represents an API error with HTTP response details.

ApiFailure

Represents an application-level failure (non-HTTP errors).

Error vs Failure

  • ApiError: HTTP-related errors from the server (4xx, 5xx status codes)
  • ApiFailure: Application-level failures (network issues, parsing errors, etc.)

Changelog

See CHANGELOG.md for a detailed history of changes.

License

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

Contributing

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

Libraries

apiarist
no_content