An Introduction to APIs with Phoenix

Written by: Micah Woods

8 min read

Phoenix is taking the internet by storm, with good reason. It's productive, fault-tolerant, concurrent, safe as a compiled language, and blazing fast. It shares many of Rails' core values, such as convention over configuration, restful resources, and a focus on developer happiness.

The cherry on the top: Phoenix was designed from the ground up for WebSockets connections -- so you know it's ready for today's web.

Today, we will build a restful API with Phoenix and compare it to a Ruby on Rails API. Before we get our hands dirty, however, let's make a case for learning Phoenix. Why should you switch?

Performance

If you're looking for performance, prepare to be delighted. Phoenix is built on top of Elixir, which uses BEAM (the Erlang virtual machine). Elixir compiles directly to BEAM byte code. Because of this, it can take advantage of the 30 years of stability and performance optimizations that Erlang offers.

Elixir is by no means a fast language. If you want to crunch numbers or manipulate images, then you should probably pick Rust, Go, or C over Elixir. However, Elixir is extremely performant when handling thousands or even millions of connected devices. This is why Phoenix responds to calls in a few milliseconds, if not microseconds.

You might say that performance is not a good reason to switch. After all, if you use Rails, that means that fast time to market is more important to you than performance. That's what Rails is best at: getting out of your way as you focus on building software.

The good news is that Phoenix is great at that as well. So you don't have to pick either performance or productivity: Phoenix delivers on both fronts.

Fault Tolerance and Concurrency

In traditional languages (imperative, procedural, object-oriented), the construct for parallel or concurrent execution of code is the thread.

Web servers or frameworks typically have some type of thread pool, and when a request comes in, the server/framework decides which thread should handle the request. This can be problematic for multiple reasons, memory management and error recovery being the two main problems.

Heroku restarts its dynos daily to prevent memory from skyrocketing. Recently, memory leaks have been found in mainstream open-source projects like Sidekiq. This is because thread management is difficult. And memory management doesn't even deal with problems associated with a thread that has a segfault and brings down the whole server.

These problems simply don't exist in Elixir. The construct used for concurrent execution is the process.

Processes are independent applications that have their own garbage collector. They are completely isolated. If a process has a catastrophic failure, only that process is affected, and the application continues to work normally.

This is exactly how Phoenix servers operate. When a request comes in, it isn't being shared with a thread pool. Instead, each request uses its own independent web server. If that process (web server) dies, the application is unaffected. And when that request is finished, that process garbage-collects itself.

Productivity

Everything so far makes for a rock-solid web framework, but it still says nothing about a developer's ability to ship products.

Rails has been the framework that stands apart in this aspect. Phoenix was written by rock-solid Rails developers. It takes all the good lessons that 10 years of Rails brought but leaves behind the things that Rails can't/won't let go of.

The easiest way to explain this is to look at some code and talk about the differences. The following assumes the reader has Elixir (1.2.0), Phoenix (1.1.2), and PostgreSQL installed. The Phoenix installation guides can be found here.

Create an App

One of the first problems a young Padawan Rails developer faces is knowing when to type rails and when to type rake. And when teaching young developers, that's surprisingly difficult to explain.

The rails command is used to run generators, start the server, create new apps, and so on, but the rake command is used to migrate the database, run the tests, etc.

There is no rhyme or reason to it. A developer will type rails generate migration to create the migration but rake db:migrate to make the migration happen. It's confusing. Thankfully much of this will be fixed in Rails 5; most commands will use rails, so look for that in a few months.

The Elixir language has a tool called mix. The mix command is always used. If you ever want to know what commands are available, you just type mix help.

In a command prompt, grep for "phoenix":

$ mix help | grep -i phoenix
mix phoenix.new       # Create a new Phoenix v1.1.2 application

Now use mix to create a new Phoenix API:

$ mix phoenix.new todo_api
...
Fetch and install dependencies? [Yn] y
* running mix deps.get
We are all set! Run your Phoenix application:
    $ cd todo_api
    $ mix phoenix.server
You can also run your app inside IEx (Interactive Elixir) as:
    $ iex -S mix phoenix.server
Before moving on, configure your database in config/dev.exs and run:
    $ mix eco.create

The option --no-brunch does not install brunch.io, which is the build tool used to build assets; it requires Node.js. And the --no-html removes code involved in creating traditional websites.

If you don't use those options, a hybrid app is generated: an app that can have API routes and traditional HTML routes. There is no performance penalty for this. The only reason I skipped those options is because they are outside the scope of this blog post.

The much anticipated Rails 5 --api option is an all-or-nothing approach. If you need HTML pages later, you cannot add them. This makes it almost unusable. Most APIs eventually have some type of traditional HTML pages for administrative use.

The instructions from the last command are nice. They tell the user how to run the server and create the database using Ecto. Ecto is an ORM much like Active Record or LINQ.

Following the instruction given by the phoenix.new command:

$ cd todo_list

Modify the config/dev.exs and config/test.exs files to have the proper credentials. On a Mac where PostgreSQL was installed with Homebrew, just remove the username and password.

config :todo_api, TodoApi.Repo,
  adapter: Ecto.Adapters.Postgres,
  # username: "postgres", # remove
  # password: "postgres", # remove
  database: "todo_api_dev",
  hostname: "localhost",
  pool_size: 10

Create the database:

$ mix ecto.create

Run the tests:

$ mix test
...
Finished in 0.1 seconds (0.1s on load, 0.00s on tests)
3 tests, 0 failures

Mix compiled the app for the first time. Don't worry, mix only recompiles when files change and then only the files that changed. Type mix test again. It's instantaneous!

Three tests for an app that does nothing. What are they?

# /test/views/error_view_test.exs
defmodule TodoApi.ErrorViewTest do
  use TodoApi.ConnCase, async: true
  # Bring render/3 and render_to_string/3 for testing custom views
  import Phoenix.View
  test "renders 404.json" do
    assert render(TodoApi.ErrorView, "404.json", []) ==
           %{errors: %{detail: "Page not found"}}
  end
  test "render 500.json" do
    assert render(TodoApi.ErrorView, "500.json", []) ==
           %{errors: %{detail: "Server internal error"}}
  end
  test "render any other" do
    assert render(TodoApi.ErrorView, "505.json", []) ==
           %{errors: %{detail: "Server internal error"}}
  end
end

These tests assert that out-of-the-box errors return proper JSON. But more importantly they test views, one of the core differences in Phoenix. A view is a group of functions used to render JSON, or if this was an HTML view, these functions would be available in the template.

Open the view and notice that it's just a module with functions:

# /web/views/error_view.ex
defmodule TodoApi.ErrorView do
  use TodoApi.Web, :view
  def render("404.json", _assigns) do
    %{errors: %{detail: "Page not found"}}
  end
  def render("500.json", _assigns) do
    %{errors: %{detail: "Server internal error"}}
  end
  # In case no render clause matches or no
  # template is found, let's render it as 500
  def template_not_found(_template, assigns) do
    render "500.json", assigns
  end
end

The important takeaway for a JSON API is this: Views create dictionaries that represent the JSON for an action. And views are easy to test. This comes in handy if views have complex logic.

However, most of the time, a controller test that asserts the proper response is good enough. Those types of tests are considered integration tests.

First Resource

Frameworks aren't really mature until they have generators. Luckily Phoenix has plenty. Listing them is easy:

$ mix help | grep -i phoenix
mix phoenix.digest      # Digests and compress static files
mix phoenix.gen.channel # Generates a Phoenix channel
mix phoenix.gen.html    # Generates controller, model and views for an HTML based resource
mix phoenix.gen.json    # Generates a controller and model for a JSON based resource
mix phoenix.gen.model   # Generates an Ecto model
mix phoenix.gen.secret  # Generates a secret
mix phoenix.new         # Create a new Phoenix v1.1.2 application
mix phoenix.routes      # Prints all routes
mix phoenix.server      # Starts applications and their servers

Use the phoenix.new.json generator to scaffold out a JSON resource:

$ mix phoenix.gen.json Todo todos description:string
complete:boolean
* creating web/controllers/todo_controller.ex
* creating web/views/todo_view.ex
* creating test/controllers/todo_controller_test.exs
* creating web/views/changeset_view.ex
* creating priv/repo/migrations/20160120010759_create_todo.exs
* creating web/models/todo.ex
* creating test/models/todo_test.exs
Add the resource to your api scope in web/router.ex:
    resources "/todos", TodoController, except: [:new, :edit]
Remember to update your repository by running migrations:
    $ mix ecto.migrate

The generator says to add the a line to the router. Add it to the /api scope:

defmodule TodoApi.Router do
  use TodoApi.Web, :router
  pipeline :api do
    plug :accepts, ["json"]
  end
  scope "/api", TodoApi do
    pipe_through :api
    resources "/todos", TodoController, except: [:new, :edit]
  end
end

Routes initially feel very familiar to a seasoned Rails developer:

# Rails
resources :todos, except: [:new, :edit]
# Phoenix
resources "/todos", TodoController, except: [:new, :edit]

They can be written very similar to Rails, with a few exceptions. The controller is specified in the route. There are no plurals. Anyone who has ever lost several hours tracking down a bug, only find out they didn't pluralize correctly, should be ecstatic about this.

Lastly, routes have pipelines, which are composable middleware stacks. They are composed of plugs which are middleware similar to Rack but with several advantages.

Run the migration and then run the tests:

$ mix eco.migrate
// stuff happens ...
$ mix test
.............
Finished in 0.2 seconds (0.1s on load, 0.07s on tests)
13 tests, 0 failures

Awesome, another passing test suite for free. For now, ignore the tests. Instead focus on the similarities and differences in the Phoenix framework. Open the model first:

# web/models/todo.ex
defmodule TodoApi.Todo do
  use TodoApi.Web, :model
  schema "todos" do
    field :description, :string
    field :complete, :boolean, default: false
    timestamps
  end
  @required_fields ~w(description complete)
  @optional_fields ~w()
  @doc """
  Creates a changeset based on the `model` and `params`.
  If no params are provided, an invalid changeset is returned
  with no validation performed.
  """
  def changeset(model, params \\ :empty) do
    model
    |> cast(params, @required_fields, @optional_fields)
  end
end

This is another major difference from Rails. In Rails, the mantra is "fat model, skinny controller." This is not the case with Phoenix.

Models are a data type and are treated as such. They are just vanilla structs that use the special schema macro to map to the database. There is one exception to this. Functions called "changesets" are used to scrub params and validate data. In the above example, the cast\3 method is being used to scrub the params.

Now open the view:

# web/views/todo_view.ex
defmodule TodoApi.TodoView do
  use TodoApi.Web, :view
  def render("index.json", %{todos: todos}) do
    %{data: render_many(todos, TodoApi.TodoView, "todo.json")}
  end
  def render("show.json", %{todo: todo}) do
    %{data: render_one(todo, TodoApi.TodoView, "todo.json")}
  end
  def render("todo.json", %{todo: todo}) do
    %{id: todo.id,
      description: todo.description,
      complete: todo.complete}
  end
end

Exactly what was expected. A set of functions that's used to generate JSON. This is great -- it will be easy to modify this code later.

Now open the controller:

# web/controllers/todo_controller.ex
defmodule TodoApi.TodoController do
  use TodoApi.Web, :controller
  alias TodoApi.Todo
  plug :scrub_params, "todo" when action in [:create, :update]
  def index(conn, _params) do
    todos = Repo.all(Todo)
    render(conn, "index.json", todos: todos)
  end
  def create(conn, %{"todo" => todo_params}) do
    changeset = Todo.changeset(%Todo{}, todo_params)
    case Repo.insert(changeset) do
      {:ok, todo} ->
        conn
        |> put_status(:created)
        |> put_resp_header("location", todo_path(conn, :show, todo))
        |> render("show.json", todo: todo)
      {:error, changeset} ->
        conn
        |> put_status(:unprocessable_entity)
        |> render(TodoApi.ChangesetView, "error.json", changeset: changeset)
    end
  end
  def show(conn, %{"id" => id}) do
    todo = Repo.get!(Todo, id)
    render(conn, "show.json", todo: todo)
  end
  def update(conn, %{"id" => id, "todo" => todo_params}) do
    todo = Repo.get!(Todo, id)
    changeset = Todo.changeset(todo, todo_params)
    case Repo.update(changeset) do
      {:ok, todo} ->
        render(conn, "show.json", todo: todo)
      {:error, changeset} ->
        conn
        |> put_status(:unprocessable_entity)
        |> render(TodoApi.ChangesetView, "error.json", changeset: changeset)
    end
  end
  def delete(conn, %{"id" => id}) do
    todo = Repo.get!(Todo, id)
    # Here we use delete! (with a bang) because we expect
    # it to always work (and if it does not, it will raise).
    Repo.delete!(todo)
    send_resp(conn, :no_content, "")
  end
end

The syntax is different from Ruby, but this is the exact same logic one would expect to find in a Rails controller.

One difference is that the create and update actions return the changeset when the request is invalid, instead of the model. Given that models are just data objects that map to the database, this makes sense. A different data type, the changeset, is used to carry information about valid and invalid data.

Conclusion

If you've been dreaming of a framework that is blazing fast, fault-tolerant, compiled, and concurrent, but you've always come back to Rails because of its productivity... dream no more. The next blog post in this series will tackle CORS, authentication from scratch, and dependency management. Until then, enjoy your new-found productivity and performance!

Stay up to date

We'll never share your email address and you can opt out at any time, we promise.