As modern applications grow in complexity, developers often find themselves writing repetitive code to handle common patterns like CRUD operations, authorisation, and data relationships. Ash Framework addresses these challenges with a declarative approach, that emphasises what your data is rather than the mechanics of how to interact with it.
This guide will walk you through setting up your first Ash project, understanding its core concepts, and leveraging the Igniter toolkit to accelerate your development.
Understanding Ash Framework
Before diving into code, let's understand what makes Ash unique:
- Declarative Design: Describe the behaviours of your app, and let Ash handle the implementation details.
- Built-in Authorization: First-class support for actors, permissions, and policies at the resource level.
- Extensible Architecture: The Ash ecosystem provides first-party plugins for PostgreSQL, Phoenix, Oban, authentication, state machines, and more. Or you can write your own!
- Reusable Actions: Define actions once and reuse them across multiple interfaces, promoting consistency while eliminating duplication.
Ash allows you to focus on modelling your domain correctly, while it handles the implementation details of persistence, validation, and authorisation.
Setting up your first project
Ash isn’t limited to only being used in web applications, but for our demo we’ll create a new Phoenix app and test it out.
Prerequisites
Before starting, make sure you have:
- Elixir and Erlang installed
- PostgreSQL installed (you can also use SQLite; MySQL is not as well supported)
- The
phx_new
Phoenix application generator installed - (Optional, but highly recommended) The
igniter_new
Igniter application archive installed
Creating a new project
Start by creating a new Phoenix project. We’ll create one called ash_demo
:
$ mix phx.new ash_demo
$ cd ash_demo
$ mix ecto.setup
Installing igniter_new
provides access to the igniter.install
Mix task, which adds dependencies and runs setup scripts for them within your app. We can use it to install Ash and its PostgreSQL integration library, AshPostgres:
$ mix igniter.install ash ash_postgres
This will set up the two libraries, and replace the Ecto repo configuration in the app with an AshPostgres.Repo
configuration instead.
Building your first resources
In Ash, the core unit is the resource. Resources are the nouns in your app, like Post
, Comment
or User
, and are grouped together into domains that describe their responsibility, like Blog
or Accounts
.
Use Ash’s generators to generate new resources for a Post
and a Comment
, in a new Blog
domain:
$ mix ash.gen.resource AshDemo.Blog.Post --extend postgres
$ mix ash.gen.resource AshDemo.Blog.Comment --extend postgres
Inside the resource in lib/ash_demo/blog/post.ex
, fill out the basic details of a Post
. This uses the attributes
, relationships
and actions
DSLs:
defmodule AshDemo.Blog.Post do
use Ash.Resource, # ...
postgres do
# ...
end
actions do
defaults [:create, :read, :update, :destroy]
default_accept [:title, :body, :author_name]
end
attributes do
uuid_primary_key :id
attribute :title, :string do
allow_nil? false
constraints [min_length: 3, max_length: 100]
end
attribute :body, :string do
allow_nil? false
constraints [min_length: 10]
end
attribute :author_name, :string do
allow_nil? false
end
create_timestamp :inserted_at
update_timestamp :updated_at
end
relationships do
has_many :comments, AshDemo.Blog.Comment
end
end
You can fill out similar details for the Comment
resource in lib/ash_demo/blog/comment.ex
:
defmodule AshDemo.Blog.Comment do
use Ash.Resource, # ...
postgres do
# ...
end
actions do
defaults [:create, :read, :update, :destroy]
default_accept [:content, :author_name, :post_id]
end
attributes do
uuid_primary_key :id
attribute :content, :string do
allow_nil? false
end
attribute :author_name, :string do
allow_nil? false
end
create_timestamp :inserted_at
update_timestamp :updated_at
end
relationships do
belongs_to :post, AshDemo.Blog.Post do
allow_nil? false
end
end
end
Generating database migrations
AshPostgres can generate migrations for you, based on the state of your resources. No more manually writing them when you change your schema!
$ mix ash.codegen create_blog_tables
$ mix ash.migrate
Interacting with your resources
We added four actions to each resource, representing CRUD behaviour.
Read actions
For read
actions, you can use the Ash.Query
API to read, filter, sort, and aggregate data.
iex(1)> AshDemo.Blog.Post
|> Ash.Query.for_read(:read)
|> Ash.Query.load(:comments)
|> Ash.read!()
{:ok, [...]}
Create, update, delete actions
For create
, update
and destroy
actions, you can use Ash.Changeset
functions to build changesets, validate data, and update the database. Each action can set the attributes listed in the default_accept
list in the resource.
iex(2)> # Create a new post
AshDemo.Blog.Post
|> Ash.Changeset.for_create(:create, %{
title: "My First Post",
body: "Hello, Ash Framework!",
author_name: "Rebecca"
})
|> Ash.create()
{:ok, %AshDemo.Blog.Post{title: "My First Post", ...}}
iex(3)> {:ok, post} = v()
{:ok, %AshDemo.Blog.Post{...}}
iex(4)> # Update an existing post
post
|> Ash.Changeset.for_update(:update, %{
title: "Not Actually My First Post!"
})
|> Ash.update()
{:ok, %AshDemo.Blog.Post{title: "Not Actually My First Post", ...}}
iex(5)> # Create a comment for a post
AshDemo.Blog.Comment
|> Ash.Changeset.for_create(:create, %{
content: "Great post!",
post_id: post.id,
author_name: "Ben"
})
|> Ash.create()
{:ok, %AshDemo.Blog.Comment{...}}
Customising generated resources
To do more than basic CRUD, you can customise your resources. You can add attribute validations, action changes and preparations, and more. (These are also configured with the Ash.Resource
DSL - bookmark this page in the docs, you’ll use it a lot!)
# In the Post resource
actions do
# Remove `:create` and `:read` from here
defaults [:update, :destroy]
default_accept [:title, :body, :author_name]
# And add a new `create` action
create :create do
# Only Rebecca and Zach can create posts
validate attribute_in(:author_name, ["Rebecca", "Zach"])
# But we'll say Ben wrote them all anyway
change set_attribute(:author_name, "Ben")
end
# And a new `read` action
read :read do
# Always load the comments when reading posts
prepare build(load: [:comments])
end
end
iex(6)> AshDemo.Blog.Post
|> Ash.Changeset.for_create(:create, %{
author_name: "Mike",
title: "Mike's Post",
body: "Hello world!"
})
|> Ash.create()
{:error,
%Ash.Error.Invalid{
errors: [
%Ash.Error.Changes.InvalidAttribute{
field: :author_name,
message: "must be in %{list}",
vars: [list: ["Rebecca", "Zach"]],
# ...
iex(7)> AshDemo.Blog.Post
|> Ash.Changeset.for_create(:create, %{
author_name: "Rebecca",
title: "Who wrote this?",
body: "I don't know!"
})
|> Ash.create()
{:ok, %AshDemo.Blog.Post{author: "Ben", ...}}
iex(8)> AshDemo.Blog.Post
|> Ash.Query.for_read(:read)
|> Ash.read()
{:ok, [%AshDemo.Blog.Post{comments: [...], ...}, ...]}
Calling actions as functions
To simplify calling actions, you can define code interfaces for them. These can have extra customisations added, such as automatically applying filters or extra loads
, or mapping action arguments to function arguments.
# In the Post resource
code_interface do
define :create, args: [:author_name]
define :get_by_id, action: :read, get_by: :id
end
iex(9)> {:ok, post} = AshDemo.Blog.Post.create(
"Rebecca",
%{title: "New Post!", body: "This is so cool"}
)
{:ok, %AshDemo.Blog.Post{name: "Ben", ...}}
iex(10)> AshDemo.Blog.Post.get_by_id(post.id)
{:ok, %AshDemo.Blog.Post{...}}
Integrating with Phoenix
To use Ash resources in your Phoenix controllers and liveviews, you can use the ash_phoenix
package. Install it with Igniter:
$ mix igniter.install ash_phoenix
This gives access to AshPhoenix.Form
helpers, for working with forms built for resource actions:
# In a liveview mount, or a controller action
form = AshPhoenix.Form.for_create(AshDemo.Blog.Post, :create)
# In a liveview change event handler, or a controller action
form = AshPhoenix.Form.validate(form, params_from_form)
# In a liveview submint handler, or a controller action
case AshPhoenix.Form.submit(form, params: params_from_form) do
{:ok, record} ->
# The save was successful - redirect, show a flash message, etc.
{:error, form} ->
# The same wasn't successful - `form` contains all of the errors to show on the form
An AshPhoenix.Form
can be used in the same way an Ecto.Changeset
can, with your existing function components for forms and inputs.
For showing data, read actions can be used, the same way context functions would be without Ash.
Conclusion
What we've looked at here really only scratches the surface of what's possible with Ash. As you dig deeper, you'll see how it can handle increasingly complex business requirements while keeping your codebase clean and maintainable.
I encourage you to check out the official Ash documentation, join the community in the Ash Framework Discord, read the Ash book, and experiment with the various extensions available in the ecosystem.
Happy coding!
Need help with Ash Framework?
Getting started with Ash Framework? We can help. Whether it's supporting your team in up-skilling, or getting stuck into building an ambitious web app with you. Find out more about our Ash Framework services.