La API RESTful JSON más simple en Elixir

¿Cómo implementar el punto final de la API JSON en Elixir sin ningún marco?


Del traductor:
El artículo proporciona un ejemplo de una aplicación web muy simple que se puede considerar como Hello, World! en crear la API más simple en Elixir.
El código de muestra se modifica ligeramente para que coincida con las versiones actuales de las bibliotecas.
El código de muestra completo con los cambios se puede ver en GitHub .



Nuevos desafíos lingüísticos


Muchos desarrolladores vienen a Elixir desde el mundo de Ruby. Este es un entorno muy maduro en términos de la cantidad de bibliotecas y marcos disponibles. Y tal madurez a veces no es suficiente para mí en Elixir. Cuando necesito un servicio de terceros, el resultado de una búsqueda adecuada puede ser el siguiente:


  • hay una biblioteca oficial bien respaldada (muy rara);
  • hay una biblioteca oficial, pero desactualizada o con errores (a veces sucede);
  • Hay una biblioteca bien apoyada desarrollada por alguien de la comunidad (a veces de vez en cuando);
  • hay una biblioteca desarrollada por alguien de la comunidad, pero ya no es compatible (un caso muy común);
  • hay varias bibliotecas, cada una de las cuales está escrita por alguien para sus propias necesidades, y carece de las características necesarias (la opción más popular);
  • existe mi propia biblioteca, que combina todo lo mejor de lo anterior ... (se encuentra con demasiada frecuencia).

API JSON simple en Elixir



Puede que se sorprenda, pero Ruby no siempre está en el carril ( Ruby on Rails, ¿recuerda? - Nota del traductor ). No siempre se requiere comunicación con la web para asistir. Aunque en este caso particular, hablemos de la web.


Cuando se trata de implementar un único punto final RESTful, generalmente hay muchas opciones:



Estos son ejemplos de herramientas que utilicé personalmente. Mis colegas son usuarios satisfechos de Sinatra. Se las arreglaron para probar Hanami. Puedo elegir cualquier opción que me convenga, incluso dependiendo de mi estado de ánimo actual.


Pero cuando cambié a Elixir, resultó que la elección era limitada. Aunque hay varios "marcos" alternativos (cuyos nombres por razones obvias no mencionaré aquí), ¡es casi imposible usarlos!


Pasé todo el día clasificando todas las bibliotecas mencionadas en Internet. Actuando como un bot de Slack, traté de implementar un servidor HTTP2 simple en Heroku , pero al final del día me di por vencido. Literalmente, ninguna de las opciones que encontré fue capaz de implementar los requisitos básicos.


No siempre es una solución - Phoenix


Phoenix es mi marco web favorito, es solo que a veces es redundante. No quería usarlo, arrastrando todo el marco al proyecto solo por un punto final; y no importa que sea muy simple.


Tampoco podía usar bibliotecas listas porque, como ya dije, todas las bibliotecas encontradas no satisfacían mis necesidades (se requería enrutamiento básico y soporte JSON), o no eran lo suficientemente convenientes para una implementación fácil y rápida en Heroku. Da un paso atrás, pensé.



Pero en realidad, Phoenix en sí está construido sobre la base de algo , ¿no?


Plug & Cowboy vienen al rescate


Si necesita crear un servidor verdaderamente minimalista en Ruby, simplemente puede usar el rack , una interfaz modular para servidores web Ruby.


Afortunadamente, algo similar está disponible en Elixir. En este caso, utilizaremos los siguientes elementos:


  • cowboy es un servidor HTTP pequeño y rápido para Erlang / OTP que implementa la pila y el enrutamiento HTTP completos, optimizado para minimizar la latencia y el uso de memoria;
  • plug : un conjunto de adaptadores para varios servidores web que se ejecutan en Erlang VM; cada adaptador proporciona una interfaz directa al servidor web ubicado detrás de él;
  • poison es una biblioteca para procesar JSON en Elixir.

Implementación


Quiero implementar componentes como Endpoint (endpoint), Router (enrutador) y JSON Parser (controlador JSON). Luego me gustaría implementar el resultado en Heroku y poder procesar las solicitudes entrantes. Veamos cómo se puede lograr esto.


App


Asegúrese de que su proyecto Elixir contenga un supervisor. Para hacer esto, cree un proyecto como este:


 mix new minimal_server --sup 

Asegúrese de que mix.exs contenga:


 def application do [ extra_applications: [:logger], mod: {MinimalServer.Application, []} ] end 

y cree el archivo 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 

Bibliotecas


Las siguientes bibliotecas deben especificarse en mix.exs :


 defp deps do [ {:poison, "~> 4.0"}, {:plug, "~> 1.7"}, {:cowboy, "~> 2.5"}, {:plug_cowboy, "~> 2.0"} ] end 

Luego descargue y compile las dependencias:


 mix do deps.get, deps.compile, compile 

Punto final


Ahora todo está listo para crear un punto de entrada al servidor. lib/minimal_server/endpoint.ex archivo lib/minimal_server/endpoint.ex con los siguientes contenidos:


 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 

El módulo Plug contiene Plug.Router para redirigir las solicitudes entrantes según la ruta utilizada y el método HTTP. Al recibir la solicitud, el enrutador llamará al módulo :match , representado por la función match/2 , que es responsable de encontrar la ruta correspondiente, y luego lo redirigirá al módulo :dispatch , que ejecutará el código correspondiente.


Dado que queremos que nuestra API sea compatible con JSON, necesitamos implementar Plug.Parsers . Como procesa las application/json con el dado :json_decoder , lo usaremos para analizar el cuerpo de la solicitud.


Como resultado, creamos una ruta temporal "cualquier solicitud" que coincide con todas las solicitudes y responde con el código HTTP no encontrado (404).


Enrutador


La implementación de un enrutador será el último paso para crear nuestra aplicación. Este es el último elemento de toda la canalización que creamos: comenzando con la recepción de una solicitud de un navegador web y terminando con la formación de una respuesta.


El enrutador procesará la solicitud entrante del cliente y enviará algún mensaje en el formato deseado ( agregue el código anterior al archivo lib/minimal_server/router.ex - nota del traductor ):


 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 

En el módulo de Router anterior, la solicitud se procesará solo si se envía por el método GET y se envía a lo largo de la ruta / . El módulo Router responderá con un encabezado Content-Type contiene application/json y body:


 { "response_type": "in_channel", "text": "Hello from BOT :)" } 

Poniendo todo junto


Ahora es el momento de cambiar el módulo Endpoint para reenviar solicitudes al enrutador y modificar la Application para iniciar el módulo Endpoint .


Lo primero se puede hacer agregando a MinimalServer.Endpoint [ antes de la regla match _ do ... end - aprox. traductor ] cadena


 forward("/bot", to: MinimalServer.Router) 

Esto garantiza que todas las solicitudes a /bot serán enrutadas y procesadas por el módulo Router .


El segundo puede implementarse agregando las funciones child_spec/1 y start_link/1 archivo 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 

Ahora puede modificar application.ex agregando MinimalServer.Endpoint a la lista devuelta por la función children/0 .


 defmodule MinimalServer.Application do # ... defp children do [ MinimalServer.Endpoint ] end end 

Para iniciar el servidor, solo haz:


 mix run --no-halt 

Finalmente puede visitar la dirección http: // localhost: 4000 / bot y ver nuestro mensaje :)


Despliegue



Config


Con mayor frecuencia, en un entorno local y para la operación, el servidor está configurado de manera diferente. Por lo tanto, necesitamos ingresar configuraciones separadas para cada uno de estos modos. En primer lugar, config.exs nuestro config.exs agregando:


 config :minimal_server, MinimalServer.Endpoint, port: 4000 

En este caso, cuando la aplicación se inicia en modo de test , prod y desarrollo, recibirá el puerto 4000 si no se cambia esta configuración.


Del traductor

En este punto, el autor del texto original olvidó mencionar cómo modificar config.exs para que pueda usar diferentes opciones para diferentes modos. Para hacer esto, agregue import_config "#{Mix.env()}.exs" ; en la última línea en config/config.exs ; el resultado es algo como:


 use Mix.Config config :minimal_server, MinimalServer.Endpoint, port: 4000 import_config "#{Mix.env()}.exs" 

Después de eso, cree los archivos prod.exs , test.exs , dev.exs en el directorio de config colocándolo en cada línea:


 use Mix.Config 

En producción, generalmente no queremos establecer el número de puerto de forma rígida, sino que dependemos de alguna variable de entorno del sistema, por ejemplo:


 config :minimal_server, MinimalServer.Endpoint, port: "PORT" |> System.get_env() |> String.to_integer() 

Agregue el texto anterior al final de config/prod.exs - aprox. traductor


Después de eso, se usará un valor fijo localmente y, en la operación operativa, una configuración de variables de entorno.


Implementemos este esquema en endpoint.ex , ( reemplazando la función start_link / 1 - comentario del traductor ):


 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 ofrece la implementación más simple con un clic sin ninguna configuración complicada. Para implementar nuestro proyecto , necesita preparar un par de archivos simples y crear una aplicación remota .



Después de instalar Heroku CLI, puede crear una nueva aplicación de la siguiente manera:


 $ heroku create minimal-server-habr Creating ⬢ minimal-server-habr... done https://minimal-server-habr.herokuapp.com/ | https://git.heroku.com/minimal-server-habr.git 

Ahora agregue el Elixir Build Kit a su aplicación:


 heroku buildpacks:set \ https://github.com/HashNuke/heroku-buildpack-elixir.git 

En el momento de esta traducción, las versiones actuales de Elixir y Erlang son (más o menos):


 erlang_version=21.1 elixir_version=1.8.1 

Para configurar el kit de construcción en sí, agregue las líneas anteriores al archivo elixir_buildpack.config .


El último paso es crear un Procfile y, de nuevo, es muy simple:


 web: mix run --no-halt 

Nota del traductor: para evitar un error durante la compilación en Heroku, debe establecer el valor de las variables de entorno que se utilizan en la aplicación:


 $ heroku config:set PORT=4000 Setting PORT and restarting ⬢ minimal-server-habr... done, v5 PORT: 4000 

Tan pronto como confirme nuevos archivos [ usando git - aprox. traductor ], puedes subirlos a Heroku:


 $ git push heroku master Initializing repository, done. updating 'refs/heads/master' ... 

¡Y eso es todo! La aplicación está disponible en https://minimal-server-habr.herokuapp.com .


Resumen


En este punto, ya entendió cómo implementar la API JSON RESTful más simple y el servidor HTTP en Elixir sin usar ningún marco, usando solo 3 bibliotecas ( 4 - traductor aprox. ).


Cuando necesita proporcionar acceso a puntos finales simples, no necesita usar Phoenix cada vez, no importa cuán genial sea, así como cualquier otro marco.


¿Curioso por qué no hay marcos confiables, bien probados y compatibles en algún lugar entre plug + cowboy y Phoenix? ¿Quizás no hay una necesidad real de implementar cosas simples? ¿Quizás cada compañía usa su propia biblioteca? ¿O tal vez todos usan Phoenix o el enfoque presentado?



El repositorio , como siempre, está disponible en mi GitHub.

Source: https://habr.com/ru/post/444554/


All Articles