Geo: A database-backed country chooser built with Ash
A slightly more than beginner project for learning Ash. Maybe Zach will critique!
https://github.com/dev-guy/geo/blob/main/README.md
3 Replies

I deployed the demo to fly.io. There were some things that I had to figure out. I left notes in the README. fly.io is fantastic and nearly free for hosting open source demos in Elixir -- even those that require Postgres. https://geo-demo.fly.dev/
Today I refactored the generic
search
action on the Country
resource and learned more about Ash.
The “search” action accepts an optional query
parameter that is used to search for countries based on name and ISO code. It returns two lists of Country records: one in which country names match the query and the other in which country codes match the query. The reason for returning two lists is to allow users to sort them separately. This UX makes more sense when country names are localized.
The output of the above search action is a classic DTO that is alien to the Ash ecosystem. The search action's return value as a tuple would have been an endless source of confusion. We need something better than documentation when it comes to correctly using APIs
How do we specify the module for the return value of a custom action when it is a struct?
Can we replace :tuple
what a specific struct? Claude said I could use Module.StructName
instead of :struct
but it didn't work. It's worth asking the real experts. It's probably possible. I didn't pursue it.
Model your domain
Instead, I modeled the return value of a search operation as an Ash resource (Geography.Country.SearchResult
). Since search results are not persistent objects, perhaps defining an Ash resource is the Wrong Thing to Do aka Overkill. Is it better to declare the return value of a generic action as a regular struct? Simpler is usually best. But... Will Ash future-proof search functionality? My intuition said yes. Ash has a lot of functionality that would be called YAGNI until You Actually Need It. Staying in the Ash ecosystem seems like a Smart Thing To Do. Let's try it ...
The SearchResult
resource has two attributes, both of which are lists of Country records:
Note the constraints
keywords. This is how we get type safety.
As with structs, now we have the ability to Add More Later without disturbing existing clients.
Should I have used Ash references instead of lists of records?
I tried. It didn't pan out.
How do we specify the fact that the return value of a custom action is a resource struct (aka Record)?
This turned out to be the wrong question. Instead of adding a function to the Country
that returns Country.SearchResult
, Claude suggested I add a read
action to Country.SearchResult
. A read
operation is more "Ashy" than a generic action, IMO. The implementation is ready for graceful improvements.
Ash makes you think. Ash is deep. I like that. On the other hand, this probably scares away a lot of would-be users of Ash. Start Simple!The
SearchResult
resource's read action is defined as a manual action (since it doesn’t run a database query but instead calls an internal cache) that accepts one argument named query:
How do we add the search action to a domain?
The search_countries
function was added to the domain within a resource
block:
This demonstrates one advantage of domains: it doesn't necessarily matter how you organize your actions since domains hide those details.
Read actions implicitly return resource records. It’s not explicitly stated in the above definition - you just have to know.
By the way, I could have defined the search action as the primary read action, which implicitly adds a “get” function to the domain:
Geography.get!(Geography.Country.SearchResult, %{query: “…”})
However, I think search_countries is a better name. There’s nothing stopping us from supporting both, but it gets a little magical.
The PR is at https://github.com/dev-guy/geo/pull/54GitHub
Refactor search results and country cache by dev-guy · Pull Reques...
Closes #53 and #36
Define country GenServer state with a struct
Use poolboy instead of dynamic supervisor
Define return value of Geography.search_countries.do_search as a resource