mszmurlo
mszmurlo

Reputation: 1339

Phoenix routing based on the payload

I'm implementing a backend API in Elixir/Phoenix. For security reasons it was decided to show as little as possible on the URL so that the action to be executed is embedded in the JSON payload of the POST requests. For example, rather than having REST-like request https://my.server.com/api/get_user/42, we'll issue a POST on https://my.server.com/api/ with the following payload:

{
    "action" : "get_user",
    "params" : {
        "id" : 42
    }
}

So far, my router looks like this:

  scope "/api", AlaaarmWeb do
     pipe_through :api

     post "/", ApiController, :process
     match :*, "/*path", ApiController, :error_404
   end

and I have a bunch of process functions that pattern match on the params map:

  def process(conn, params = %{"action" => "get_user"}) do
    # ... get the user from the DB in resp, and sed back
    json(conn, resp)
  end

It works fine but the code is not very clear: one big file, use the same controller, ApiController, while it would be much clearer to have a UserApiController for managing the users, a ProductApiController for managing the products, etc..

So, I was wondering is there was a way to do the selection of the module and of the function to call also based on the content of the payload and not only on the URL.

Upvotes: 2

Views: 744

Answers (1)

Gurwinder
Gurwinder

Reputation: 509

If you have defined structure as "action" and "params" key-value pair coming through payload and have dozens of requests that you want to filter and delegate to different controllers based on the path then you should be able to have a plug like this defined in endpoint. ex before plug YourApp.Router call

  plug Plug.Session, @session_options
  plug UrlFromPayloadAction, []
  plug StackoverflowApiWeb.Router

This plug would mutate path info so our modified url would be parsed by router and we can have defined controller like post "/get_user/:id", ApiUserController, :process and others based on action defined.

defmodule UrlFromPayloadAction do
        import Plug.Conn

        def init(default), do: default

        def call(%Plug.Conn{params: %{"action" => action,
                                         "params" => %{ "id" => id }
                                     }} = conn, _default) do
              conn = conn
                     |> assign(:original_path_info, conn.path_info)
                     |> assign(:original_request_path, conn.request_path)
               
              %Plug.Conn{conn | path_info: conn.path_info ++ [ "#{action}", "#{id}" ] , request_path: conn.request_path <> "/#{action}/#{id}" }
        end
        def call(conn, _default), do: conn
end

This is more of a mutable way of resolving this problem which could be against the general philosophy of functional frameworks like an elixir

Upvotes: 5

Related Questions