We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
Post 70
Ash Framework 01 - Getting Started
Published on: 2025-09-10
Tags:
elixir, Generators , Blog, Side Project, LiveView, Ecto, Phoenix
This is going to be a larger post as I wasn't able to get everything that I did over a couple days into multiple posts. Coded to late most days and just didn't transfer it here.
Chapter 1: Building Our First Resource (1)
Ash is a set of tools you can use to describe and build the Domain Model. For this we will set up the Tunez Application, install Ash, and build our first reasource.
Getting the Ball Rolling
This will be a lightweight spotify app (without the music).
Setting Up Your Development Environement
They created a starter github for this project so run
git clone https://github.com/sevenseacat/tunez
cd tunez
mix setup
mix phx.server
IF all that works you have everthing you need. if not make sure you have asdf and insall the files in the .tool-versions
Welcome to ASH!!
Now that we know its all working we can now be sure to insall an other depedency which is ingiter, this is already ready to go from the Tunez project. This will set some new deps and formatter lines as well as fixing the config to be sure to have all the right depedency.
mix igniter.insall ash
Next we need to set up the post_gres for ash as well. This will do the following add and fetch ash_postgres Hex package, add more autoformatting to config and formatter, update the database to use ASH, update some alias to use ASH instead of Ecto, Generate our first migration, generate the extensions for the config.
mix igniter.install ash_postgres
Reasources and Domains
One of the most important concept is resource in ASH. They are domain model objects (nouns our app revolves around).
Related resources are grouped together into Domains. What this means for our app is that we will define several domainds for distinct ideas within the app, Music, Album, Artist, Track User, Notifications
We will set attributes for each. which is data that maps to keys of the resource's struct.
Generating the Artist Resource
First we can create the Artist resource. It will hold the artists name and biography. For this we will use a generator with this command. It will do the following: create a new module Tunez.Music.Artist, a new domain module named Tunez.Music (check out lib/tunez/music/artist.ex)
mix ash.gen.resource Tunez.Music.Artist -- extend postgres
Now that we have the general domain we can now populate it with a table and some attributes
defmodule Tunez.Music.Artist do
use Ash.Resource, otp_app: :tunez, domain: Tunez.Music, data_layer: AshPostgres.DataLayer
postgres do
table("artists")
repo(Tunez.Repo)
end
end
Then we can set some attributes: first we want a primary key UUID key, time stamp fields (create_timestamp, update_timestamp, name, biography) as you can see below a lot of this can be done with just simple keywords.
attributes do
uuid_primary_key(:id)
attribute :name, :string do
allow_nil?(false)
end
attribute(:biography, :string)
create_timestamp(:inserted_at)
update_timestamp(:updated_at)
end
We still need to create the database and there is a command for that (ash.codegen) let's start to look into that now.
Auto-generating Database Migrations
If you have run with ecto before you will know that everyting needs to stay up to date and if you update a schema or datebases independently you will not have an up to date DB. Ash will sidestep this and make sure that everything is done together. `mix ash.codegen' will do the following:
create shapshots of your current Reasources
compare them to the older snapshot (if extisting)
generate deltas for the changes.
Let's run that command to create the artists. This created a few files for us, so that we can keep everything up to date: a snapshot priv/resource_snapshots/repo/artists/[*].json, and the migration for the artist resource priv/repo/migrations/[*]_create_artists.ex
mix ash.codegen create_artists
Let's check out the migration that was just created.
def up do
create table(:artists, primary_key: false) do
add :id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true
add :name, :text, null: false
add :biography, :text
add :inserted_at, :utc_datetime_usec,
null: false,
default: fragment("(now() AT TIME ZONE 'utc')")
add :updated_at, :utc_datetime_usec,
null: false,
default: fragment("(now() AT TIME ZONE 'utc')")
end
end
def down do
drop table(:artists)
end
Looks just like something that you would create doing it all yourself. This all came from the schema we created and then the Ash generator. We can now migrate with the mix ash command.
mix ash.migrate
Oh, CRUD! - Defining Basic Actions
Remember that CRUSD is:
Create
Read
Update
Delete
We will need to define those actions within our database. Remembering that everything that we want to do can be done within the artists.ex file simply add in an action block and go.
Defining a Create Action
All actions require a type (create, read, update, destroy) I will put all the changes to the module below but there will be a few thing to look for. As of right now we just the action and an atom to call it with, and then what it will accept
actions do
create :create do
accept([:name, :biography])
end
Creating Records via a Changeset
Create a changeset and then pass it to a process to add it.
iex -S mix
Tunez.Music.Artist
|> Ash.Changeset.for_create(:create, %{
name: "Valkyrie's Fury",
biography: "A power metal band hailing from Tallinn, Estonia"
})
|> Ash.create()
Now we can see if it did infact work with a psql command
psql tunez_dev
tunez_dev=# select * from artists;
-[ RECORD 1 ]----------------------------------------------
id | [uuid]
name | Valkyrie's Fury
biography | A power metal band hailing from Tallinn, Estonia
inserted_at | [now]
updated_at | [now]
We can now check to see if you can do some other creates with or without name etc.
Creating Records via a Code Interface
So we want to now use a Domain to interact with a database so that we can have lower level (reasources) work done through a domain. Let's head to the file /lib/tunez/music.ex. This is a domain.
resources do
resource Tunez.Music.Artist do
define :create_artist, action: :create
end
end
With this in place we can no interact with the create_artist function.
iex(5)> h Tunez.Music.create_artist
def create_artist(params \\ nil, opts \\ nil)
Calls the create action on Tunez.Music.Artist.
# Inputs
• name
• biography
Okay so now we can use this syntax to create an artist from the music module.
iex > Tunez.Music.create_artist(%{
name: "Valkyrie's Fury",
biography: "A power metal band hailing from Tallinn, Estonia"
})
Let's quickly look at the seed logic in the mix.exs so that we can see what kind of work is done with the `mix seed` command.
defp aliases do
[
setup: ["deps.get", "ash.setup", "assets.setup", "assets.build", "run priv/repo/seeds.exs"],
"ecto.setup": ["ecto.create", "ecto.migrate"],
seed: [
"run priv/repo/seeds/01-artists.exs",
With this line uncommented we can run the mix seed to create some artists.
mix seed
Defining a Read Action
now we can add in the read action to the resource Artist
read :read do
primary?(true)
end
Then add it to the music Domain
define :read_artist, action: :read
iex> Tunez.Music.read_artists()
``Manually Reading Records Via a Query``
This will now work for us. Right now as we didn't set some params for the read we will just pull everything the other way is to create a temp artist struct and then pass some queries to it. The basic idea is: create basic query > pipe through functions to add params > Pass to Ash for processing, see below.
iex(2)> Tunez.Music.Artist
Tunez.Music.Artist
iex(3)> |> Ash.Query.for_read(:read)
#Ash.Query<resource: Tunez.Music.Artist, action: :read>
Then you can pipe that query into Ash’s query functions like sort and limit. The
query keeps getting the extra conditions added to it, but it isn’t yet being run
in the database.
iex(4)> |> Ash.Query.sort(name: :asc)
#Ash.Query<resource: Tunez.Music.Artist, action: :read, sort: [name: :asc]>
iex(5)> |> Ash.Query.limit(1)
#Ash.Query<resource: Tunez.Music.Artist, action: :read, sort: [name: :asc],
limit: 1>
Then, when it’s time to go, Ash can call it and return the data you requested,
with all conditions applied:
iex(6)> |> Ash.read()
SELECT a0."id", a0."name", a0."biography", a0."inserted_at", a0."updated_at"
FROM "artists" AS a0 ORDER BY a0."name" LIMIT $1 [1]
{:ok, [#Tunez.Music.Artist<...>]}
`Reading a Single Record by Primary Key`
Right now again if we were to try a read it will retieve everything but there is a simple way to use a resource and be able to pass a param and get a record by that param. Head back to the music domain and then add this.
define :get_artist_by_id, action: :read, get_by: :id
Defining an Update Action
Now we can add in an update. Remember that we now just need to add in the action and then add it to a module.
update :update do
accept [:name, :biography]
end
define :update_artist, action: :update
How do we use this? Well we need an artist and then we need to send it the information that will be updated.
iex(3)> artist = Tunez.Music.get_artist_by_id!("an-artist-id")
#Tunez.Music.Artist<id: "an-artist-id", ...>
Now we can either use the code interface we added or create a changeset and
apply it, as we did for create.
iex(4)> # Via the code interface
iex(5)> Tunez.Music.update_artist(artist, %{name: "Hello"})
UPDATE "artists" AS a0 SET "updated_at" = (CASE WHEN $1::text !=
a0."name"::text THEN $2::timestamp ELSE a0."updated_at"::timestamp END)
::timestamp, "name" = $3::text WHERE (a0."id"::uuid = $4::uuid) RETURNING
a0."id", a0."name", a0."biography", a0."inserted_at", a0."updated_at"
["Hello", [now], "Hello", "an-artist-id"]
{:ok, #Tunez.Music.Artist<id: "an-artist-id", name: "Hello", ...>}
iex(6)> # Or via a changeset
iex(7)> artist
|> Ash.Changeset.for_update(:update, %{name: "World"})
|> Ash.update()
«an almost-identical SQL statement»
{:ok, #Tunez.Music.Artist<id: "an-artist-id", name: "World", ...>}
Defining a Destroy Action
Now we simply need to add in a delete (destroy) and we have at least the start of CRUD
destroy :destroy do
end
define :destroy_artist, action: :destroy
How we will use it is slightly different as we would like to use a changeset
ex(3)> artist = Tunez.Music.get_artist_by_id!("the-artist-id")
#Tunez.Music.Artist<id: "an-artist-id", ...>
iex(4)> # Via the code interface
iex(5)> Tunez.Music.destroy_artist(artist)
DELETE FROM "artists" AS a0 WHERE (a0."id" = $1) ["the-artist-id"]
:ok
iex(6)> # Or via a changeset
iex(7)> artist
|> Ash.Changeset.for_destroy(:destroy)
|> Ash.destroy()
DELETE FROM "artists" AS a0 WHERE (a0."id" = $1) ["the-artist-id"]
:ok
Default Actions
Now that we know how to setup some of the actions we can just use a module and set some default actions as ASH is set to just use CRUD off the bat. Let's go to the artist.ex resource and replace everything with this.
defaults [:create, :read, :update, :destroy]
default_accept [:name, :biography]
What is great about this is that the normal default here add in a lot of functionality that you would want (or and not use) like pagination etc.
Next we will add in Phoenix Integrations.