Build your own library to render json response in Phoenix
In my previous article, I introduced my library call JsonView to render json response easier. You can read it here: Render Ecto schema to json with …
Credit: filter image taken from svgrepo.com
In web developments, server receives lots of request data from client side. And when working with request params from client, my first rule is:
Don’t believe the client
Imagine that you provide API to list all post using the filter from client, and user may add user_id
which point to other user, and you don’t remove that unexpected field from request params. If you don’t handle your logic carefully, you may accidentally leak data.
So every request should be cleaned from unexpected params, casted to the proper data type, and validated before passing to business layer.
You can achieve this by:
If you are building a web server using Phoenix, I guess Ecto
is already in your dependencies. Just use it.
Thank to Ecto schemaless, you can build changeset from a dynamic schema:
1defmodule MyApp.PostController do
2 ...
3 defp index_params(params) do
4 default = %{
5 status: nil,
6 q: nil,
7 is_published: true
8 }
9
10 types = %{
11 status: :string,
12 q: :string,
13 is_published: :boolean
14 }
15
16 changeset =
17 {default, types}
18 |> Ecto.Changeset.cast(params, Map.keys(types))
19
20 if changeset.valid? do
21 {:ok, Ecto.Changeset.apply_changes(changeset)}
22 else
23 {:error, changeset}
24 end
25 end
26
27 def index(conn, params) do
28 with {:ok, valid_params} <- index_params(params) do
29 # do your logic
30 end
31 end
32 ...
33end
With Ecto you can do validation on your params as you do with your schema changeset.
This way is simple and most of you are familiar with it. But you have to write much code and cannot cast and validate nested params.
Tarams
This library provide a simple way to define schema. Let’s rewrite example above using tarams
.
First add this to your dependency list:
{:tarams, "~> 1.0.0"}
1defmodule MyApp.PostController do
2 ...
3 @index_params %{
4 status: :string,
5 q: :string
6 is_published: [type: :boolean, default: true],
7 page: [type: :integer, number: [min: 1]],
8 size: [type: :integer, number: [min: 10, max: 100]]
9 }
10 def index(conn, params) do
11 with {:ok, valid_params} <- Tarams.cast(params, @index_params) do
12 # do your logic
13 end
14 end
15 ...
16end
And it support nested params too
1defmodule MyApp.PostController do
2 ...
3 @create_params %{
4 title: [type: :string, required: true],
5 content: [type: :string, required: true],
6 tags: [type: {:array, :string}],
7 published_at: :naive_datetime,
8 meta: %{
9 tile: :string,
10 description: :string,
11 image: :string
12 }
13 }
14 def create(conn, params) do
15 with {:ok, valid_params} <- Tarams.cast(params, @create_params) do
16 MyApp.Content.create_post(valid_params)
17 end
18 end
19 ...
20end
All request params should be casted and validated at controller. Then you only work with data that you know what it is, and you don’t have to worry about unexpected parameters.
Thanks for reading, hope it can helps.
In my previous article, I introduced my library call JsonView to render json response easier. You can read it here: Render Ecto schema to json with …
When writing API with Phoenix and render json to client, For some fields I want to keep it original value. For some fields, I want to do some …