Reputation: 1339
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
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