MikeyBower93

MikeyBower93

Recently I have been using elixir phoenix contexts to structure my software development projects to allow for a clean/cohesive/reusable code base which works really well and makes a lot of sense. However I have trouble reconciling this with creating a flexible and open web API for front ends to use, whether that standard be JSON API, GraphQL etc.

To further understand this problem, let me give you an example, imagine you have a Users context with an exposed function called list_users which might look like following:

defmodule App.Users do
   def list_users() do
      Repo.all(User)
   end 
end

and then imagine that you have a Web API controller that uses that code like the following:

defmodule AppWeb.UserController do
   def index(conn, _params) do
       users = App.Users.list_users()

       json(conn, users)
   end
end

This is absolutely fine and works great. However for rich client side applications its often the case that you will need a lot of flexibility over the data that is returned, for example in a couple of projects I have worked on we use JSON API and allow for filtering, nested filtering (in our example imagine a user is associated to a company and you want your user listing filtered by a company name), sorting and pagination.

As soon as we get into this realm things start to get tricky in my opinion, because we want to keep the context and the list function isolated without exposing too many details of the inner works (in this circumstance it’s using ecto), however we now need to support multi layers of functionality for the listing of users if we want our Web API to allow for a rich level of functionality. If you try and keep that within the context function you might end up with the following:

defmodule AppWeb.UserController do
   def list_users(params) do
      filter_params = Map.get(params, :filters)
      pagination_params = Map.get(params, :pagination)
      sort_params = Map.get(params, :sort)

    # ...reduce over filter params and use ecto composability to build the query up

    # perhaps do some pattern matching on the sort_params, if non has been passed through
    # then do a regular Repo.all, otherwise can run limit and offset etc. 
   end
end 

All of sudden that function has become very general and very abstract in what it can and can’t do. Not to mention that we don’t want the context function to know if its JSON API, GraphQL etc calling it, so we will likely end up formatting the request parameters into a general format that can be understand by the contexts (support for like, greater than, less than etc). This is crucial because if another part of the application such as a background job needs to do call the list_users it won’t be natural to pass parameters to it in the form of a JSON API request etc.

It feels like we have to put a lot of plumbing in to create that separation between the 2 whilst having that rich web API for the client, to the point we have needed to define a query language to pass to the list function (we would also need to create a layer that converts the request params into the form).

This approach starting to seem so overblown that a few months back I ended up writing a library that takes in request parameters in the JSON API format and creates an ecto query that you can execute which leverages ecto named bindings (see GitHub - MikeyBower93/json_api_ecto_builder · GitHub). However after reading into more context design/general design principles it does feel like that is essentially coupling your web API to your database and ecto etc.

I would be interested to see what people have done in these circumstances, how people believe you should approach such design considerations.

Thanks,
Mike.

First 10 of 18 Posts Switch mode

tfwright

tfwright

I’m not following how you think Phoenix Contexts increases coupling. It seems like they make sense because they decrease coupling. That inherently involves abstraction. list_users abstracts the Ecto API (which is itself already an abstraction over the SQL or whatever you’re using as a Repo) and this abstraction has made your list_users function a bit complex.

In a Rails app you’d probably use a new Class to handle the complexity, in Elixir I would start by using arity and pattern matching to keep the logic separated within the context, rather than using a single function. If necessary you could add another module to your API context.

def list_users, do: User |> list_users
def list_users(%{filters: filters}), do: filters |> build_filtered_query |> list_users # this could also go in the API context so this always works on a query
def list_users(query), do: query |> Repo.all

If you had to do a lot of that for a lot of schemas I would probably consider a macro.

baldwindavid

baldwindavid

I use the following pattern…

# context
def list_users(queries \\ & &1) do
  from(User)
  |> queries.()
  |> Repo.all()
end
# controller

Accounts.list_users(fn query ->
  query
  |> Accounts.include_user_profile()
  |> Accounts.filter_non_executive_users()
  |> Accounts.filter_users_by_company(company)
  |> Accounts.order_users_by_first_name()
end)

All of the needed queries are publicly exposed from the context. This is very explicit, dependency-free, and dead simple to find the call sites where you might be using a specific query.

I previously experimented with a more dynamic method via a package, but found it to be a little more difficult to maintain and more magic than needed. That dynamic method is wrapped up in the TokenOperator package. There is a good bit of discussion on some of the things you mention in the thread for TokenOperator and another for the QueryBuilder package.

19
Post #2
derek-zhou

derek-zhou

Nice and straightforward. The only suggestion I’ve is to import Accounts either at module level or at function level to save some typing.

baldwindavid

baldwindavid

I’ve tried to avoid importing in a lot of cases for explicitness/searchability, but import is totally an option to slim down those queries.

MikeyBower93

MikeyBower93 OP

Thanks for your input, I probably didn’t word if well but I agree contexts decrease coupling, not increase it. I think if you are referring to the part where I reference the json ecto builder library I built, what I was meaning to say was that library I created couples the Web api with ecto/db.

In terms of your solution, it makes sense to me and its how I’ve approached it in the past, the part which has felt slightly icky about it was that for complex sets of filtering, for example operator types (gt, lt, eq etc) and nested filtering. You end up in the context function having to parse a fairly complex query, which sometimes seems a bit bloated for whats trying to be achieved.

baldwindavid

baldwindavid

If taking in dynamic query parameters is more the rule than the exception for your app you might also take a look at packages like filterable, filterex, inquisitor, ex_sieve, rummage_ecto. I haven’t had the need for them, but they are catered to that sort of thing.

dimitarvp

dimitarvp

Very informative list, thank you!

MikeyBower93

MikeyBower93 OP

Thanks for your feedback, that’s a nice pattern, I do like the fact that it gives extendability to a query, without exposing the inner details of the list_users function.

I think my only criticism would be that, where it doesn’t expose the repo etc, the callback function is still essentially telling the caller of the code that we are using ecto queries in the inner workings of the function.

Perhaps I’m reading too much into things, to me what seems to the ideal solution would be one where the list_users (or any context function) allows extendability through some kind of parameter format, but the internal function usage is hidden from the caller, therefore we could parse the parameters into an ecto query, however if it it ended up being some different method of data retrieval, say a separate call to another API etc it still wouldn’t matter to the caller of the function, and the inner function could format the request to the specific data retrieval method it is using.

It seems like the libraries you added achieve this. In fact, I have I’ve seen the QueryBuilder library before, it’s probably something I will look at further.

Thanks.

baldwindavid

baldwindavid

My TokenOperator package includes that extra level abstraction that you might be looking for. I like the resulting clean interfaces, but it didn’t really improve my day-to-day clarity and maintenance of the codebase. Here are some goals of that project that might jibe with a library you use or something you write.

I would only note that the aforementioned package-less pattern is, at its core, just a context function that takes an optional argument. That argument is expected to be a function (anonymous or otherwise) that takes one argument (an Ecto query or otherwise). You can do anything in that function. In practice, it has always been an Ecto query for me, but there is nothing precluding you from injecting something else there, to the extent it provides you with an interface you like.

cheerfulstoic

cheerfulstoic

This is an interesting pattern. I’ve actually been writing my context’s function to return query objects and then just piping them like this:

# controller

query =
  Accounts.User
  |> Accounts.include_user_profile()
  |> Accounts.filter_non_executive_users()
  |> Accounts.filter_users_by_company(company)
  |> Accounts.order_users_by_first_name()

users = Repo.all(query)

This is something where I don’t feel like I understand what is recommended by the Phoenix team / community regarding contexts. Your solution hides Repo inside of the context, and I could see the advantage of that, I guess, though practically it hasn’t been much of a problem, yet.

Where Next? Top

Trending in Discussions Top

AstonJ
As the title says, please share what you’ve been up to with Elixir. Whether that’s been learning it, looking into it, making stuff with i...
2977 91898 914
New
AstonJ
The obligatory hello world thread! Who are you and where are you from? :stuck_out_tongue:
4616 55835 594
New
byu
@chrismccord : I just saw the Extract AGENTS.md from Phoenix.new into phx.new generator commit to the phoenix project. My initial shotgu...
New
arcanemachine
I was working on an Ecto migration and I needed a timestamp. So, for the nth time, I looked up the different data types for timestamps, a...
New
alexslade
Fly’s CEO posted this recently - Turn And Face The Strange · The Fly Blog It says that Fly is going all-in on sprites, which is a worry ...
New
Herve37
We’re evaluating API mocking tools for OpenAPI-based projects and would love to hear what other teams are using. We’re particularly inte...
New
matt-savvy
Is there a word for the ~> symbol used in Version strings? Do you also just call it a Squiggle Arrow™ ?!
New

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge & Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New

We're in Beta

About us Mission Statement