Skip to main content

Working with REST APIs

1. Understanding REST Principles

Principle Description Example
Statelessness Each request from client to server must contain all information needed to understand and process the request. GET /api/users/123 includes all necessary data to retrieve user details.
Client-Server Architecture Separation of concerns between client and server, allowing them to evolve independently. Mobile app (client) communicates with cloud backend (server) via HTTP.
Uniform Interface Consistent and standardized way to interact with resources using standard HTTP methods. GET, POST, PUT, DELETE used for CRUD operations.
Resource-Based Everything is treated as a resource identified by a URI. /api/orders/456 represents an order resource.
Representation Resources can be represented in various formats like JSON, XML. Accept: application/json header specifies format.

Example: REST API Endpoints

GET    /api/users          # Retrieve list of users
POST   /api/users          # Create a new user
GET    /api/users/123      # Retrieve specific user
PUT    /api/users/123      # Update specific user
DELETE /api/users/123      # Delete specific user
Note: REST is an architectural style, not a protocol or standard. It relies on HTTP for communication.

2. Implementing Repository Pattern

Example: Basic Repository Implementation

// Repository interface
abstract class UserRepository {
  Future<User> fetchUser(int id);
  Future<List<User>> fetchUsers();
  Future<User> saveUser(User user);
}

// Concrete implementation
class UserRepositoryImpl implements UserRepository {
  final HttpClient client;

  UserRepositoryImpl(this.client);

  @override
  Future<User> fetchUser(int id) async {
    final response = await client.get('/users/$id');
    return User.fromJson(response.data);
  }

  @override
  Future<List<User>> fetchUsers() async {
    final response = await client.get('/users');
    return (response.data as List)
        .map((e) => User.fromJson(e))
        .toList();
  }

  @override
  Future<User> saveUser(User user) async {
    final response = await client.post('/users', data: user.toJson());
    return User.fromJson(response.data);
  }
}
Component Description Usage
Repository Interface Defines contract for data access operations Enforces consistency across implementations
Concrete Implementation Actual implementation using HTTP client Handles network calls and data mapping
Dependency Injection Inject repository into use cases or widgets Allows easy mocking and testing
Note: The repository pattern abstracts data source logic, making it easier to switch between local and remote data sources.

Repository vs Direct API Calls

  • Repository: Provides a clean abstraction layer, supports mocking, and centralizes data logic
  • Direct API: Simpler but tightly couples UI to network logic

3. Creating API Service Classes

Property Description Example
Base URL Root endpoint for API calls https://api.example.com/v1
HTTP Client Instance used to make requests http.Client()
Headers Authentication and content-type headers {'Authorization': 'Bearer token', 'Content-Type': 'application/json'}

Example: Basic API Service Class

import 'dart:convert';
import 'package:http/http.dart' as http;

class ApiService {
  static const String baseUrl = 'https://api.example.com/v1';
  final http.Client client = http.Client();

  Future<http.Response> fetchUserData(int userId) async {
    final response = await client.get(
      Uri.parse('$baseUrl/users/$userId'),
      headers: {'Authorization': 'Bearer token'},
    );
    return response;
  }

  Future<http.Response> postUserData(Map<String, dynamic> data) async {
    final response = await client.post(
      Uri.parse('$baseUrl/users'),
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode(data),
    );
    return response;
  }
}
Note: Always dispose of http.Client() in production to prevent resource leaks.

Example: Using Dio for API Service

import 'package:dio/dio.dart';

class DioApiService {
  final Dio dio = Dio(BaseOptions(
    baseUrl: 'https://api.example.com/v1',
    headers: {'Authorization': 'Bearer token'},
  ));

  Future<Response> fetchUser(int userId) async {
    return await dio.get('/users/$userId');
  }

  Future<Response> createUser(Map<String, dynamic> data) async {
    return await dio.post('/users', data: data);
  }
}

4. Handling API Errors

Error Type Description Handling Strategy Code Example
NetworkError Occurs when the device cannot connect to the internet or the server is unreachable. Display a user-friendly message and provide retry options.
try {
  final response = await http.get(Uri.parse('https://api.example.com/data'));
} catch (e) {
  if (e is SocketException) {
    // Handle network error
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('No internet connection')),
    );
  }
}
TimeoutException Occurs when the request exceeds the specified timeout duration. Inform the user and allow for a retry or fallback action.
try {
  final response = await http.get(
    Uri.parse('https://api.example.com/data'),
    timeout: Duration(seconds: 10),
  );
} catch (e) {
  if (e is TimeoutException) {
    // Handle timeout
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('Request timed out')),
    );
  }
}
HttpError (4xx/5xx) Server returns an HTTP error status code indicating client or server issues. Parse the error code and provide context-specific messages to the user.
if (response.statusCode >= 400) {
  String errorMessage = 'Error: ${response.statusCode}';
  if (response.statusCode == 401) {
    errorMessage = 'Unauthorized access';
  } else if (response.statusCode == 404) {
    errorMessage = 'Resource not found';
  }
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(content: Text(errorMessage)),
  );
}

Example: Comprehensive Error Handling

Future<dynamic> fetchData() async {
  try {
    final response = await http.get(
      Uri.parse('https://api.example.com/data'),
      timeout: Duration(seconds: 10),
    );

    if (response.statusCode == 200) {
      return json.decode(response.body);
    } else if (response.statusCode == 401) {
      throw Exception('Unauthorized: Please log in again');
    } else if (response.statusCode == 404) {
      throw Exception('Resource not found');
    } else {
      throw Exception('HTTP Error: ${response.statusCode}');
    }
  } on SocketException {
    throw Exception('No internet connection');
  } on TimeoutException {
    throw Exception('Request timed out');
  } catch (e) {
    throw Exception('An unexpected error occurred: $e');
  }
}

5. Implementing Request Retry Logic

Property Description Default Value Notes
maxRetries Maximum number of retry attempts for failed requests 3 Set to 0 to disable retries
retryDelay Initial delay (in milliseconds) before retrying a request 1000 Exponential backoff is applied by default
retryableStatusCodes List of HTTP status codes that trigger a retry [429, 500, 503] Customize based on your API's retry policy

Example: Basic Retry Logic with Dio

import 'package:dio/dio.dart';

final dio = Dio();

// Add retry interceptor
dio.interceptors.add(InterceptorsWrapper(
  onRequest: (options, handler) {
    // Add custom logic if needed
    return handler.next(options);
  },
  onResponse: (response, handler) {
    return handler.next(response);
  },
  onError: (error, handler) async {
    if (error.response?.statusCode == 503 && error.retryCount < 3) {
      await Future.delayed(Duration(milliseconds: 1000 * (error.retryCount + 1)));
      return handler.retry();
    }
    return handler.next(error);
  },
));

// Use dio to make requests
final response = await dio.get('https://api.example.com/data');
Note: The retryCount property is added by Dio's retry interceptor and can be used to track how many times a request has been retried.
Warning: Be cautious with retry logic to avoid infinite loops or excessive load on the server. Always implement a maximum retry limit.

6. Using Interceptors

Interceptor Type Description Usage
Dio Interceptor Allows intercepting requests and responses for logging, authentication, or transformation. final dio = Dio();\ndio.interceptors.add(InterceptorsWrapper(\n onRequest: (options, handler) {\n // Modify request\n return handler.next(options);\n },\n onResponse: (response, handler) {\n // Modify response\n return handler.next(response);\n },\n));

Example: Logging Requests

final dio = Dio();\ndio.interceptors.add(LoggingInterceptor());

Example: Adding Authentication Token

final dio = Dio();\ndio.interceptors.add(InterceptorsWrapper(\n  onRequest: (options, handler) {\n    options.headers['Authorization'] = 'Bearer $token';\n    return handler.next(options);\n  },\n));

7. Caching API Responses

Property Description Default
cacheDuration Duration to cache API responses in seconds 300
maxCacheSize Maximum number of cached responses to store 100
cacheKey Unique identifier for caching a specific API endpoint null

Example: Using http package with caching

import 'package:http/http.dart' as http;
import 'dart:convert';

class CachedApiService {
  static final Map<String, dynamic> _cache = {};
  static final Map<String, DateTime> _cacheTimestamps = {};

  static Future<dynamic> getCachedResponse(
    String url, {
    int cacheDuration = 300,
  }) async {
    final cached = _cache[url];
    final timestamp = _cacheTimestamps[url];

    if (cached != null && timestamp != null) {
      final now = DateTime.now();
      if (now.difference(timestamp).inSeconds < cacheDuration) {
        return cached;
      }
    }

    final response = await http.get(Uri.parse(url));
    final data = json.decode(response.body);

    _cache[url] = data;
    _cacheTimestamps[url] = DateTime.now();

    return data;
  }
}

Example: Using shared_preferences for persistent caching

import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';

class PersistentCacheService {
  static Future<dynamic> getCachedData(String key) async {
    final prefs = await SharedPreferences.getInstance();
    final cached = prefs.getString(key);

    if (cached != null) {
      try {
        return json.decode(cached);
      } catch (e) {
        return null;
      }
    }

    return null;
  }

  static Future<void> setCachedData(String key, dynamic data) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(key, json.encode(data));
  }
}
Note: Caching strategies should consider data freshness and user experience trade-offs. For real-time data, shorter cache durations or no caching may be preferred.

8. Implementing Pagination

Example: Fetching paginated data

class ApiService {
  static const String baseUrl = 'https://api.example.com';
  
  Future<List<Post>> fetchPosts(int page, int limit) async {
    final response = await http.get(
      Uri.parse('$baseUrl/posts?page=$page&limit=$limit'),
    );
    
    if (response.statusCode == 200) {
      final data = json.decode(response.body);
      return (data as List).map((e) => Post.fromJson(e)).toList();
    } else {
      throw Exception('Failed to load posts');
    }
  }
}
Property Description Usage
page Current page number Used in API request to fetch specific page of data
limit Number of items per page Controls how many items are returned in each API response
total Total number of items available Used to calculate total pages and enable pagination controls

Workflow: Implementing Pagination in Flutter

  1. Create a service class to handle API calls with page and limit parameters
  2. Fetch initial data (e.g., page 1, limit 20)
  3. Implement scroll listener or button to load next page
  4. Append new data to existing list when loading more pages
  5. Update UI with new page data and disable loading indicator

Comparison: Pagination Approaches

  • Offset-based: Uses offset parameter (e.g., page=2, limit=10) - Simple but can be inefficient with large datasets
  • Cursor-based: Uses cursor/token (e.g., last ID from previous page) - More efficient for large datasets
  • Keyset-based: Uses keyset (e.g., timestamp or ID) - Good for ordered data
Note: When implementing pagination, always handle loading states and potential errors gracefully to provide a smooth user experience.

9. Using Query Parameters

Parameter Type Description Example
query Map<String, String> Map of query parameters to be appended to the URL {'page': '1', 'limit': '20'}

Example: Making a GET request with query parameters

import 'package:http/http.dart' as http;

Future<http.Response> fetchPosts() async {
  final response = await http
      .get(Uri.parse('https://jsonplaceholder.typicode.com/posts'), 
          queryParameters: {'userId': '1', 'page': '1'});
  return response;
}
Note: Query parameters are automatically URL-encoded by the http package.

10. Handling Rate Limiting

Property Description Values
http.Response Response object from HTTP requests statusCode, headers
X-RateLimit-Limit Maximum number of requests allowed in a window 1000
X-RateLimit-Remaining Number of requests remaining in the current window 999
X-RateLimit-Reset Timestamp when the rate limit window resets 1678886400

Example: Checking Rate Limits

import 'package:http/http.dart' as http;

Future<void> fetchWithRateLimitCheck() async {
  final response = await http.get(Uri.parse('https://api.example.com/data'));
  final rateLimit = response.headers['x-ratelimit-remaining'];
  if (rateLimit != null && int.parse(rateLimit) <= 5) {
    print("Warning: Low API calls remaining");
  }
}

Example: Implementing Retry Logic

import 'package:http/http.dart' as http;

Future<http.Response> fetchWithRetry(String url, int maxRetries) async {
  for (int i = 0; i <= maxRetries; i++) {
    try {
      final response = await http.get(Uri.parse(url));
      if (response.statusCode == 429) {
        // Handle rate limit
        final resetTime = int.parse(response.headers['x-ratelimit-reset']!);
        final delay = resetTime - DateTime.now().millisecondsSinceEpoch ~/ 1000;
        await Future.delayed(Duration(seconds: delay));
        continue;
      }
      return response;
    } catch (e) {
      if (i == maxRetries) rethrow;
    }
  }
  throw Exception("Max retries exceeded");
}
Note: Implementing exponential backoff can improve resilience when handling rate limits.

Rate Limiting Best Practices

  • Monitor X-RateLimit-Remaining to avoid hitting limits
  • Implement retry logic with delays based on X-RateLimit-Reset
  • Use caching to reduce redundant API calls
  • Log rate limit events for debugging and monitoring

Was this article helpful?