Affichage en temps réel de millions d’utilisateurs avec Elixir et React - partie 1: création de l’API Rest
Publié le
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
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
#elixir #react #socket