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 https://github.com/MikeyBower93/json_api_ecto_builder). 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.