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

L’idée en fil rouge

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

De quoi ai-je besoin ?

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

Les notions abordées dans cet article

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

Installation des pré-requis

  • 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

Initialisation du projet Phoenix

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

Vérification de l’installation

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

Définition des schémas et contexte

Comment manipuler les données Postgres ?

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

Qu’est ce qu’un contexte ?

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

Génération du code via les commandes Mix

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

Team.ex (partiel)

Appliquer des changements dans la base de données

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

mix ecto.migrate

Vérifier les tests

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

mix test

Gestion des routes, contrôleurs et vues

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

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

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

Ajout de règles métier

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

L’api doit exposer le username et la géolocalisation des utilisateurs

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

Vérification des tests

mix test

Ajouter des jeux de données

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'

Ajout d’un joueur sans utiliser le changeset

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

Ajout d’un joueur avec changeset

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

Ajout d’un joueur via le contexte Team

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

Chargement d’un jeu de données via un module Seed

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

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

Midgard.DatabaseSeeder.seed(5000)

Documenter son api

Nous allons documenter notre API avec Swagger

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

Ajout des dépendances

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

{:phoenix_swagger, "~> 0.8"}

Récupérer les dépendances

mix deps.get

Ajout d’une route pour l’UI swagger

Définition des objets Swagger

Génération de la documentation

mix phx.swagger.generate

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

Ce que nous avons réussi

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

Bonus

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