Phoenix, like most modern frameworks, has built-in functionality to reduce boilerplate for the common Create, Read, Update, Delete (CRUD) pattern.

See the code

Read the docs

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 or text/xml)
  • Plain Text (text/plain)
  • CSV (text/csv or application/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.