Creating Your First REST API with Elixir and Phoenix Framework

Photo by Alison Pang on Unsplash

Creating Your First REST API with Elixir and Phoenix Framework

We are going to create a basic REST API with Phoenix with CRUD operations but without database integration for now.

This is my first REST API with Phoenix too. So, we are in this together.

Creating a Phoenix Project

First, we have to install Phoenix.

mix archive.install hex phx_new

Then, we have to create a Phoenix Project, we do it by running the following command:

mix phx.new api_phoenix --database sqlite3 --no-html --no-assets --no-live

We use the cd api_phoenix command and start the server by running the mix phx.server command. Then, we go to our browser and navigate to the URL:http://127.0.0.1:4000.

Phoenix uses Postgres by default. So, I switch to Sqlite3 to avoid the following messages in the console:

These are caused because Phoenix assumes that we have a PostgreSQL database with the user: postgres and with the password "postgres". We can avoid this issue by switching the database or setting the appropriate Postgres credentials in config/config.exs file.

config :my_app, Repo,
  database: "ecto_simple",
  username: "postgres",
  password: "postgres",
  hostname: "localhost",
  # OR use a URL to connect instead
  url: "postgres://postgres:postgres@localhost/ecto_simple"

Now, we run the command mix ecto.create, to create a database.

The database for ApiPhoenix.Repo has already been created

I want to create an inventory app, so we have to create a database for the items in the inventory.

We have to run the following command:

mix phx.gen.context Inventory Item items name:string:unique quantity:integer

After we run the command, it will create <app name>/lib/inventory/item.ex file, which contains the schema definition.

item.ex

defmodule ApiPhoenix.Inventory.Item do
  use Ecto.Schema
  import Ecto.Changeset

  schema "items" do
    field :name, :string
    field :quantity, :integer

    timestamps(type: :utc_datetime)
  end

  @doc false
  def changeset(item, attrs) do
    item
    |> cast(attrs, [:name, :quantity])
    |> validate_required([:name, :quantity])
    |> unique_constraint(:name)
  end
end

Also, the command creates the inventory.ex file, which includes the functions to list, create, update, and delete the inventory items.

defmodule ApiPhoenix.Inventory do
  @moduledoc """
  The Inventory context.
  """

  import Ecto.Query, warn: false
  alias ApiPhoenix.Repo

  alias ApiPhoenix.Inventory.Item

  @doc """
  Returns the list of items.

  ## Examples

      iex> list_items()
      [%Item{}, ...]

  """
  def list_items do
    Repo.all(Item)
  end

  @doc """
  Gets a single item.

  Raises `Ecto.NoResultsError` if the Item does not exist.

  ## Examples

      iex> get_item!(123)
      %Item{}

      iex> get_item!(456)
      ** (Ecto.NoResultsError)

  """
  def get_item!(id), do: Repo.get!(Item, id)

  @doc """
  Creates a item.

  ## Examples

      iex> create_item(%{field: value})
      {:ok, %Item{}}

      iex> create_item(%{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def create_item(attrs \\ %{}) do
    %Item{}
    |> Item.changeset(attrs)
    |> Repo.insert()
  end

  @doc """
  Updates a item.

  ## Examples

      iex> update_item(item, %{field: new_value})
      {:ok, %Item{}}

      iex> update_item(item, %{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def update_item(%Item{} = item, attrs) do
    item
    |> Item.changeset(attrs)
    |> Repo.update()
  end

  @doc """
  Deletes a item.

  ## Examples

      iex> delete_item(item)
      {:ok, %Item{}}

      iex> delete_item(item)
      {:error, %Ecto.Changeset{}}

  """
  def delete_item(%Item{} = item) do
    Repo.delete(item)
  end

  @doc """
  Returns an `%Ecto.Changeset{}` for tracking item changes.

  ## Examples

      iex> change_item(item)
      %Ecto.Changeset{data: %Item{}}

  """
  def change_item(%Item{} = item, attrs \\ %{}) do
    Item.changeset(item, attrs)
  end
end

Then, we run the command: mix ecto.migrate, to create the table in the database with the defined schema.

We need to create a Controller and View to return the response.

To generate Controller and View for a specific module, we have to execute the following command

mix phx.gen.json Inventory Item items name:string:unique quantity:integer --no-context --no-schema

The above command generates a CRUD Controller for the item at lib/api_phoenix_web/controllers/item_controller.ex

item_controller.ex

defmodule ApiPhoenixWeb.ItemController do
  use ApiPhoenixWeb, :controller

  alias ApiPhoenix.Inventory
  alias ApiPhoenix.Inventory.Item

  action_fallback ApiPhoenixWeb.FallbackController

  def index(conn, _params) do
    items = Inventory.list_items()
    render(conn, :index, items: items)
  end

In the code above, we defined the index function that calls the Inventory.list_items() and shows all the items stored in the database.

def create(conn, %{"item" => item_params}) do
    with {:ok, %Item{} = item} <- Inventory.create_item(item_params) do
      conn
      |> put_status(:created)
      |> put_resp_header("location", ~p"/api/items/#{item}")
      |> render(:show, item: item)
    end
  end

The create function receives the request to add the data in the request body and sends a response with the status code “Created” and the data added to the database if the parameters match correctly.

 def show(conn, %{"id" => id}) do
    item = Inventory.get_item!(id)
    render(conn, :show, item: item)
  end

The show function retrieves an item with the specified ID in the params.

def update(conn, %{"id" => id, "item" => item_params}) do
    item = Inventory.get_item!(id)

    with {:ok, %Item{} = item} <- Inventory.update_item(item, item_params) do
      render(conn, :show, item: item)
    end
  end

The update function will change the data in any row given its ID with the data received in the request body.

def delete(conn, %{"id" => id}) do
    item = Inventory.get_item!(id)

    with {:ok, %Item{}} <- Inventory.delete_item(item) do
      send_resp(conn, :no_content, "")
    end
  end

The Also, if you like my work consider supporting me using the Ko-Fi button below. function deletes a row by the ID.

Complete lib/api_phoenix_web/controllers/item_controller.ex file:

defmodule ApiPhoenixWeb.ItemController do
  use ApiPhoenixWeb, :controller

  alias ApiPhoenix.Inventory
  alias ApiPhoenix.Inventory.Item

  action_fallback ApiPhoenixWeb.FallbackController

  def index(conn, _params) do
    items = Inventory.list_items()
    render(conn, :index, items: items)
  end

  def create(conn, %{"item" => item_params}) do
    with {:ok, %Item{} = item} <- Inventory.create_item(item_params) do
      conn
      |> put_status(:created)
      |> put_resp_header("location", ~p"/api/items/#{item}")
      |> render(:show, item: item)
    end
  end

  def show(conn, %{"id" => id}) do
    item = Inventory.get_item!(id)
    render(conn, :show, item: item)
  end

  def update(conn, %{"id" => id, "item" => item_params}) do
    item = Inventory.get_item!(id)

    with {:ok, %Item{} = item} <- Inventory.update_item(item, item_params) do
      render(conn, :show, item: item)
    end
  end

  def delete(conn, %{"id" => id}) do
    item = Inventory.get_item!(id)

    with {:ok, %Item{}} <- Inventory.delete_item(item) do
      send_resp(conn, :no_content, "")
    end
  end
end

Next, we need to add the routes for the controller in the lib/api_phoenix_web/router.ex file.

defmodule ApiPhoenixWeb.Router do
  use ApiPhoenixWeb, :router

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

  scope "/api", ApiPhoenixWeb do
    pipe_through :api
      get "/items",ItemController, :index
      get "/items/:id",ItemController, :show
      put "/items/:id", ItemController, :update
      post "/items", ItemController, :create
      delete "/items/:id", ItemController, :delete
  end

...
end

According to the Phoenix documentation, routers are the main hubs of Phoenix applications. They match HTTP requests to controller actions, wire up real-time channel handlers. The router provides a set of macros for generating routes that dispatch to specific controllers and actions.

For example, in the lib/api_phoenix_web/router.ex file we have the macro get that accepts a request to /items and dispatches it to the index action in ItemController.

Now, we are starting up our server and testing it.

You can use any client you want.

POST Route

GET Route

GET Route

PUT Route

DELETE Route

Conclusion

I have tried many web frameworks, but this is the first time I feel the framework does everything for you. This is a framework if we want to build and deploy a prototype as fast as possible. We need to execute the commands and it writes the model, the schema, controllers, and the functions to query the database. We have to focus on the domain model and Phoenix takes care of the rest.

It will sound like bad advice, but I won’t suggest Phoenix to anyone new to backend development because I fear they will grow spoiled.

Source code here.

Thank you for taking the time to read this article.

If you have any recommendations about other packages, architectures, how to improve my code, my English, or anything; please leave a comment or contact me through Twitter, or LinkedIn.

Also, if you want to support my work, you can tip me using the Ko-Fi button below.

Resources

Building a REST API with Elixir and Phoenix

Phoenix Documentation