MikeyBower93

MikeyBower93

Balancing Elixir Context Design with Flexible Web API's

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.

Most Liked

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 #3
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.

mbuhot

mbuhot

When you have the requirement for flexible queries, but want to use contexts for well defined update operations, then a CQRS approach might be worth considering.

By having separate code paths for queries and commands, you can optimise each along different dimensions.

You can start with separating the controllers for the index and show actions. Those will be primarily concerned with composing the appropriate ecto query to serve the request efficiently.

The controller for the other actions would be primarily concerned with validating parameters, possibly building a struct that represents the command and passing that into your context module.

Unfortunately CQRS is often introduced in the context of event sourcing, but I think it can be valuable on its own.

Last Post!

edisonywh

edisonywh

Yup like I pointed out in the README, it’s basically a glorified Enum.reduce with some slight difference, some examples being:

  • with Enum.reduce your reduction runs in the order user specified
  • you get some niceties like validating the options user passed in is valid
  • I’d argue that the API is a bit better (your function retains the same shape, rather than having to rearrange everything to fit Enum.reduce)

The package itself is very small!

For now I’ve ported my app http://slickinbox.com/ to use Condiment and I really like it

Where Next?

Popular in Discussions Top

AstonJ
Please see the new poll here: Which code editor or IDE do you use? (Poll) (2022 Edition) It’s been a while since we first asked this, I...
208 31695 143
New
rms.mrcs
A couple of days ago I was discussing with a friend about different approaches to write microservices. He said that if he was going to w...
New
chulkilee
Here are the list of HTTP client libraries/wrappers, and some thoughts on HTTP client in general. I’d like to hear from others how they w...
New
Fl4m3Ph03n1x
Background This question comes mainly from my ignorance. Today is Black Friday, one of my favorite days of the year to buy books. One boo...
New
klo
Got a question about when to concat vs. prepending items to list then reversing to achieve appending. So i know lists boil down to [1 | ...
New
AngeloChecked
What learn first? Rust or Elixir Hi Elixir community! I’m here because i want learn a new language. I’m a junior developer and mainly i ...
New
AstonJ
Can you believe the first professionally published Elixir book was published just 8 years ago? Since then I think we’ve seen more books f...
New

Other popular topics Top

minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 records...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New

We're in Beta

About us Mission Statement