mattei

mattei

User-friendly API errors in Phoenix?

Hi, newcomer to Phoenix and Elixir here, moving over from Django and REST Framework.

Say I have a login function like this…

  def login(conn, %{"username" => username, "password" => password}) do
    ...
  end

If I input the correct parameters, the pattern matching works and the function is accessed just fine. However, if I miss a parameter, it does not find the function, and returns a rather ugly error:

    ** (Phoenix.ActionClauseError) no function clause matching in AuthController.login/2

In production, it simply returns a 400 Bad Request, which is the appropriate error code for this.

However, in something like Django REST Framework the function would return a friendly error detailing which fields are missing, for example:

{
"username": "This field is required."
... etc
}

I was wondering if there’s a way to add friendly field validation errors like this to Phoenix controller functions? It’d improve user experience for API development.

Thanks all!

Marked As Solved

mattei

mattei

I do, but what I’m using isn’t to be inserted to the database, it’s just a login view. Is there a way to use Ecto to validate incoming parameters, even if you’re not using them for database stuff?

EDIT: Oh shoot, I just found this, and it looks perfect: GitHub - vic/params: Easy parameters validation/casting with Ecto.Schema, akin to Rails' strong parameters. · GitHub
I had no idea you can use regular Ecto schemas as “serializers” to validate any data, even non-database input.

EDIT 2: Whoa, thanks for pointing me in the right direction! Turns out this can be done with vanilla Ecto.Changesets, you can use them schemaless to validate any data (they don’t hit the database until they touch a repo). This is brilliant!

Here’s the end solution.

  1. Create a schemaless changeset to validate the input parameters.
  def login_changeset(params) do
    types = %{username: :string, password: :string}

    {%{}, types}
    |> cast(params, Map.keys(types))
    |> validate_required(:username)
    |> validate_required(:password)
  end
  1. Validate the schema in the controller, making sure to return {:error, changeset} to your FallbackController.
  def login(conn, params) do
    changeset = Accounts.User.login_changeset(params)

    if changeset.valid? do
      %{:username => username, :password => password} = changeset

      case Accounts.authenticate_user(username, password) do
        {:ok, user} ->
          {:ok, jwt, _full_claims} = Guardian.encode_and_sign(user, :api)
          render(conn, "token.json", user, jwt)

        {:error, _reason} ->
          conn
          |> put_status(401)
          |> render("error.json")
      end
    else
      {:error, changeset}
    end
  end

  1. Voila! Your controllers return friendly errors:
HTTP/1.1 422 Unprocessable Entity
cache-control: max-age=0, private, must-revalidate
connection: close
content-length: 116
content-type: application/json; charset=utf-8
date: Sat, 12 Jun 2021 23:20:57 GMT
server: Cowboy
x-request-id: Fof5FK1SQzCkJR8AABFD

{
  "errors": {
    "password": [
      "can't be blank"
    ],
    "username": [
      "can't be blank"
    ]
  }
}

I’m so happy right now :slight_smile: This is actually more straightforward than DRF serializers.

Thanks y’all!

Last Post!

leather-s

leather-s

Here is a native Phoenix solution. You cast with types, then validate.

Where Next?

Popular in Questions Top

Emily
I have VueJS GUIs with the project generated using Webpack. I have Elixir modules that will need to be used by the VueJS GUIs. I forese...
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New

Other popular topics Top

vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New
hariharasudhan94
Lets say I have map like this fetching from my database %{"_id" => #BSON.ObjectId<58eb1a7a9ad169198c3dXXXX>, "email" => ...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
dogweather
I wrote this comment on r/haskell, and it’s not popular there. :wink: But I think I’m on to something… Haskell reminds me of Java, and e...
New
AngeloChecked
What learn first? Rust or Elixir Hi Elixir community! I’m here because i want learn a new language. I’m a junior developer and mainly i ...
New

We're in Beta

About us Mission Statement