Building json-api endpoints with Phoenix

Bridge connecting skyscrapers
Brian Cardarella

CEO & Founder

Brian Cardarella

I’m currently building a Phoenix backend API that is being consumed by Ember Data 1.13 which uses JSON API. Here is how I hooked everything up.

Configuration

You first need to tell Phoenix that it should accept json-api format. I created a new pipeline/2 for the API:

# web/router.ex

pipeline :api do
  plug :accepts, ["json-api"]
end

We next need to add the corresponding MIME type:

# config/config.exs

config :plug, :mimes, %{
  "application/vnd.api+json" => ["json-api"]
}

Now we have to force Plug to recompile. To do this we have to touch a file in the dependency. This may seem a little odd but the documentation recommends this.

> touch deps/plug/mix.exs

> mix deps.compile plug

I like to namespace my APIs so I created a scope and piped it through my api pipeline:

# web/router.ex

scope "api/v1", MyApp do
  pipe_through :api
end

I declare all of my API routes in that scope.

Emitting

To emit json-api responses I am currently using ja_serializer. The author has indicated that some big changes are likely to better align with Phoenix conventions but for now this is the only serializer I am aware of. The README explains how to serialize, it is pretty simple. For example, to serialize a collection form an index action:

def index(conn, _) do
  foos = Repo.all(Foo)
  |> MyApp.FooSerializer.format(conn)

  json(conn, foos)
end

You may wish to use Views but I’ve opted not to.

The serializer itself would look like:

# serializers/foo_serializer.ex

defmodule MyApp.FooSerializer do
  use JaSerializer

  serialize "foo" do
    attributes [
      "name",
      "description"
    ]
  end
end 

You can embed relationships as well, check out the project for more deatils.

Consuming

Ember Data not only expects to get JSON API format but also sends JSON API format back to the server when you are creating or updating. The attributes keys come in hyphenated and everythig is nested under "data" => "attributes". Here is how I made life easier for mmyself.

I first wrote a new plug that deserializes all params that are coming in by forcing hyphenated keys to underscore format. It works recursively on all keys and produces a new params object that is Phoenix friendly. Here is the code:

# web/plugs/deserialize.ex

defmodule MyApp.DeserializePlug do
  def init(options) do
    options
  end

  def call(%Plug.Conn{params: %{"format" => "json-api"}, method: "POST"}=conn, _opts) do
    result = _deserialize(conn)
  end

  def call(%Plug.Conn{params: %{"format" => "json-api"}, method: "PUT"}=conn, _opts) do
    _deserialize(conn)
  end

  def call(%Plug.Conn{params: %{"format" => "json-api"}, method: "PATCH"}=conn, _opts) do
    _deserialize(conn)
  end

  def call(conn, _opts), do: conn

  defp _deserialize(%Plug.Conn{}=conn) do
    Map.put(conn, :params, _deserialize(conn.params))
  end

  defp _deserialize(%{}=params) do
    Enum.into(params, %{}, fn({key, value}) -> { _underscore(key), _deserialize(value) } end)
  end

  defp _deserialize(value), do: value
  defp _underscore(key), do: String.replace(key, "-", "_")
end

I then added this custom plug to my api pipeline:

# web/router.ex

pipeline :api do
  plug :accepts, ["json-api"]
  plug MyApp.DeserializePlug
end

Next, JSON API’s schema is verbose and I didn’t want to have to deal with this in my actions, Elixir’s pattern matching is perfect for this. I capture all the attributes into params:

def create(conn, %{"data" => %{"attributes" => params}, "type" => "json-api"}) do
  # create action now has
  # a "params" object with all the
  # attribute date from the client request
end

We can even guard the action for JSON API specific types.

Conclusion

Getting JSON API working with Phoenix takes a few hoops to jump through, but hopefully this helps you get up and running.

Newsletter

Stay in the Know

Get the latest news and insights on Elixir, Phoenix, machine learning, product strategy, and more—delivered straight to your inbox.

Narwin holding a press release sheet while opening the DockYard brand kit box