How to implement endpoint JSON API on Elixir without any frameworks?
From the translator:
The article provides an example of a very simple web application that can be thought of as Hello, World! in creation of the elementary API on the Elixir.
The sample code is slightly modified to match the current library versions.
The full code of the example with the changes can be seen on GitHub .
Many developers come to the Elixir from the world of Ruby. This is a very mature environment in terms of the number of libraries and frameworks available. And such maturity I sometimes lack in Elixir. When I need a third-party service, the search result may be as follows:
You may be surprised, but Ruby is not always on the rails ( remember, Ruby on Rails? - comment of the translator ). Communication with the web is also not always required to be present. Although in this particular case, let's talk about the web.
When it comes to implementing a single RESTful endpoint (single RESTful endpoint), there are usually a variety of options:
These are examples of tools that I personally used. My colleagues are satisfied users of Sinatra. They managed to try and Hanami. I can choose any option that suits me, even depending on my current mood.
But when I switched to Elixir, it turned out that the choice was limited. Although there are several alternative “frameworks” (the names of which, for obvious reasons, I will not mention here), it is almost impossible to use them!
I spent the whole day sorting out every library that was ever mentioned on the web. Acting as a Slack-bot, I tried to deploy a simple HTTP2 server to Heroku , but by the end of the day I gave up. Literally none of the options that I found could not implement the basic requirements.
Phoenix is ​​my favorite web framework, just sometimes it's redundant. I didn’t want to use it, pulling the whole framework into the project solely for the sake of one end point; and no matter what to do it is very simple.
I couldn’t use ready-made libraries either, because, as I said, all those found were either not suitable for my needs (basic routing and JSON support were required), or were not convenient enough for easy and quick deployment to Heroku. Take a step back, I thought.
But in fact, Phoenix itself is built on the basis of something , isn't it?
If you want to create a truly minimalistic server on Ruby, then you can simply use rack
, a modular interface for web servers on Ruby.
Fortunately, something similar is available in Elixir. In this case, we will use the following elements:
I want to implement components like Endpoint (endpoint), Router (router) and JSON Parser (JSON handler). Then I would like to deploy the resulting on Heroku and be able to process incoming requests. Let's see how this can be achieved.
Make sure your project on the Elixir contains a supervisor. To do this, you need to create a project like this:
mix new minimal_server --sup
Ensure that mix.exs contains:
def application do [ extra_applications: [:logger], mod: {MinimalServer.Application, []} ] end
and create 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
In mix.exs
you must specify the following libraries:
defp deps do [ {:poison, "~> 4.0"}, {:plug, "~> 1.7"}, {:cowboy, "~> 2.5"}, {:plug_cowboy, "~> 2.0"} ] end
Then download and compile the dependencies:
mix do deps.get, deps.compile, compile
Now everything is ready to create an entry point to the server. Let's create the file lib/minimal_server/endpoint.ex
with the following contents:
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
The Plug
module contains a Plug.Router
to redirect incoming requests depending on the path used and the HTTP method. Upon receiving a request, the router will call the module :match
, represented by the match/2
function responsible for finding the appropriate route, and then redirect it to the :dispatch
module, which will execute the appropriate code.
Since we want our API to be JSON compliant, you need to implement Plug.Parsers
. Since it processes application/json
requests with the given :json_decoder
, we will use it to analyze the request body.
As a result, we created a temporary "any request" route that matches all requests and responds with the HTTP not found code (404).
The implementation of the router will be the last step in creating our application. This is the last element of the entire pipeline that we created: from receiving a request from a web browser and ending with the formation of a response.
The router will process the incoming request from the client and send back some message in the required format ( add the code in the file lib/minimal_server/router.ex
- comment of the translator ):
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
In the Router
module above, the request will be processed only if it is sent using the GET
method and sent along the route /
. The Router module will respond with a Content-Type
header containing application/json
and the body:
{ "response_type": "in_channel", "text": "Hello from BOT :)" }
Now it’s time to change the Endpoint
module to forward requests to the router and finalize Application
to run the Endpoint
module itself.
The first can be done by adding to MinimalServer.Endpoint
[ before the match _ do ... end
rule - approx. translator ] string
forward("/bot", to: MinimalServer.Router)
This ensures that all requests to /bot
will be sent to the Router
module and processed by it.
The second can be implemented by adding the child_spec/1
and start_link/1
functions to the endpoint.ex
file:
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
You can now modify application.ex
by adding MinimalServer.Endpoint
to the list returned by the children/0
function.
defmodule MinimalServer.Application do # ... defp children do [ MinimalServer.Endpoint ] end end
To start the server, just run:
mix run --no-halt
Finally you can visit http: // localhost: 4000 / bot and see our message :)
Most often in the local environment and for operation the server is configured in different ways. Therefore, we need to enter a separate setting for each of these modes. First of all, change our config.exs
by adding:
config :minimal_server, MinimalServer.Endpoint, port: 4000
In this case, when you start the application in test
mode, prod
and dev
it will receive port 4000, if these settings are not changed.
In this place, the author of the original text forgot to mention how to refine the config.exs so that you can use different options for different modes. To do this, in config/config.exs
add import_config "#{Mix.env()}.exs"
; the result is something like:
use Mix.Config config :minimal_server, MinimalServer.Endpoint, port: 4000 import_config "#{Mix.env()}.exs"
After that, in the config
directory create the files prod.exs
, test.exs
, dev.exs
, putting in each line:
use Mix.Config
In production, we usually do not want to set the port number hard, but rely on a certain system environment variable, for example:
config :minimal_server, MinimalServer.Endpoint, port: "PORT" |> System.get_env() |> String.to_integer()
Add the text above to the end of config/prod.exs
- approx. translator
After that, a fixed value will be used locally, and in operation, the configuration from environment variables.
Let's implement this scheme in endpoint.ex
, ( replacing the start_link / 1 function - comment of the translator ):
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 offers the simplest one-click deployment without any complicated configuration. To deploy our project you need to prepare a couple of simple files and create a remote application .
After installing Heroku CLI, you can create a new application as follows:
$ heroku create minimal-server-habr Creating ⬢ minimal-server-habr... done https://minimal-server-habr.herokuapp.com/ | https://git.heroku.com/minimal-server-habr.git
Now add to your application the Elixir assembly kit :
heroku buildpacks:set \ https://github.com/HashNuke/heroku-buildpack-elixir.git
At the time of creation of this translation, the current versions of Elixir and Erlang are (plus or minus):
erlang_version=21.1 elixir_version=1.8.1
To configure the build itself, add the lines above to the elixir_buildpack.config
file.
The last step is to create a Procfile, and, again, it is very simple:
web: mix run --no-halt
Translator's note: in order to avoid an error during assembly on Heroku, it is necessary to set the value of environment variables used in the application:
$ heroku config:set PORT=4000 Setting PORT and restarting ⬢ minimal-server-habr... done, v5 PORT: 4000
As soon as you commit new files [ with git - approx. translator ], you can upload them to Heroku:
$ git push heroku master Initializing repository, done. updating 'refs/heads/master' ...
And it's all! The application is available at https://minimal-server-habr.herokuapp.com .
At this point, you already understood how to implement the simplest JSON RESTful API and HTTP server on the Elixir without using any frameworks, using only 3 ( 4 - approx. Translator ) libraries.
When you need to provide access to simple endpoints, you absolutely do not need to use Phoenix every time, no matter how cool it is, just like any other framework.
It is curious why there are no reliable, well tested and supported frameworks somewhere between plug
+ cowboy
and Phoenix? Maybe there is no real need to implement simple things? Maybe each company uses its own library? Or perhaps everyone uses either Phoenix or the presented approach?
The repository , as always, is available on my github.
Source: https://habr.com/ru/post/444554/
All Articles