Comment implémenter le point de terminaison de l'API JSON sur Elixir sans aucun framework?
Du traducteur:
L'article fournit un exemple d'une application Web très simple qui peut être considérée comme Hello, World! dans la création de l'API la plus simple sur Elixir.
L'exemple de code est légèrement modifié afin de correspondre aux versions actuelles des bibliothèques.
L'exemple de code complet avec les modifications peut être vu sur GitHub .

Nouveaux défis linguistiques
De nombreux développeurs viennent à Elixir du monde de Ruby. Il s'agit d'un environnement très mature en termes de nombre de bibliothèques et de frameworks disponibles. Et une telle maturité ne me suffit parfois pas dans Elixir. Lorsque j'ai besoin d'un service tiers, le résultat d'une recherche appropriée peut être le suivant:
- il y a une bibliothèque officielle bien supportée (très rare);
- il existe une bibliothèque officielle, mais dépassée ou boguée (cela arrive parfois);
- Il y a une bibliothèque bien supportée développée par quelqu'un de la communauté (parfois de temps en temps);
- il y a une bibliothèque développée par quelqu'un de la communauté, mais qui n'est plus supportée (un cas très courant);
- il existe plusieurs bibliothèques, dont chacune est écrite par quelqu'un pour ses propres besoins, et il lui manque les fonctionnalités nécessaires (l'option la plus populaire);
- il y a ma propre bibliothèque, combinant tout le meilleur de ce qui précède ... (trouvé trop souvent).
API JSON simple sur Elixir

Vous pourriez être surpris, mais Ruby n'est pas toujours sur les rails ( Ruby on Rails, souvenez-vous? - Note du traducteur ). La communication avec le Web n'est pas non plus toujours nécessaire pour participer. Bien que dans ce cas particulier, parlons du Web.
Lorsqu'il s'agit d'implémenter un seul point de terminaison RESTful, il existe généralement de nombreuses options:
Ce sont des exemples d'outils que j'ai personnellement utilisés. Mes collègues sont des utilisateurs satisfaits de Sinatra. Ils ont réussi à essayer Hanami. Je peux choisir n'importe quelle option qui me convient, même en fonction de mon humeur actuelle.
Mais quand je suis passé à Elixir, il s'est avéré que le choix était limité. Bien qu'il existe plusieurs «frameworks» alternatifs (dont je ne citerai pas les noms pour des raisons évidentes), il est presque impossible de les utiliser!
J'ai passé toute la journée à trier toutes les bibliothèques jamais mentionnées sur Internet. Agissant en tant que bot Slack, j'ai essayé de déployer un simple serveur HTTP2 sur Heroku , mais à la fin de la journée, j'ai abandonné. Littéralement, aucune des options que j'ai trouvées n'a pu mettre en œuvre les exigences de base.
Pas toujours une solution - Phoenix
Phoenix est mon framework web préféré, c'est juste que parfois c'est redondant. Je ne voulais pas l'utiliser, tirant l'ensemble du cadre dans le projet exclusivement pour un seul point final; et peu importe que ce soit très simple.
Je ne pouvais pas non plus utiliser de bibliothèques prêtes à l'emploi, car, comme je l'ai déjà dit, toutes les bibliothèques que j'ai trouvées n'étaient pas adaptées à mes besoins (un routage de base et une prise en charge JSON étaient nécessaires) ou n'étaient pas assez pratiques pour un déploiement facile et rapide sur Heroku. Prenez du recul, pensai-je.

Mais en fait, Phoenix lui-même est construit sur la base de quelque chose , n'est-ce pas?
Plug & Cowboy vient à la rescousse
Si vous devez créer un serveur vraiment minimaliste sur Ruby, vous pouvez simplement utiliser rack
- une interface modulaire pour les serveurs Web Ruby.
Heureusement, quelque chose de similaire est disponible dans Elixir. Dans ce cas, nous utiliserons les éléments suivants:
- cowboy est un petit serveur HTTP rapide pour Erlang / OTP qui implémente la pile HTTP complète et le routage, optimisé pour minimiser la latence et l'utilisation de la mémoire;
- plug - un ensemble d'adaptateurs pour divers serveurs Web fonctionnant dans Erlang VM; chaque adaptateur fournit une interface directe avec le serveur Web situé derrière lui;
- poison est une bibliothèque de traitement de JSON sur Elixir.
Implémentation
Je veux implémenter des composants comme Endpoint (endpoint), Router (router) et JSON Parser (gestionnaire JSON). Ensuite, je voudrais déployer le résultat sur Heroku et pouvoir traiter les demandes entrantes. Voyons comment cela peut être réalisé.
App
Assurez-vous que votre projet Elixir contient un superviseur. Pour ce faire, créez un projet comme celui-ci:
mix new minimal_server --sup
Assurez-vous que mix.exs contient:
def application do [ extra_applications: [:logger], mod: {MinimalServer.Application, []} ] end
et créez le fichier lib/minimal_server/application.ex
:
defmodule MinimalServer.Application do use Application def start(_type, _args), do: Supervisor.start_link(children(), opts()) defp children do [] end defp opts do [ strategy: :one_for_one, name: MinimalServer.Supervisor ] end end
Bibliothèques
Les bibliothèques suivantes doivent être spécifiées dans mix.exs
:
defp deps do [ {:poison, "~> 4.0"}, {:plug, "~> 1.7"}, {:cowboy, "~> 2.5"}, {:plug_cowboy, "~> 2.0"} ] end
Ensuite, téléchargez et compilez les dépendances:
mix do deps.get, deps.compile, compile
Endpoint
Maintenant, tout est prêt pour créer un point d'entrée sur le serveur. Créons un fichier lib/minimal_server/endpoint.ex
avec le contenu suivant:
defmodule MinimalServer.Endpoint do use Plug.Router plug(:match) plug(Plug.Parsers, parsers: [:json], pass: ["application/json"], json_decoder: Poison ) plug(:dispatch) match _ do send_resp(conn, 404, "Requested page not found!") end end
Le module Plug
contient Plug.Router
pour rediriger les requêtes entrantes en fonction du chemin utilisé et de la méthode HTTP. Dès réception de la demande, le routeur appellera le module :match
, représenté par la fonction match/2
, qui est responsable de trouver l'itinéraire correspondant, puis le redirigera vers le module :dispatch
, qui exécutera le code correspondant.
Puisque nous voulons que notre API soit compatible JSON, nous devons implémenter Plug.Parsers
. Puisqu'il traite les application/json
avec le donné :json_decoder
, nous l'utiliserons pour analyser le corps de la requête.
En conséquence, nous avons créé une route temporaire «toute demande» qui correspond à toutes les demandes et répond avec du code HTTP non trouvé (404).
Routeur
L'implémentation d'un routeur sera la dernière étape de la création de notre application. C'est le dernier élément de l'ensemble du pipeline que nous avons créé: en commençant par recevoir une demande d'un navigateur Web et en terminant par la formation d'une réponse.
Le routeur traitera la demande entrante du client et renverra un message au format souhaité ( ajoutez le code donné dans le fichier lib/minimal_server/router.ex
- note du traducteur ):
defmodule MinimalServer.Router do use Plug.Router plug(:match) plug(:dispatch) get "/" do conn |> put_resp_content_type("application/json") |> send_resp(200, Poison.encode!(message())) end defp message do %{ response_type: "in_channel", text: "Hello from BOT :)" } end end
Dans le module Router
ci-dessus, la demande sera traitée uniquement si elle est envoyée par la méthode GET
et envoyée le long de la route /
. Le module Router répondra par un en Content-Type
tête Content-Type
contenant application/json
et body:
{ "response_type": "in_channel", "text": "Hello from BOT :)" }
Tout mettre ensemble
Il est maintenant temps de changer le module Endpoint
pour transférer les demandes au routeur et de modifier l' Application
pour lancer le module Endpoint
lui-même.
La première peut être effectuée en ajoutant à MinimalServer.Endpoint
[ avant la match _ do ... end
règle de match _ do ... end
- env. traducteur ] chaîne
forward("/bot", to: MinimalServer.Router)
Cela garantit que toutes les demandes à /bot
seront acheminées et traitées par le module Router
.
La seconde peut être implémentée en ajoutant les fonctions child_spec/1
et start_link/1
fichier endpoint.ex
:
defmodule MinimalServer.Endpoint do # ... def child_spec(opts) do %{ id: __MODULE__, start: {__MODULE__, :start_link, [opts]} } end def start_link(_opts), do: Plug.Cowboy.http(__MODULE__, []) end
Vous pouvez maintenant modifier application.ex
en ajoutant MinimalServer.Endpoint
à la liste renvoyée par la fonction children/0
.
defmodule MinimalServer.Application do # ... defp children do [ MinimalServer.Endpoint ] end end
Pour démarrer le serveur, faites simplement:
mix run --no-halt
Enfin, vous pouvez visiter l'adresse http: // localhost: 4000 / bot et voir notre message :)
Déploiement

Config
Le plus souvent, dans un environnement local et pour le fonctionnement, le serveur est configuré différemment. Par conséquent, nous devons saisir des paramètres distincts pour chacun de ces modes. Tout d'abord, config.exs
nos config.exs
en ajoutant:
config :minimal_server, MinimalServer.Endpoint, port: 4000
Dans ce cas, lorsque l'application démarre en mode test
, prod
et dev
, elle recevra le port 4000 si ces paramètres ne sont pas modifiés.
Du traducteurÀ ce stade, l'auteur du texte d'origine a oublié de mentionner comment modifier config.exs afin que vous puissiez utiliser différentes options pour différents modes. Pour ce faire, ajoutez import_config "#{Mix.env()}.exs"
; dans la dernière ligne de config/config.exs
; le résultat est quelque chose comme:
use Mix.Config config :minimal_server, MinimalServer.Endpoint, port: 4000 import_config "#{Mix.env()}.exs"
Après cela, créez les fichiers prod.exs
, test.exs
, dev.exs
dans le répertoire config
en le plaçant sur chaque ligne:
use Mix.Config
En production, nous ne voulons généralement pas définir le numéro de port dur, mais nous comptons sur une variable d'environnement système, par exemple:
config :minimal_server, MinimalServer.Endpoint, port: "PORT" |> System.get_env() |> String.to_integer()
Ajoutez le texte ci-dessus à la fin de config/prod.exs
- env. traducteur
Après cela, une valeur fixe sera utilisée localement, et en fonctionnement opérationnel, une configuration de variables d'environnement.
Implémentons ce schéma dans endpoint.ex
, (en remplaçant la fonction start_link / 1 - commentaire du traducteur ):
defmodule MinimalServer.Endpoint do # ... require Logger def start_link(_opts) do with {:ok, [port: port] = config} <- Application.fetch_env(:minimal_server, __MODULE__) do Logger.info("Starting server at http://localhost:#{port}/") Plug.Adapters.Cowboy2.http(__MODULE__, [], config) end end end
Heroku
Heroku offre le déploiement en un clic le plus simple sans configuration compliquée. Pour déployer notre projet, vous devez préparer quelques fichiers simples et créer une application distante .

Après avoir installé Heroku CLI, vous pouvez créer une nouvelle application comme suit:
$ heroku create minimal-server-habr Creating ⬢ minimal-server-habr... done https://minimal-server-habr.herokuapp.com/ | https://git.heroku.com/minimal-server-habr.git
Ajoutez maintenant le Elixir Build Kit à votre application:
heroku buildpacks:set \ https://github.com/HashNuke/heroku-buildpack-elixir.git
Au moment de cette traduction, les versions actuelles d'Elixir et d'Erlang sont (plus ou moins):
erlang_version=21.1 elixir_version=1.8.1
Pour configurer le kit de construction lui-même, ajoutez les lignes ci-dessus au fichier elixir_buildpack.config
.
La dernière étape consiste à créer un Procfile, et, encore une fois, c'est très simple:
web: mix run --no-halt
Note du traducteur: pour éviter une erreur lors de la construction sur Heroku, vous devez définir la valeur des variables d'environnement utilisées dans l'application:
$ heroku config:set PORT=4000 Setting PORT and restarting ⬢ minimal-server-habr... done, v5 PORT: 4000
Dès que vous validez de nouveaux fichiers [en utilisant git - env. traducteur ], vous pouvez les télécharger sur Heroku:
$ git push heroku master Initializing repository, done. updating 'refs/heads/master' ...
Et c'est tout! L'application est disponible sur https://minimal-server-habr.herokuapp.com .
Résumé
À ce stade, vous avez déjà compris comment implémenter l'API JSON RESTful et le serveur HTTP les plus simples sur Elixir sans utiliser de frameworks, en utilisant seulement 3 ( 4 - environ Translator ) bibliothèques.
Lorsque vous devez fournir un accès à des points de terminaison simples, vous n'avez absolument pas besoin d'utiliser Phoenix à chaque fois, quelle que soit sa fraîcheur, ainsi que tout autre framework.
Curieux de savoir pourquoi il n'y a pas de frameworks fiables, bien testés et pris en charge quelque part entre plug
+ cowboy
et Phoenix? Peut-être qu'il n'y a pas vraiment besoin d'implémenter des choses simples? Peut-être que chaque entreprise utilise sa propre bibliothèque? Ou peut-être que tout le monde utilise Phoenix ou l'approche présentée?

Le référentiel , comme toujours, est disponible sur mon GitHub.