I can’t remember the last time I dabbled with Phoenix. Whenever it was, I was messing with channels and all the real time goodness.
This year at Elixir Conf I heard about a lot of new changes in Phoenix from Chris McCord and sure enough, when I sat down to create a Phoenix JSON API, most of the tutorials and examples I found were outdated.
Phoenix 1.3 was released in July and with it came Contexts. They land with an admittedly bad name (says Chris), but provide a beneficial grouping of related functionality and a good foundation for application growth. The addition of Contexts also means one extra parameter for the mix phx.gen.json generator to specify its grouping.
Be sure to have the dependencies for creating a new Phoenix app installed. If you’re not sure what that means, take a look at the guides for installation.
Once Phoenix is installed you can verify the version with mix.
> mix phx.new --version
Phoenix v1.3.0
mix phx.new also happens to be the command to create a new Phoenix project. Brunch.io is included in a default Phoenix app for compiling assets and uses npm, but when developing strictly a JSON API there are no assets and thus, brunch can be omitted. There is also no visual piece and the html layer can also be omitted.
> mix phx.new --no-brunch --no-html {api_name}
This will kick off creation of the API and prompt to download dependencies. Once completed there are just a few minor “clean up” items to make it truly an API-only Phoenix application.
lib/{api_name}_web/channels directory
> rm -rf lib/{api_name}_web/channels
test/{api_name}_web/channels directory
> rm -rf test/{api_name}_web/channels
lib/{api_name}_web/endpoint.ex file, remove this line:
socket "/socket", {ApiName}Web.UserSocket
lib/{api_name}_web/endpoint.ex file, remove this line:
only: ~w(css fonts images js favicon.ico robots.txt)
and be sure to remove the , on the preceding line.
lib/{api_name}_web.ex file, remove these lines:
def channel do
quote do
use Phoenix.Channel
import TossWeb.Gettext
end
end
Inside of config/dev.exs the settings for the database connection in development can be found. Be sure the username and password properties reflect your environment.
username: "postgres",
password: "",
The next step is to create the database for this API: run mix ecto.create.
This example will provide blog post records, so we will need a Post module. To create the module the mix phx.gen.json generator will be used. It requires the Context, the singular module name, and the plural table name. Everything after the initial 3 arguments will be attributes for the model. For this example Post will be grouped inside the Blog context, and we will start with just title and an is_published boolean flag.
As mentioned earlier, having context now allows us to group logical pieces of the API together. If later a Comment module was added to this API, for example, it could also be grouped within the Blog context. I’m sure you can think of other useful progressions of this grouping as the API grows.
> mix phx.gen.json Blog Post posts title:string is_published:boolean
Remove the functions in the newly generated post_controller that will not be used for this example: the create, update, and delete methods.
Removing these methods means we can also remove the alias for the Post module on line 5.
alias {ApiName}.Blog.Post
Be sure to run mix ecto.migrate.
The API now has the changes in the database and the Post module and controller. In order to now hit the controller, the routes need to be added into the lib/{api_name}_web/router.ex file.
When writing an API I prefer to namespace resources into a specific API version. In order to do this, the scope "/api" block needs to be expanded a bit.
scope "/api", {ApiName}iWeb, as: :api do
pipe_through :api
scope "/v1", V1, as: :v1 do
resources "/posts", PostController, only: [:index, :show]
end
end
To confirm that the new routes exist as expected, run mix phx.routes and see
them listed out:
> mix phx.routes
v1_post_path GET /api/v1/posts {ApiName}Web.V1.PostController :index
v1_post_path GET /api/v1/posts/:id {ApiName}Web.V1.PostController :show
When looking closely at the routes that are listed above, you can see a naming difference. The listed controller name is {ApiName}Web.V1.PostController but opening the lib/{api_name}_web/controllers/posts_controller.ex shows a different module name: {ApiName}Web.PostController.
This will have to be renamed to match and to group controllers within the same API version, it can be contained within its own lib/{api_name}_web/controllers/v1 folder.
> mkdir v1
> mv posts_controller.ex v1/
And then rename the controller module to {ApiName}Web.V1.PostController.
A similar thing will have to happen for the corresponding view. Right now we don’t have to worry too much about the contents of the lib/{api_name}_web/views/post_view.ex because it will be replaced soon enough - but this file will have to also belong to a lib/{api_name}_web/views/v1 folder and the module renamed to {ApiName}Web.V1.PostView.
> mkdir v1
> mv posts_view.ex v1
And then rename the view module to {ApiName}Web.V1.PostView. In the View module, there also is an alias that needs to be updated on line 3 to reflect the new namespace:
alias {ApiName}Web.V1.PostView
Spin up the server with mix phx.server and using your preferred REST client try and hit https://localhost:4000/api/v1/posts. Make sure you have Content-Type: application/json in whatever REST client you happen to use.
You should receive an empty response.
{
"data": []
}
Seed data in a Phoenix app is straightforward and there is some foundation laid to create consistency. There is a file named priv/repo/seeds.ex which we can put our seed data for the database.
We can just write direct inserts to our database in here and then we can run it with mix run.
alias {ApiName}.Repo
alias {ApiName}.Blog.Post
Repo.insert!(%Post{title: "Getting started with Phoenix and JSON API", is_published: true})
Repo.insert!(%Post{title: "Next steps with Phoenix and JSON API", is_published: false})
This can be executed with mix run priv/repo/seeds.ex.
Now hit https://localhost:4000/api/v1/posts again and you should see the two records returned as JSON.
{
"data": [
{
"id": 1,
"is_published": true,
"title": "Getting started with Phoenix and JSON API"
},
{
"id": 2,
"is_published": false,
"title": "Next steps with Phoenix and JSON API"
}
]
}
This is all great and it’s almost a good beginning. All that needs to happen now is for the response to be in the JSON-API specification. This is made easier with the use of the [jaserializer][ja-serializer] package.
Dependencies for a Phoenix app get added to mix.exs in the defp deps do block. At the time of writing this, the newest version of jaserializer is 0.12.0 so be sure to confirm your versions are up to date.
Add a line in the defp deps do block of mix.exs to include this new dependency.
{:ja_serializer, "~> 0.12.0"}
Whenever a new dependency is added to an Elixir app, mix deps.get needs to be run.
json-api mime-typeWe need to configure the json-api mime-type to serialize JSON API. This can be done inside config/config.exs
config :phoenix, :format_encoders,
"json-api": Poison
config :mime, :types, %{
"application/vnd.api+json" => ["json-api"]
}
After modifying Plug, we have to recompile:
> mix deps.clean plug --build
> mix deps.get
And now we can add the json-api plug to the api pipeline defined in lib/{api_name}_web/router.ex. If you know for sure your API will only accept json-api then you can remove the existing json from the plug list.
pipeline :api do
plug :accepts, ["json-api"]
end
There are two different ways to configure the views in a Phoenix API to serialize with jaserializer. One is by adding a use statement to each individual View module:
use JaSerializer.PhoenixView
This is useful if you want to pick and choose when you will be serializing in JSON API or something else. If this is an evergreen API, though, chances are you plan on building everything out as a JSON API endpoint, so you’ll need this for each View.
We can add the use statement collectively to all View modules by adding it to the lib/{api_name}_web.ex file under the def view do block:
def view do
quote do
use Phoenix.View, root: "lib/{api_name}_web/templates",
namespace: {ApiName}Web
use JaSerializer.PhoenixView
...
Now the alteration to the lib/{api_name}_web/views/v1/post_view.ex can happen. Now that we are using jaserializer our views become a definition of what we want to serialize. Inside the view file we could define relationships or individual attributes. The Post model in this API is simple and just has two attributes but more info on how to serialize relationships can be found in the jaserializer github README. Update the View file to serialize the attributes for our model:
defmodule {ApiName}Web.V1.PostView do
use {ApiName}Web, :view
attributes [:title, :is_published]
end
Right now the controller is rendering index.json and show.json in the respective action handlers. This needs to be updated with the correct content type of json-api, and the posts and post attributes need to be replaced with data to correspond to the JSON API specification.
Update the index block:
render(conn, "index.json-api", data: posts)
Update the show block:
render(conn, "show.json-api", data: posts)
Now if we hit https://localhost:4000/api/v1/posts we will see the expected response for our inserted data returned in JSON API format!
{
"data": [
{
"attributes": {
"is-published": true,
"title": "Getting started with Phoenix and JSON API"
},
"id": "1",
"type": "post"
},
{
"attributes": {
"is-published": false,
"title": "Next steps with Phoenix and JSON API"
},
"id": "2",
"type": "post"
}
],
"jsonapi": {
"version": "1.0"
}
}
And similarly, if we travel to https://localhost:4000/api/v1/posts/1 our first record will be returned in JSON API format as well.
{
"data": {
"attributes": {
"is-published": true,
"title": "Getting started with Phoenix and JSON API"
},
"id": "1",
"type": "post"
},
"jsonapi": {
"version": "1.0"
}
}
Congratulations! You have your first JSON API with Phoenix!
Please feel free to kindly share your corrections, misinformations, or suggestions in the comments.