Elixir API: Implementing CRUD operations with Phoenix
Phoenix, like most modern frameworks, has built-in functionality to reduce boilerplate for the common Create, Read, Update, Delete (CRUD) pattern.
Router
Phoenix’s router includes the resources
macro, which generates routes for CRUD operations.
- GET /api/superheroes: Routes to
SuperheroController.index/2
- POST /api/superheroes: Routes to
SuperheroController.create/2
- GET /api/superheroes/:id: Routes to
SuperheroController.show/2
- PUT/PATCH /api/superheroes/:id: Routes to
SuperheroController.update/2
- DELETE /api/superheroes/:id: Routes to
SuperheroController.delete/2
- GET /api/superheroes/new: Routes to
SuperheroController.new/2
- GET /api/superheroes/:id/edit: Routes to
SuperheroController.edit/2
The routes for :new
or :edit
are for rendering HTML forms, which are not needed for an API that responds with JSON-formatted data.
scope "/api", GameAppWeb do
pipe_through :api
resources "/superheroes", SuperheroController, except: [:new, :edit]
end
Controllers
The router uses a controller, which is bound to a resource. For instance, for superheroes, the controller handles incoming HTTP requests, performs actions using the business logic in the superhero context, and sends back appropriate HTTP responses.
The line use GameAppWeb, :controller
is a macro that implements:
index/2
: Handles GET requests to list all superheroes.create/2
: Handles POST requests to create a new superhero.show/2
: Handles GET requests to show a single superhero by ID.update/2
: Handles PUT/PATCH requests to update an existing superhero by ID.delete/2
: Handles DELETE requests to delete a superhero by ID.
The action_fallback GameAppWeb.FallbackController
specifies a fallback controller to centralize the handling of errors and unexpected outcomes.
defmodule GameAppWeb.SuperheroController do
use GameAppWeb, :controller
alias GameApp.Superheroes
alias GameApp.Superheroes.Superhero
action_fallback GameAppWeb.FallbackController
def index(conn, _params) do
superheroes = Superheroes.list_superheroes()
render(conn, :index, superheroes: superheroes)
end
def create(conn, %{"superhero" => superhero_params}) do
with {:ok, %Superhero{} = superhero} <- Superheroes.create_superhero(superhero_params) do
conn
|> put_status(:created)
|> put_resp_header("location", ~p"/api/superheroes/#{superhero}")
|> render(:show, superhero: superhero)
end
end
def show(conn, %{"id" => id}) do
superhero = Superheroes.get_superhero!(id)
render(conn, :show, superhero: superhero)
end
def update(conn, %{"id" => id, "superhero" => superhero_params}) do
superhero = Superheroes.get_superhero!(id)
with {:ok, %Superhero{} = superhero} <- Superheroes.update_superhero(superhero, superhero_params) do
render(conn, :show, superhero: superhero)
end
end
def delete(conn, %{"id" => id}) do
superhero = Superheroes.get_superhero!(id)
with {:ok, %Superhero{}} <- Superheroes.delete_superhero(superhero) do
send_resp(conn, :no_content, "")
end
end
end
Data Formatting
A web-based API can request data in multiple formats, including:
- HTML (
text/html
) - JSON (
application/json
) - XML (
application/xml
ortext/xml
) - Plain Text (
text/plain
) - CSV (
text/csv
orapplication/csv
)
Phoenix handles these different formats by looking for the corresponding module with the appropriate format type suffix.
SuperheroView
(HTML)SuperheroJSON
(JSON)SuperheroXML
(XML)SuperheroText
(Plain Text)SuperheroCSV
(CSV)
This API will only implement the JSON format. Requests for all other formats will fail.
JSON Formatting
In a typical CRUD API, the returned data is either a resource or a list of resources. These are handled by specific rendering functions:
index/1
: Renders a list of superheroes.show/1
: Renders a single superhero.
The module GameAppWeb.SuperheroJSON
is responsible for transforming the superhero data into JSON format.
defmodule GameAppWeb.SuperheroJSON do
alias GameApp.Superheroes.Superhero
def index(%{superheroes: superheroes}) do
%{data: for(superhero <- superheroes, do: data(superhero))}
end
def show(%{superhero: superhero}) do
%{data: data(superhero)}
end
defp data(%Superhero{} = superhero) do
%{
id: superhero.id,
name: superhero.name,
location: superhero.location,
power: superhero.power
}
end
end
Extracting and encapsulating the transformations simplifies the controller code, making it easier to maintain and update.