Table of contents
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.