Building a Production-Ready REST API Client with Dio in Flutter
Calling a REST API is easy. Building a networking layer that remains reliable as a Flutter application grows is much harder.
A production API client should handle authentication, timeouts, errors, logging, retries, serialization, and consistent response handling.
In this tutorial, we will build a reusable REST API client using Dio.
Why Dio?
Dio provides features useful for production Flutter applications:
- Interceptors
- Request cancellation
- Timeouts
- Multipart requests
- Custom adapters
- Centralized error handling
Add Dio
Add Dio to pubspec.yaml:
dependencies:
dio: ^latest
Use the current compatible version when creating your application.
Create a Dio Client
import 'package:dio/dio.dart';
class ApiClient {
late final Dio dio;
ApiClient() {
dio = Dio(
BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 15),
sendTimeout: const Duration(seconds: 15),
headers: {
'Content-Type': 'application/json',
},
),
);
}
}
Centralizing configuration prevents individual screens from creating inconsistent HTTP clients.
Add an Authentication Interceptor
Tokens should be managed outside widgets.
class AuthInterceptor extends Interceptor {
final Future<String?> Function() readToken;
AuthInterceptor(this.readToken);
@override
Future<void> onRequest(
RequestOptions options,
RequestInterceptorHandler handler,
) async {
final token = await readToken();
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
}
}
Register the interceptor:
dio.interceptors.add(
AuthInterceptor(() async {
return await tokenStorage.readAccessToken();
}),
);
Create a Repository
Keep API details out of your UI.
class UserRepository {
final Dio dio;
UserRepository(this.dio);
Future<User> getUser() async {
final response = await dio.get('/users/me');
return User.fromJson(response.data);
}
}
Model API Responses
Use typed models rather than passing raw JSON throughout the application.
class User {
final String id;
final String name;
const User({
required this.id,
required this.name,
});
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as String,
name: json['name'] as String,
);
}
}
Centralize Error Handling
Dio exposes errors through DioException.
try {
final response = await dio.get('/users/me');
} on DioException catch (error) {
switch (error.type) {
case DioExceptionType.connectionTimeout:
// Handle timeout.
break;
case DioExceptionType.connectionError:
// Handle network error.
break;
default:
// Handle server or application error.
break;
}
}
For a large application, convert these exceptions into your own domain-level error types.
Add Logging
Logging is useful during development.
dio.interceptors.add(
LogInterceptor(
requestBody: true,
responseBody: true,
),
);
Avoid logging access tokens, passwords, medical data, payment information, or other sensitive information in production.
Retry Carefully
Retries are useful for transient failures, but not every request should be retried.
A good retry policy should consider:
- Network failures
- HTTP 5xx responses
- Idempotent requests
- Maximum attempts
- Exponential backoff
Do not blindly retry every POST request because it could create duplicate server-side operations.
Suggested Architecture
A scalable structure can look like:
lib/
├── core/
│ ├── network/
│ │ ├── api_client.dart
│ │ └── auth_interceptor.dart
│ └── errors/
├── features/
│ └── users/
│ ├── data/
│ ├── domain/
│ └── presentation/
└── main.dart
This separation keeps networking reusable and feature code organized.
Security Best Practices
- Always use HTTPS.
- Store tokens in secure platform storage.
- Never hard-code API secrets.
- Avoid logging sensitive data.
- Validate server responses.
- Use certificate pinning where the threat model requires it.
Conclusion
Dio is more than a convenient HTTP package. With a well-designed architecture, it can become the foundation of a reliable Flutter networking layer.
The key is to keep transport concerns centralized while exposing simple repositories to the rest of your application. Add authentication, consistent error handling, safe retries, observability, and secure token storage as your project grows.
Stay tuned for more production Flutter tutorials!
Useful Links
SDK Flutter: https://github.com/v-modal/vmodal_sdk_flutter
SDK Android: https://github.com/v-modal/vmodal_sdk_android
Discord: https://discord.gg/K72z28KUx
Top comments (0)