https://github.com/tyalt1/petal_stack_tutorial
Teaching examples for PETAL stack (Phoenix, Elixir, Tailwind, Ash, LiveView)
https://github.com/tyalt1/petal_stack_tutorial
elixir elixir-examples elixir-lang elixir-language elixir-phoenix elixir-phoenix-framework elixir-programming-language liveview phoenix tutorial tutorials
Last synced: 2 months ago
JSON representation
Teaching examples for PETAL stack (Phoenix, Elixir, Tailwind, Ash, LiveView)
- Host: GitHub
- URL: https://github.com/tyalt1/petal_stack_tutorial
- Owner: tyalt1
- Created: 2024-06-02T19:31:30.000Z (12 months ago)
- Default Branch: main
- Last Pushed: 2024-06-05T19:43:26.000Z (12 months ago)
- Last Synced: 2025-01-18T00:26:13.927Z (4 months ago)
- Topics: elixir, elixir-examples, elixir-lang, elixir-language, elixir-phoenix, elixir-phoenix-framework, elixir-programming-language, liveview, phoenix, tutorial, tutorials
- Language: Elixir
- Homepage:
- Size: 146 KB
- Stars: 2
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# Petal Stack Tutorial
This is tutorial repo for the learning the PETAL stack.
Disclaimer: This stack does not use Alpine.js. The A in the PETAL stack usually refers to Alpine.js however Alpine.js is typically no longer used in Phoenix applications because of additional functionallity added to LiveView, specifically in the `Phoenix.LiveView.JS` module. This stack adds the use of the Ash framework, as it helps declare models quickly and derive functionality with minimal code. And because it starts with an A.
## PETAL Stack
Letter | Name/Link | Description
------|------|------
P | [Phoenix](https://www.phoenixframework.org/) | Server-sided web framework built in Elixir.
E | [Elixir](https://hexdocs.pm/elixir/introduction.html) | Functional, concurrent, high-level programming language that runs on the Erlang (BEAM) virtual machine.
T | [Tailwind](https://tailwindcss.com/) | Utility-first CSS framework.
A | [Ash](https://ash-hq.org/) | Declarative model framework for Elixir.
L | [LiveView](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html) | Library for creating UIs in declarative HTML and updated via websocket events.## Why the PETAL stack?
The PETAL stack is scalable and productive.
### Examples of scalability
Elixir runs on the Erlang VM which has many examples of scalability
- The company that created Erlang boasts only 5.2 minutes of downtime a year in their systems running Erlang.
- Whatsup was able to maintain 2 million TCP connections on a single server using 40% CPU of a server from 2012 using Erlang. [Source](https://blog.whatsapp.com/1-million-is-so-2011)
- A developer rewrote an AWS microservice application in Elixir. The resulting application was faster and cost less to run in the cloud. [Source](https://medium.com/coryodaniel/from-erverless-to-elixir-48752db4d7bc)
- Discord has many examples of using Elixir as thier primay language to scale to a large amount of users. [Source](https://discord.com/blog/using-rust-to-scale-elixir-for-11-million-concurrent-users)### Examples of productivity
- The only dependancy is Elixir. Phoenix is built on Elixir, Phoenix comes with a utility to run Tailwind by default, and the other parts of the stack are Elixir packages.
- Elixir features a macro system so that users can do more with less code.
- Phoenix is a batteries included framework that's designed to get started as fast as possible.
- [Source: Build a real-time twitter clone in 15 minutes](https://www.phoenixframework.org/blog/build-a-real-time-twitter-clone-in-15-minutes-with-live-view-and-phoenix-1-5)
- Including Tailwind means your project starts with over 37,000 CSS classes already defined.
- LiveView handles the connection to between backend and frontend so you don't need to maintain a backend REST API or a Javascript client for your frontend.
- With LiveView developers can write reactive single-page frontends in HTML and Elixir. (No Javascript)
- Ash lets you define a model and then with minimal code derive database persistance, code wrappers, REST API, GraphQL API, and more.## PETAL Stack Q/A
### Q: Do I have to use Ash?
No. Ash is not needed. But I recommend Ash because it puts a lot of complexity up front and pays dividends later. With Ecto, the default database library for Phoenix, you declare the schema and only derive Database persistance and some validation. With Ash you declare the schema and get the following:
- Resource and Domain level functions
- Database persistance (Postgres, SQLite3)
- alternative persistance (in memory, ETS table, Mnesia table, CSV file)
- Form validation
- JSON API mapping
- GraphQL API mapping
- and more with a robust extension system ...## Commit References
This is a summary of notable commits, and sections that go into that code in greater detail.
Section # | Commit | Description
------|------|------
1 | [`12eb0e4`](https://github.com/tyalt1/petal_stack_tutorial/commit/12eb0e4dea49628c4dcfa6df3e7c6b6b4c622715) | All files generated by running `mix phx.new petal_stack_tutorial`
2 | [`d2a1733`](https://github.com/tyalt1/petal_stack_tutorial/commit/d2a17336ceb58cb769fdd052cf838e73a344b453) | LiveView Example
3 | [`0658c29`](https://github.com/tyalt1/petal_stack_tutorial/commit/0658c2911d7ca8b52ebeabcc8e930d4336edbdf7) | Ash.Resource Example
4 | [`da74838`](https://github.com/tyalt1/petal_stack_tutorial/commit/da74838c458e23868d344bcfce907bc964eb5d22) | AshAuthentication Example
5 | [`1d0338f`](https://github.com/tyalt1/petal_stack_tutorial/commit/1d0338f287dcdbb68104174f895400acc9a33f32) | AshJsonApi Example
6 | [`5ca4a08`](https://github.com/tyalt1/petal_stack_tutorial/commit/5ca4a088e00289b6cb8334b04f3244a55e3c4fd2) | AshGraphql Example## Tutorial
### Table of Contents
1. [Setup Elixir and Phoenix](#section-1-setup-elixir-and-phoenix)
2. [Intro to LiveView](#section-2-intro-to-liveview)
3. [Intro to Ash](#section-3-intro-to-ash)
4. [Users with Ash Authentication](#section-4-users-with-ash-authentication)
5. [REST API with with Ash JSON API](#section-5-rest-api-with-ash-json-api)
6. [GraphQL API with Ash GraphQL](#section-6-graphql-api-with-ash-graphql)### Section 1: Setup Elixir and Phoenix
Install Elixir
```bash
# install asdf (a tool for managing runtimes)
# Getting started: https://asdf-vm.com/guide/getting-started.html# install erlang and elixir plugins
asdf plugin add erlang
asdf plugin add elixir# install specific version for plugin
asdf install $PLUGIN $VERSION# or if you have a .tool-versions file
# install from .tool-versions file
asdf install
```Create a new Phoenix Project
```bash
# install Hex (Elixir package manager, like npm or pip)
mix local.hex# update hex and install Phoenix creation script
mix archive.install hex phx_new# create a new phoenix app
mix phx.new my_app# set elixir versions, this will create a .tool-versions file
asdf local erlang 26.2.4
asdf local elixir 1.16.2-otp-26
```Run the server
```bash
# run Postgres via Docker
docker run -d \
--name phx_db \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_HOST_AUTH_METHOD=trust \
-p 5432:5432 \
postgres# install dependencies, run migrations
mix setup# run the server
mix phx.server# run the server and a console
iex -S mix phx.server
```### Section 2: Intro to LiveView
We're going to implement a simple counter.
In newer version of Pheonix, LiveView comes included so there is no new dependency needed.
The file `lib/petal_stack_tutorial_web/router.ex` contains the routing logic to associate controllers with verb-path pairs. Add the following line to add a new route that will point to our live view.
```elixir
live "/counter", Counter
```Create a new file `lib/petal_stack_tutorial_web/live/counter.ex` with the following content
```elixir
defmodule PetalStackTutorialWeb.Counter do
use PetalStackTutorialWeb, :live_view# more code here
end
```Note: `use PetalStackTutorialWeb, :live_view` inserts `use Phoenix.LiveView` which makes this a live view.
We need to implement minimum 3 callbacks.
- `mount/3` which is called when the websocket is mounted, and returns the inital state.
- `render/1` which takes the state and returns HEEx (HTML Embedded Elixir) template.
- `handle_update/3` which handles events and updates the state.Implment the `mount/3` function.
```elixir
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, counter: 0)}
end
```Note: Add the `@impl true` to explicitly say you're implementing a callback.
`mount/3` must return a tuple with `:ok` being first and the socket object being the second. Inside the socket object is a hashmap called `assigns` which we can add state to. The helper function `assign` takes a socket and a key-value and returns a socket object with the updated state. There is other information about the websocket connection in the socket object but we will only care about our own custom state for this exercise.
Implment the `render/1` function.
```elixir
attr :click, :string, required: true
attr :debounce, :integer, default: 20
slot :inner_blockdefp my_button(assigns) do
~H"""
<%= render_slot(@inner_block) %>
"""
endattr :counter, :integer, required: true
@impl true
def render(assigns) do
~H"""
<%= @counter %>
<.my_button click="inc">+
<.my_button click="dec">-
"""
end
```Note: The `~H` sigil is a macro that turns the string into a HEEx template.
Note: The `@` is a macro within a HEEx template that accesses the assigns map. `@counter` is equivalent to `assigns[:counter]`.
Note: When displaying a Elixir value in a HEEx template, use `<%= ... %>` for values in HTML and `{ ... }` for values as attributes.
We implement the `render/1` function that take the assigns map in the socket object. It returns an HEEx template. The template consists of a span to display the value of the counter, and two buttons to increment and decrement the counter. The two buttons have some similar functionality so we can implment another function that is private and returns the button. The `attr` and `slot` macros are helpers for declaring values in the assigns map. A `attr` can be a value that is required or have a default value. You will get a compiler error if a required `attr` is not given. A `slot` is nested HTML.
After we implmented this function, we can start the server and open http://localhost:4000/counter in out browser.
We see the following logs.
```
[info] GET /counter
[debug] Processing with PetalStackTutorialWeb.Counter.Elixir.PetalStackTutorialWeb.Counter/2
Parameters: %{}
Pipelines: [:browser]
[info] Sent 200 in 28ms
[info] CONNECTED TO Phoenix.LiveView.Socket in 12µs
Transport: :websocket
Serializer: Phoenix.Socket.V2.JSONSerializer
Parameters: %{"_csrf_token" => "Bwg5ATcfGAQMIHJEPC5OGzYDIUYwcHlXcypQuqhSgj6teC7IpuU7fEL3", "_live_referer" => "undefined", "_mounts" => "0", "_track_static" => %{"0" => "http://localhost:4000/assets/app.css", "1" => "http://localhost:4000/assets/app.js"}, "vsn" => "2.0.0"}
[debug] MOUNT PetalStackTutorialWeb.Counter
Parameters: %{}
Session: %{"_csrf_token" => "dqIPBnpWkJD0YmyRFvtqV55d"}
[debug] Replied in 87µs
```Here we see the following
1. The browser makes a `GET` to `/counter`
2. The router routes to the `Counter` live view.
3. A response is made in 28ms, which sends the LiveView response.
4. That response sends another request to establish a websocket connection.
5. Connect in 12µs
6. The `mount/3` callback is called to initialize the state.
7. The `render/1` callback is called rendering the HTML. This reply is sent in 87µs.The web page displays "0+-"
Recall we set the `phx-click` and `phx-debounce` attributes on the buttons, which are specific Phoenix LiveView attributes. `phx-click` for the + button is set to the string "inc", which means when that element is clicked that string will be sent via the websocket as an event. The `phx-debounce` attribute is set to 20, which is how many milliseconds all events for this element are debounced for. When we click the + we see the following error.
```
[error] GenServer #PID<0.804.0> terminating
** (UndefinedFunctionError) function PetalStackTutorialWeb.Counter.handle_event/3 is undefined or private
```What happens when we click the +
1. The "inc" event is sent via the websocket.
2. We try to call `handle_event/3` and get a function not defined error.
3. The Elixir process dies due to a unhandled exception.
4. The process is restarted by a supervisor and reconnects to our websocket.You will see a small red flash alert telling you the page loses connection for a moment, then disappear when the connection is reestablished.
Let's implment `handle_event/3`
```elixir
@impl true
def handle_event("inc", _params, socket) do
{:noreply, update(socket, :counter, fn x -> x+1 end)}
end@impl true
def handle_event("dec", _params, socket) do
f = fn
x when x <= 0 -> 0
x -> x - 1
end
{:noreply, update(socket, :counter, f)}
end
```We use pattern matching to implment the function across 2 seperate clauses. Here if we were to emit an event besides "inc" or "dec" from our frontend we'd get a function undefined exception. Optionally we could add a thrid catch-all clause to log the result. In both callbacks we use the `update` helper function to take the socket, key of value we want to change, and a function that will transform the value. We use pattern matching to implment the callback so that it can't decrement below 0.
Now when we click the + button we see the log and the counter update.
```
[debug] HANDLE EVENT "inc" in PetalStackTutorialWeb.Counter
Parameters: %{"value" => ""}
[debug] Replied in 413µs
```That is mostly everything you need to get started with LiveView but not all the functionality. Below are notable examples:
- [`Phoenix.LiveComponent`](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveComponent.html) which are components that maintain their own state, handle their own events, and cant be embedded in a LiveView.
- Handle events locally with [`Phoenix.LiveView.JS`](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.JS.html)
- [Forms](https://hexdocs.pm/phoenix_live_view/form-bindings.html)
- Implement `handle_info/2` to handle server-side events sent from other processes, for real-time updates to the UI.
- [File uploads](https://hexdocs.pm/phoenix_live_view/uploads.html)
- Lazy-Evaluation style [streaming values](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#stream/4)### Section 3: Intro to Ash
Ash is used to declare models and derive other functionality from those declarations. A declaration is an `Ash.Resource`. All `Ash.Resource`s are inside an `Ash.Domain`.
We'll follow the [Ash Phoenix tutorial](https://ash-hq.org/docs/guides/ash_phoenix/latest/tutorials/getting-started-with-ash-and-phoenix) for creating a Blog.
#### Ash Boilerplate
Ash is the only element of the PETAL Stack that does not come with Elixir or Phoenix. Install Ash by adding the following and run `mix deps.get`
```elixir
defp deps do
[
{:ash, "~> 3.0"},
{:ash_phoenix, "~> 2.0"},
{:ash_postgres, "~> 2.0"},
...
]
end
```Note: `AshPostgres` is built on top of `Ecto`, a database library for Elixir. `Ecto` is what Phoenix uses by default for communicating with the database.
Optionally, make the changes to `.formatter.exs`
```elixir
[
import_deps: [..., :ash, :ash_phoenix, :ash_postgres],
...
]
```Change the file `lib/petal_stack_tutorial/repo.ex` to contain this.
```elixir
defmodule PetalStackTutorial.Repo do
use AshPostgres.Repo,
otp_app: :petal_stack_tutorial# Installs extensions that ash commonly uses
def installed_extensions do
["ash-functions", "uuid-ossp", "citext"]
end
end
```Note: `otp_app` must be set to the application name.
Remove references to `ecto` in your `mix.exs` `aliases`. Replace `ecto.setup` with `ash.setup`. It should look something like this:
```elixir
defp aliases do
[
setup: ["deps.get", "ash.setup", "assets.setup", "assets.build"],
...
]
end
```Make the change to the `config/config.exs` files. We need to list all domains. For now we'll add the Blog domain we're adding next.
```elixir
config :petal_stack_tutorial,
ash_domains: [PetalStackTutorial.Blog]
```Create the file `lib/petal_stack_tutorial/blog/blog.ex` which will be our `Blog` domain. This domain will contain the Post resource we're adding next.
```elixir
defmodule PetalStackTutorial.Blog do
use Ash.Domainresources do
resource PetalStackTutorial.Blog.Post
end
end
```Now we can create a Post resource.
```elixir
defmodule PetalStackTutorial.Blog.Post do
use Ash.Resource,
domain: PetalStackTutorial.Blog,
data_layer: AshPostgres.DataLayer,
extensions: []postgres do
table "posts"
repo PetalStackTutorial.Repo
endattributes do
uuid_primary_key :id
create_timestamp :created_at
update_timestamp :updated_atattribute :title, :string do
allow_nil? false
endattribute :content, :string
endactions do
defaults [:read, :destroy]create :create do
accept [:title]
endupdate :update do
accept [:content]
endread :get do
argument :id, :uuid, allow_nil?: false
get? true # read will only return 1 value, not a list
filter expr(id == ^arg(:id))
end
end
end
```The syntax for `Ash.Resource` and `Ash.Domain` should look odd. This is not Elixir, this is a DSL defined using Elixir macros. Refer to the [Ash DSL documentation](https://hexdocs.pm/ash/3.0.9/dsl-ash-resource.html) for more info.
#### Deep dive into `Ash.Resource`
Review of what we've done so far
1. We added Ash, AshPhoenix, and AshPostgres as dependancies
2. Added formatting rules for `mix format`
3. Refactored `PetalStackTutorial.Repo` to use `AshPostgres` instead of `Ecto`.
4. Added `ash.setup` to our `setup` mix alias.
5. Add list of domains to our config file, include the Blog domain.
6. Create the Blog domain, which contains the Post resource.
7. Created the Post resource.The first 4 steps were boiler plate we don't need to touch again. The config file only needs to be updated if we create a new doamin. The domain was made to hold the resource. There isn't much to do in the domain yet. Let's look at the Post resource.
```elixir
use Ash.Resource,
domain: PetalStackTutorial.Blog,
data_layer: AshPostgres.DataLayer,
extensions: []
```This declares the module to be a `Ash.Resource`. `domain` is the domain module. We've already defined the Blog module. `data_layer` is the module that defines how this module will be persisted. Ash comes with data layers that saves these models in memory, in ETS, and more. The `AshPostgres.DataLayer` is a data layer we installed that writes the value to a Postgres database. `extensions` can be used to add more blocks than what Ash comes with. These can be used to add the `json_api` block for generating REST APIs or the `graphql` block for generating GraphQL APIs. For now we will have no extensions.
```elixir
postgres do
table "posts"
repo PetalStackTutorial.Repo
end
```This block declares the name of the table and what `Repo` module is used to read/write to/from the database.
```elixir
attributes do
uuid_primary_key :id
create_timestamp :created_at
update_timestamp :updated_atattribute :title, :string do
allow_nil? false
endattribute :content, :string
endactions do
defaults [:read, :destroy]create :create do
accept [:title, :content]
endupdate :update do
accept [:content] # only edit content, not title
endread :get do
argument :id, :uuid, allow_nil?: false
get? true # read will only return 1 value, not a list
filter expr(id == ^arg(:id))
end
end
```These are the two blocks every `Ash.Resource` will have. `attributes` is the data and `actions` are the operations and workflows it supports.
Post has the UUID primary key that is auto-generated and included. Post also has the created and updated timestamps that are auto-updated. Finally Post includes two standard attributes: title and content. The title attribute has an additional block include to specify it can not be nil. This validation will be performed on all write actions.
Developers can write CRUD operations in their sleep. Ash writes them for you. `defaults` is a list of default actions added to the resource. We explicitly have a `create` and `update` action so we can state what attributes we can accpet. In `update` we can change the content of the post, but not the title. We can pass both in `create`. We also include another `read` function called `get`. The default `read` function returns a list of all elements, so we also add an extra `read` action called `get` that returns a Post for the given ID.
We need to do one more thing before we run our code. Run the following to generate and run migrations. We'll use "blog_post" as a name of the migrations but it could be anything.
```
mix ash.codegen blog_post
mix ash.setup
```#### Using `Ash.Resource` in code
Now we can run the following in the console:
```elixir
alias PetalStackTutorial.Blog.Post# create post
new_post = Post |> Ash.Changeset.for_create(:create, %{title: "hello world"}) |> Ash.create!()# read all posts
Post |> Ash.Query.for_read(:read) |> Ash.read!()# get single post by id
Post |> Ash.Query.for_read(:get, %{id: new_post.id}) |> Ash.read_one!()
# OR
Post |> Ash.get!(new_post.id)# update post
updated_post = new_post |> Ash.Changeset.for_update(:update, %{content: "hello to you too!"}) |> Ash.update!()# delete post
new_post |> Ash.Changeset.for_destroy(:destroy) |> Ash.destroy!()
```Here we can change the model with `Ash.Changeset` and read it with `Ash.Query`. We create a `Ash.Changeset` and `Ash.Query` with the name of the module, the name of the action, and additional options like new fields or filtering criteria. We then pass this to the correct `Ash` function to run the action. All the callbacks are either implented by Ash or derived from the `attributes` and `actions` blocks. All of these actions are reading and writing values to/from the given Data Layer, in this case Postgres.
This is a lot of code. We can define wrappers with the `code_interface` block in the resource. However for now we'll only define them as part of the domain. Change the Blog doamin to now contain.
```elixir
defmodule PetalStackTutorial.Blog do
use Ash.Domainresources do
resource PetalStackTutorial.Blog.Post do
define :create_post, action: :create
define :list_posts, action: :read
define :update_post, action: :update
define :destroy_post, action: :destroy
define :get_post, action: :get, args: [:id]
end
end
end
```The line `define :create_post, action: :create` defines a function called `create_post` that will run the `create` action for the `Post` resource. The other defines work similarly.
Now we can use the Blog domain functions to run the actions of Post.
```elixir
alias PetalStackTutorial.Blog# create post
new_post = Blog.create_post!(%{title: "hello world"})# read post
Blog.list_posts!()# get post by id
Blog.get_post!(new_post.id)# update post
updated_post = Blog.update_post!(new_post, %{content: "hello to you too!"})# delete post
Blog.destroy_post!(updated_post)
```The implementation of these actions are only defined once in the `actions` block of the Post resource. This lays the foundation for deriving more functionality from these actions. We could also create other resources and domains.
In further sections I'll discuss
- Integrating Ash models with LiveView forms
- User and Login with [`AshAuthentication`](https://hexdocs.pm/ash_authentication_phoenix/get-started.html)
- REST API with [`AshJsonApi`](https://ash-hq.org/docs/guides/ash_json_api/latest/tutorials/getting-started-with-ash-json-api)
- GraphQL API with [`AshGraphql`](https://ash-hq.org/docs/guides/ash_graphql/latest/tutorials/getting-started-with-graphql)### Section 4: Users with Ash Authentication
I followed [this tutorial](https://hexdocs.pm/ash_authentication_phoenix/get-started.html) for adding users to a Phoenix app with Ash Authentication and Ash Phoenix Authentication. I did not write additional code.
### Section 5: REST API with Ash JSON API
We can add a REST API for the Blog domain we added earlier.
I followed [this getting started guide](https://ash-hq.org/docs/guides/ash_json_api/latest/tutorials/getting-started-with-ash-json-api) for adding `AshJsonApi`.
Add the `ash_json_api` dependency:
```elixir
defp deps do
[
# ...
{:ash_json_api, "~> 1.0"},
{:open_api_spex, "~> 3.16"}, # <- Optional, used later
# ...
]
end
```Add to `.formatter.exs`:
```elixir
[
import_deps: [
# ...
:ash_json_api
],
#...
]
```Add `AshJsonApi.Domain` extension to the domain
```elixir
defmodule PetalStackTutorial.Blog do
use Ash.Domain, extensions: [
AshJsonApi.Domain # <-- add this
]
# ...
end
```You can add a `json_api` block in the domain to declare routes, but for now we'll add them to the resource.
Make the following changes to the Post resource, including adding the extension:
```elixir
defmodule PetalStackTutorial.Blog.Post do
use Ash.Resource,
domain: PetalStackTutorial.Blog,
data_layer: AshPostgres.DataLayer,
extensions: [
AshJsonApi.Resource # <-- add this
]attributes do
# ...
attribute :title, :string do
public? true # <-- add this
allow_nil? false
end
# ...
endjson_api do
type "post"routes do
base "/posts"get :read
index :read
post :create
patch :update
delete :destroy
end
end
end
```And that's all you need to add to your model.
1. Add the `AshJsonApi.Resource` extention.
2. We made the title attribute public. (Note: the content attribute is implicilty private, meaning it will not show in responses)
3. Add the `json_api` block which declares a base route, and which HTTP verbs trigger which actions.Now we need to create a router for our API and forward to it from our main router:
```elixir
defmodule PetalStackTutorialWeb.Router do
use PetalStackTutorialWeb, :router
# ...scope "/api/json" do
pipe_through :apiforward "/", PetalStackTutorialWeb.JsonApiRouter
end# ...
enddefmodule PetalStackTutorialWeb.JsonApiRouter do
use AshJsonApi.Router,
domains: [PetalStackTutorial.Blog],
json_schema: "/json_schema",
open_api: "/open_api"
end
```If user curl, Postman, or a browswer on http://localhost:4000/api/json/posts I see a list all posts.
When I go to http://localhost:4000/api/json/open_api I see an OpenAPI JSON spec of my API. If `open_api_spex` is installed then you can added the following (recommended to the dev scope) that will render a SwaggerUI to dispay documentation of your API.
```elixir
# ...
scope "/dev" do
pipe_through :browser
# ...
forward "/api/swaggerui",
OpenApiSpex.Plug.SwaggerUI,
path: "/api/json/open_api",
title: "PetalStackTutorialWeb JSON-API - Swagger UI",
default_model_expand_depth: 4
# ...
end
# ...
```And now visiting http://localhost:4000/dev/api/swagger shows that SwaggerUI.
To add new domains to the API, simply add the domain to the `JsonApiRouter` domain list. The behavoir is already implented in the `attributes` and `actions` blocks of the resource. This is where Ash really starts paying dividends.
For the sake of completeness, next I want to show how to add a GraphQL API.
### Section 6: GraphQL API with Ash GraphQL
We can add a GraphQL API for the Blog domain.
I followed [this getting started guide](https://ash-hq.org/docs/guides/ash_graphql/latest/tutorials/getting-started-with-graphql) for adding `AshGraphql`.
Add the `ash_json_api` dependency:
```elixir
defp deps do
[
# ...
{:ash_graphql, "~> 1.1.1"},
# ...
]
end
```Note: `AshGraphql` is built on top of `Absinthe`, a GraphQL library for Elixir.
Add to `.formatter.exs`:
```elixir
[
import_deps: [
# ...
:ash_graphql
],
#...
]
```Add the `AshGraphql.Domain` extension to the domain. Also, disable authorization for easy prototyping.
```elixir
defmodule PetalStackTutorial.Blog do
use Ash.Domain,
extensions: [
# ...
AshGraphql.Domain # <-- add this
]
# ...
graphql do
authorize? false
end
end
```Add the `AshGraphql.Domain` extension to the resource. Then specify what queries or mutations trigger which actions.
```elixir
defmodule PetalStackTutorial.Blog.Post do
use Ash.Resource,
domain: PetalStackTutorial.Blog,
data_layer: AshPostgres.DataLayer,
extensions: [
AshJsonApi.Resource,
AshGraphql.Resource
]
graphql do
type :postqueries do
get :get_post, :read
list :list_posts, :read
endmutations do
create :create_post, :create
update :update_post, :update
destroy :destroy_post, :destroy
end
end
end
```Create a schema. Here you can create top level queries or mutations, but for now we'll leave them blank.
```elixir
defmodule PetalStackTutorialWeb.GraphqlSchema do
use Absinthe.Schema
use AshGraphql, domains: [PetalStackTutorial.Blog]query do
endmutation do
end
end
```Add the schema to `router.ex`. Use the `Absinthe.Plug` with your schema to serve the main GraphQL API. Also use the `Absinthe.Plug.GraphiQL` to create an interactive version of GraphQL.
```elixir
defmodule PetalStackTutorialWeb.Router do
use PetalStackTutorialWeb, :router
# ...
pipeline :graphql do
plug AshGraphql.Plug
end
# ...
scope "/gql" do
pipe_through :graphqlforward "/playground",
Absinthe.Plug.GraphiQL,
schema: PetalStackTutorialWeb.GraphqlSchema,
interface: :playgroundforward "/", Absinthe.Plug, schema: PetalStackTutorialWeb.GraphqlSchema
end
end
```Now when I visit http://localhost:4000/api/gql/playground it shows an interactive version of GraphQL. There is a "SCHEMA" tab on the right that contains all queries and mutations.
I can make the following query:
```graphql
query {
getPost(id: "7e9cc199-7c5f-4196-bb7c-d28550d6b7c2") {
id
}
}
```### Section 7: TBD