🚀 Building a Fullstack App with dart_frog and Flutter in a Monorepo - Part 4
Tags: programming, tutorial, dart, flutter
Published: 2023-01-02 06:32:43

---
title: 🚀 Building a Fullstack App with dart_frog and Flutter in a Monorepo - Part 4
published: true
date: 2023-01-02 06:09:19 UTC
series: 🚀 Building a Fullstack App with dart_frog and Flutter in a Monorepo
tags: programming, tutorial, dart, flutter
canonical_url: https://saileshdahal.com.np/building-a-fullstack-app-with-dartfrog-and-flutter-in-a-monorepo-part-4
---
![Cover Image](https://cdn.hashnode.com/res/hashnode/image/upload/v1672575039013/d1d63842-e6b8-49c0-b40e-9d6aae5224ca.png)
In the previous part, we set up models, data sources, repositories, exceptions and failures for the full-stack to-do application. We also made some changes to our packages. In this part, we will:

- Connect to a [Postgres](https://www.postgresql.org/) database
- Complete the backend routes
- Add a new controller to handle HTTP requests
- Add necessary failures and exceptions
- Fully implement CRUD operations for the to-do application

# 🚀 Implementing the Backend

It's time to tackle the backend of our to-do app! 💪 Let's get coding! 💻

## Importing necessary dependencies 📦

> Time to bring in the big guns! 💪 Let's import those dependencies

We will import all the necessary dependencies in the `pubspec.yaml` file.

```yaml
dependencies:
  dart_frog: ^0.3.0
  data_source:
    path: ../data_source
  dotenv: ^4.0.1
  either_dart: ^0.3.0
  exceptions:
    path: ../exceptions
  failures:
    path: ../failures
  http: ^0.13.5
  models:
    path: ../models
  postgres: ^2.5.2
  repository:
    path: ../repository
  typedefs:
    path: ../typedefs
```

## 🛠️ Setting up the Postgres database

> 💾 Let's create a database

We will be using the [PostgreSQL](https://www.postgresql.org/) database for this tutorial. To use the [PostgreSQL](https://www.postgresql.org/) database for this tutorial, we can set up a test database on [elephantsql.com](http://elephantsql.com). Simply sign up on the website and click on the option to create a new instance. You should see something similar to this.

![Elephant SQL New DB](https://cdn.hashnode.com/res/hashnode/image/upload/v1672567276279/aed8bde3-4e4f-4be1-8870-249f1ccacf4f.png)

Once you add a name, you will be prompted to choose a name for your instance and a region that is nearest to you. I have selected the `AP-East-1` region, but you can choose any region that you prefer.

![Elephant SQL Select Region](https://cdn.hashnode.com/res/hashnode/image/upload/v1672567316874/4a5d9897-4fad-44a7-8aba-97aac2734568.png)

Once you have chosen a name and selected a region, click the `Create Instance` button. This will redirect you to a dashboard where you can click on the instance name to access the credentials for the database you have just created. The credentials should look similar to this.

![Elephant SQL Creds](https://cdn.hashnode.com/res/hashnode/image/upload/v1672567334246/aba373d5-7eb6-417b-872e-70e3c7a53b70.png)

## Connecting to the Database 🔌

> 💻 Setting up our database connection like a boss

### Setting up environment 🌿

Now that we have created a database, we can connect to it from our application. To do this, we will create a new file `.env` at the root of the `backend` directory. This file will contain the credentials for the database that we have just created. The `.env` file should look similar to this.

Once this is done, we will use the [dotenv](https://pub.dev/packages/dotenv) package to load the environment variables from the `.env` file. We will also use the [postgres](https://pub.dev/packages/postgres) package to connect to the database. You can run the following command in the backend directory to add the necessary dependencies.

```bash
dart pub add dotenv postgres
```

This is how `.env` file should look. Make sure to use your database credentials

```apache
DB_HOST=tiny.db.elephantsql.com
DB_PORT=5432
DB_DATABASE=asztgqfq
DB_USERNAME=asztgqfq
DB_PASSWORD=PcIXbvXQLwpEON61GVPzqs0zHyzHyHZc
```

### Creating database connection 🔗

Now, create a `backend/lib/db/database_connection.dart` file and add the following code.

```dart
import 'dart:developer';

import 'package:dotenv/dotenv.dart';
import 'package:postgres/postgres.dart';

class DatabaseConnection {
  DatabaseConnection(this._dotEnv) {
    _host = _dotEnv['DB_HOST'] ?? 'localhost';
    _port = int.tryParse(_dotEnv['DB_PORT'] ?? '') ?? 5432;
    _database = _dotEnv['DB_DATABASE'] ?? 'test';
    _username = _dotEnv['DB_USERNAME'] ?? 'test';
    _password = _dotEnv['DB_PASSWORD'] ?? 'test';
  }

  final DotEnv _dotEnv;
  late final String _host;
  late final int _port;
  late final String _database;
  late final String _username;
  late final String _password;
  PostgreSQLConnection? _connection;

  PostgreSQLConnection get db =>
      _connection ??= throw Exception('Database connection not initialized');

  Future<void> connect() async {
    try {
      _connection = PostgreSQLConnection(
        _host,
        _port,
        _database,
        username: _username,
        password: _password,
      );
      log('Database connection successful');
      return _connection!.open();
    } catch (e) {
      log('Database connection failed: $e');
    }
  }

  Future<void> close() => _connection!.close();
}
```

Whenever we want to query, we will open a connection to the database and close it once we are done. This will ensure that we are not keeping the connection open for too long.

### 💉Injecting `DatabaseConnection` through `provider`

Create a new file `routes/_middleware.dart` and add the following code.

```dart
import 'package:backend/db/database_connection.dart';
import 'package:dart_frog/dart_frog.dart';
import 'package:dotenv/dotenv.dart';

final env = DotEnv()..load();
final _db = DatabaseConnection(env);

Handler middleware(Handler handler) {
  return handler.use(provider<DatabaseConnection>((_) => _db));
}
```

This middleware is used to provide the `DatabaseConnection` instance to other parts of the application through a `provider`.

### Fetching `DatabaseConnection` from `provider` 🔍

Now in `routes/index.dart` you can get this `DatabaseConnection` from `RequestContext` and use it to query the database.

```dart
import 'package:backend/db/database_connection.dart';
import 'package:dart_frog/dart_frog.dart';

Future<Response> onRequest(RequestContext context) async {
  final connection = context.read<DatabaseConnection>();
  await connection.connect();
  final response =
      await connection.db.query('select * from information_schema.tables');
  await connection.close();
  return Response.json(body: response.map((e) => e.toColumnMap()).toList());
}
```

If you run `dart_frog dev`, then you should be able to open `http://localhost:8080` and see the following output.

![Database Connection Successful](https://cdn.hashnode.com/res/hashnode/image/upload/v1672567384241/52a4d34e-c9b0-4237-bf5c-c399941321f9.png)
We have successfully connected to the database.

### Create Database Table 📝

> 🗃️ Let's build our database table

Before implementing the `TodoDataSource`, we will need to create the table in the database. To do this, open the [elephantsql.com](elephantsql.com) dashboard and click on the **`BROWSER`** tab.

![Elephant SQL Query Execution](https://cdn.hashnode.com/res/hashnode/image/upload/v1672567429967/334dd3bf-3d31-49fd-abc2-d2235d7865b4.png)

Then, execute the following query:

```sql
CREATE TABLE todos(
    id SERIAL PRIMARY KEY NOT NULL,
    title VARCHAR(255) NOT NULL,
    description TEXT NOT NULL,
    completed BOOL DEFAULT FALSE,
    created_at timestamp default current_timestamp NOT NULL,
    updated_at timestamp null
);
```

This [PostgreSQL query](https://www.tutorialspoint.com/postgresql/postgresql_create_table.htm) creates a new table called todos with the following columns:

- **`id`**: an integer column that is the table's primary key and is generated automatically by the database (using the [`SERIAL`](https://www.educba.com/postgresql-serial/) type). The [`NOT NULL`](https://www.tutorialspoint.com/postgresql/postgresql_constraints.htm) constraint ensures that this column cannot contain a `NULL` value.
- **`title`**: a string column with a maximum length of 255 characters. The [`NOT NULL`](https://www.tutorialspoint.com/postgresql/postgresql_constraints.htm) constraint ensures that this column cannot contain a `NULL` value.
- **`description`**: a text column. The `NOT NULL` constraint ensures that this column cannot contain a `NULL` value.
- `completed`: a boolean column with a default value of `FALSE`.
- **`created_at`**: a [`timestamp`](https://www.educba.com/postgresql-timestamp/) column with a default value of the current timestamp. The `NOT NULL` constraint ensures that this column cannot contain a `NULL` value.
- **`updated_at`**: a [`timestamp`](https://www.educba.com/postgresql-timestamp/) column that can contain a `NULL` value.

The `todos` table will be used to store the to-do items in our application. Each row in the table represents a single to-do item, and the table's columns store the data for that to-do item.

Once you run the query, you should see a toast message at the top.

![Elephant SQL Query Success](https://cdn.hashnode.com/res/hashnode/image/upload/v1672567447946/08dd1b1d-1c5b-4f4d-bd8b-7562dc7ffc3f.png)

## Implementing `TodoDataSource` 💪

> Cooking up some `TodoDataSource` magic 🔮

We will implement the todo data source in `backend/lib/todo/data_source/todo_data_source_impl.dart`. This file will contain the implementation of the `TodoDataSource` interface. We will pass a `DatabaseConnection` as a dependency to this class.

Create `TodoDataSourceImpl` and implement `TodoDataSource` interface, and override necessary methods. The empty implementation should look like this.

### Empty `TodoDataSource` implementation

```dart
import 'package:backend/db/database_connection.dart';
import 'package:data_source/data_source.dart';
import 'package:models/models.dart';
import 'package:typedefs/typedefs.dart';

class TodoDataSourceImpl implements TodoDataSource {
  const TodoDataSourceImpl(this._databaseConnection);
  final DatabaseConnection _databaseConnection;

  @override
  Future<Todo> createTodo(CreateTodoDto todo) {
    throw UnimplementedError();
  }

  @override
  Future<void> deleteTodoById(TodoId id) {
    throw UnimplementedError();
  }

  @override
  Future<List<Todo>> getAllTodo() {
    throw UnimplementedError();
  }

  @override
  Future<Todo> getTodoById(TodoId id) {
    throw UnimplementedError();
  }

  @override
  Future<Todo> updateTodo({required TodoId id, required UpdateTodoDto todo}) {
    throw UnimplementedError();
  }
}
```

### 💉Injecting `TodoDataSource` through `provider`

> Let's add this to our global middleware in `routes/_middleware.dart` file

```dart
import 'package:backend/db/database_connection.dart';
import 'package:backend/todo/data_source/todo_data_source_impl.dart';
import 'package:dart_frog/dart_frog.dart';
import 'package:dotenv/dotenv.dart';
final env = DotEnv()..load();
final _db = DatabaseConnection(env);
final _ds = TodoDataSourceImpl(_db);

Handler middleware(Handler handler) {
  return handler
      .use(requestLogger())
      .use(provider<DatabaseConnection>((_) => _db))
      .use(provider<TodoDataSource>((_) => _ds));
}
```

### `createTodo` implementation

Now we will implement the `createTodo` method.

```dart
  @override
  Future<Todo> createTodo(CreateTodoDto todo) async {
    try {
      await _databaseConnection.connect();
      final result = await _databaseConnection.db.query(
        '''
        INSERT INTO todos (title, description, completed, created_at)
        VALUES (@title, @description, @completed, @created_at) RETURNING *
        ''',
        substitutionValues: {
          'title': todo.title,
          'description': todo.description,
          'completed': false,
          'created_at': DateTime.now(),
        },
      );
      if (result.affectedRowCount == 0) {
        throw const ServerException('Failed to create todo');
      }
      final todoMap = result.first.toColumnMap();
      return Todo(
        id: todoMap['id'] as int,
        title: todoMap['title'] as String,
        description: todoMap['description'] as String,
        createdAt: todoMap['created_at'] as DateTime,
      );
    } on PostgreSQLException catch (e) {
      throw ServerException(e.message ?? 'Unexpected error');
    } finally {
      await _databaseConnection.close();
    }
  }
```

First, the method establishes a connection to the database using the `_databaseConnection` object. Then, it uses the query method on the `db` object to execute an `INSERT` statement The `substitutionValues` parameter is used to bind the values from the `CreateTodoDto` .

If the `INSERT` statement is successful, the method retrieves the inserted row from the database using the `RETURNING *` clause and converts it to a map using the `toColumnMap` method. The method then uses this map to create and return a new `Todo` object.

### `getAllTodo` implementation

Now we will implement the `getAllTodo` method.

```dart
  @override
  Future<List<Todo>> getAllTodo() async {
    try {
      await _databaseConnection.connect();
      final result = await _databaseConnection.db.query(
        'SELECT * FROM todos',
      );
      final data =
          result.map((e) => e.toColumnMap()).map(Todo.fromJson).toList();
      return data;
    } on PostgreSQLException catch (e) {
      throw ServerException(e.message ?? 'Unexpected error');
    } finally {
      await _databaseConnection.close();
    }
  }
```

This `getAllTodo` method is used to retrieve a list of all the to-do items stored in the database. This executes a `SELECT` query to retrieve all rows from the `todos` table, maps each row to a `Todo` object using the `Todo.fromJson` function, and returns the list of `Todo` objects.

### `getTodoById` implementation 🔍

Now we will implement the `getTodoById` method.

```dart
@override
Future<Todo> getTodoById(TodoId id) async {
  try {
    await _databaseConnection.connect();
    final result = await _databaseConnection.db.query(
      'SELECT * FROM todos WHERE id = @id',
      substitutionValues: {'id': id},
    );
    if (result.isEmpty) {
      throw const NotFoundException('Todo not found');
    }
    return Todo.fromJson(result.first.toColumnMap());
  } on PostgreSQLException catch (e) {
    throw ServerException(e.message ?? 'Unexpected error');
  } finally {
    await _databaseConnection.close();
  }
}
```

We execute a `SELECT` query that selects from `todos` table where the id equals the provided id. If the query returns an empty result set, we throw a `NotFoundException`, indicating that the requested to-do item could not be found, else it returns the mapped todo object.

### `updateTodo` implementation 🔧

Here is the implementation of the `updateTodo` method.

```dart
  @override
  Future<Todo> updateTodo({
    required TodoId id,
    required UpdateTodoDto todo,
  }) async {
    try {
      await _databaseConnection.connect();
      final result = await _databaseConnection.db.query(
        '''
        UPDATE todos
        SET title = COALESCE(@new_title, title),
            description = COALESCE(@new_description, description),
            completed = COALESCE(@new_completed, completed),
            updated_at = current_timestamp
        WHERE id = @id
        RETURNING *
        ''',
        substitutionValues: {
          'id': id,
          'new_title': todo.title,
          'new_description': todo.description,
          'new_completed': todo.completed,
        },
      );
      if (result.isEmpty) {
        throw const NotFoundException('Todo not found');
      }
      return Todo.fromJson(result.first.toColumnMap());
    } on PostgreSQLException catch (e) {
      throw ServerException(e.message ?? 'Unexpected error');
    } finally {
      await _databaseConnection.close();
    }
  }
```

It executes an `UPDATE` query on the todos table. If no value is provided for a column, then the `COALESCE` function is used to keep the existing value in the database unchanged. The `updated_at` column is set to the `current_timestamp`. If the result set is empty, it means that no row with the given id was found, so a `NotFoundException` is thrown.

### `deleteTodoById` implementation 🗑️

Now, we will implement the `deleteTodoById` method.

```dart
  @override
  Future<void> deleteTodoById(TodoId id) async {
    try {
      await _databaseConnection.connect();
      await _databaseConnection.db.query(
        '''
        DELETE FROM todos
        WHERE id = @id
        ''',
        substitutionValues: {'id': id},
      );
    } on PostgreSQLException catch (e) {
      throw ServerException(e.message ?? 'Unexpected error');
    } finally {
      await _databaseConnection.close();
    }
  }
```

If the delete statement is successful, the method does not return anything.

### `PostgresSQLException` handling 🚨

In all of the methods above, if there is an exception while querying, like `PostgresSQLException`, it is caught and a `ServerException` is thrown with a more general error message. Finally, the database connection is closed before the method finishes executing.

## Implementing `TodoRepository` 💪

> 💪 Time to make that `TodoRepository` do some work!

We will implement the todo repository in `backend/lib/todo/repositories/todo_repository_impl.dart`. This file will contain the implementation of the `TodoRepository` interface. We will pass a `TodoDataSource` as a dependency to this class.

### Empty `TodoRepository` implementation

Create `TodoRepositoryImpl` and implement `TodoRepository` interface, and override necessary methods. The empty implementation should look like this.

```dart
import 'package:data_source/data_source.dart';
import 'package:either_dart/either.dart';
import 'package:failures/failures.dart';
import 'package:models/models.dart';
import 'package:repository/repository.dart';
import 'package:typedefs/src/typedefs.dart';

class TodoRepositoryImpl implements TodoRepository {
  TodoRepositoryImpl(this.dataSource);

  final TodoDataSource dataSource;

  @override
  Future<Either<Failure, Todo>> createTodo(CreateTodoDto createTodoDto) {
    throw UnimplementedError();
  }

  @override
  Future<Either<Failure, void>> deleteTodo(TodoId id) {
    throw UnimplementedError();
  }

  @override
  Future<Either<Failure, Todo>> getTodoById(TodoId id) {
    throw UnimplementedError();
  }

  @override
  Future<Either<Failure, List<Todo>>> getTodos() {
    throw UnimplementedError();
  }

  @override
  Future<Either<Failure, Todo>> updateTodo({
    required TodoId id,
    required UpdateTodoDto updateTodoDto,
  }) {
    throw UnimplementedError();
  }
}
```

### 💉Injecting `TodoRepository` through `provider`

Before implementing the methods of `TodoRepository`, we will add it to our global middleware so that we can access it from our routes.

```dart
import 'package:backend/db/database_connection.dart';
import 'package:backend/todo/data_source/todo_data_source_impl.dart';
import 'package:backend/todo/repositories/todo_repository_impl.dart';
import 'package:dart_frog/dart_frog.dart';
import 'package:data_source/data_source.dart';
import 'package:dotenv/dotenv.dart';
import 'package:repository/repository.dart';
final env = DotEnv()..load();
final _db = DatabaseConnection(env);
final _ds = TodoDataSourceImpl(_db);
final _repo = TodoRepositoryImpl(_ds);

Handler middleware(Handler handler) {
  return handler
      .use(requestLogger())
      .use(provider<DatabaseConnection>((_) => _db))
      .use(provider<TodoDataSource>((_) => _ds))
      .use(provider<TodoRepository>((_) => _repo));
}
```

### `createTodo` implementation

Now we will implement the `createTodo` method.

```dart
  @override
  Future<Either<Failure, Todo>> createTodo(CreateTodoDto createTodoDto) async {
    try {
      final todo = await dataSource.createTodo(createTodoDto);
      return Right(todo);
    } on ServerException catch (e) {
      log(e.message);
      return Left(
        ServerFailure(message: e.message),
      );
    }
  }
```

In this code, we are implementing the `createTodo` method which is part of the `TodoRepository` interface using `dataSource.createTodo` method. The `dataSource.createTodo` method is responsible for inserting the todo into the database. If the insertion is successful, it returns the todo object.

### `getTodoById` implementation

Now we will implement the `getTodoById` method.

```dart
  @override
  Future<Either<Failure, Todo>> getTodoById(TodoId id) async {
    try {
      final res = await dataSource.getTodoById(id);
      return Right(res);
    } on NotFoundException catch (e) {
      log(e.message);
      return Left(
        ServerFailure(
          message: e.message,
          statusCode: e.statusCode,
        ),
      );
    } on ServerException catch (e) {
      log(e.message);
      return Left(
        ServerFailure(message: e.message),
      );
    }
  }
```

The method calls the `dataSource.getTodoById` method, which is responsible for querying the database and returning the todo object. If the todo is found, it returns the value. A `NotFoundException` is thrown when the todo with the given id is not found in the database.

### `getTodos` implementation

Here, we will implement the `getTodos` method.

```dart
  @override
  Future<Either<Failure, List<Todo>>> getTodos() async {
    try {
      return Right(await dataSource.getAllTodo());
    } on ServerException catch (e) {
      log(e.message);
      return Left(
        ServerFailure(message: e.message),
      );
    }
  }
```

We call the `dataSource.getAllTodo` method. If the method execution is successful, we return the list of todo items.

### `updateTodo` implementation 🔧

Now we will implement the `updateTodo` method.

```dart
  @override
  Future<Either<Failure, Todo>> updateTodo({
    required TodoId id,
    required UpdateTodoDto updateTodoDto,
  }) async {
    try {
      return Right(
        await dataSource.updateTodo(
          id: id,
          todo: updateTodoDto,
        ),
      );
    } on NotFoundException catch (e) {
      log(e.message);
      return Left(
        ServerFailure(
          message: e.message,
          statusCode: e.statusCode,
        ),
      );
    } on ServerException catch (e) {
      log(e.message);
      return Left(
        ServerFailure(message: e.message),
      );
    }
  }
```

The method updates the todo using the `dataSource.updateTodo` method. If the update is successful, it returns the updated todo. If the update fails and a `NotFoundException` is thrown, it logs the error message and returns a `ServerFailure` object with the appropriate status code.

### `deleteTodo` implementation

Finally, we will implement the `deleteTodo` method.

```dart
  @override
  Future<Either<Failure, void>> deleteTodo(TodoId id) async {
    try {
      final exists = await getTodoById(id);
      if (exists.isLeft) return exists;
      final todo = await dataSource.deleteTodoById(id);
      return Right(todo);
    } on ServerException catch (e) {
      log(e.message);
      return Left(
        ServerFailure(message: e.message),
      );
    }
  }
```

We check if a todo exists by calling the `this.getTodoById` method. If it does not exist, we return a `Failure`. If the todo does exist, we delete it by calling the `dataSource.deleteTodoById`.

### Handling `Exception` 🛑

The `dataSource` methods throw exceptions like `ServerException` and `NotFoundException`. We catch these exceptions and log the error message. We then return a `Left` object containing a `ServerFailure` object. The `ServerFailure` object is a custom failure type that we can use to indicate that a server error occurred.

## Building the `TodoController` 🔨

> Bringing it all together with our fancy new controller 🎉

The controller will be responsible for handling HTTP requests and sending back the appropriate response. We will implement five methods in the controller:

- **index** `GET /resource` 🔍
- **show** `GET /resource/{id}` 📖
- **store** `POST /resource` 📤
- **update** `PUT/PATCH /resource/{id}` 🔗
- **delete** `DELETE /resource/{id}` 🗑️

These methods will correspond to the standard HTTP methods for retrieving, creating, updating, and deleting data. By using these methods, we can keep our code clean and organized. This approach is inspired by the [Laravel framework's API controller](https://laravel.com/docs/9.x/controllers) methods.

### Abstract `HttpController` 🔮

We will create a new abstract class in the `backend/lib/controller/http_controller.dart` called `HttpController` and which will have five methods.

```dart
import 'dart:async';

import 'package:dart_frog/dart_frog.dart';

abstract class HttpController {
  FutureOr<Response> index(Request request);

  FutureOr<Response> store(Request request);

  FutureOr<Response> show(Request request, String id);

  FutureOr<Response> update(Request request, String id);

  FutureOr<Response> destroy(Request request, String id);
}
```

### Empty `TodoController` implementation

Now for the implementation of this contract, we will create a new class `TodoController` in the `backend/lib/controller/todo_controller.dart` file. We will implement each method in the `TodoController` class.

```dart
import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:backend/controller/http_controller.dart';
import 'package:dart_frog/dart_frog.dart';
import 'package:either_dart/either.dart';
import 'package:exceptions/exceptions.dart';
import 'package:failures/failures.dart';
import 'package:models/models.dart';
import 'package:repository/repository.dart';
import 'package:typedefs/typedefs.dart';

class TodoController extends HttpController {
  TodoController(this._repo);

  final TodoRepository _repo;
  @override
  FutureOr<Response> index(Request request) async {
    throw UnimplementedError();
  }

  @override
  FutureOr<Response> show(Request request, String id) async {
    throw UnimplementedError();
  }

  @override
  FutureOr<Response> destroy(Request request, String id) async {
    throw UnimplementedError();
  }

  @override
  FutureOr<Response> store(Request request) async {
    throw UnimplementedError();
  }

  @override
  FutureOr<Response> update(Request request, String id) async {
    throw UnimplementedError();
  }

  Future<Either<Failure, Map<String, dynamic>>> parseJson(
    Request request,
  ) async {
    throw UnimplementedError();
  }
}
```

### Parse request body 🔬

Before we implement the methods, we will create a new helper method in `HttpController` which will be responsible to parse the request body.

If the request body is not a valid JSON, it will return a `Left` object containing a `BadRequestFailure` object. If the request body is a valid JSON, it will return a `Right` object containing the parsed JSON.

Add the following method to the `HttpController` class.

```dart
  Future<Either<Failure, Map<String, dynamic>>> parseJson(
    Request request,
  ) async {
    try {
      final body = await request.body();
      if (body.isEmpty) {
        throw const BadRequestException(message: 'Invalid body');
      }
      late final Map<String, dynamic> json;
      try {
        json = jsonDecode(body) as Map<String, dynamic>;
        return Right(json);
      } catch (e) {
        throw const BadRequestException(message: 'Invalid body');
      }
    } on BadRequestException catch (e) {
      return Left(
        ValidationFailure(
          message: e.message,
          errors: {},
        ),
      );
    }
  }
```

### 💉Injecting `TodoController` through `provider`

> Let's add this to our global middleware `routes/_middleware.dart` file.

```dart
import 'package:backend/db/database_connection.dart';
import 'package:backend/todo/controller/todo_controller.dart';
import 'package:backend/todo/data_source/todo_data_source_impl.dart';
import 'package:backend/todo/repositories/todo_repository_impl.dart';
import 'package:dart_frog/dart_frog.dart';
import 'package:data_source/data_source.dart';
import 'package:dotenv/dotenv.dart';
import 'package:repository/repository.dart';

final env = DotEnv()..load();
final _db = DatabaseConnection(env);
final _ds = TodoDataSourceImpl(_db);
final _repo = TodoRepositoryImpl(_ds);
final _todoController = TodoController(_repo);

Handler middleware(Handler handler) {
  return handler
      .use(requestLogger())
      .use(provider<DatabaseConnection>((_) => _db))
      .use(provider<TodoDataSource>((_) => _ds))
      .use(provider<TodoRepository>((_) => _repo))
      .use(provider<TodoController>((_) => _todoController));
}
```

> Note: We are using `requestLogger` middleware to log the request and response.

## Implementing `TodoController` 🚀

We will implement the `TodoController` methods as follows:

### `index` implementation

The `index` method will be responsible for retrieving all todo items from the database. We will implement it as follows:

```dart
  @override
  FutureOr<Response> index(Request request) async {
    final res = await _repo.getTodos();
    return res.fold(
      (left) => Response.json(
        body: {'message': left.message},
        statusCode: left.statusCode,
      ),
      (right) => Response.json(
        body: right.map((e) => e.toJson()).toList(),
      ),
    );
  }
```

We will call the `getTodos` method and map the response. If the failure case is returned, we will return a response with an error status code and the error message. Else, we will return a `200` status code and the list of todos.

### `show` implementation

`show` method will be responsible for retrieving a single to-do item from the database. We will implement it as follows:

```dart
  @override
  FutureOr<Response> show(Request request, String id) async {
    final todoId = mapTodoId(id);
    if (todoId.isLeft) {
      return Response.json(
        body: {'message': todoId.left.message},
        statusCode: todoId.left.statusCode,
      );
    }
    final res = await _repo.getTodoById(todoId.right);
    return res.fold(
      (left) => Response.json(
        body: {'message': left.message},
        statusCode: left.statusCode,
      ),
      (right) => Response.json(
        body: right.toJson(),
      ),
    );
  }
```

We will first call `mapTodoId` method to validate the `id` parameter. If it returns a failure, we will return a failure response with the status code. Then we will get the todo item from the repository and return the todo item if it is found. Else, we will return a failure response with the status code.

### `store` implementation

`store` method is as follows

```dart
  @override
  FutureOr<Response> store(Request request) async {
    final parsedBody = await parseJson(request);
    if (parsedBody.isLeft) {
      return Response.json(
        body: {'message': parsedBody.left.message},
        statusCode: parsedBody.left.statusCode,
      );
    }
    final json = parsedBody.right;
    final createTodoDto = CreateTodoDto.validated(json);
    if (createTodoDto.isLeft) {
      return Response.json(
        body: {
          'message': createTodoDto.left.message,
          'errors': createTodoDto.left.errors,
        },
        statusCode: createTodoDto.left.statusCode,
      );
    }
    final res = await _repo.createTodo(createTodoDto.right);
    return res.fold(
      (left) => Response.json(
        body: {'message': left.message},
        statusCode: left.statusCode,
      ),
      (right) => Response.json(
        body: right.toJson(),
        statusCode: HttpStatus.created,
      ),
    );
  }
```

If `parseJson` resolves to a failure, we will return a response with an error status code and message.

We will pass the JSON object to the `CreateTodoDto.validated` method. If this returns a failure, we will return a response with an error status code and message, here it will be `ValidationFailure`.

We will pass the DTO to `createTodo` method of the `TodoRepository`. If creating fails we will return a response with an error status code and message.

If everything goes right, we will return a response with a `201` status code and the to-do item.

### `destroy` implementation

`destroy` method is as follows

```dart
  @override
  FutureOr<Response> destroy(Request request, String id) async {
    final todoId = mapTodoId(id);
    if (todoId.isLeft) {
      return Response.json(
        body: {'message': todoId.left.message},
        statusCode: todoId.left.statusCode,
      );
    }
    final res = await _repo.deleteTodo(todoId.right);
    return res.fold(
      (left) => Response.json(
        body: {'message': left.message},
        statusCode: left.statusCode,
      ),
      (right) => Response.json(body: {'message': 'OK'}),
    );
  }
```

We will first call `mapTodoId` method to validate the `id` parameter. If it returns a failure, we will return a failure response with the status code. Then we will get the delete todo item from the repository and return OK with 200 status code if. If there is a failure, we will return a failure response with the status code.

### `update` implementation

`update` method will be responsible for updating a single to-do item from the database. We will implement it as follows:

```dart
  @override
  FutureOr<Response> update(Request request, String id) async {
    final parsedBody = await parseJson(request);
    final todoId = mapTodoId(id);
    if (todoId.isLeft) {
      return Response.json(
        body: {'message': todoId.left.message},
        statusCode: todoId.left.statusCode,
      );
    }
    if (parsedBody.isLeft) {
      return Response.json(
        body: {'message': parsedBody.left.message},
        statusCode: parsedBody.left.statusCode,
      );
    }

    final json = parsedBody.right;
    final updateTodoDto = UpdateTodoDto.validated(json);
    if (updateTodoDto.isLeft) {
      return Response.json(
        body: {
          'message': updateTodoDto.left.message,
          'errors': updateTodoDto.left.errors,
        },
        statusCode: updateTodoDto.left.statusCode,
      );
    }
    final res = await _repo.updateTodo(
      id: todoId.right,
      updateTodoDto: updateTodoDto.right,
    );
    return res.fold(
      (left) => Response.json(
        body: {'message': left.message},
        statusCode: left.statusCode,
      ),
      (right) => Response.json(
        body: right.toJson(),
      ),
    );
  }
```

This method is similar to the `store` method. We will first validate the `id` parameter and then the JSON body. If both are valid, we will call the `updateTodo` method of the `TodoRepository` and then resolve the failure or the updated value.

## Implementing Routes 🛣️

We will now implement the routes. These are the routes that we will have

- **GET** `/todos` - Get all todos
- **GET** `/todos/:id` - Get a single todo
- **POST** `/todos` - Create a todo
- **PUT** `/todos/:id` - Update a todo
- **PATCH** `/todos/:id` - Update a todo
- **DELETE** `/todos/:id` - Delete a todo

`dart_frog` has a file system routing. For example, let's say we need to create `todos/1` route. We will create a file `routes/todos/[id].dart` and it will be mapped to `todos/1` route.

### Implementing `todos/` route

To implement all the HTTP methods related to `todos/` route, we will create a new file `routes/todos/index.dart`.

We will create a file `routes/todos/index.dart` and implement the routes as follows:

```dart
import 'dart:io';

import 'package:backend/todo/controller/todo_controller.dart';
import 'package:dart_frog/dart_frog.dart';

Future<Response> onRequest(RequestContext context) async {
  final controller = context.read<TodoController>();
  switch (context.request.method) {
    case HttpMethod.get:
      return controller.index(context.request);
    case HttpMethod.post:
      return controller.store(context.request);
    case HttpMethod.put:
    case HttpMethod.patch:
    case HttpMethod.delete:
    case HttpMethod.head:
    case HttpMethod.options:
      return Response.json(
        body: {'error': '👀 Looks like you are lost 🔦'},
        statusCode: HttpStatus.methodNotAllowed,
      );
  }
}
```

Here, we are getting the `TodoController` from the `context` and mapping the respective HTTP method to the controller method. We are also handling cases when the HTTP method is not allowed.

### Implementing `todos/:id` route

Similarly, we will create a file `routes/todos/[id].dart` and implement the routes as follows:

```dart
import 'dart:io';

import 'package:backend/todo/controller/todo_controller.dart';
import 'package:dart_frog/dart_frog.dart';

Future<Response> onRequest(RequestContext context, String id) async {
  final todoController = context.read<TodoController>();
  switch (context.request.method) {
    case HttpMethod.get:
      return todoController.show(context.request, id);
    case HttpMethod.put:
    case HttpMethod.patch:
      return todoController.update(context.request, id);
    case HttpMethod.delete:
      return todoController.destroy(context.request, id);
    case HttpMethod.head:
    case HttpMethod.options:
    case HttpMethod.post:
      return Response.json(
        body: {'error': '👀 Looks like you are lost 🔦'},
        statusCode: HttpStatus.methodNotAllowed,
      );
  }
}
```

Here, we will get the `id` parameter from the route as a second parameter in `onRequest` method as a string. This can be anything. This explains the use of `mapTodoId` function in `typedefs` package.

We will pass the `id` parameter to the controller methods. We will also handle the case when the HTTP method is not allowed.

### Implementing `/` route

Finally, we will update `routes/index.dart` file to return `methodNotAllowed` response. This is our `/` route, which will be handled as follows:

```dart
import 'dart:io';

import 'package:dart_frog/dart_frog.dart';

Future<Response> onRequest(RequestContext context) async {
  return Response.json(
    body: {'error': '👀 Looks like you are lost 🔦'},
    statusCode: HttpStatus.methodNotAllowed,
  );
}
```

# 🧪 Testing backend

> Time to put our backend to the test! 🔍

We will run some e2e tests, to verify the backend works fine. create a file `backend/e2e/routes_test.dart` and implement the tests as follows:

```dart
import 'dart:convert';
import 'dart:io';

import 'package:http/http.dart' as http;
import 'package:models/models.dart';
import 'package:test/test.dart';

void main() {
  late Todo createdTodo;
  tearDownAll(() async {
    final response = await http.get(Uri.parse('http://localhost:8080/todos'));
    final todos = (jsonDecode(response.body) as List)
        .map((e) => Todo.fromJson(e as Map<String, dynamic>))
        .toList();
    for (final todo in todos) {
      await http.delete(Uri.parse('http://localhost:8080/todos/${todo.id}'));
    }
  });
  group('E2E -', () {
    test('GET /todos returns empty list of todos', () async {
      final response = await http.get(Uri.parse('http://localhost:8080/todos'));
      expect(response.statusCode, HttpStatus.ok);
      expect(response.body, equals('[]'));
    });

    test('POST /todos to create a new todo', () async {
      final response = await http.post(
        Uri.parse('http://localhost:8080/todos'),
        headers: {
          'Content-Type': 'application/json',
        },
        body: jsonEncode(_createTodoDto.toJson()),
      );
      expect(response.statusCode, HttpStatus.created);
      createdTodo =
          Todo.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
      expect(createdTodo.title, equals(_createTodoDto.title));
      expect(createdTodo.description, equals(_createTodoDto.description));
    });

    test('GET /todos returns list of todos with one todo', () async {
      final response = await http.get(Uri.parse('http://localhost:8080/todos'));
      expect(response.statusCode, HttpStatus.ok);
      final todos = (jsonDecode(response.body) as List)
          .map((e) => Todo.fromJson(e as Map<String, dynamic>))
          .toList();
      expect(todos.length, equals(1));
      expect(todos.first, equals(createdTodo));
    });

    test('GET /todos/:id returns the created todo', () async {
      final response = await http.get(
        Uri.parse('http://localhost:8080/todos/${createdTodo.id}'),
      );
      expect(response.statusCode, HttpStatus.ok);
      final todo =
          Todo.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
      expect(todo, equals(createdTodo));
    });

    test('PUT /todos/:id to update the created todo', () async {
      final updateTodoDto = UpdateTodoDto(
        title: 'updated title',
        description: 'updated description',
      );
      final response = await http.put(
        Uri.parse('http://localhost:8080/todos/${createdTodo.id}'),
        headers: {
          'Content-Type': 'application/json',
        },
        body: jsonEncode(updateTodoDto.toJson()),
      );
      expect(response.statusCode, HttpStatus.ok);
      final todo =
          Todo.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
      expect(todo.title, equals(updateTodoDto.title));
      expect(todo.description, equals(updateTodoDto.description));
    });

    test('PATCH /todos/:id to update the created todo', () async {
      final updateTodoDto = UpdateTodoDto(
        title: 'UPDATED TITLE',
        description: 'UPDATED DESCRIPTION',
      );
      final response = await http.patch(
        Uri.parse('http://localhost:8080/todos/${createdTodo.id}'),
        headers: {
          'Content-Type': 'application/json',
        },
        body: jsonEncode(updateTodoDto.toJson()),
      );
      expect(response.statusCode, HttpStatus.ok);
      final todo =
          Todo.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
      expect(todo.title, equals(updateTodoDto.title));
      expect(todo.description, equals(updateTodoDto.description));
    });

    test('DELETE /todos/:id to delete the created todo', () async {
      final response = await http.delete(
        Uri.parse('http://localhost:8080/todos/${createdTodo.id}'),
      );
      expect(response.statusCode, HttpStatus.ok);
      expect(response.body, jsonEncode({'message': 'OK'}));
    });
    test('GET /todos returns empty list of todos', () async {
      final response = await http.get(Uri.parse('http://localhost:8080/todos'));
      expect(response.statusCode, HttpStatus.ok);
      expect(response.body, equals('[]'));
    });
  });
}

final _createTodoDto = CreateTodoDto(
  title: 'title',
  description: 'description',
);
```

To run the tests, first, start the backend server by running the following command:

```bash
dart_frog dev
```

And then on a new terminal run the tests:

```bash
dart test e2e/routes_test.dart
```

![Tests Passing fine](https://cdn.hashnode.com/res/hashnode/image/upload/v1672567494374/49437eba-3aac-4eda-af60-fe4bd0850487.png)

---

Wow, we've made it to the end of part 4! 🎉 It's been a wild ride, but we've finally completed the backend of our full-stack to-do application. We connected to a Postgres database, completed all our backend routes, and fully implemented CRUD operations. We even tested our backend to make sure everything is running smoothly.

But we're not done yet! In the final part of this tutorial, we'll be building the front end of our to-do app using Flutter. It's going to be a blast! 💻

Don't forget, you can always refer back to the GitHub repo for this tutorial at [https://github.com/saileshbro/full_stack_todo_dart](https://github.com/saileshbro/full_stack_todo_dart) if you need a little help along the way.

Until next time, happy coding! 😄