Handling a client's API state

Hi all,

I’ve been reading the forum for a while and I am trying to wrap my (object-oriented) head about the language (and its ecosystem).

Just for learning, I am writing a library (I am not sure this term is correct in this context) to access a 3rd party API and I do not know which is the best way to handle state in this scenario. So I am posting the two approaches I’m considering and it would be great to get some advice from you.

Of course, I could use Tesla or Maxwell (cool names for Faraday-based libraries ;)), but for the time being I am more interested in learning a more low-level approach.

This API asks the user to log-in in order to get a session id that can be used for subsequent requests, so you need to store the sid somewhere. I am not sure who should be responsible for: 1) the user of this library or 2) the library itself.

In the first option, I would have a MyModule.login(host, user, password) function that could return something like {:ok, sid} or, even better, an struct with all the related information (let’s call it %MyModule.Session{}). So on every call, the user of the client would pass this struct as an argument.

session = MyModule.login()
videochats = MyModule.query(:videochats, session) # this API could be different

The second option I am considering is storing hosts, users and session ids in the client itself (for instance, using an agent). It could even receive an optional identifier so you can connect to different hosts at the same time:

MyModule.start()
MyModule.login(user, password, as: :default)
videochats = MyModule.query(:videochats)

The first one looks correct to me (as you are not overloading the library with additional responsabilities). However, the second one looks pretty convenient from a user’s perspective. However, what happens if the agent crashes? In such a case, the session id would be lost. I guess that, for some applications, it’s acceptable (but not for others).

If that’s not acceptable, the library could receive some module which implements an API to store/retrieve that information from somewhere (an ETS table or a database or…), so the user could provide a customized implementation. But maybe that’s too much and I should just go for the first approach.

I would really appreciate any comment/suggestion about this topic.

Thanks in advance!

I think the Elixir way would be roughly your option (1) above. The convention would be to have your “connection structure” be an argument to the functions in your module. And typically it would be the first argument so that piping works well. For example:

conn = 
  ApiModule.Connection.new
  |> ApiModule.put_credentials(user, pass)
  |> ApiModule.put_host(host_name)

with {:ok, result} <- ApiModule.request(conn, query) do
  do_something
end

Phoenix is a good example of this kind of approach.

1 Like

Definitely (1). This gives users the flexibility to use the library as they want, and avoids convoluted calls where the user’s code is calling into the library which calls the user’s code which calls the library etc.

Thanks a lot for your feedback! I will go for option 1 which, indeed, looks better to me.