Elixir/Phoenix Implementation for Todo-Backend
Deployed to http://todobackend-phoenix.herokuapp.com/api/todos
Tested against Todo-Backend API tests
Here's a step-by-step guide to implementing an Elixir/Phoenix Todo-Backend deployed to Heroku.
$ mix phoenix.new todobackend
* creating todobackend/config/config.exs
* creating todobackend/config/dev.exs
* creating todobackend/config/prod.exs
* creating todobackend/config/prod.secret.exs
* creating todobackend/config/test.exs
* creating todobackend/lib/todobackend.ex
* creating todobackend/lib/todobackend/endpoint.ex
* creating todobackend/test/controllers/page_controller_test.exs
* creating todobackend/test/views/error_view_test.exs
* creating todobackend/test/views/page_view_test.exs
* creating todobackend/test/support/conn_case.ex
* creating todobackend/test/test_helper.exs
* creating todobackend/web/controllers/page_controller.ex
* creating todobackend/web/templates/layout/application.html.eex
* creating todobackend/web/templates/page/index.html.eex
* creating todobackend/web/views/error_view.ex
* creating todobackend/web/views/layout_view.ex
* creating todobackend/web/views/page_view.ex
* creating todobackend/web/router.ex
* creating todobackend/web/web.ex
* creating todobackend/mix.exs
* creating todobackend/README.md
* creating todobackend/lib/todobackend/repo.ex
* creating todobackend/test/support/model_case.ex
* creating todobackend/.gitignore
* creating todobackend/brunch-config.js
* creating todobackend/package.json
* creating todobackend/web/static/css/app.scss
* creating todobackend/web/static/js/app.js
* creating todobackend/web/static/vendor/phoenix.js
* creating todobackend/priv/static/images/phoenix.png
Install mix dependencies? [Yn] Y
* running mix deps.get
Install brunch.io dependencies? [Yn] Y
* running npm install
We are all set! Run your Phoenix application:
$ cd todobackend
$ mix phoenix.server
You can also run it inside IEx (Interactive Elixir) as:
$ iex -S mix phoenix.server
Let's go ahead and commit the base application
$ cd todobackend
$ git init
$ git add .
$ git commit -m "initial phoenix 0.12 application"
First we'll provision our Heroku application
$ heroku create --buildpack "https://github.com/HashNuke/heroku-buildpack-elixir.git" --app todobackend-phoenix
We'll rebuild our app for every dyno restart, for now, at least.
# Procfile
web: mix clean && mix phoenix.server
We'll set our app to use the database that the Heroku buildpack includes
# config/prod.secret.exs
config :todobackend, Todobackend.Repo,
adapter: Ecto.Adapters.Postgres,
url: {:system, "DATABASE_URL"},
size: 20 # The amount of database connections in the pool
Heroku has our app running behind a routing layer, so our app runs on a non-standard port and doesn't really know its hostname. Let's set those for when we generate full urls for the client.
# config/prod.exs
config :todobackend, Todobackend.Endpoint,
http: [port: {:system, "PORT"}],
url: [host: "todobackend-phoenix.herokuapp.com", port: 80],
cache_static_manifest: "priv/static/manifest.json"
We'll also create the digest versions of our assets now
$ mix phoenix.digest
Let's add and commit all the files. Since config/prod.secrets.exs
was initially included in .gitignore
, we'll need to tell git, that we do indeed want it versioned.
And then we'll ship what we have to Heroku
$ git add .
$ git add -f config/prod.secret.exs
$ git commit -m "prep for Heroku"
$ git push heroku master
$ heroku open
$ mix phoenix.gen.json Todo todos title:string order:integer completed:boolean
If you want to go ahead and run mix ecto.create
and mix ecto.migrate
for your local development database, you may.
We'll tell Heroku to in a few minutes
# web/router.ex
scope "/api", Todobackend do
pipe_through :api
resources "/todos", TodoController
end
$ git add .
$ git commit -m "add initial Todo model"
$ git push heroku master
$ heroku run mix ecto.create
$ heroku run mix ecto.migrate
Note: heroku run mix ecto.create
may return an error like ** (Mix) The database for Todobackend.Repo couldn't be created, reason given: Error: You must install at least one postgresql-client-<version> package.
That's ok. The command probably still worked and you should be able to proceed with heroku run mix ecto.migrate
.
If we run the Todo-Backend tests against our new service, they'll fail because we haven't enabled CORS yet.
# web/router.ex
def cors(conn, _opts) do
conn
|> put_resp_header("Access-Control-Allow-Origin", "*")
|> put_resp_header("Access-Control-Allow-Headers", "Content-Type")
|> put_resp_header("Access-Control-Allow-Methods", "GET,PUT,PATCH,OPTIONS,DELETE,POST")
end
# web/router.ex
pipeline :api do
plug :cors
plug :accepts, ["json"]
end
TodoBackend's CORS test will also require the OPTIONS
HTTP verb for the Todos
# web/router
scope "/api", Todobackend do
pipe_through :api
resources "/todos", TodoController
options "/todos", TodoController, :options
options "/todos/:id", TodoController, :options
end
# web/controllers/todo_controller.ex
def options(conn, _params) do
conn
|> send_resp(200, "GET,POST,DELETE,OPTIONS,PUT")
end
$ git add .
$ git commit -m "add CORS support"
$ git push heroku master
Yay! Our first TodoBackend test passes!
We don't need any of the parameters scrubbed for logging for this app, so let's remove it.
# web/controllers/todo_controller.ex
# plug :scrub_params, "todo" when action in [:create, :update]
plug :action
We also have a mismatch between what Phoenix expects by default and what the Todo Frontend is sending. By default Phoenix expects our Todo to look like the following.
{ "todo":
{ "order": 10,
"title": "blah"
}
}
The Todo Frontend is only sending that inner bit, so let's excise the outer bit from our controller.
# web/controllers/todo_controller.ex
def create(conn, todo_params) do
# ...
def update(conn, todo_params = %{"id" => id}) do
Similarly, for the response, by default Phoenix will nest our Todo model under data
like the following.
{ "data":
{ "id": 1
}
}
Let's edit the TodoView to remove the data
and to add more attributes other than id
.
# web/views/todo_view.ex
def render("index.json", %{todos: todos}) do
render_many(todos, "todo.json")
end
def render("show.json", %{todo: todo}) do
render_one(todo, "todo.json")
end
def render("todo.json", %{todo: todo}) do
%{id: todo.id,
title: todo.title,
order: todo.order,
completed: todo.completed,
url: todo_url(Todobackend.Endpoint, :show, todo),
}
end
We also want to specify that order
and completed
are optional fields.
# web/models/todo.ex
@required_fields ~w(title)
@optional_fields ~w(order completed)
$ git add .
$ git commit -m "match JSON serialization of Todo to structure of client's expectations"
$ git push heroku master
Whoa! All of a sudden 3 tests pass now!
Todo Frontend expects us to implement a delete all
if we receive DELETE /api/todos
. This functionality isn't part of the Phoenix resources
, so we'll need to add it.
# web/router.ex
scope "/api", Todobackend do
pipe_through :api
resources "/todos", TodoController
options "/todos", TodoController, :options
options "/todos/:id", TodoController, :options
delete "/todos", TodoController, :delete_all
end
# web/controllers/todo_controller.ex
def delete_all(conn, _params) do
Repo.delete_all(Todo)
todos = Repo.all(Todo)
render(conn, "index.json", todos: todos)
end
$ git add .
$ git commit -m "implement delete all"
$ git push heroku master
Holy Smokes! All the tests pass now. Congratulations!