Part 1 — Getting Started with Your Backend Project by Developing a Post List API using Phoenix Framework and Elixir

Loi Le
7 min readOct 19, 2023

--

This is a part of the Tutorial to build a blog platform with Elixir Phoenix and Next.js that help you to develop a web application from scratch using modern technologies such as Elixir Phoenix, Next.js, and more.

Index | Next >

In this part, we will learn how to create a Blog API manually without using the generators that Phoenix provides. This way, we can understand how the different modules work together and also practice our coding skills.

Up and Running

We plan to use Phoenix for the API development. Phoenix requires us to code in Elixir, which has a Mix utility for running various tasks. Mix simplifies the programming cycle by automating repetitive actions. To get started, you need to open a new terminal and run the first Mix command to create the API project

$ mix phx.new lani_blog --database mysql --no-html --no-assets

This command has several parameters that specify the project details:

  • lani_blog: the name of the project
  • — database mysql: we want to use mySQL as our database instead of the default postgreSQL
  • — no-html and — no-assets: we don’t need these options since we are building an api, not a web app

When you install the app, you will get this message:

Fetch and install dependencies? [Yn] Y

Choose Y to continue.

After the installation is done, run this command to start the server:

$ cd lani_blog
$ mix phx.server

Then, go to: http://localhost:4000 and see the result

the error page will be shown when you access http://localhost:4000

The main problem is that we don’t have any apis yet, so please start working on creating our first api

Develop the Post API

Phoenix follows the MVC pattern, so we need to create a controller in lib/lani_blog_web/controllers/post_controller.ex

defmodule LaniBlogWeb.PostController do
use LaniBlogWeb, :controller

def index(conn, _params) do
posts = [
%{
id: 1,
title: "My first post",
description: "My first post",
content: "My first post content",
inserted_at: "2023-10-18 08:56:06"
},
%{
id: 2,
title: "My second post",
description: "My second post",
content: "My first second content",
inserted_at: "2023-10-18 11:10:41"
}
]

render(conn, "index.json", posts: posts)
end
end

This module defines our controller and uses the :controller API. The only action we have is index. Notice the line

render(conn, "index.json", posts: posts)

This requires a view. In some web frameworks, the view in the MVC pattern is in charge of rendering html after the controller does its task. But for the API, they don’t need the view. However, in Phoenix, a view is a module that has rendering functions that change data into a format that the user can use, like HTML or JSON. So, let’s make the view in lib/lani_blog_web/views/post_view.ex

defmodule LaniBlogWeb.PostView do
use LaniBlogWeb, :view

def render("index.json", %{posts: posts}) do
%{data: render_many(posts, __MODULE__, "post.json")}
end

def render("post.json", %{post: post}) do
%{
id: post.id,
title: post.title,
description: post.description,
content: post.content,
created_at: post.inserted_at,
}
end
end

We defined two functions in our view: one for rendering a single post and another for rendering a list of posts that uses the first one.

Then, we need to register our controller to the lib/lani_blog_web/router.ex file with this code:

scope "/api", LaniBlogWeb do
pipe_through :api

get "/posts", PostController, :index
end

To test our API, We can use the curl command with theGET method . We don’t need to pass any data or headers for this request. Here is the command:

$ curl --location --request GET 'http://localhost:4000/api/posts'

The response should match what we expected.

{
"data": [
{
"content": "My first post content",
"created_at": "2023-10-18 08:56:06",
"description": "My first post",
"id": 1,
"title": "My first post"
},
{
"content": "My first second content",
"created_at": "2023-10-18 11:10:41",
"description": "My second post",
"id": 2,
"title": "My second post"
}
]
}

The Context

Let’s take another look at our PostController

defmodule LaniBlogWeb.PostController do
use LaniBlogWeb, :controller

def index(conn, _params) do
posts = [
%{
id: 1,
title: "My first post",
description: "My first post",
content: "My first post content",
inserted_at: "2023-10-18 08:56:06"
},
%{
id: 2,
title: "My second post",
description: "My second post",
content: "My first second content",
inserted_at: "2023-10-18 11:10:41"
}
]

render(conn, "index.json", posts: posts)
end
end

You can see that we hard-coded the list of posts here, but we want to replace it with the database query logic. However, the controller should not be responsible for the post data query logic. As the app grows, we will have more logic to implement, and the controller will become bloated. This is not a good practice, the controller should only handle request serving and delegate the business logic to another module. That module is called the Context.

The Context is a module in Phoenix that groups all business logic for a common goal. The Context and the Controller are good friends. The Controller handles request serving and delegates the business logic to the Context. The context is unaware of the controller, and the controller is unaware of the business rules. This makes the code easier to maintain as the feature grows.

We need to create a new file lib/lani_blog/blog.ex with this code:

defmodule LaniBlog.Blog do
def list_posts do
[
%{
id: 1,
title: "My first post",
description: "My first post",
content: "My first post content",
inserted_at: "2023-10-18 08:56:06"
},
%{
id: 2,
title: "My second post",
description: "My second post",
content: "My first second content",
inserted_at: "2023-10-18 11:10:41"
}
]
end
end

Then, we need to update our lib/lani_blog_web/controllers/post_controller.ex to use the Blog Context like this:

defmodule LaniBlogWeb.PostController do
use LaniBlogWeb, :controller
alias LaniBlog.Blog

def index(conn, _params) do
posts = Blog.list_posts()

render(conn, "index.json", posts: posts)
end
end

We can test our API again with this command:

$ curl --location --request GET 'http://localhost:4000/api/posts'

…and we should get the same data as before.

{
"data": [
{
"content": "My first post content",
"created_at": "2023-10-18 08:56:06",
"description": "My first post",
"id": 1,
"title": "My first post"
},
{
"content": "My first second content",
"created_at": "2023-10-18 11:10:41",
"description": "My second post",
"id": 2,
"title": "My second post"
}
]
}

Now we need to replace the hard-coded data with the real data from the database. Let’s use Ecto to interact with the database.

Ecto

Ecto is a framework for working with data in Elixir, similar to LINQ in .NET or Active Record in Rails. Ecto mainly supports relational databases, and lets developers query and save data to underlying storage systems like MySQL or PostgreSQL.

Ecto uses the Repository pattern to interact with the data store. Phoenix helps you create lib/lani_blog/repo.ex when you generate the app.

defmodule LaniBlog.Repo do
use Ecto.Repo,
otp_app: :lani_blog,
adapter: Ecto.Adapters.MyXQL
end

Our Repository relies on MyXQL Adapter to connect to MySQL database.

You can set up the repository and the development credentials for the database in config/dev.exs:

config :lani_blog, LaniBlog.Repo,
username: "root",
password: "",
hostname: "localhost",
database: "lani_blog_dev",
stacktrace: true,
show_sensitive_data_on_connection_error: true,
pool_size: 10

Let’s check if our credentials are valid. Run the following command and Ecto will create the database, unless it already exists:

$ mix ecto.create 
The database for LaniBlog.Repo has been created

Lets define the structure and fields of our blog post by creating the lib/lani_blog/blog/post.ex schema:

defmodule LaniBlog.Blog.Post do
use Ecto.Schema

schema "posts" do
field :title, :string
field :description, :string
field :content, :string

timestamps()
end
end

The schema and field macros let us define both the database table and the Elixir struct at once. Each field matches a field in the database and a field in our local Blog.Post struct.

Ecto automatically creates the primary key called :id by default. Ecto also automatically creates an Elixir %LaniBlog.Blog.Post{} struct for us from the schema definition.

Now that our Repo and User schema are ready, we need to sync the database with the structure of our app. Ecto uses migrations for this. A migration changes a database to suit the structure our app needs. For our new feature, we need a migration to build our users table with columns that correspond to our User schema. Let’s create one:

$ mix ecto.gen.migration create_posts
* creating priv/repo/migrations/20230625051113_create_posts.exs

The mix ecto.gen.migration command generates a migration file for us with a special timestamp to maintain our database migrations in sequence. That’s why your migration filename will have a different prefix than ours.

Put these changes within your empty change function:

defmodule LaniBlog.Repo.Migrations.CreatePosts do
use Ecto.Migration

def change do
create table(:posts) do
add :title, :string
add :description, :string
add :content, :string

timestamps()
end
end
end

The only thing left is to migrate up our database:

$ mix ecto.migrate
== Running 20230625051113 LaniBlog.Repo.Migrations.CreatePosts.change/0 forward
create table posts
== Migrated 20230625051113 in 0.0s

We can now access our Repo from the Blog Context. Modify your lib/lani_blog/blog.ex file like this:

defmodule LaniBlog.Blog do
alias LaniBlog.Repo
alias LaniBlog.Blog.Post

def list_posts do
Repo.all(Post, order_by: [desc: :inserted_at])
end
end

We use the Repo to query the list of posts. The alias in the Context header allows us to define the module shortcuts and use them shorter. Then, test our api again:

$ curl --location --request GET 'http://localhost:4000/api/posts'

The response status is 200 with the body:

{
"data": []
}

Great work! You have successfully implemented the first API and we can get a successful response. The response data is empty though, because our database has no data. Next up, we will implement the feature to create new posts and then you can see the data from the get post list API.

--

--