Building a versioned REST API with Phoenix Framework

I’ve been playing Elixir lately and I gotta say the more I toy with it, the more I love it.

Erglang syntax is simple, concise and easy to understand but Elixir is that and much more. In one word, Beautiful, it’s like Ruby’s cute new little sister.

I’ve also been toying with Phoenix Framework which is a great framework to build blazing fast web applications with Elixir.

This time, in order to learn the language and the framework, I decided to build a REST API for a personal project.

Dependencies

I will assume you have the following already installed in your system:

Getting started

To create a new phoenix project run:

$ mix phoenix.new rest_api --no-brunch

where rest_api will be the folder of our project. Phoenix also accept an absolute path to a folder. The --no-brunch flag tells phoenix.new task we don’t want to use brunch.io for our static asset compilation.

You’ll be prompted if you want to install mix dependencies, which we should do.

Install mix dependencies? [Yn] Y

Done. You can now access the folder Phoenix created for our project.

Once mix dependencies are installed, the task will prompt you to change into the project directory and start the application.

Let’s remove all auto-generated files Phoenix created for us and start from scratch.

$ rm web/controllers/page_controller.ex web/views/page_view.ex test/controllers/page_controller_test.exs test/views/page_view_test.exs

Now let’s start building our API resources.

Generating API resources

Phoenix ships with tasks to generate resources for us. These tasks are:

The one we’re really interested here is phoenix.gen.json.

Let’s create a new resource called posts:

$ mix phoenix.gen.json Post posts title:string content:string

This will generate:

  • A model web/models/post.ex.

  • A model test file test/models/post_test.exs.

  • A migration file in priv/repo/migrations/20150516125431_create_post.exs

  • A post view web/views/post_view.ex.

  • A changeset view web/views/changeset_view.ex.

  • A controller web/controllers/post_controller.ex.

  • A controller test file in test/controllers/post_controller_test.exs.

It will also prompt you with a few instructions to update the router web/router.ex and to run the migrations. Let’s do that now.

Open web/router.ex and change it to this:

defmodule RestApi.Router do
  use RestApi.Web, :router

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

  scope "/", RestApi do
    pipe_through :api

    scope "/v1", V1, as: :v1 do
      resources "/posts", PostController
    end
  end
end

We got rid of a lot of code we’re not using for an API. We added the resource "/posts" within the "/v1" scope and we setup the :api pipeline within "/" scope.

Let’s run phoenix.routes to list our routes:

$ mix phoenix.routes
...
Generated rest_api app
v1_post_path  GET     /v1/posts           RestApi.V1.PostController.index/2
v1_post_path  GET     /v1/posts/:id/edit  RestApi.V1.PostController.edit/2
v1_post_path  GET     /v1/posts/new       RestApi.V1.PostController.new/2
v1_post_path  GET     /v1/posts/:id       RestApi.V1.PostController.show/2
v1_post_path  POST    /v1/posts           RestApi.V1.PostController.create/2
v1_post_path  PATCH   /v1/posts/:id       RestApi.V1.PostController.update/2
              PUT     /v1/posts/:id       RestApi.V1.PostController.update/2
v1_post_path  DELETE  /v1/posts/:id       RestApi.V1.PostController.delete/2

Routes all set, next thing is to create our development and test database and run the migration:

$ mix ecto.create
...
The database for RestApi.Repo has been created.
$ mix ecto.migrate
[info] == Running RestApi.Repo.Migrations.CreatePost.change/0 forward
[info] create table posts
[info] == Migrated in 0.1s

If you’re seeing an error while executing any of these commands, you need to make sure you have PostgreSQL running and you have a role "postgres" with password "postgres" created. If you want to change the database configuration you can change it in config/dev.ex line 32-33 and config/test.ex line 15-16.

Next we need to version our controllers and views and make some additional modifications. First let’s create some folders and move the files:

$ mkdir -p web/controllers/v1
$ mv web/controllers/post_controller.ex web/controllers/v1
$ mkdir -p web/views/v1
$ mv web/views/post_view.ex web/views/v1
$ mkdir -p test/controllers/v1
$ mv test/controllers/post_controller_test.exs test/controllers/v1/

Add V1 to each module name we’re versioning like this:

# web/controllers/v1/post_controller.ex
defmodule RestApi.V1.PostController
...
end

# test/controllers/v1/post_controller_test.exs
defmodule RestApi.V1.PostControllerTest do
...
end

# web/views/v1/post_view.ex
defmodule RestApi.V1.PostView do
...
end

If you run the test now, you’ll see an error like this:

$ mix test
...
** (CompileError) test/controllers/v1/post_controller_test.exs:14: function post_path/2 undefined
    (stdlib) lists.erl:1336: :lists.foreach/2
    (stdlib) erl_eval.erl:657: :erl_eval.do_apply/6

This is because the router helper post_path/2 doesn’t exists. When we setup our resource within the "/v1" scope, we specified the option as: :v1 which adds the prefix to the router helper post_path/2.

To fix this, we need to replace post_path calls for v1_post_path in test/controllers/v1/post_controller_test.exs.

Once fixed, if you run the test again, you should see four failing tests with the following error:

...
** (UndefinedFunctionError) undefined function: RestApi.PostView.__resource__/0 (module RestApi.PostView is not available)
...

This seems like a bug at first, but then I looked through the source code and I found that the issue is because we’re calling render_many/2 and render_one/2 in web/views/v1/post_view.ex.

If you follow the code, you’ll notice that Phoenix.View.render_many/2 and Phoenix.View.render_one/2 call Phoenix.View.render_many/3 and Phoenix.View.render_one/3 respectively and these two functions call Phoenix.View.view_for_model/1 which inflects the view for a model and does not considering our versioning model.

I gotta admit, I pulled my hair for a few minutes over this but then I found the solution thanks to Chris McCord.

To fix it you need to call Phoenix.View.render_many/3 and Phoenix.View.render_one/3 respectively instead and pass the View module name like this:

defmodule RestApi.V1.PostView do
  use RestApi.Web, :view

  def render("index.json", %{posts: posts}) do
    %{data: render_many(posts, RestApi.V1.PostView, "post.json")}
  end

  def render("show.json", %{post: post}) do
    %{data: render_one(post, RestApi.V1.PostView, "post.json")}
  end

  def render("post.json", %{post: post}) do
    %{id: post.id}
  end
end

This will fix the specs:

$ mix test
...
Generated rest_api app
............

Finished in 1.1 seconds (0.9s on load, 0.2s on tests)
12 tests, 0 failures

Randomized with seed 848797

There you have it, a simple REST API built with Phoenix Framework and Elixir.

You can get the code in Github: github.com/renatomoya/rest_api

comments powered by Disqus