Designing The Client
Start with a small, intention-revealing API surface. Define high-level operations that map to application use cases (e.g., fetchCurrentUser, searchProducts). Keep domain types in your model layer — DTOs should be private to the data layer and converted to domain models using explicit mappers. This prevents leaking API contracts into business logic and preserves type safety.
A recommended folder layout:
lib/src/network/api_client.dart (retrofit interface)
lib/src/network/interceptors/*.dart
lib/src/network/models/*.dart (json_serializable DTOs)
lib/src/data/repository.dart (exposes domain types)
Type safety comes from generated code: retrofit creates strongly-typed method signatures and json_serializable generates parsing code. Avoid returning Map or raw dynamic from the network layer.
Integrating Retrofit And Models
Use retrofit with Dio and json_serializable. Annotate an abstract class with @RestApi and let retrofit generate the implementation. Keep method signatures returning concrete DTOs or Future for simple endpoints.
Example retrofit interface:
@RestApi(baseUrl: "https://api.example.com")
abstract class ApiClient {
factory ApiClient(Dio dio, {String baseUrl}) = _ApiClient;
@GET('/users/{id}')
Future<UserDto> getUser(@Path('id') String id);
}Generate DTOs with json_serializable. Use explicit converters when the API encodes dates or nested polymorphic objects. If you need a custom converter, register it with Dio's transformer or the retrofit converterFactory to keep parsing logic centralized and type-safe.
Creating Interceptors
Interceptors let you encapsulate authentication, logging, and retry logic without contaminating endpoint definitions. Implement Dio's Interceptor to mutate requests, handle 401 refresh flows, and attach standard headers. Keep interceptors small and single-purpose.
Example auth interceptor:
class AuthInterceptor extends Interceptor {
final String token;
AuthInterceptor(this.token);
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
options.headers['Authorization'] = 'Bearer $token';
handler.next(options);
}
}Compose interceptors when creating Dio before passing it to retrofit:
Add logging interceptor in development only.
Add retry/backoff interceptor for idempotent calls.
Add an auth interceptor that attempts token refresh on 401 and retries the failed request.
Make sure interceptors do not perform heavy synchronous work; they should use async where necessary and coordinate refresh with mutual exclusion to avoid parallel refresh attempts.
Testing And Error Handling
Type-safe clients are easier to test. Generate a fake implementation with retrofit by creating a mock Dio that returns pre-recorded responses, or use the retrofit-generated interface against a MockAdapter for Dio.
Handle errors by mapping DioError to domain failures. Create a sealed Failure type (or use a simple enum/class union) and map the common cases: network, timeout, unauthorized, parsing, server. Return results using a Result/Either type instead of throwing in the repository boundary. This keeps upstream code explicit and type-safe.
Example mapping flow:
Catch DioError in repository/data source.
Inspect error.type and response.statusCode.
Map JSON error payloads to specific domain errors when possible.
Also validate your DTOs post-parse if the API frequently returns inconsistent shapes; fail early with clear parse errors rather than propagating null or invalid fields.
Vibe Studio

Vibe Studio, powered by Steve’s advanced AI agents, is a revolutionary no-code, conversational platform that empowers users to quickly and efficiently create full-stack Flutter applications integrated seamlessly with Firebase backend services. Ideal for solo founders, startups, and agile engineering teams, Vibe Studio allows users to visually manage and deploy Flutter apps, greatly accelerating the development process. The intuitive conversational interface simplifies complex development tasks, making app creation accessible even for non-coders.
Conclusion
A type-safe API client in Flutter using retrofit and Dio interceptors gives you clear contracts, centralized cross-cutting logic, and safer parsing. Design small, intention-revealing methods, rely on generated DTOs for parsing, and use interceptors for auth/logging/retries. Map transport errors to domain-level failures and prefer returning Result types. This pattern keeps networking predictable and maintainable as your app grows.