¿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 traductorEn 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.