DTOs and Domain Models
When building Flutter apps that interact with REST APIs, you often receive data in the form of raw JSON. However, letting your UI code depend directly on this JSON, or even on data transfer objects (DTOs) that mirror the API, is risky and limiting. Instead, you should introduce two layers: DTOs and domain models.
A DTO (Data Transfer Object) is a simple class that represents the data structure exactly as it comes from the API. It is designed for serialization and deserialization, making it easy to convert between Dart objects and JSON. DTOs are often tightly coupled to the API contract and may include nullable fields, naming conventions, or types that match the backend.
A domain model represents the core concepts and business logic of your app, independent of how the data is fetched or stored. Domain models are what your UI and business logic should use. They are shaped according to your appβs needs, not the backendβs structure. By mapping DTOs to domain models, you shield your app from backend changes and keep your UI logic clean and focused.
main.dart
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869// DTO representing the API response for a user class UserDto { final int id; final String? first_name; final String? last_name; final String? email; UserDto({ required this.id, this.first_name, this.last_name, this.email, }); factory UserDto.fromJson(Map<String, dynamic> json) { return UserDto( id: json['id'] as int, first_name: json['first_name'] as String?, last_name: json['last_name'] as String?, email: json['email'] as String?, ); } } // Domain model representing a user in the app class User { final int id; final String fullName; final String email; User({ required this.id, required this.fullName, required this.email, }); } // Mapper function to convert a UserDto to a User domain model User userFromDto(UserDto dto) { final firstName = dto.first_name ?? ''; final lastName = dto.last_name ?? ''; return User( id: dto.id, fullName: (firstName + ' ' + lastName).trim(), email: dto.email ?? '', ); } // Example usage void main() { // Simulated API response final json = { 'id': 42, 'first_name': 'Ada', 'last_name': 'Lovelace', 'email': 'ada@example.com', }; // Parse DTO from JSON final userDto = UserDto.fromJson(json); // Map DTO to domain model final user = userFromDto(userDto); print('User domain model:'); print('ID: [1m${user.id}[0m'); print('Full Name: [1m${user.fullName}[0m'); print('Email: [1m${user.email}[0m'); }
By separating DTOs from domain models, you gain important flexibility. Your UI and business logic can work with clean, well-defined models that fit your app's needs, rather than being forced to match the backend's data structure. If the API changes, you only need to update the DTO and mapping layer, not your entire app.
This also lets you add computed fields, validation, or transformations in the mapping step.
Thanks for your feedback!
Ask AI
Ask AI
Ask anything or try one of the suggested questions to begin our chat
Can you explain the difference between DTOs and domain models?
How do I implement a mapping layer between DTOs and domain models in Flutter?
What are some best practices for managing DTOs and domain models in a Flutter project?
Awesome!
Completion rate improved to 6.67
DTOs and Domain Models
Swipe to show menu
When building Flutter apps that interact with REST APIs, you often receive data in the form of raw JSON. However, letting your UI code depend directly on this JSON, or even on data transfer objects (DTOs) that mirror the API, is risky and limiting. Instead, you should introduce two layers: DTOs and domain models.
A DTO (Data Transfer Object) is a simple class that represents the data structure exactly as it comes from the API. It is designed for serialization and deserialization, making it easy to convert between Dart objects and JSON. DTOs are often tightly coupled to the API contract and may include nullable fields, naming conventions, or types that match the backend.
A domain model represents the core concepts and business logic of your app, independent of how the data is fetched or stored. Domain models are what your UI and business logic should use. They are shaped according to your appβs needs, not the backendβs structure. By mapping DTOs to domain models, you shield your app from backend changes and keep your UI logic clean and focused.
main.dart
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869// DTO representing the API response for a user class UserDto { final int id; final String? first_name; final String? last_name; final String? email; UserDto({ required this.id, this.first_name, this.last_name, this.email, }); factory UserDto.fromJson(Map<String, dynamic> json) { return UserDto( id: json['id'] as int, first_name: json['first_name'] as String?, last_name: json['last_name'] as String?, email: json['email'] as String?, ); } } // Domain model representing a user in the app class User { final int id; final String fullName; final String email; User({ required this.id, required this.fullName, required this.email, }); } // Mapper function to convert a UserDto to a User domain model User userFromDto(UserDto dto) { final firstName = dto.first_name ?? ''; final lastName = dto.last_name ?? ''; return User( id: dto.id, fullName: (firstName + ' ' + lastName).trim(), email: dto.email ?? '', ); } // Example usage void main() { // Simulated API response final json = { 'id': 42, 'first_name': 'Ada', 'last_name': 'Lovelace', 'email': 'ada@example.com', }; // Parse DTO from JSON final userDto = UserDto.fromJson(json); // Map DTO to domain model final user = userFromDto(userDto); print('User domain model:'); print('ID: [1m${user.id}[0m'); print('Full Name: [1m${user.fullName}[0m'); print('Email: [1m${user.email}[0m'); }
By separating DTOs from domain models, you gain important flexibility. Your UI and business logic can work with clean, well-defined models that fit your app's needs, rather than being forced to match the backend's data structure. If the API changes, you only need to update the DTO and mapping layer, not your entire app.
This also lets you add computed fields, validation, or transformations in the mapping step.
Thanks for your feedback!