Let's build a full-stack dart app
Tags: fullstack, dart, flutter, monorepo
Published: 2023-07-20 20:33:57

---
title: Let's build a full-stack dart app
published: true
description: 
tags: #fullstack #dart #flutter #monorepo
---

I've been working with Flutter for a while and love it. It's an excellent tool for building cross-platform applications. But what if you want to build a full-stack application? We will explore how to build a full-stack application using Flutter and Dart.

![Screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j544h5zyvtakq5man43x.gif)

The complete example code: https://github.com/leehack/fullstack_dart_todo

## What will we build?

The todo application with Flutter and Dart! We will use Flutter for the client and Dart Frog for the server. Flutter will display the UI, and Dart Frog will handle the business logic and data storage.

## Why full-stack in Dart?

The most significant benefit of using Dart for full-stack development is that you can use the same language on both the client and server. It means that you don't have to learn two different languages, which makes it easier to get started with full-stack development. Of course, you can also share your code between the client and server, which is a huge benefit.

## Project Structure

We will use the mono-repo structure for this project. We will create three packages: `backend`, `frontend`, and `shared`. `shared` package will contain all the code that is shared between the client and server. `backend` package will contain all the code for the server(Dart Frog) and `frontend` package will contain all the code for the client(Flutter).

### Shared package (packages/todo)

* `todo_entity.dart` - The Todo data model with the business logic.  
* `todo_repository.dart` - The repository interface. The backend will implement it for storing data. The frontend will implement it for communicating with the backend.

### Backend package (packages/backend_frog)

* `routes/todos/index.dart` - The rest api handlers for the route `https://address.com/todos`. It handles the GET and POST requests.  
* `routes/todos/[id].dart` - The rest api handlers for the route `https://address.com/todos/[id]`. It handles each todo item's GET/PUT/DELETE requests.  
* `lib/data/in_memory_todo_repository.dart` - The repository implementation of the in-memory database.  
* `routes/todos/_middleware.dart` - Dependency Injection of the in-memory database that handlers will use.  

### Frontend package (packages/frontend_flutter)

* `data/remote_todo_repository.dart` - The repository implementation of the remote database (rest api client).
* `providers/todo_list_provider.dart` - The riverpod provider handles business logic and data storage.
* `presentation` folder - The UI code for widgets.
* `pages/todo_page.dart` - The todo page displays the todo list and form.


## Mono-repo setup

We will use [melos](https://melos.invertase.dev/) to manage the mono-repo. Melos is a tool for managing Dart projects with multiple packages.

1. Install melos cli

   ```bash
    dart pub global activate melos
   ```

2. Create a project folder and initialize the melos workspace

   ```bash
    mkdir fullstack_dart_todo
    cd fullstack_dart_todo
    mkdir packages
   ```

   Add the following to the `pubspec.yaml` file:

   ```yaml
    name: remote_todo

    environment:
      sdk: '>=2.18.0 <3.0.0'
   ```

   Add the melos as a development dependency:

   ```bash
    dart pub add melos --dev
   ```

   Add the following to the `melos.yaml` file:

   ```yaml
    name: remote_todo

    packages:
      - packages/*

    scripts:
      analyze:
        exec: Dart analyze .
   ```

Now you can run the `melos bs` for bootstrapping the mono-repo. Bootstrapping has two primary roles:

   1. Installing all package dependencies (internally using `pub get`).
   2. Locally linking any packages together.

_Every time creating a new package, you must run the `melos bs` command. If you clone the project from GitHub, you must run the `melos bs` command._


## Create the backend package with the Dart Frog

We'll use the [Dart Frog](https://dartfrog.vgv.dev/) framework for the backend. [Dart Frog](https://dartfrog.vgv.dev/) is built on top of [shelf](https://pub.dev/packages/shelf) and [mason](https://pub.dev/packages/mason) and is inspired by many tools, including [remix.run](https://remix.run/), [next.js](https://nextjs.org/), and [express.js](https://expressjs.com/).

We need to install the `dart_frog_cli` first.
   ```bash
    dart pub global activate dart_frog_cli
   ```

Now we are ready to create the backend project.

   ```bash
    cd packages
    dart_frog create backend_frog
    cd backend_frog
   ```

Start the dev server:

   ```bash
    dart_frog dev
   ```

By default, it opens the web server at `http://localhost:8080`. You can see the default page. Thanks to the hot reload, you can see the changes immediately.

## Create the frontend package with the Flutter

We'll use [Flutter](https://flutter.dev/) for the frontend. Flutter is Google's UI toolkit for building beautiful, natively compiled mobile, web, and desktop applications from a single codebase.

Let's create the Flutter project under the packages folder.

   ```bash
    cd packages
    flutter create frontend_flutter
    cd frontend_flutter
   ```

## Create the pure Dart shared package

The shared package will contain all the code that is shared between the client and server. We will create the pure dart package since it has no UI code.

Let's create the library project under the packages folder.

  ```bash
    cd packages
    dart create -t package todo
    cd todo
  ```

To make the backend and frontend projects use the shared package, we must add the dependency to each project's `pubspec.yaml` file (`backend_frog`, `frontend_flutter`).

  ```yaml
    dependencies:
      todo: ^0.0.1
  ```

Once we run the `melos bs` command, the shared package will be linked to the backend and frontend project.

So, now we have everything ready to start the coding!

## Global Architecture

![Architecture](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sh7equ0x8hneg1o1rumn.png)

The shared package has the Todo data model and the repository interface. The backend and frontend packages implement the repository interface. The backend implements it as the in-memory database, and the frontend implements it as the rest api client. The Todo entity is the data model with the business logic that will be referenced in both the backend and frontend packages.

## Backend

The backend is a simple CRUD application that exposes REST APIs. It uses the in-memory database for storing data. The in-memory database is a simple List that stores the Todo data model. The backend has two routes: `https://address.com/todos` and `https://address.com/todos/[id]`. The first route handles the GET and POST requests. The second route handles each todo item's GET/PUT/DELETE requests.

### Routes

Dart Frog uses directory-based routing. Each route is a directory with the `*.dart` file. `index.dart` represents the root route of the path. For example, the `routes/todos/index.dart` represents the `https://address.com/todos` route. The `routes/todos/[id].dart` represents the `https://address.com/todos/[id]` route.

* Handlers for `/todos` in `routes/todos/index.dart`
  ```dart
  Future<Response> onRequest(RequestContext context) async {
    return switch (context.request.method) {
      HttpMethod.get => await _get(context),
      HttpMethod.post => await _post(context),
      _ => Response(statusCode: HttpStatus.notFound)
    };
  }

  Future<Response> _get(RequestContext context) async {
    final todos = await context.read<TodoRepository>().fetchAll();
    final json = [for (var todo in todos) todo.asMap];
    return Response.json(body: json);
  }

  Future<Response> _post(RequestContext context) async {
    final json = await context.request.json() as Map<String, dynamic>;
    final todo = await context.read<TodoRepository>().add(Todo.fromMap(json));
    return Response.json(body: todo.asMap);
  }
  ```

* Handlers for `/todos/[id]` in `routes/todos/[id].dart`
  ```dart
  Future<Response> onRequest(RequestContext context, String id) async {
    return switch (context.request.method) {
      HttpMethod.get => await _get(context, id),
      HttpMethod.put => await _put(context, id),
      HttpMethod.delete => await _delete(context, id),
      _ => Response(statusCode: HttpStatus.notFound)
    };
  }

  Future<Response> _get(RequestContext context, String id) async {
    final todo = await context.read<TodoRepository>().getById(id);

    return Response.json(body: todo.asMap);
  }

  Future<Response> _put(RequestContext context, String id) async {
    final json = await context.request.json() as Map<String, dynamic>;
    final todo = Todo.fromMap(json);
    await context.read<TodoRepository>().updateById(todo);
    return Response();
  }

  Future<Response> _delete(RequestContext context, String id) async {
    final json = await context.request.json() as Map<String, dynamic>;
    final id = json['id'] as String;
    await context.read<TodoRepository>().deleteById(id);
    return Response();
  }
  ```

Each handler does the simple thing. Just call the repository methods and return the response. The repository is injected into the handler using the dependency injection with the middleware.

### Dependency Injection with the middleware

Dart Frog supports the dependency injection natively. We can inject the repository into the handler using the middleware. The middlewares are also directory based. For example, `routes/todos/_middleware.dart` is the middleware for the `routes/todos` route. The `_middleware.dart` file is a particular file name that represents the middleware for the directory.

```dart
InMemoryTodoRepository? _todoData;

Handler middleware(Handler handler) {
  return handler.use(
    provider<TodoRepository>(
      (context) => _todoData ??= InMemoryTodoRepository(),
    ),
  );
}
```

The code above injects the `InMemoryTodoRepository` into the provider so the handler can access the repository. Thanks to the middleware with the dependency injection, we can easily use it from the handler as below.

```dart
await context.read<TodoRepository>()
```

### Repository with in-memory database

I said it's a database, but it's just a simple List. It's not an actual database. It's just an example. The repository interface is in the shared package. The backend implements it as the in-memory database repository. The repository contains the List of Todo data model.
   
  ```dart
  class InMemoryTodoRepository implements TodoRepository {
    /// Local data
    List<Todo> data = [];

    /// Get List of [Todo]
    List<Todo> get todos => data;

    @override
    Future<Todo> getById(String id) async {
      return data.firstWhere((todo) => todo.id == id);
    }

    /// Add a new [todo] and return [Todo] with new id
    @override
    Future<Todo> add(Todo todo) async {
      final newTodo = todo.copyWith(id: const Uuid().v4());
      data = [...data, newTodo];
      return newTodo;
    }

    @override
    Future<void> deleteById(String id) async {
      data = [
        for (final t in data)
          if (t.id != id) t
      ];
    }

    @override
    Future<List<Todo>> fetchAll() async {
      return data;
    }

    @override
    Future<void> updateById(Todo todo) async {
      data = [
        for (final t in data)
          if (t.id == todo.id) todo else t
      ];
    }
  }
  ```

As you can see, it has some CRUD methods for mutating the `List<Todo> data`.

## Frontend

The Frontend app is written in Flutter. It uses the Riverpod for state management and dependency injection. Same as the backend architecture, we store the implemented repository in the provider so the `todoListProvider` can access the repository. The `todoListProvider` is the riverpod provider that handles the business logic of the todo list.

### Presentation and pages

There are two folders for UI code: `presentation` and `pages`. The `presentation` folder contains the code for widgets. The `pages` folder contains the UI code for pages. The `pages/todo_page.dart` is the main page that displays the todo list and form using the `TodoListView` and `TodoTextField` widgets in the `presentation` folder. `main.dart` is the entry point of the Flutter application that displays the `TodoPage`.

In the body of the `TodoPage`, it renders a column with the `TodoListView` and `TodoTextField` widgets.

```dart
const Column(
  children: [
    Padding(
      padding: EdgeInsets.all(16.0),
      child: TodoTextField(),
    ),
    Expanded(child: SelectionArea(child: TodoListView())),
  ],
),
```

The `TodoTextFiled` Widget is a simple text field that handles the input and submits the todo item. When the user submits the text field, it's calling todoListProvier's `add` method.

```dart
class _TodoTextFieldState extends ConsumerState<TodoTextField> {
  final controller = TextEditingController();
  final focusNode = FocusNode();

  @override
  Widget build(BuildContext context) {
    return TextField(
      decoration: const InputDecoration(
        hintText: 'Enter a new todo',
      ),
      focusNode: focusNode,
      controller: controller,
      onSubmitted: (value) async {
        await ref.read(todoListProvider.notifier).add(
              Todo(id: '', title: value),
            );
        controller.clear();
        focusNode.requestFocus();
      },
    );
  }
}
```

The `TodoListView` Widget is a simple list view that displays the todo list. It watches the todoListProvider and rebuilds the list view when the todo list is changed. When the user clicks the checkbox, it calls todoListProvier's `toggle` method.

```dart
class TodoListView extends ConsumerWidget {
  const TodoListView({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return switch (ref.watch(todoListProvider)) {
      AsyncData(:final value) => ListView.builder(
          padding: const EdgeInsets.all(16),
          itemCount: value.length,
          itemBuilder: (context, index) => TodoItem(todo: value[index]),
        ),
      AsyncLoading() => const Center(child: CircularProgressIndicator()),
      _ => const Center(child: Text("Error")),
    };
  }
}

class TodoItem extends ConsumerWidget {
  const TodoItem({super.key, required this.todo});

  final Todo todo;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: [
        Text(todo.title),
        Checkbox(
          value: todo.isDone,
          onChanged: (value) {
            ref.read(todoListProvider.notifier).toggle(todo);
          },
        ),
      ],
    );
  }
}
```

### Business logic with the Riverpod

The `todoListProvider` is the riverpod's AsyncNotifier provider that handles the business logic of the todo list. It calls the `RemoteTodoRepository`'s methods for communicating with the backend. When it's initialized, it calls the `fetchAll` method of the `TodoRepository` to fetch all the Todo items from the backend and cache them. The `add` and `toggle` methods call the `add` and `updateById` methods of the `TodoRepository`. It updates the backend todo list and renews the local cache with the latest todo items in the provider.

```dart
@riverpod
class TodoList extends _$TodoList {
  @override
  FutureOr<List<Todo>> build() {
    return ref.read(todoRepositoryProvider).fetchAll();
  }

  Future<void> add(Todo todo) async {
    state = await AsyncValue.guard(() async {
      await ref.read(todoRepositoryProvider).add(todo);
      return ref.read(todoRepositoryProvider).fetchAll();
    });
  }

  void toggle(Todo todo) async {
    state = await AsyncValue.guard(() async {
      await ref
          .read(todoRepositoryProvider)
          .updateById(todo.copyWith(isDone: !todo.isDone));
      return ref.read(todoRepositoryProvider).fetchAll();
    });
  }
}
```

### Rest API Client

`RemoteTodoRepository` is the rest api client for communicating with the backend, which is implementing the `TodoRepository` interface. It uses the `http` package for making the HTTP request. It's calling the `http` package's `get`, `post`, `put`, and `delete` methods for communicating with the backend.

```dart
class RemoteTodoRepository implements TodoRepository {
  @override
  Future<Todo> add(Todo todo) async {
    final client = http.Client();
    final response = await client.post(
      Uri.parse("$baseURL/todos"),
      headers: {
        'Origin': 'http://localhost:8080',
        'Content-Type': 'application/json',
      },
      body: jsonEncode(todo.asMap),
    );
    final json = response.body;

    return Todo.fromMap(jsonDecode(json));
  }

  @override
  Future<void> deleteById(String id) async {
    final client = http.Client();
    await client.delete(
      Uri.parse("$baseURL/todos/$id"),
      headers: {
        'Origin': 'http://localhost:8080',
        'Content-Type': 'application/json',
      },
    );
  }

  @override
  Future<List<Todo>> fetchAll() async {
    final client = http.Client();
    final response = await client.get(
      Uri.parse("$baseURL/todos"),
      headers: {
        'Origin': 'http://localhost:8080',
        'Content-Type': 'application/json',
      },
    );
    final json = response.body;

    return [for (final todo in jsonDecode(json)) Todo.fromMap(todo)];
  }

  @override
  Future<Todo> getById(String id) async {
    final client = http.Client();
    final response = await client.get(
      Uri.parse("$baseURL/todos/$id"),
      headers: {
        'Origin': 'http://localhost:8080',
        'Content-Type': 'application/json',
      },
    );
    final json = response.body;

    return Todo.fromMap(jsonDecode(json));
  }

  @override
  Future<void> updateById(Todo todo) async {
    final client = http.Client();
    await client.put(
      Uri.parse("$baseURL/todos/${todo.id}"),
      headers: {
        'Origin': 'http://localhost:8080',
        'Content-Type': 'application/json',
      },
      body: jsonEncode(todo.asMap),
    );
  }
}
```


## Summary

We've built a full-stack application with Flutter and Dart. We used Flutter for the client and Dart Frog for the server. Flutter displayed the UI, and Dart Frog handled the business logic and data storage. We used the mono-repo structure for the project. We created three packages: `backend`, `frontend`, and `shared`. `shared` package contained all the code that is shared between the client and server. `backend` package contained all the code for the server(Dart Frog) and `frontend` package contained all the code for the client(Flutter).

We would need to run performance benchmarks to use dart as the backend language before we decided to build a production application, but the development experience was great. I liked the basic tooling of the dart_frog. It's simple and easy to use, like Flutter. I also liked the dependency injection with the middleware. I could build the full-stack application with the Dart Frog without spending too much time learning the framework.

The complete code example is available at https://github.com/leehack/fullstack_dart_todo