Créer un back-end avec Dart Frog

Introduction

Flutter est aujourd’hui l’un des frameworks incontournables de l’écosystème de développement cross platform pour le mobile.

Avec les dernières versions, il a également su présenter des solutions cohérentes pour partager son code avec des applications web et desktop.

Une corde semble cependant manquer à cet arc, à savoir la possibilité de partager du code avec la partie back d’une solution.

Il existe pourtant des solutions pour développer en Dart (le langage derrière Flutter) des API back-end depuis un certain temps, notamment à travers la librairie shelf (shelf | Dart Package).

Et récemment, une nouvelle librairie se basant sur shelf et développée par Very Good Ventures, un acteur incontournable du développement sur Flutter, a vue le jour : Dart Frog (dart_frog | Dart Package).

Nous avons souhaité tester cette solution lors d’un de nos après-midi de communautés de pratique, voici le résumé de ce que nous en avons retenu.

Présentation du sujet

L’idée de cet après midi était de mettre en place un service simple de gestion d’utilisateurs, avec un front mobile en Flutter permettant de consulter une base distante et de la manipuler (ajout / suppression / modification des éléments), un back écrit en Dart exposant un ensemble de endpoints permettant l’accès à la base, et de partager le code des entités manipulées par la base entre ces deux éléments.

Article dart frog - back-end

Afin d’aller au bout du sujet, nous avons décidé de gérer le projet en monorepo, c’est à dire d’intégrer au sein d’un même espace git les différentes briques du projet (front / back / shared) et de simplifier leur intégration avec melos (melos | Dart Package), un outil en ligne de commande dédié à ce type de projet.

Nous sommes arrivés à un projet structuré de la façon suivante :

  • common : contient tout le code commun au deux projets
  • flutter_app : application Flutter
  • frog_back : back développé avec Dart Frog
  • melos.yaml : fichier de configuration melos

La configuration melos proprement dite est assez simple :

1  name: dart-frog
2
3  packages:
4    - common/**
5    - flutter_app/**
6    - frog_back/**
7
8  command:
9    bootstrap:
10      usePubspecOverrides: true

On référence les différents packages du projet, et on indique avec le paramètre usePubspecOverrides que melos doit résoudre les dépendances à common localement.

Mise en place de Dart Frog

Pour l’installation de Dart Frog, rien de plus simple il suffit d’installer le paquet correspondant:

1  dart pub global activate dart_frog_cli

On peut vérifier l’installation en lançant la commande dart frog

1  A fast, minimalistic backend framework for Dart.
2
3  Usage: dart_frog <command> [arguments]
4
5  Global options:
6  -h, --help       Print this usage information.
7      --version    Print the current version.
8
9  Available commands:
10    build    Create a production build.
11    create   Creates a new Dart Frog app.
12    dev      Run a local development server.
13
14  Run "dart_frog help <command>" for more information about a command.

Pour initialiser le projet dart frog :

1  dart_frog create my_project

Il faut ensuite démarrer le serveur :

1  dart_frog dev

Création de routes

Dans Dart Frog, la définition des endpoints se fait grâce à l’arborescence des fichiers sous le répertoire racine /routes. Chaque nom de dossier correspond à un chemin. Ainsi, le fichier /routes/users/index.dart gérera toutes les requêtes faites sur l’endpoint /users.

1  import 'package:dart_frog/dart_frog.dart';
2
3  Response onRequest(RequestContext context) {
4    // Access the incoming request.
5    final request = context.request;
6
7    // Return a response.
8    return Response(body: 'Hello World');
9  }

routes/index.dart correspondant au endpoint /

La méthode onRequest va être appelé à chaque nouvelle requête, et permettre de récupérer les informations de l’appel, comme par exemple la méthode HTTP utilisée :

1  switch (method) {
2   case HttpMethod.get:
3     // TODO: GET method
4    break;
5    case HttpMethod.post:
6    // TODO: POST method
7    break;
8    case HttpMethod.put:
9    // TODO: PUT method
10    break;
11    case HttpMethod.delete:
12  // TODO: DELETE method
13    break;
14  }

Pour créer une route dynamique, le nom du fichier doit indiquer le paramètre correspondant entre crochets. Ainsi /routes/users/[id].dart gèrera les requêtes de la forme /users/<id>.

Il suffit ensuite de définir une méthode onRequest avec un paramètre supplémentaire:

1  import 'package:dart_frog/dart_frog.dart';
2
3  Response onRequest(RequestContext context, String id) {
4    return Response(body: 'post id: $id');
5  }

Base de données

Bien que la documentation officielle ne le mentionne pas, il est tout à fait possible d’utiliser une base de données pour sauvegarder nos modifications. Nous avons choisi ObjectBox (objectbox | Dart Package), une base de données NoSQL, pour sa simplicité de mise en place. Il suffit d’installer une bibliothèque afin que les données soient sauvegardées en local sur le poste.

Il est possible de personnaliser la fonction qui sera appelée au lancement du serveur, et ainsi de créer notre instance de store. On peut ensuite utiliser un middleware pour rendre cette instance disponible dans toute l’application serveur.

Au final, le main.dart de notre projet Dart Frog ressemble à ça :

1 late final Store _store;
2
3  Future run(Handler handler, InternetAddress ip, int port) {
4    _store = openStore();
5    _store.box()
6      ..removeAll()
7      ..putMany(
8        // mocks
9        [
10          User(0, 'Maurice', 'Dupont'),
11          User(0, 'Melon', 'Tusk'),
12          User(0, 'Michel', 'Blanc'),
13          User(0, 'Dark', 'Vador'),
14          User(0, 'Jean', 'Valjean'),
15          User(0, 'Yoda', 'Star'),
16          User(0, 'John', 'Doe'),
17          User(0, 'Bill', 'Gates'),
18          User(0, 'Steve', 'Jobs'),
19          User(0, 'Miguel', 'Brésil'),
20          User(0, 'Simon', 'Cat'),
21        ],
22      );
23
24    return serve(handler.use(databaseHandler()), ip, port);
25  }
26
27  Middleware databaseHandler() {
28    return (handler) {
29      return handler.use(
30        provider(
31          (context) => _store,
32        ),
33      );
34    };
35  }

On peut ensuite s’interfacer avec la base de données dans une route à l’aide du code suivant :

1  final userBox = context.read().box();
2
3  final users = userBox.getAll();

Le projet “common”

Le coeur de notre expérimentation, la librairie partagée entre les projets front et back, contient notre entité User qui ressemble à ça :

1  @Entity()
2  class User {
3    @Id()
4    int id;
5
6    String? firstname;
7    String? lastname;
8
9    User(this.id, this.firstname, this.lastname);
10
11    factory User.fromJson(Map<String, dynamic> json) => User(
12          json['id'] != null ? json['id'] : 0,
13          json['firstname'],
14          json['lastname'],
15        );
16
17    Map<String, dynamic> toJson() => <String, dynamic>{
18          'id': id,
19          'firstname': firstname,
20          'lastname': lastname,
21        };
22  }

Les annotations @Entity et @Id sont fournies par ObjectBox, elles permettent d’identifier les données à intégrer en base.

Au final, notre modèle est plutôt simple, mais l’idée de pouvoir réutiliser la même base entre le back et le front reste séduisante et prend tout son sens sur des projets complexes.

Déploiement

Pour le déploiement:

dart_frog build

3 services sont proposés pour le déploiement :

  • Google Cloud Run
  • AWS App Runner
  • Digital Ocean App Platform

Un tutoriel pour le déploiement avec chacun des services est proposé dans la documentation officielle de Dart Frog.

C’est lors du lancement de cet commande que nous avons constaté le seul problème rencontré que nous décrivons plus bas.

Tests

Avec Dart Frog, il est également possible de mettre en place des tests unitaires pour les tests des routes et des middlewares grâce au package:test et au package:mocktail.

Nous n’avons malheureusement pas eu le temps de creuser cet aspect.

Limites

La limite constatée n’est apparue qu’au moment de lancer la commande dart_frog build afin de voir ce qui était produit par l’outil : la commande n’aboutissait pas, un message d’erreur peu explicite était alors affiché :

1  Because frog_back depends on common from path which doesn't exist
2  (could not find package common at "../common"),
3  version solving failed.

Après quelques recherches et l’ouverture d’une issue sur le github du projet Dart Frog, nous avons fini par comprendre que, pour qu’un projet puisse être buildé, toutes ses sources devaient se trouver dans le répertoire du projet.

Ceci s’explique par le fait que le build prépare un répertoire contenant un fichier Dockerfile permettant simplement l’exécution du projet.

Docker doit avoir sous la main tous les fichiers à embarquer, donc notre architecture était mise en défaut.

La solution suggérée par l’équipe de développement de l’outil : déplacer le projet common dans le répertoire du projet frog_back.

Une fois cette manipulation mise en oeuvre, le build a pu fonctionner.

L’inconvénient de cette solution est que le projet common, pourtant utilisé par flutter_app, n’est pas clairement visible à la racine du repository.

Conclusion

Dart Frog est un outil très facile à prendre en main.

En une après midi, nous avons réussi à mettre en place un ensemble de endpoints, une base de données, et de permettre la communication avec une application Flutter.

Il s’agit d’une librairie en cours de développement, la version disponible au moment d’écrire l’article n’étant qu’une 0.3.1, mais elle montre déjà la volonté de l’équipe de fournir un outil puissant et simple à utiliser.

Vous souhaitez en savoir plus ? Contactez-nous !

Guillaume CAZE FRANCOIS – Développeur Mobile
Simon BERNARDIN – Lead Developer Mobile
Majid SERHANE – Développeur Mobile
Fabien DHERMY – Développeur Mobile
Quentin LEBRETON – Développeur Mobile