Affichage en temps réel de millions d’utilisateurs avec Elixir et React - partie 1: création de l’API Rest

A la manière du professeur Xavier, on va construire une application pour visualiser en temps réel la position de milliers (voire millions ?) de joueurs connectés

todo: image map avec points qui

Un serveur API Rest construit en Elixir grâce au framework Phoenix

Une interface Client en React pour consommer notre API et afficher les joueurs sur une carte openstreet

Phoenix React

Dans ce premier article, nous allons détailler comment mettre en place une API REST grâce à Phoenix

Les grandes étapes :

  • Installation des pré-requis
  • Initialisation du projet Phoenix
  • Définition des schémas et contexte
  • Gestion des routes, contrôleur et vues
  • Ajouter des jeux de données
  • Documenter son api

Nb: nous n’implémenterons pas de système d’autentification pour le moment. Cela fera l’objet d’un autre article

  • erlang / elixir
  • une base de donnée (Postgres dans notre cas)

Si vous n’avez pas encore un environnement, consultez mon article sur la découverte d’Elixir où je décris les étapes

Utiliser la commande Phoenix pour généraer un nouveau projet

mix phx.new article-elixir-midgard --app midgard  --no-webpack --no-html

L’application sera créée dans le dossier “article-elixir-midgard” et le nom du module sera “Midgard”

Nous n’installons pas webpack ni les controllers web HTML car nous ne mettons à disposition qu’un endpoint d’API REST

Intitilisation de la bdd

mix ecto.create

Lancer la suite de tests

mix test

Lancement du serveur HTTP

mix phx.server

vérification sur http://localhost:4000

Pour changer le port du serveur utilisé

//config/dev.exs

http: [port: System.get_env("PORT") || 4000],

Relancer la commande avec le port souhaité

PORT=5000  mix phx.server

La couche d’abstraction à la base de donnée est gérée par Ecto

C’est ce paquet qui gère les modules suivants:

  • Repo : repository avec mapping des données
  • Changeset : filtre, cast, validations des structs Elixir
  • Query : requête et manipulation des données de la base
  • Schema : map les données de la base à des structs Elixir

Phoenix recommande d’exposer sa logique métier à travers des interfaces appelées “Context module”

Cela permet d’avoir un couplage faible en séparant la couche métier et son usage (ex : API REst, Graphql, Web)

Nb: la notion de contexte est également développée dans les bonnes pratiques du Domain Driven Domain (DDD)_

Dans notre cas, le controller API n’a pas besoin de savoir que nous récupérons les données de Postgres. Ces dernières pourraient très bien venir d’un fichier, d’un cache voir même de plusieurs sources de données agrégées..

Nous avons donc un contexte Team qui encapsule les appels au Repository et validations de Changeset

Notre model Player a les propriétés suivantes :

  • id : clé primaire autogénérée
  • username: string unique
  • status: string
  • latitude: float
  • longitude: float

Utilisons la commande mix pour créer le contexte Team et le schma Player

mix phx.gen.context Team Player players username:string:unique status latitude:float longitude:float

player.ex

{{< gist jrollin 0287cd3238e30fe6bdd304961b36b6f2 >}}

Team.ex (partiel)

{{< gist jrollin ba1904f6d824a5c9b764ec9725b48e1f >}}

La commande ̀ mix phx.gen.context a généré un fichier de migration dans le répertoire “/priv/repo/migrations”

{{< gist jrollin b1d9d75e6cce958ec19121c15e28fce3 >}}

mix ecto.migrate

NB: des tests ont été automatiquement créés par Phoeniix dans le dossier /test/midgard/team/team_test.exs

mix test

Actuellement il n’y a pas de route créée pour lister les joueurs

Il faut créer un controleur et utliser le contexte Team pour le rendu

Encore une fois, une commande mix permet de générer les fichiers pour nous

phx.gen.json Team Player players --no-schema --no-context

Notez bien le suffixe .json dans notre cas. Il existe aussi le suffixe html pour générer des vues avec des templates HTML

Nb: pas besoin de générer un schema et un contexte car nous l’avons déjà fait

Ajoutons la référence au controller créé dans le routeur

/lib/midgard_web/router.ex

{{< gist jrollin 9a6a63d58fe1f39686eb103bd3ee53a5 >}}

mix test

=> Attention : les tests échouent !

Des tests de contrôleurs et vues ont été rajoutés par la commande mix mais elles sont incomplètes

Il faut remplir les attributs @attrs du fichier test

{{< gist jrollin 5bb1cf5e6078dd25d96139464d9656f4 >}}

Rajouter la définition du rendu d’erreur 422 dans fallback controller

def call(conn, {:error, %Ecto.Changeset{}}) do
    conn
    |> put_status(:unprocessable_entity)
    |> put_view(MidgardWeb.ErrorView)
    |> render(:"422")
  end

La génération automatique du code via mix génère également des tests

Cependant, il ne faut pas perdre de vue que nous devons ajouter nos propres règles métiers

{{< gist jrollin 463c40c6e8030352710ef27eb3f83946 >}}

Nb: setup ”[:create_player]” est un hook de ExUnit qui permet de lancer la fonction create_player avant le test

mix test

En consultant http://localhost:4000/api/players, notre API renvoit bien du JSON mais aucune donnée n’est encore stockée en BDD

Grâce au shell Elixir, nous pouvons rajouter des données manuellement

iex -S mix

Nb: ne pas oublier l’argument -S pour que iex ait accès au composant Mix

Si le code est changé après l’ouverture du shell, il faut dire à iex de recharger le code du fichier

r 'path/to/file.ex'

Midgard.Repo.insert %Midgard.Team.Player{username: "test"}

Avantages:

  • Un repo qui reçoit un struct Player

Inconvénients:

  • pas de validation des données
  • appel au repo manuellement

changeset = Midgard.Team.Player.changeset(%Midgard.Team.Player{}, %{username: "thor", status: "whoo", latitude: 0.7265072451, longitude: 0.2344109292})
Midgard.Repo.insert changeset

Avantages:

  • validation des données

Inconvénients:

  • appel au repo manuel

Midgard.Team.create_player %{username: "thor", status: "whoo", latitude: 0.7265072451, longitude: 0.2344109292}

Avantages:

  • L’interface du contexte permet une compréhension très claire de ce que fait la méthode et les arguments requis
  • l’appel au repo n’est pas exposé
  • l’usage du changeset n’est pas exposé

Inconvénients:

  • Plus verbeux

C’est la méthode privilégiée pour la manipulation des données
Elle permet un couplage faible du code et une meilleure compréhension de la logique métier

Phoenix met à disposition un fichier “/priv/repo/seeds.exs” qui permet de déterminer les données à charger dans la base de données

Ce dernier est appelé automatiquement après la commande

mix ecto.setup
//content file mix.exs
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],

Nous allons créer un module “DatabaseSeeder” qui va être appelé dans ce fichier “seed.exs”

Astuce : pour générer des jeux de données cohérent, nous allons rajouter la dépendance à Faker dans “mix.exs”

{:faker, "~> 0.12"},
mix deps.get

{{< gist jrollin 5dd44e6d2f28f7061ab87a2f4bbc8931 >}}

Rajouter l’appel au module dans le fichier “priv/repo/seeds.exs”

Midgard.DatabaseSeeder.seed(5000)

Nous allons documenter notre API avec Swagger

Ce dernier met à disposition une interface pour consulter et tester les URL à disposition dans l’application

Ajout de la dépendance dans “mix.exs”

{:phoenix_swagger, "~> 0.8"}

Récupérer les dépendances

mix deps.get

{{< gist jrollin 63739979c9db371812c2f206add4ee29 >}}

{{< gist jrollin bf33c93f71ad7f9c63f83f4cc3b1874f >}}

mix phx.swagger.generate

L’url swagger est alors disponible selon la route définie http://localhost:4000/api/swagger/index.html

Nous avons créé une API REST qui renvoit une liste JSON de joueurs stockés dans la base de données Postgres Il ne reste plus qu’à afficher leur position sur une carte grâce à la leur géolocalisation

Le dépôt Github l’api décrite dans cet article