Building A Type Safe API Client With Retrofit And Interceptors
Summary
Summary

This tutorial shows how to build a type-safe API client in Flutter using retrofit for generated endpoint interfaces and Dio interceptors for authentication, logging, and retries. It covers client design, DTO generation with json_serializable, composing interceptors, and mapping Dio errors to domain-level failures, recommending Result/Either types for explicit error handling.

This tutorial shows how to build a type-safe API client in Flutter using retrofit for generated endpoint interfaces and Dio interceptors for authentication, logging, and retries. It covers client design, DTO generation with json_serializable, composing interceptors, and mapping Dio errors to domain-level failures, recommending Result/Either types for explicit error handling.

Key insights:
Key insights:
  • Designing The Client: Keep endpoint methods small and domain types separate from DTOs to avoid leaking API contracts.

  • Integrating Retrofit And Models: Use retrofit and json_serializable to generate strongly-typed method signatures and parsing code.

  • Creating Interceptors: Encapsulate auth, logging, and retry logic in single-purpose Dio interceptors and compose them when building Dio.

  • Error Handling And Type Safety: Map DioError to explicit domain failures and return Result/Either types instead of throwing.

  • Testing And Mocking: Test with generated retrofit interfaces and mock Dio adapters to verify parsing and error mappings without hitting real servers.

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.

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.

Build Flutter Apps Faster with Vibe Studio

Vibe Studio is your AI-powered Flutter development companion. Skip boilerplate, build in real-time, and deploy without hassle. Start creating apps at lightning speed with zero setup.

Other Insights

Join a growing community of builders today

Join a growing community of builders today

Join a growing community of builders today

Join a growing community of builders today

Join a growing community of builders today

28-07 Jackson Ave

Walturn

New York NY 11101 United States

© Steve • All Rights Reserved 2025

28-07 Jackson Ave

Walturn

New York NY 11101 United States

© Steve • All Rights Reserved 2025

28-07 Jackson Ave

Walturn

New York NY 11101 United States

© Steve • All Rights Reserved 2025

28-07 Jackson Ave

Walturn

New York NY 11101 United States

© Steve • All Rights Reserved 2025

28-07 Jackson Ave

Walturn

New York NY 11101 United States

© Steve • All Rights Reserved 2025

28-07 Jackson Ave

Walturn

New York NY 11101 United States

© Steve • All Rights Reserved 2025