8.9 KiB
Getting started with Ash and Phoenix
In this guide we will convert the sample app from the getting stated guide into a full blown service backed by PostgreSQL as a storage and a Json Web API.
For the web part of the application we will rely on the Phoenix framework as both frameworks are complementary. Keep in mind that using Phoenix is not a requirement, you could alternatively use Plug.
You can check out the completed application and source code in this repo.
Create Phoenix app
We create a simple Phoenix application and we remove some unnecessary parts,
also we are using --app
to rename the application so it matches the name from
the getting started guide.
mix phx.new my_app --no-html --no-webpack --no-gettext
Add dependencies and formatter
Now we need to add the dependencies, ash
and ash_postgres. To find out what the latest available version is you can use mix hex.info
:
mix hex.info ash_postgres
mix hex.info ash
Next modify the the .formatter
and mix.exs
files:
--- a/.formatter.exs
+++ b/.formatter.exs
@@ -1,4 +1,13 @@
[
+ import_deps: [
+ :ash_json_api,
+ :ash_postgres
+ ],
--- b/mix.exs
+++ b/mix.exs
@@ -33,6 +33,8 @@ defmodule MyAppPhx.MixProject do
# Type `mix help deps` for examples and options.
defp deps do
[
+ {:ash_postgres, "~> 0.25.5"},
+ {:ash, "~> 1.24"}
Next, modify MyApp.Repo
to use AshPostgres.Repo
instead of Ecto.Repo
.
defmodule MyApp.Repo do
use AshPostgres.Repo,
otp_app: :my_app
end
Make sure you can connect to Postgres by verifying that the credentials in config/dev.exs
are correct and create the database by running:
mix ecto.create
* The database for MyApp.Repo has been created
To configure Phoenix to support the jsonapi content type, add the following configuration to config/config.exs
:
--- a/config/config.exs
+++ b/config/config.exs
@@ -10,6 +10,10 @@ use Mix.Config
config :my_app,
ecto_repos: [MyApp.Repo]
+config :mime, :types, %{
+ "application/vnd.api+json" => ["json"]
+}
+
Reuse the files from the Getting Started guide
Copy the lib/my_app/api.ex
, lib/my_app/resources/tweet.ex
and lib/my_app/resources/user.ex
from the Getting Started
sample app into this project in the same path.
Switch data layer to Postgres
We can now proceed to switch the data layer from ETS
to PostgreSQL
simply by changing the data_layer
to
AshPostgres.DataLayer
in our resources
and adding the table name and our repo. In this case we will
use the default repo created by Phoenix.
--- a/my_app_phx/lib/my_app/resources/tweet.ex
+++ b/my_app_phx/lib/my_app/resources/tweet.ex
@@ -1,6 +1,11 @@
# in my_app_phx/lib/my_app/resources/tweet.ex
defmodule MyApp.Tweet do
- use Ash.Resource, data_layer: Ash.DataLayer.Ets
+ use Ash.Resource, data_layer: AshPostgres.DataLayer
+
+ postgres do
+ table "tweets"
+ repo MyApp.Repo
+ end
--- a/my_app_phx/lib/my_app/resources/user.ex
+++ b/my_app_phx/lib/my_app/resources/user.ex
@@ -1,6 +1,11 @@
# in my_app_phx/lib/my_app/resources/user.ex
defmodule MyApp.User do
- use Ash.Resource, data_layer: Ash.DataLayer.Ets
+ use Ash.Resource, data_layer: AshPostgres.DataLayer
+
+ postgres do
+ table "users"
+ repo MyApp.Repo
+ end
Now you can tell Ash to generate the migrations from your API:
mix ash_postgres.generate_migrations --apis MyApp.Api
* creating priv/repo/migrations/20201120214857_migrate_resources1.exs
and run the ecto migration to generate the tables:
run mix ecto.migrate
23:23:46.067 [info] == Running 20201120222312 MyApp.Repo.Migrations.MigrateResources1.up/0 forward
23:23:46.070 [info] create table users
23:23:46.076 [info] create table tweets
23:23:46.090 [info] == Migrated 20201120222312 in 0.0s
Test PostgreSQL integration
Start IEx with iex -S mix phx.server
and lets run the same test
we ran in the initial my_app
. You will now see that SQL statements
are being executed and data is now stored in your PostgreSQL database.
iex(1)> {:ok, user} = Ash.Changeset.new(MyApp.User, %{email: "ash.man@enguento.com"}) |> MyApp.Api.create()
[debug] QUERY OK db=1.2ms idle=1432.0ms
begin []
[debug] QUERY OK db=0.4ms
INSERT INTO "users" ("email","id") VALUES ($1,$2) ["ash.man@enguento.com", <<72, 22
6, 94, 187, 145, 81, 66, 25, 183, 79, 59, 199, 93, 88, 32, 243>>]
[debug] QUERY OK db=0.3ms
commit []
{:ok,
%MyApp.User{
__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
__metadata__: %{},
aggregates: %{},
calculations: %{},
email: "ash.man@enguento.com",
id: "48e25ebb-9151-4219-b74f-3bc75d5820f3",
tweets: #Ash.NotLoaded<:relationship>
}}
iex(2)> MyApp.Tweet |> Ash.Changeset.new(%{body: "ashy slashy"}) |> Ash.Changeset.r
eplace_relationship(:user, user) |> MyApp.Api.create()
[debug] QUERY OK db=0.1ms idle=1197.5ms
begin []
[debug] QUERY OK db=2.2ms
INSERT INTO "tweets" ("body","inserted_at","id","public","updated_at","user_id") VAL
UES ($1,$2,$3,$4,$5,$6) ["ashy slashy", ~U[2020-11-22 21:15:33Z], <<163, 22, 225, 4
3, 217, 10, 67, 242, 152, 149, 197, 133, 253, 154, 244, 95>>, false, ~U[2020-11-22
21:15:33Z], <<72, 226, 94, 187, 145, 81, 66, 25, 183, 79, 59, 199, 93, 88, 32, 243>
>]
[debug] QUERY OK db=0.3ms
commit []
{:ok,
%MyApp.Tweet{
__meta__: #Ecto.Schema.Metadata<:loaded, "tweets">,
__metadata__: %{},
aggregates: %{},
body: "ashy slashy",
calculations: %{},
inserted_at: ~U[2020-11-22 21:15:33Z],
id: "a316e12b-d90a-43f2-9895-c585fd9af45f",
public: false,
updated_at: ~U[2020-11-22 21:15:33Z],
user: %MyApp.User{
__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
__metadata__: %{},
aggregates: %{},
calculations: %{},
email: "ash.man@enguento.com",
id: "48e25ebb-9151-4219-b74f-3bc75d5820f3",
tweets: #Ash.NotLoaded<:relationship>
},
user_id: "48e25ebb-9151-4219-b74f-3bc75d5820f3"
}}
Exposing the API with a JSON API
First we need to add the extension dependency for ash_json_api.
mix hex.info ash_json_api
Add it to your dependencies and don't forget to run mix deps.get
:
--- a/mix.exs
+++ b/mix.exs
@@ -33,6 +33,7 @@ defmodule MyApp.MixProject do
# Type `mix help deps` for examples and options.
defp deps do
[
+ {:ash_json_api, "~> 0.24.1"},
{:ash_postgres, "~> 0.25.5"},
{:ash, "~> 1.24"},
{:phoenix, "~> 1.5.6"},
Create a router module for your Api
defmodule MyApp.MyApi.Router do
# The registry must be explicitly provided here
use AshJsonApi.Api.Router, api: Api, registry: Registry
end
We can proceed to add a route in the Phoenix router to forward requests
to our Ash API. To do so we use AshJsonApi.forward/3
as shown in
lib/my_app_web/router.ex
:
--- a/lib/my_app_web/router.ex
+++ b/lib/my_app_web/router.ex
@@ -1,12 +1,14 @@
defmodule MyAppWeb.Router do
- scope "/api", MyAppWeb do
+ scope "/api" do
pipe_through :api
+ forward("/", MyApp.MyApi.Router)
end
After that, all we have to do is configure our resources for the JSON:API.
In this guide we will only expose an API for the user
resource, exposing the tweet
resource is left as an exercise for the reader.
We need to add the extension to our resource and define a mapping between the REST verbs and our internal API actions.
--- a/lib/my_app/resources/user.ex
+++ b/lib/my_app/resources/user.ex
@@ -1,7 +1,24 @@
# in lib/my_app/resources/user.ex
defmodule MyApp.User do
- use Ash.Resource, data_layer: AshPostgres.DataLayer
+ use Ash.Resource, data_layer: AshPostgres.DataLayer,
+ extensions: [
+ AshJsonApi.Resource
+ ]
+ json_api do
+ type "user"
+
+ routes do
+ base "/users"
+
+ get :read
+ index :read
+ post :create
+ patch :update
+ delete :destroy
+ end
+ end
+
Test Web Json API
Fire up IEx with iex -S mix phx.server
and curl the API:
curl -s --request GET --url 'http://localhost:4000/api/users' | jq
{
"data": [
{
"attributes": {
"email": "ash.man@enguento.com"
},
"id": "46b60ec8-5b0f-461d-95ab-bcc5169ff831",
"links": {},
"meta": {},
"relationships": {},
"type": "user"
},
{
"attributes": {
"email": "ash.man@enguento.com"
},
"id": "cd84148a-4af4-4f9f-952f-9daa28946e01",
"links": {},
"meta": {},
"relationships": {},
"type": "user"
},
{
"attributes": {
"email": "ash.man@enguento.com"
},
"id": "48e25ebb-9151-4219-b74f-3bc75d5820f3",
"links": {},
"meta": {},
"relationships": {},
"type": "user"
}
],
"jsonapi": {
"version": "1.0"
},
"links": {
"self": "http://localhost:4000/api/users"
}
}