Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Parameter Validation in Phoenix Apps

Parameter Validation in Phoenix Apps

We explore a Validator pattern for APIs that leverages Ecto Changesets to form a boundary layer of data in your system and return helpful error messages to your clients.

Desmond Bowe

February 19, 2020
Tweet

More Decks by Desmond Bowe

Other Decks in Technology

Transcript

  1. Ecto Changesets • you probably don't hand params off immediately

    to a CRUD operation • input params in your system with string keys, string dates, etc. %{"dateTime" => dt} and %{"date_time" => dt}
  2. # deal with user-supplied data def handle(%{"user" => params}), do:

    ... # deal with system-generated data def handle(%{user: params}), do: ...
  3. User-Supplied Data ☹ • you have unsanitized data in your

    system • you want to establish a boundary layer past which you can say things for certain about the data. • these won't be the only validations you have in your system, and Let it Crash is still a reasonable attitude • this pattern only guarantees that types are correct and provides helpful error messages to your users. Which we should all be doing :)
  4. Validator • ensure shape of data is correct • only

    known (ie, whitelisted) params are let into system • ensure required fields are present • coerce keys into atoms • coerce string values into meaningful types like DateTimes • return helpful errors Goals
  5. Validator • should not perform db queries • should not

    have knowledge of business logic • should not know anything about the state of the system Anti-Goals did the user give us something we can operate on?
  6. User-Supplied Data • the distance is not negative • the

    coordinates are valid • the filters are legal (ie, restaurant_type is "sushi", not 5) • ^^ aren't business logic, exactly. whether "sushi" is a valid type is for elsewhere in the system Finding suitable restaurants near the user
  7. Param Validator defmodule Api.V1.Restaraunt.ShowNearbyParams do use Ecto.Schema import Ecto.Changeset embedded_schema

    do field :distance, :integer field :type_of_food, :string field :user_x, :float field :user_y, :float end @required [:distance, :type_of_food, :user_x, :user_y] def changeset(params) do __MODULE__ |> cast(params) |> validate_required(@required) |> validate_number(:distance, greater_than: 0) end end
  8. Param Validator def show_nearby(conn, params) do mod = Api.V1.Restaurant.ShowNearbyParams with

    {:ok, validated_params} <- validate(mod, params), {:ok, user} <- show_nearby_restaurants(validated_params) do render("user.json", user: user) end end
  9. Param Validator def validate_params(module, params) do cs = module.changeset(params) case

    cs.valid? do true -> {:ok, Ecto.Changeset.apply_changes(cs)} false -> {:error, cs} end end
  10. Param Validator def show_nearby(conn, params) do mod = Api.V1.Restaurant.ShowNearbyParams with

    {:ok, validated_params} <- validate(mod, params), {:ok, user} <- show_nearby_restaurants(validated_params) do render("user.json", user: user) else err -> err # {:error, changeset} end end
  11. Param Validator defmodule FallbackController do def call(conn, {:error, %Ecto.Changeset{} =

    cs}) do conn |> put_status(422) |> put_view(ErrorView) |> render("422.json", %{changeset: cs}) end end
  12. Param Coercion # Api.V1.Restaurant.ShowNearbyParams def changeset(params) do __MODULE__ |> cast(coerce(params))

    |> validate_required(...) end def coerce(params) do params |> coerce_x_coordinate() end def coerce_x_coordinate(%{"x" => x} = params) do Map.merge(params, %{"user_x" => x}) end def coerce_x_coordinate(params), do: params
  13. Param Defaults embedded_schema do field :distance, :integer, default: 5 field

    :type_of_food, :string field :user_x, :float field :user_y, :float end this can help the user out if you want to assume a reasonable default
  14. Notes • 1:1 mapping of controller action <> param module

    • these modules don’t change much • avoid the temptation to get clever with macros • easy to customize error messages • fewer responsibilities for the controller Good things
  15. Notes • can be verbose, add a lot of boilerplate

    • duplicates schema-level validation • don’t forget to do this! • what, exactly, is business logic? Less Good things