
In the previous part, we set up models, data sources, repositories, and failures for the full-stack to-do application. In this part, we will:
- Make changes to the failure classes
- Create new failures
- Create new Exceptions
- Add validation to DTOs
- Add JSON converters
Creating and updating packages π¦
In this section, we will create new packages using very_good_cli and update existing ones to efficiently manage the packages in our full-stack to-do application.
Working with Failure classes
It's time to shake things up in the failure department! π₯ Let's get to work on updating our failure classes.
Create build.yaml
We will now create a new file called build.yaml in the failures directory and add the following code. This will change the behaviour of the json_serializable so that it generates JSON keys in snake_case.
targets:
$default:
builders:
json_serializable:
options:
any_map: false
checked: false
create_factory: true
disallow_unrecognized_keys: false
explicit_to_json: true
field_rename: snake
generic_argument_factories: false
ignore_unannotated: false
include_if_null: true
Update Failure
Let's add a new getter to the Failure abstract class to get the status code of the failure. This will allow us to map the failure to the appropriate HTTP status code in the controller.
abstract class Failure {
String get message;
+ int get statusCode;
}
Update NetworkFailure
We will update our failure package and add more failures to it. To do this, we will begin by renaming the code field in our NetworkFailure class to statusCode. This will make the field more meaningful and easier for our readers to understand.
class NetworkFailure extends Failure with _$NetworkFailure {
/// {@macro network_failure}
const factory NetworkFailure({
required String message,
- required int code,
+ required int statusCode,
@Default([]) List<String> errors,
}) = _NetworkFailure;
Create new failures
Time to add some more failure friends to our app! π
We'll be creating RequestFailure, ServerFailure, and ValidationFailure to help us map out any potential errors that may occur.
RequestFailure
To create a RequestFailure class, we will create a new file in the src directory called request_failure/request_failure.dart.
import 'dart:io';
import 'package:failures/failures.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'request_failure.freezed.dart';
@freezed
class RequestFailure extends Failure with _$RequestFailure {
const factory RequestFailure({
required String message,
@Default(HttpStatus.badRequest) int statusCode,
}) = _RequestFailure;
}
We will create two new classes in the src directory, ServerFailure and ValidationFailure. The ServerFailure class will be used to represent errors that occur on the server side of our application, and the ValidationFailure class will be used to represent validation errors in our application.
ServerFailure
import 'dart:io';
import 'package:failures/failures.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'server_failure.freezed.dart';
@freezed
class ServerFailure extends Failure with _$ServerFailure {
const factory ServerFailure({
required String message,
@Default(HttpStatus.internalServerError) int statusCode,
}) = _ServerFailure;
}
ValidationFailure
import 'dart:io';
import 'package:failures/failures.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'validation_failure.freezed.dart';
@freezed
class ValidationFailure extends Failure with _$ValidationFailure {
const factory ValidationFailure({
required String message,
@Default(HttpStatus.badRequest) int statusCode,
@Default({}) Map<String, List<String>> errors,
}) = _ValidationFailure;
}
Don't forget to export and run
build_runnerπ
library failures;
export 'src/failure.dart';
export 'src/network_failure/network_failure.dart';
export 'src/request_failure/request_failure.dart';
export 'src/server_failure/server_failure.dart';
export 'src/validation_failure/validation_failure.dart';
π‘ Note: You can run the
build_runnercommand by runningflutter pub run build_runner buildin the terminal.
Working with packages
Get ready for some package updating fun! π¦
Update typedefs package
We will be creating a function called mapTodoId in the typedefs package. This function will be responsible for converting a string id to a TodoId object. This is necessary because we need a way to convert user input into a format our application can understand and use. If the id is not valid, the mapTodoId function will return a RequestFailure object. This will allow us to handle any errors or invalid input gracefully, ensuring that our application is robust and can handle any potential issues
Either<Failure, TodoId> mapTodoId(String id) {
try {
final todoId = int.tryParse(id);
if (todoId == null) throw const BadRequestException(message: 'Invalid id');
return Right(todoId);
} on BadRequestException catch (e) {
return Left(
RequestFailure(
message: e.message,
statusCode: e.statusCode,
),
);
}
}
The mapTodoId method will return either a TodoId object or a RequestFailure object. Make sure to add the dependencies either_dart and failures to the pubspec.yaml file of the typedefs package.
dependencies:
either_dart: ^0.3.0
exceptions:
path: ../exceptions
failures:
path: ../failures
Create a new exceptions package
To handle internal exceptions and map them to the appropriate failures, we will create a new package called exceptions and throw our custom exceptions. For example, if we encounter a PostgreSQLException while inserting a new to-do, we will throw a ServerException and map it to the ServerFailure class. To create the exceptions package, run the following command in the terminal:
very_good create -t dart_pkg exceptions
This package will include different types of exceptions such as ServerException, HttpException, and NotFoundException. The ServerException will be thrown in the case of an internal server error, while the HttpException is an abstract class that will be extended by other exceptions like NotFoundException and BadRequestException. We can use these custom exceptions to handle internal errors and map them to the appropriate failure classes, such as ServerFailure or RequestFailure. We will start by creating a new file inside src/server_exception/server_exception.dart and add the following code:
ServerException
To create ServerException we will create a new file src/server_exception/server_exception.dart and add the following code:
class ServerException implements Exception {
const ServerException(this.message);
final String message;
@override
String toString() => 'ServerException: $message';
}
HttpExpection
To create HttpException we will create a new file src/http_exception/http_exception.dart and add the following code:
abstract class HttpException implements Exception {
const HttpException(this.message, this.statusCode);
final String message;
final int statusCode;
@override
String toString() => '$runtimeType: $message';
}
NotFoundException
Next, we will create NotFoundException, which will be thrown when a resource is not found. To do this, create a new file called src/http_exception/not_found_exception.dart and add the following code:
class NotFoundException extends HttpException {
const NotFoundException(String message) : super(message, HttpStatus.notFound);
}
BadRequestException
Similarly, we will create a new file inside src/http_exception/bad_request_exception.dart and add the following code:
class BadRequestException extends HttpException {
const BadRequestException({
required String message,
this.errors = const {},
}) : super(message, HttpStatus.badRequest);
final Map<String, List<String>> errors;
}
Make sure to import the HttpException. Once you are done with HttpException, add an export statement in http_exception.dart file.
export './bad_request_exception.dart';
export './not_found_exception.dart';
And finally, export the HttpException from exceptions/lib/exceptions.dart file.
library exceptions;
export 'src/http_exception/http_exception.dart';
export 'src/server_exception/server_exception.dart';
Update models package
Let's handle the validation
Ready to add some sass to your data validation? Let's get those DTOs in shape! πͺ
To add validation to our CreateTodoDto class, we will create a new static method called validated in models/lib/src/create_todo_dto/create_todo_dto.dart. This method will return either a ValidationFailure object or a CreateTodoDto object. We will use this method to ensure that our to-do creation requests contain all necessary information before they are processed. The validation rules are:
the
titleshould not be emptythe
descriptionshould not be empty
Before we can add the validated method to the CreateTodoDto class, we need to make sure that the necessary packages are added to the pubspec.yaml file.
dependencies:
either_dart: ^0.3.0
exceptions:
path: ../exceptions
failures:
path: ../failures
freezed_annotation: ^2.2.0
json_annotation: ^4.7.0
typedefs:
path: ../typedefs
Now we will create a validated method inside CreateTodoDto
static Either<ValidationFailure, CreateTodoDto> validated(
Map<String, dynamic> json,
) {
try {
final errors = <String, List<String>>{};
if (json['title'] == null) {
errors['title'] = ['Title is required'];
}
if (json['description'] == null) {
errors['description'] = ['Description is required'];
}
if (errors.isEmpty) return Right(CreateTodoDto.fromJson(json));
throw BadRequestException(
message: 'Validation failed',
errors: errors,
);
} on BadRequestException catch (e) {
return Left(
ValidationFailure(
message: e.message,
errors: e.errors,
statusCode: e.statusCode,
),
);
}
}
Similarly, we will create a new static method called validated to validate the UpdateTodoDto. We will ensure that at least one field is present.
static Either<ValidationFailure, UpdateTodoDto> validated(
Map<String, dynamic> json,
) {
try {
final errors = <String, List<String>>{};
if (json['title'] == null || json['title'] == '') {
errors['title'] = ['At least one field must be provided'];
}
if (json['description'] == null || json['description'] == '') {
errors['description'] = ['At least one field must be provided'];
}
if (json['completed'] == null) {
errors['completed'] = ['At least one field must be provided'];
}
if (errors.length < 3) return Right(UpdateTodoDto.fromJson(json));
throw BadRequestException(
message: 'Validation failed',
errors: errors,
);
} on BadRequestException catch (e) {
return Left(
ValidationFailure(
message: e.message,
statusCode: e.statusCode,
errors: e.errors,
),
);
}
}
Custom JSON converters
To serialize and deserialize our DateTime objects, we will create a new file called models/lib/src/serializers/date_time_serializer.dart. In this file, we will add the necessary code to handle the serialization and deserialization of DateTime objects.
These classes will implement the JsonConverter interface and provide the necessary logic to convert DateTime objects to and from JSON. The DateTimeConverterNullable class will handle cases where the DateTime object may be null, while the DateTimeConverter class will handle non-null DateTime objects. With these classes in place, we will be able to correctly handle the formatting of DateTime objects when retrieving data from the database.
class DateTimeConverterNullable extends JsonConverter<DateTime?, dynamic> {
const DateTimeConverterNullable();
@override
DateTime? fromJson(dynamic json) {
if (json == null) return null;
return const DateTimeConverter().fromJson(json);
}
@override
String? toJson(DateTime? object) {
if (object == null) return null;
return const DateTimeConverter().toJson(object);
}
}
class DateTimeConverter extends JsonConverter<DateTime, dynamic> {
const DateTimeConverter();
@override
DateTime fromJson(dynamic json) {
if (json is DateTime) return json;
return DateTime.parse(json as String);
}
@override
String toJson(DateTime object) {
return object.toIso8601String();
}
}
Now we can use this converter in our Todo model.
factory Todo({
required TodoId id,
required String title,
@Default('') String description,
@Default(false) bool? completed,
- required DateTime createdAt,
+ @DateTimeConverter() required DateTime createdAt,
- DateTime? updatedAt,
+ @DateTimeConverterNullable() DateTime? updatedAt,
}) = _Todo;
Once you have finished updating the Todo model to use the converters.
Don't forget to run
build_runnerπ
Woo hoo! We made it to the end of part 3 π
In this part, we gave our failure classes a little update and created some new ones. We also added some shiny new exceptions and made sure our DTOs were properly validated. We're almost there, friends! Just a few more steps until we can fully implement the CRUD operations for our awesome to-do app.
In the final part, we'll finally be able to connect to a Postgres database and complete all our backend routes. It's going to be a coding party π Are you ready to join the fun? I know I am! π€©
And if you ever need a reference, just head on over to the GitHub repo for this tutorial at https://github.com/saileshbro/full_stack_todo_dart.
Let's get ready to code some magic in part 4! π
Top comments (0)