The logic could be extracted to an use-case module.
The origin verification could also be a plug.
At a very high and very verbose level :
plug YourAppWeb.Plugs, :verify_zendesk_origin when action in [:onboard, ...]
alias YourAppWeb.UserFlows.OnboardUser
def onboard(conn, params) do
with {_, {:ok, organisation}} <- {:upsert_organisation, OnboardUser.upsert_organisation(conn.assigns.organisation_attrs)},
{_, {:ok, user_attrs}} <- {:user_attrs, OnboardUser.user_attrs_from_params_and_org(params, organisation)}
{_, {:ok, user}} <- {:upsert_external_user, OnboardUser.upsert_external_user(user_attrs)} do
conn |> put_status(:ok) |> json(%{user_id: user.id})
else
{:upsert_organisation, {:error, e}} -> # handle this case
{:user_attrs, {:error, e}} -> #handle this one
{:upsert_external_user, {:error, e}} # handle this one
_ -> # etc
end
end
I’ve put this fictional OnboardUser module in YourAppWeb because the helper function user_attrs_from_params_and_org depends on the params arg, so it is linked to the transport.
You can of course refine that and have a pure non-web logic OnboardUser use-case while having other extracted utilities to properly construct the arguments it consumes from the request.
There also are a few different ways to tag error tuples or triples with with.
I like to do it at the call site to keep the logic free of this tagging.
The more non-web your logic is, the more testable it becomes ![]()
Edit : use-case based modules are an opinionated choice and not the idiomatic choice.






















