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
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