A step-by-step guide on creating a REST API with Phoenix, covering everything from database setup to endpoint implementation.
When I first dove into the Phoenix framework, I felt intimidated. It quickly became clear that I needed to understand a few key concepts before I could build my first app.
However, reading through the book Programming Phoenix LiveView clarified many of the aspects that confused me. The book gave me enough confidence to build a simple REST API that serves quotes from Stoic philosophers, to apply what I learned.
In this post, I’ll outline the step-by-step process of creating this API using Phoenix. The API provides these endpoints:
/api/quotes/
to list all quotes/api/quotes/random
to fetch a random quotePhoenix is a web development framework built using the Elixir programming language. It’s akin to Ruby on Rails and Django in the Elixir ecosystem as it implements the server-side Model-View-Controller design pattern. This makes Phoenix suitable for crafting backend APIs, full-stack web apps, and other applications.
A key advantage of Phoenix is its robust support for real-time functionalities via websockets, making it ideal for building chat rooms, multiplayer games, and other apps requiring live updates.
In this tutorial, I’ll assume the following:
Every Phoenix project begins with the command mix phx.new
. Run the commands below in your terminal to get started:
mix phx.new stoic_quotes
cd stoic_quotes
mix ecto.create
This will create a new directory called stoic_quotes/
with all the necessary files and folders and set up a database for the app.
Next, we’ll create a quotes database. This will involve:
quotes
table schema.First, we’ll create a database table named quotes
to store quotes. This table will have these fields:
quote
: the quote itselfauthor
: author of the quotesource
: the literary work (and chapter) containing the quotePhoenix provides generators that we can leverage for this. Here, we’ll use the phx.gen.context
generator.
This generator executes three tasks:
quotes
table and a changeset
function used for validating new entries.quotes
table.Furthermore, phx.gen.context
requires at least 3 arguments: the context module name, the singular and plural names of the generated resources, and any attributes to include in the schema, specifying the data type for each attribute.
Run the following command in your terminal to generate the necessary files for the quotes
table:
mix phx.gen.context Quotes Quote quotes quote:text author:string source:string
We need to implement a constraint to prevent users from inserting duplicate quotes into the database. For this, we’ll edit both the migration file and the schema module.
Navigate to the /priv/repo/migrations
directory and find a timestamped file labeled create_quotes
.
This file contains instructions on creating the quotes table with the fields we specified earlier. The timestamps()
macro adds the inserted_at
and updated_at
fields, which store timestamps at which an entry was inserted and updated, respectively.
Now, edit the migration file to look like this:
defmodule StoicQuotes.Repo.Migrations.CreateQuotes do
use Ecto.Migration
def change do
create table(:quotes) do
add :quote, :text
add :author, :string
add :source, :string
timestamps()
end
# add this line below to enforce unique quotes
create unique_index(:quotes, [:quote], name: :index_for_duplicate_quotes)
end
end
The new line we’ve added to the migration file creates a unique index, ensuring each quote is distinct.
After making these edits, execute the command mix ecto.migrate
in the terminal to create the quotes
table.
The schema module, located in lib/stoic_quotes/quotes/quote.ex
, defines the schema and includes a changeset function used for validating new entries before their insertion into the quotes
table.
These changeset functions are used by methods in the generated context module StoicQuotes.Quotes
, found in lib/stoic_quotes/quotes.ex
. For example, the context module provides this function for creating quotes:
def create_quote(attrs \\ %{}) do
%Quote{}
|> Quote.changeset(attrs)
|> Repo.insert()
end
Here, create_quote
takes a struct containing the fields of the new quote entry. It applies the Quote.changeset
to this input before passing it to Repo.insert()
function. Entries that violate any constraints won’t be inserted.
By default, the changeset function in the Quote
schema module checks that the necessary fields— quote, author, and source— are provided for each new entry. Let’s add another line to implement the unique constraint:
defmodule StoicQuotes.Quotes.Quote do
use Ecto.Schema
import Ecto.Changeset
schema "quotes" do
field :author, :string
field :source, :string
field :quote, :string
timestamps()
end
@doc false
def changeset(quote, attrs) do
quote
|> cast(attrs, [:quote, :author, :source])
|> validate_required([:quote, :author, :source])
|> unique_constraint(:quote, name: :index_for_duplicate_quotes) #Add this new line
end
end
The new line we’ve added leverages the index we defined in the migration file to prevent the insertion of duplicate quotes and provides an error message when this occurs.
Now, let’s add quotes to the database. Create a JSON file named quotes.json
in the priv/repo/
directory. We’ll populate the JSON file with 3 quotes to start:
[
{
"quote": "Seldom are any found unhappy from not observing what is in the minds of others. But such as observe not well the stirrings of their own souls must of necessity be unhappy.",
"author": "Marcus Aurelius",
"source": "Book II, Meditations"
},
{
"quote": "Consider whence each thing came, of what it was compounded, into what it will be changed, how it will be with it when changed, and that it will suffer no evil.",
"author": "Marcus Aurelius",
"source": "Book XI, Meditations"
},
{
"quote": "Accustom yourself as much as possible, when any one takes any action, to consider only: To what end is he working? But begin at home; and examine yourself first of all.",
"author": "Marcus Aurelius",
"source": "Book X, Meditations"
}
]
Next, we’ll write a script to insert these quotes into the database. Create a script called seeds.exs
in priv/repo/
with the following contents:
alias StoicQuotes.Quotes
# Read quotes from the JSON file
quotes_path = "priv/repo/quotes.json"
quotes_path
|> File.read!()
|> Jason.decode!()
|> Enum.each(fn attrs ->
# Construct a quote struct and attempt to insert it
quote = %{quote: attrs["quote"], author: attrs["author"], source: attrs["source"]}
case Quotes.create_quote(quote) do
{:ok, _quote} -> :ok
{:error, _changeset} -> :duplicate
end
end)
This script uses the Quotes
context module to insert each quote into the database.
Run the following command to execute the script:
mix run priv/repo/seeds.exs
To verify the quotes in the database:
iex -S mix
in the terminal to open an Elixir shell.alias StoicQuotes.Quotes
to alias the Quotes
module.Quotes.list_quotes()
to list all quotes in the database.Now that the database is set up, we need to establish endpoints for our REST API. These include the following:
/api/quotes/
to list all quotes/api/quotes/random
to fetch a random quoteIn this project, routes are defined in lib/stoic_quotes_web/router.ex
. We can implement the REST API routes by adding the following declaration into the body of this file:
scope "/api", StoicQuotesWeb do
pipe_through :api
get "/quotes", QuotesController, :index
get "/quotes/random", QuotesController, :show
end
This snippet sets up the /api/
routes to use the pipeline defined by pipe_through :api
. Requests to these routes will be processed by the :api
pipeline, which is defined in the file as
pipeline :api do
plug :accepts, ["json"]
end
Routes using this pipeline will accept JSON requests.
Furthermore, the defined routes specify that GET
requests to the /api/quotes/
and /api/quotes/random
will be handled by the :index
and :show
actions in the QuotesController
module, respectively.
In the following section, we’ll learn how to create these controller actions.
Phoenix will look for controller definitions in the directory lib/stoic_quotes_web/controllers/
. Let’s create a file named quotes_controller.ex
here to define our API’s behavior:
defmodule StoicQuotesWeb.QuotesController do
use Phoenix.Controller, formats: [:json]
alias StoicQuotes.Quotes
def index(conn, _params) do
quotes = %{quotes: Quotes.list_quotes()}
render(conn, :index, quotes)
end
def show(conn, _params) do
quote = %{quote: Quotes.get_random_quote()}
render(conn, :show, quote)
end
end
The index
and show
functions implement the logic for the :index
and :show
actions defined in the router, respectively. These require two arguments: the connection struct conn
and a set of parameters params
, which we won’t use (for more info on actions read here).
Outputs of these functions are dictated by the render/3
function, which generates the views for these actions. Here, render
takes a connection struct, an atom corresponding to the view template and data to pass to the template.
In addition, the macro use Phoenix.Controller, formats: [:json]
specifies that this controller renders JSON responses. Phoenix will look for the view templates in the file quotes_json.ex
, which we’ll define in the next section.
For the show
action, we’ll need to define the get_random_quote
in the StoicQuotes.Quotes
module located in lib/stoic_quotes/quotes.ex
. Here, we can define this function as shown below:
def get_random_quote() do
query =
from q in Quote,
order_by: fragment("RANDOM()"),
limit: 1
Repo.one(query)
end
In the lib/stoic_quotes_web/controllers/
directory, let’s create the quotes_json.ex
file to format data for the API responses:
defmodule StoicQuotesWeb.QuotesJSON do
alias StoicQuotes.Quotes.Quote
def index(%{quotes: quotes}) do
%{data: for(quote <- quotes, do: data(quote))}
end
def show(%{quote: quote}) do
%{data: data(quote)}
end
defp data(%Quote{} = datum) do
%{
quote: datum.quote,
author: datum.author,
source: datum.source
}
end
end
These functions define the view templates for the index
and the show
actions. Both functions return a struct with a data
field, which Phoenix automatically converts into appropriate JSON response formats.
Now, let’s test this API. An easy way to do this is to use the curl
command, available in terminals, that transfers data from or to a server.
First, run mix phx.server
to start the server. After that, test the endpoint routes using these commands:
curl -i localhost:4000/api/quotes/ #get all quotes
curl -i localhost:4000/api/quotes/random #get random quote
In this blog post, we covered the necessary steps to build a simple REST API using the Phoenix Framework. First, we set up a database, populated it with quote entries, and implemented the endpoints for our REST API. Finally, we created controllers and corresponding views to generate JSON responses.