Elixir and Phoenix provide a powerful combination for building robust and scalable web applications. In this tutorial, we’ll dive into the world of building RESTful APIs using Elixir and the Phoenix framework. By the end of this tutorial, you’ll have a solid understanding of how to create a performant and maintainable API.
Prerequisites
Before we get started, make sure you have Elixir and Phoenix installed on your machine. You can follow the official installation guide on the Elixir and Phoenix websites.
Setting Up a New Phoenix Project
Let’s start by creating a new Phoenix project. Open your terminal and run the following commands:
mix phx.new my_api
cd my_api
This will generate a new Phoenix project named “my_api.” Follow the on-screen instructions to set up your project.
Creating a Resource
In Phoenix, resources are representations of data entities. Let’s create a simple resource called “todos” for our API.
mix phx.gen.json Todo todos title:string completed:boolean
This command generates a Todo resource with a title and a completion status. Run the migrations to update the database:
mix ecto.migrate
Defining Routes
Next, let’s define the routes for our API. Open the lib/my_api_web/router.ex file and add the following:
scope “/api”, MyApiWeb do
pipe_through :api
resources “/todos”, TodoController, except: [:new, :edit]
end
This sets up RESTful routes for our Todo resource under the “/api/todos” endpoint.
Implementing Controllers
Now, let’s implement the TodoController to handle the CRUD operations. Open the lib/my_api_web/controllers/todo_controller.ex file and add the following:
defmodule MyApiWeb.TodoController do
use MyApiWeb, :controller
alias MyApi.Todo
def index(conn, _params) do
todos = Repo.all(Todo)
render(conn, “index.json”, todos: todos)
end
def show(conn, %{“id” => id}) do
todo = Repo.get(Todo, id)
render(conn, “show.json”, todo: todo)
end
def create(conn, %{“todo” => todo_params}) do
changeset = Todo.changeset(%Todo{}, todo_params)
case Repo.insert(changeset) do
{:ok, todo} ->
conn
|> put_status(:created)
|> put_resp_header(“location”, todo_path(conn, :show, todo))
|> render(“show.json”, todo: todo)
{:error, changeset} ->
conn
|> put_status(:unprocessable_entity)
|> render(“error.json”, changeset: changeset)
end
end
def update(conn, %{“id” => id, “todo” => todo_params}) do
todo = Repo.get(Todo, id)
changeset = Todo.changeset(todo, todo_params)
case Repo.update(changeset) do
{:ok, todo} ->
render(conn, “show.json”, todo: todo)
{:error, changeset} ->
conn
|> put_status(:unprocessable_entity)
|> render(“error.json”, changeset: changeset)
end
end
def delete(conn, %{“id” => id}) do
todo = Repo.get(Todo, id)
Repo.delete(todo)
conn
|> put_status(:no_content)
|> halt()
end
end
This controller defines actions for listing, showing, creating, updating, and deleting todos.
Views and Templates
Create the corresponding views and templates for rendering JSON responses. In the lib/my_api_web/views/todo_view.ex file:
defmodule MyApiWeb.TodoView do
use MyApiWeb, :view
def render(“index.json”, %{todos: todos}) do
%{data: render_many(todos, MyApiWeb.TodoView, “todo.json”)}
end
def render(“show.json”, %{todo: todo}) do
%{data: render_one(todo, MyApiWeb.TodoView, “todo.json”)}
end
def render(“error.json”, %{changeset: changeset}) do
%{errors: Changeset.errors(changeset)}
end
def render(“todo.json”, %{todo: todo}) do
%{
id: todo.id,
title: todo.title,
completed: todo.completed
}
end
end
Testing the API
Start your Phoenix server with:
mix phx.server
Now, you can use your favourite API testing tool (e.g., Postman) or a simple curl command to interact with your API:
Create a Todo:
curl -X POST -H “Content-Type: application/json” -d ‘{“todo”: {“title”: “Learn Elixir”, “completed”: false}}’ http://localhost:4000/api/todos
Get Todos:
curl http://localhost:4000/api/todos
Update a Todo:
curl -X PUT -H “Content-Type: application/json” -d ‘{“todo”: {“completed”: true}}’ http://localhost:4000/api/todos/<todo_id>
Delete a Todo:
curl -X DELETE http://localhost:4000/api/todos/<todo_id>
Adding Pagination
For large datasets, it’s essential to implement pagination to avoid overwhelming clients with a massive response. Let’s enhance our API by adding pagination to the Todo listing.
In your TodoController, update the index action:
def index(conn, %{“page” => page} = params) do
todos = Repo.paginate(Todo, page: page || 1, page_size: 10)
render(conn, “index.json”, todos: todos)
end
This modification allows clients to request a specific page by including a page parameter in the query string.
Error Handling
Robust APIs handle errors gracefully. Improve the error handling in your TodoController:
def create(conn, %{“todo” => todo_params}) do
changeset = Todo.changeset(%Todo{}, todo_params)
case Repo.insert(changeset) do
{:ok, todo} ->
conn
|> put_status(:created)
|> put_resp_header(“location”, todo_path(conn, :show, todo))
|> render(“show.json”, todo: todo)
{:error, changeset} ->
conn
|> put_status(:unprocessable_entity)
|> render(“error.json”, errors: Changeset.errors(changeset))
end
end
This modification simplifies error responses, providing a clear list of validation errors.
Versioning Your API
As your API evolves, versioning becomes crucial to ensure backward compatibility. Let’s version our API by adding a versioned namespace to the routes.
Update lib/my_api_web/router.ex:
scope “/api/v1”, MyApiWeb do
pipe_through :api
resources “/todos”, TodoController, except: [:new, :edit]
end
Now, your API is versioned under “/api/v1/todos.”
Adding Authentication with Guardian

Security is paramount in API development. Let’s integrate Guardian for token-based authentication.
Add the Guardian package to your mix.exs:
defp deps do
[
{:phoenix, “~> 1.5.9”},
# … other dependencies
{:guardian, “~> 2.0”}
]
End
Install Guardian:
mix deps.get
Follow the Guardian documentation to set up token-based authentication in your Phoenix application.
Automated Testing with ExUnit
Ensure the reliability of your API by adding automated tests. Create test files in the test directory, covering controller actions, authentication, and error handling.
Here’s a basic example for testing the TodoController:
defmodule MyApiWeb.TodoControllerTest do
use MyApiWeb.ConnCase
describe “GET /api/v1/todos”, %{conn: conn} do
test “returns a list of todos”, %{conn: conn} do
conn = get conn, “/api/v1/todos”
assert json_response(conn, 200)[“data”] |> length() > 0
end
end
end
Extend these tests to cover all aspects of your API.
Custom Query Parameters
Allow clients to filter todos based on certain criteria. Update the index action in TodoController:
def index(conn, %{“completed” => completed} = params) do
query = case completed do
“true” -> from(t in Todo, where: t.completed == true)
“false” -> from(t in Todo, where: t.completed == false)
_ -> from(t in Todo, where: not is_nil(t.completed))
end
todos = Repo.all(query)
render(conn, “index.json”, todos: todos)
end
Now, clients can request completed or incomplete todos by including the completed parameter.
Example:
curl http://localhost:4000/api/v1/todos?completed=true
Caching Responses with ETag
Improve API performance by implementing ETag-based caching. Update the show action in TodoController:
def show(conn, %{“id” => id}) do
todo = Repo.get(Todo, id)
if MatchEtag(conn, todo) do
conn |> put_status(:not_modified) |> halt()
else
conn |> put_resp_header(“etag”, ETag(conn, todo)) |> render(“show.json”, todo: todo)
end
end
This modification adds ETag headers to responses and checks for conditional requests, reducing unnecessary data transfer.
Rate Limiting with Rack-Attack
Protect your API from abuse by implementing rate limiting with Rack-Attack.
Add the Rack-Attack package to your mix.exs:
defp deps do
[
# … other dependencies
{:rack_attack, “~> 6.1”}
]
end
Install Rack-Attack:
mix deps.get
- Configure Rack-Attack in your lib/my_api_web/endpoint.ex:
plug RackAttack
- Create a config/config.exs file for Rack-Attack configuration:
use Mix.Config
config :rack_attack, limiters: [
MyAppWeb.ApiLimiter
]
- Define the ApiLimiter module:
defmodule MyAppWeb.ApiLimiter do
use RackAttack.Limiter
throttle(
:api,
limit: 100,
period: 60,
strategy: {:fixed, :reset_after, 60}
)
end
Now, your API is protected from excessive requests, ensuring fair usage.
Documenting Your API with Swagger
Enhance the developer experience by adding API documentation with Swagger.
Add the phoenix_swagger package to your mix.exs:
defp deps do
[
# … other dependencies
{:phoenix_swagger, “~> 0.5”}
]
end
Install Phoenix Swagger:
mix deps.get
- Mount Swagger in your lib/my_api_web/router.ex:
forward “/swagger”, PhoenixSwagger.Router
Now, you can access API documentation at http://localhost:4000/swagger.
Conclusion
Building RESTful APIs with Elixir and Phoenix offers a powerful and maintainable solution. This tutorial provided a solid foundation, covering pagination, error handling, versioning, authentication, and automated testing. As you continue to develop your API, explore additional features, such as background processing with GenStage or integrating with a WebSocket layer for real-time updates. Happy coding!
BACK

USA
CANADA
AUSTRALIA
PAKISTAN