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/xmlortext/xml) - Plain Text (
text/plain) - CSV (
text/csvorapplication/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.