I’m a big fan of Ecto. For years, it has been the backbone of how I build Elixir applications — managing schemas, changesets, validations, and database interactions. It gives me hands-on control, which I really enjoy. But lately, I’ve been hearing a lot of hype about the Ash Framework. Curious about where it fits in the Elixir ecosystem, I decided to give it another shot.
This was actually my second try. About 18 months ago, I tested Ash, but the project wasn’t mature enough, and the documentation was sparse. I felt I couldn’t keep control when writing APIs, especially since I loved the flexibility of changesets. Fast forward to today, the ecosystem has improved significantly. The docs are clearer, and the framework now offers a lot more out of the box.
And you know what? I see Ash as a perfect complement to Phoenix — taking away much of the manual, error-prone boilerplate while letting me keep control over core logic.
Why Try Ash? What Does It Bring?
Ash is designed to model your domain declaratively, reducing boilerplate and automating repetitive tasks. Instead of writing separate schemas, validations, and API logic, you define resources that include:
- Data attributes and types
- Relationships
- Validations
- Actions (create, update, delete)
- Authorization and API generation features
It’s like a higher-level layer over Ecto that makes common patterns simple and expressive.
Comparing Ecto and Ash: A Look at User & Comments
I’ll show example with very minimal functionality, let’s say we have a user and a user has multiple comments so we have 2 schemas we are going to play with.
In Ecto (Phoenix) — The Manual Way
Here’s a minimal example:
defmodule MyApp.User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field :name, :string
has_many :comments, MyApp.Comment
end
def changeset(user, attrs) do
user
|> cast(attrs, [:name])
|> validate_required([:name])
end
end
defmodule MyApp.Comment do
use Ecto.Schema
import Ecto.Changeset
schema "comments" do
field :content, :string
belongs_to :user, MyApp.User
end
def changeset(comment, attrs) do
comment
|> cast(attrs, [:content, :user_id])
|> validate_required([:content, :user_id])
end
endCode language: Elixir (elixir)
When working with data, I create a changeset and call Repo.insert or Repo.update:
# Creating a user
attrs = %{name: "Alice"}
changeset = MyApp.User.changeset(%MyApp.User{}, attrs)
case Repo.insert(changeset) do
{:ok, user} -> # success
{:error, changeset} -> # handle errors
endCode language: Elixir (elixir)
Similarly for comments, I manage the process manually.
In Ash — The Declarative Way
In Ash, resources are defined with all the necessary info:
defmodule MyApp.User do
use Ash.Resource
attributes do
attribute :name, :string, allow_nil?: false
end
relationships do
has_many :comments, MyApp.Comment
end
end
defmodule MyApp.Comment do
use Ash.Resource
attributes do
attribute :content, :string, allow_nil?: false
end
relationships do
belongs_to :user, MyApp.User
end
endCode language: Elixir (elixir)
Creating a user or comment becomes a single API call:
# Create a user
{:ok, user} = MyApp.Api.create(MyApp.User, %{name: "Alice"})
# Create a comment for the user
{:ok, comment} = MyApp.Api.create(MyApp.Comment, %{content: "Nice post!", user_id: user.id})
# Fetch user with comments, load association
loaded_user = MyApp.Api.get(MyApp.User, user.id, load: [:comments])Code language: Elixir (elixir)
The API module looks like this:
defmodule MyApp.Api do
use Ash.Api, resources: [MyApp.User, MyApp.Comment]
endCode language: Elixir (elixir)
And in your Phoenix controller:
def create(conn, %{"user" => user_params}) do
case MyApp.Api.create(MyApp.User, user_params) do
{:ok, user} -> json(conn, user)
{:error, error} -> json(conn, error)
end
endCode language: Elixir (elixir)
It’s simple, clean, and super easy to extend.
What I Love (and Still Love) About Ash
- Less boilerplate: No more juggling changesets, validations, and API endpoints separately.
- Declarative resources: Define your data, relationships, validations, and actions all in one place.
- Automatic API generation: Build REST or GraphQL APIs from your resources.
- Integration with Phoenix: Just call your Ash API; controllers remain straightforward.
I still love Ecto and will keep using it when I need fine control. But for many projects, Ash makes development faster, cleaner, and more reliable.
When Should You Consider Ash?
- When you want less repetitive code.
- If you prefer fully declarative domain modeling.
- When you need auto-generated APIs with minimal setup.
- If you’re building complex business rules, state machines, or multi-tenant systems.

Final Thoughts
As an Ecto fan, I’ve been happily building with it for years. But Ash has opened a new perspective: building domain-centric, declarative backends with less fuss. It’s a promising addition to the Elixir ecosystem and might just become your new favorite tool for backend design.
If you haven’t checked out Ash recently, I highly recommend giving it a try. The docs are clearer, the community is active, and it’s genuinely powerful.
Happy coding!

