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.
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 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.