Introduction:
Elixir, a dynamic and functional programming language, is gaining popularity for building scalable and fault-tolerant applications. One of the prominent web frameworks for Elixir is Phoenix, which provides an excellent foundation for building web applications and APIs.
In this article, we will walk through the process of building a REST API with Elixir and Phoenix, covering everything from setting up a new Phoenix project to writing API endpoints, handling requests, and generating responses.
We will also discuss various concepts such as routing, controllers, and views in Phoenix, along with error handling and authentication. So let's dive in and explore how to build a REST API with Elixir and Phoenix!
Prerequisites:
Before we begin, you should have some basic knowledge of Elixir and Phoenix.
If you are new to Elixir, you can check out the official Elixir documentation (https://elixir-lang.org/docs.html) and Phoenix documentation (https://hexdocs.pm/phoenix/overview.html) to get started. You will also need to have Elixir and Phoenix installed on your local machine. You can install Elixir by following the instructions on the Elixir website (https://elixir-lang.org/install.html) and Phoenix by following the instructions on the Phoenix website (https://hexdocs.pm/phoenix/installation.html). Once you have Elixir and Phoenix installed, you are ready to build your REST API!
Creating a New Phoenix Project:
To create a new Phoenix project, we can use the mix
the command-line tool, which comes with Elixir. Open your terminal and run the following command:
mix phx.new my_api
This will create a new Phoenix project with the name "my_api" in a directory with the same name. You can replace "my_api" with the desired name for your project. This command will generate the basic structure of a Phoenix project, including configuration files, a supervision tree, and an initial endpoint.
Next, navigate to the project directory by running:
cd my_api
Now, let's start the Phoenix server by running:
mix phx.server
This will start the Phoenix server on the default port 4000, and you should be able to access the default Phoenix welcome page by opening your web browser and navigating to http://localhost:4000. This confirms that your Phoenix project is up and running!
Routing in Phoenix:
In Phoenix, routing is the process of mapping URLs to functions that handle requests. Phoenix uses a router to define routes, which are configured in the router.ex
file located in the lib/my_api_web
directory. Let's take a look at an example of how to define a route in Phoenix.
Open the router.ex
file and replace the default code with the following:
defmodule MyApiWeb.Router do
use MyApiWeb, :router
# Define a route for the root URL
get "/", PageController, :index
# Define a route for a RESTful resource "users"
resources "/users", UserController
end
In this example, we defined two routes. The first route maps the root URL ("/") to the index
function in the PageController
module. The second route defines a RESTful resource "users", which maps various HTTP methods (GET, POST, PUT, DELETE, etc.) to corresponding functions in the UserController
module. This is a common pattern for defining routes in Phoenix, where URLs are mapped to controller functions using the HTTP method and the name of the resource.
Controllers in Phoenix:
In Phoenix, controllers handle requests and generate responses. Controllers are responsible for processing incoming requests, performing business logic, and returning appropriate responses. Let's create a controller for our REST API.
To create a new controller, we can use the mix
command-line tool. In the terminal, run the following command:
mix phx.gen.html User users name:string age:integer
This command will generate a UserController
module with CRUD (Create, Read, Update, Delete) actions for a "User" resource, along with HTML views for rendering the responses. Since we are building a REST API, we don't need the HTML views, so we can remove them to keep our code clean and focused on the API functionality.
Open the generated user_controller.ex
file located in the lib/my_api_web/controllers
directory, and remove the HTML views, leaving only the actions like this:
defmodule MyApiWeb.UserController do
use MyApiWeb, :controller
def index(conn, _params) do
# Implement logic to fetch and return all users
end
def show(conn, %{"id" => id}) do
# Implement logic to fetch and return a specific user by id
end
def create(conn, %{"user" => user_params}) do
# Implement logic to create a new user with user_params
end
def update(conn, %{"id" => id, "user" => user_params}) do
# Implement logic to update a specific user by id with user_params
end
def delete(conn, %{"id" => id}) do
# Implement logic to delete a specific user by id
end
end
In each action, we can implement the business logic to handle the corresponding CRUD operation for the "User" resource. For example, in the index
action, we can fetch all users from the database and return them as a JSON response.
Generating JSON Responses:
In a REST API, responses are usually returned in JSON format. Phoenix provides built-in support for generating JSON responses using the render
function and the :json
view.
Let's update the UserController
to generate JSON responses. Replace the code in the index
action with the following:
def index(conn, _params) do
users = Repo.all(User)
render(conn, "index.json", users: users)
end
In this example, we fetch all users from the database using the Repo.all
function, and then pass them to the render
function along with the name of the JSON view ("index.json"). We can create this view by running the following command in the terminal:
mix phx.gen.json User users name:string age:integer
This command generates a UserView
module with functions for rendering JSON responses. Open the generated user_view.ex
file located in the lib/my_api_web/views
directory, and update the index
function to define how the users should be rendered as JSON:
defmodule MyApiWeb.UserView do
use MyApiWeb, :view
def render("index.json", %{users: users}) do
%{data: users}
end
# Implement other functions for rendering JSON responses
end
In this example, we define a function render("index.json", %{users: users})
that takes a map with a key "users" as an argument, and returns a JSON response with a key "data" that contains the users. We can define similar functions for other actions to customize the JSON responses according to our needs.
Handling Requests and Parameters:
In Phoenix, requests from clients are represented as connection (conn
) structures, which contain information about the HTTP method, URL path, query parameters, request body, headers, and more. We can use pattern matching to extract data from the conn
structure and use it in our controller actions.
For example, in the show
action of the UserController
, we can extract the id
parameter from the URL path and use it to fetch a specific user from the database. Here's an example implementation:
def show(conn, %{"id" => id}) do
user = Repo.get(User, id)
case user do
nil ->
render(conn, "error.json", message: "User not found", status: 404)
_ ->
render(conn, "show.json", user: user)
end
end
In this example, we use pattern matching to extract the id
parameter from the URL path, and then use the Repo.get
function to fetch the user with the corresponding id
from the database. We then use the render
function to generate a JSON response with the user data, or an error message if the user is not found.
We can also handle request parameters in the request body, such as when creating or updating a resource. In the create
and update
actions of the UserController
, we can extract the user_params
from the request body and use them to create or update a user in the database. Here's an example implementation:
def create(conn, %{"user" => user_params}) do
changeset = User.changeset(%User{}, user_params)
case Repo.insert(changeset) do
{:ok, user} ->
render(conn, "show.json", user: user)
{:error, changeset} ->
render(conn, "error.json", message: "Failed to create user", errors: changeset.errors, status: 422)
end
end
def update(conn, %{"id" => id, "user" => user_params}) do
user = Repo.get(User, id)
case User.update(user, user_params) do
{:ok, user} ->
render(conn, "show.json", user: user)
{:error, changeset} ->
render(conn, "error.json", message: "Failed to update user", errors: changeset.errors, status: 422)
end
end
In these examples, we use the changeset
concept in Ecto, which represents the changes we want to make to a resource. We use the changeset
to validate and manipulate the user_params
before inserting or updating the user in the database. If the changeset is valid, we use the Repo.insert
or User.update
functions to perform the corresponding CRUD operation, and then generate a JSON response with the updated user data or an error message if the changeset is invalid.
Testing the REST API:
As with any software development project, testing is an important aspect of building a REST API to ensure its correctness and reliability. Phoenix provides a built-in testing framework that allows us to write tests for our controller actions and views.
Let's write some tests for the UserController
to ensure that our REST API is working correctly. Create a user_controller_test.exs
file in the test/my_api_web/controllers
directory, and write the following tests:
defmodule MyApiWeb.UserControllerTest do
use MyApiWeb.ConnCase
alias MyApiWeb.User
test "GET /users returns all users as JSON", %{conn: conn} do
user1 = insert(:user)
user2 = insert(:user)
conn = get(conn, "/users")
assert json_response(conn, 200) == [
%{id: user1.id, name: user1.name, age: user1.age},
%{id: user2.id, name: user2.name, age: user2.age}
]
end
test "GET /users/:id returns a specific user as JSON", %{conn: conn} do
user = insert(:user)
conn = get(conn, "/users/#{user.id}")
assert json_response(conn, 200) == %{id: user.id, name: user.name, age: user.age}
end
test "GET /users/:id returns a 404 error for non-existent user", %{conn: conn} do
conn = get(conn, "/users/999")
assert json_response(conn, 404) == %{error: "User not found"}
end
test "POST /users creates a new user and returns the user as JSON", %{conn: conn} do
user_params = %{name: "John Doe", age: 30}
conn = post(conn, "/users", %{user: user_params})
assert json_response(conn, 201) == %{id: _, name: "John Doe", age: 30}
end
test "POST /users returns a 422 error for invalid user params", %{conn: conn} do
user_params = %{name: "", age: -5}
conn = post(conn, "/users", %{user: user_params})
assert json_response(conn, 422) == %{error: "Failed to create user", errors: _}
end
test "PUT /users/:id updates an existing user and returns the user as JSON", %{conn: conn} do
user = insert(:user)
user_params = %{name: "Jane Smith", age: 35}
conn = put(conn, "/users/#{user.id}", %{user: user_params})
assert json_response(conn, 200) == %{id: user.id, name: "Jane Smith", age: 35}
end
test "PUT /users/:id returns a 404 error for non-existent user", %{conn: conn} do
user_params = %{name: "Jane Smith", age: 35}
conn = put(conn, "/users/999", %{user: user_params})
assert json_response(conn, 404) == %{error: "User not found"}
end
test "PUT /users/:id returns a 422 error for invalid user params", %{conn: conn} do
user = insert(:user)
user_params = %{name: "", age: -5}
conn = put(conn, "/users/#{user.id}", %{user: user_params})
assert json_response(conn, 422) == %{error: "Failed to update user", errors: _}
end
test "DELETE /users/:id deletes an existing user and returns a 204 status", %{conn: conn} do
user = insert(:user)
conn = delete(conn, "/users/#{user.id}")
assert conn.status == 204
assert Repo.get(User, user.id) == nil
end
test "DELETE /users/:id returns a 404 error for non-existent user", %{conn: conn} do
conn = delete(conn, "/users/999")
assert json_response(conn, 404) == %{error: "User not found"}
end
end
Now that we have our tests written, let's run them using mix test
in the terminal to make sure everything is working as expected. If all tests pass, we can move on to running the application and testing the API endpoints with a tool like curl
or a web-based API client like Postman
.
Once the tests have passed, we can start the Phoenix server by running mix phx.server
in the terminal. This will start the server at the default http://localhost:4000
URL.
Now let's test the API endpoints using curl
in the terminal. Here are some example commands:
Get all users:
curl -X GET http://localhost:4000/users
Output:
[
{
"id": 1,
"name": "John Doe",
"age": 30
},
{
"id": 2,
"name": "Jane Smith",
"age": 35
}
]
Get a specific user:
curl -X GET http://localhost:4000/users/1
Output:
{
"id": 1,
"name": "John Doe",
"age": 30
}
Create a new user:
curl -X POST -H "Content-Type: application/json" -d '{"user": {"name": "Alice", "age": 25}}' http://localhost:4000/users
Output:
{
"id": 3,
"name": "Alice",
"age": 25
}
Update an existing user:
curl -X PUT -H "Content-Type: application/json" -d '{"user": {"name": "Bob", "age": 40}}' http://localhost:4000/users/1
Output:
{
"id": 1,
"name": "Bob",
"age": 40
}
Delete a user:
curl -X DELETE http://localhost:4000/users/3
Output:
No output, just a 204 status indicating that the user has been successfully deleted.
Great! Our API is now functional and we have tested all the CRUD operations for the UserController
endpoints using curl
commands.
Conclusion:
In this article, we built a REST API using Elixir and Phoenix, a powerful web framework for building scalable applications. We covered the basics of setting up a Phoenix project, defining a User schema, creating a UserController with CRUD endpoints, writing tests for the controller, and testing the API endpoints using curl
commands.
Elixir and Phoenix provide a robust and efficient way to build high-performance APIs, with features such as concurrency, fault tolerance, and scalability. With Phoenix's powerful router and controller system, it's easy to define RESTful endpoints and handle HTTP requests and responses in a clean and organized manner.
Comments (0)