What to return from the public API of umbrella apps?

Sorry for the title. I was having a hard time figuring out how to describe this question.

Let’s say you have an umbrella app with two child applications: Domain and API

Domain contains the business logic of the application and uses Ecto to interact with a Postgres database. API is a Phoenix app that interfaces with the Domain app to perform whatever action is requested by the client.

Now, is it appropriate for the Domain app to return Ecto changesets and schemas to the API app? Are you coupling Domain and API together by using Domain-specific data structures within API? Would it make more sense to return something like a map?

Thanks!

4 Likes

I have mixed feelings about this so I’m curious to hear some other opinions on this, but in my opinion it is okay for the schemas to be a part of the applications public interface but changesets are iffy.

The schemas that ecto creates are really just structs whose fields are related to the domain logic so I think they are appropriate. Changesets on the other hand contain implementation details about the data layer that I don’t think are appropriate outside of the domain. I don’t think that your public api needs to expose specifics like which database table is being used, what column the primary key is, what the cardinality is between the relationships, etc, all of which are included in the changeset.

That being said, phoenix provides some integration directly with changesets (forms for example) where you get some added convenience. The phoenix examples and docs also show changesets being interacted with directly from controllers, which suggests that their use outside of the domain is acceptable according to the devs.

Like I said, I’m curious to hear more opinions about this. My opinion about changesets comes from gut feeling more than anything.

That being said, phoenix provides some integration directly with changesets
(forms for example) where you get some added convenience. The phoenix examples
and docs also show changesets being interacted with directly from controllers,
which suggests that their use outside of the domain is acceptable according to
the devs.

My gut feeling doesn’t like this either. My take on it is that you
should not export something that the caller shouldn’t be aware of. I
can’t say why this is done but I am pretty sure “convenient” and
“pragmatic” are terms that may be used :slight_smile: (EDITING: I am not meaning this in a bad way. There are trade-offs with everything and convenient and pragmatic are very valid reasons to make something work)

I think forms and database validation are two separate things and in my
experience they are quite different in functionality too. So much that
for all but the simpler case it doesn’t make sense to couple them. The
html forms are usually a combination of a subset of various tables and
they don’t really map into the underlying database schemas anyway.

I don’t really like exposing schema structs either. It is just a few
extra fields in a struct but perhaps it enables people to start
relying on them in the code and it will be harder to change your public
API. This is the classic: don’t export implementation detail (and that
your struct is a schema is an implementation detail).

I would like two see two different set of validations.

  1. Is your schema validations which happens inside your module. You only
    pass the wanted parameters in and you get an {error, msg} back.

  2. You create forms which are validated in your web layer.

I don’t think these should be mixed as they are now in phoenix (at least
in the generators and documentation).

I agree about validations, It’s easy enough to return {:error, changeset.errors} instead of the returning the entire changeset on failure, this lets you share validation constraints between the two without passing changesets around. The changeset integration with phoenix forms is possible with anything that implements the Phoenix.HTML.FormData protocol, so it’s possible to retain some of the form conveniences without passing changesets around. That translation from [domain response] -> [FormData type] would need to happen on the web side of things though, otherwise you are putting phoenix specific stuff in your domain. You have to juggle convenience for strict separation with these I think.

I think schemas are trickier to address because it makes sense for Accounts.get_user(id) to return a %User{} struct to be used inside of your application, but should you use a separate function from phoenix that returns a map for external use? Or should the schema struct never even leave the context? You could have an Accounts.get_user(id) that returns a map with just the fields you want to expose externally, and then an Accounts.get_user_struct(id) for use within the application. That might make sense, where Accounts.get_user() just trims the implementation details from the struct. You lose the ability to pattern match on the struct type which is an inconvenience, but maybe that’s okay.

2 Likes

I think the cleanest would be to return a %User{} struct but where the struct is “clean” and does just define the fields you would like to access. This of course assumes that structs in elixir are much easier to change afterwards than for example erlang records which can be trouble-some if used and manipulated directly outside of the defining module.

Struct is just a data type, but if it is an “ecto” struct it contains more than just data.

Yes. In the end I think it all boils down to this. Purity vs convenience.

If you hide your implementation of the data structure behind functions you are safe from bad use. This is fine if you have a queue or a set or something else but when you have a struct you end up with getters and setters which are obviously less convenient and perhaps harder to maintain.

1 Like

Thanks for the discussion! This has been great so far!

I agree with @kylethebaker’s original statement regarding schemas. I think that returning an Ecto schema is OK if you are responsible with it. Ecto schemas aren’t much different from structs or even maps. You could rip out Ecto and just start returning structs and the consumer wouldn’t notice as long a the contract hadn’t changed.

If keeping the apps completely decoupled is the goal then I’d think the producer would need to return a generic structure like a Map. If a struct was needed then it would live on the consumer. This would be akin to a pure microservice type architecture (I think, not an expert here).

I also agree that changesets are harder to reason about. A changeset is just a struct so you can apply what I said above. However, the changeset is purely an Ecto thing and like @kylethebaker said it might reveal too much.

I think schemas boil down to the “purity vs convenience” argument. I’m not so sure about changesets.