Building RESTful APIs with Elixir and Phoenix: A Hands-On Tutorial

  • By: Hammad ur Rahman CO-Founder & CTO
  • Category: APIs
  • Date: November 1, 2023
image removebg preview (90)

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

image removebg preview (91)

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

Have Question? Write a Message

    Talk To Our Sales Team

    Maria Majid

    Head of Sales and Marketing

    10+ years

    Experience

    500+

    Team Members

    600+

    Clients

    700+

    Project Complete

    4

    Global Offices

    USA

    1630 Commonwealth Avenue, Boston Massachusettes, 90213 +1-336-660-4750

    CANADA

    1867 Eglinton Avenue, Toronto, Ontario +44-20-7021-1600

    AUSTRALIA

    300 George St, Brisbane City QLD 4000, Australia +61-07-5391-9847

    PAKISTAN

    Plot 94-B Sunflower Housing Society, Block J1 Phase 2 Johar Town, Lahore +92-317-2722222

    Tavoli da Gioco dal Vivo: Esperienza Reale

    https://zenmilano.com/ utilizza avanzate tecnologie di sicurezza per proteggere i dati degli utenti.