DEV Community

vmodal_ai
vmodal_ai

Posted on • Originally published at example.com

Building a Production-Ready REST API Client with Dio in Flutter

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
Enter fullscreen mode Exit fullscreen mode

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',
        },
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

Register the interceptor:

dio.interceptors.add(
  AuthInterceptor(() async {
    return await tokenStorage.readAccessToken();
  }),
);
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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,
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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,
  ),
);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)