Skip to content

Instantly share code, notes, and snippets.

@holtbp
Last active June 26, 2023 18:01
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save holtbp/8a075d8afb9072a4e7c7a748f6f9ee43 to your computer and use it in GitHub Desktop.
Save holtbp/8a075d8afb9072a4e7c7a748f6f9ee43 to your computer and use it in GitHub Desktop.
Build and test Phoenix JSON API

Build and test a blazing fast JSON API with Phoenix, an Elixir framework

Original Post

Note: This guide was written for Phoenix 1.1.4. Parts of it may no longer work if you are using a newer version.

Let’s build a JSON API that serves a list of contacts. We’ll be writing it using Elixir (version 1.2.5) and Phoenix (version 1.1.4). Phoenix is a framework written in Elixir that aims to make writing fast, low latency web applications as enjoyable as possible.

Source Code: The source code after finishing this guide can be found here.

Why Elixir and Phoenix?

Erlang is a Ferrari wrapped in the sheet metal of an old beater. It has immense power, but to many people, it looks ugly. It has been used by WhatsApp to handle billions of connections, but many people struggle with the unfamiliar syntax and lack of tooling. Elixir fixes that. It is built on top of Erlang, but has a beautiful and enjoyable syntax, with tooling like mix to help build, test and work with applications efficiently.

Phoenix builds on top of Elixir to create very low latency web applications, in an environment that is still enjoyable. Blazing fast applications and an enjoyable development environments are no longer mutually exclusive. Elixir and Phoenix give you both. Response times in Phoenix are often measured in microseconds instead of milliseconds.

Now that we’ve discussed why we might want to build something in with this framework, let’s build something!

Installation

To install Elixir and Phoenix on Mac OS X, I have written a guide here. See the Phoenix docs for installation steps and other guides.

Project creation

Please follow the steps in the Phoenix Up And Running docs to create your first Phoenix project.

Writing the test

See getting started on the Phoenix website to see how to create a new app called HelloPhoenix. We’ll be using Phoenix 1.1.4 for this exercise.

Now that you have your Phoenix app setup let’s start by writing a test. Let’s create a file at test/controllers/contact_controller_test.exs

defmodule HelloPhoenix.ContactControllerTest do
  use HelloPhoenix.ConnCase

  alias HelloPhoenix.Contact

  test "index returns 200" do
    response = get conn, "/api/contacts"
    assert response.status == 200
  end

  test "index returns all contacts" do
    contact = %Contact{name: "Gumbo", phone: "(801) 555-5555"}
      |> Repo.insert

    contacts_as_json = Repo.all(Contact)
      |> Poison.encode!

    response = get conn, "/api/contacts"
    assert response.resp_body == contacts_as_json
  end
end

By default, Ecto calls are wrapped in a transaction that will ensure that our database is always empty when we start our tests.

The test itself does what you would expect. use HelloPhoenix.ConnCase gives us access to the conn/2 function for creating test connections (through the line use Phoenix.ConnTest in Phoenix.ConnCase). We assert that the response was successful.

You will notice that Repo is used but is not aliased. The controller function in HelloPhoenix.Web (web/web.ex) has an alias for HelloPhoenix.Repo.

Run mix test and we’ll see the error HelloPhoenix.Contact.struct/0 is undefined, cannot expand struct HelloPhoenix.Contact. This means we haven’t yet created our model. Let’s use Ecto for hooking up to a Postgres database.

Creating our databases

Ecto uses a repository for saving and retrieving data from a database. Phoenix already comes with a repo set up and a default configuration. Make sure your Postgres username and password are correct in config/dev.exs and config/test.exs.

Let’s see what new mix tasks we get from Ecto by running mix -h | grep ecto.

You’ll see a number of tasks you can use. For now let’s create the dev and test databases. After that we can add our first model.

# This will create your dev database
$ mix ecto.create
# This will create your test database
$ env MIX_ENV=test mix ecto.create

Adding the Contact model

Let’s add a schema for Contact at web/models/contact.ex.

defmodule HelloPhoenix.Contact do
  use HelloPhoenix.Web, :model
  
  @derive {Poison.Encoder, only: [:id, :name, :phone]}

  schema "contacts" do
    field :name, :string
    field :phone, :string

    timestamps
  end
end

Next we’ll create a migration with mix ecto.gen.migration create_contacts. In the newly generated migration, write this:

defmodule HelloPhoenix.Repo.Migrations.CreateContacts do
  use Ecto.Migration

  def change do
    create table(:contacts) do
      add :name, :string
      add :phone, :string

      timestamps
    end
  end
end

The column type is required for Ecto migrations, and I have used :string here. For other types, check the Ecto.Migration docs.

Now run mix ecto.migrate to create the new table, and once more for the test database MIX_ENV=test mix ecto.migrate.

Adding the routes and controller

Let’s get to our API endpoint with a route that will look like /api/contacts.

# In our web/router.ex
defmodule HelloPhoenix.Router do
  use Phoenix.Router

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

  scope "/api", HelloPhoenix do
    pipe_through :api

    resources "/contacts", ContactController
  end
end

If you’re coming from Rails, you’ll note that /api/contacts.json will result in a 404 not found. You are expected set the appropriate request header. In a pinch you can do /api/contacts?format=json, but this is not recommended. The trailing format param was not added for performance reasons and because HTTP headers already enable this functionality.

Now, if we run mix test we see that we still need a ContactController.

** (UndefinedFunctionError) undefined function: HelloPhoenix.ContactController.init/1 (module HelloPhoenix.ContactController is not available) Let’s create our controller at web/controllers/contact_controller.ex

defmodule HelloPhoenix.ContactController do
  use HelloPhoenix.Web, :controller
  
  alias HelloPhoenix.Contact

  def index(conn, _params) do
    contacts = Repo.all(Contact)
    render conn, "index.json", contacts: contacts
  end
end

First we make sure to get all the Contacts with Repo.all(Contact). Then we render JSON with Phoenix.Controller.render/2. The function is automatically imported when we call use HelloPhoenix.Web, :controller. Check out web/web.ex to see what else is imported.

If we run mix test our tests won’t pass quite yet.

** (UndefinedFunctionError) undefined function: HelloPhoenix.ContactView.render/2 (module HelloPhoenix.ContactView is not available) We need a view to render our JSON.

Rendering our JSON with a view

Views handle how to output our JSON. Right now it’s pretty simple, but in the future, this is where we could change what we send based on user’s permissions for example.

Let’s create a file in web/views/contact_view.ex

defmodule HelloPhoenix.ContactView do
  use HelloPhoenix.Web, :view

  def render("index.json", %{contacts: contacts}) do
    render_many(contacts, HelloPhoenix.ContactView, "contact.json")
  end

  def render("contact.json", %{contact: contact}) do
    %{id: contact.id,
      name: contact.name,
      phone: contact.phone}
  end
end

This will use pattern matching to set and then return contacts. Phoenix will automatically encode the array of contacts to JSON. You can use this view function to customize how the JSON is presented, but we’ll cover that in a later post.

At this point when you run mix test all tests should pass.

That’s a wrap

Now you’ve seen how to create and test a Phoenix JSON API. You can now deploy this app to Heroku with the Elixir buildpack.

@devrafaelantunes
Copy link

Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment