Expose ecto schema struct from context module

Hello!

I’m new to elixir and trying to follow best practices by exposing my Public API through context modules.

Imagine I have a simple ecto schema like this:

# lib/businesses/business_schema.ex

defmodule Business.BusinessSchema do
  use Ecto.Schema
  
  schema "businesses" do
    field :name, :string
    field :status, :string
    # other fields...
  end
end

And then I have a context module like that uses the schema:

# lib/businesses_context.ex

defmodule BusinessContext do
  alias Business.BusinessSchema
  
  # Context functions
  def get_business(id), do: # implementation...
  def list_businesses(), do: # implementation...
  # other functions...
end

I want all other parts of my app to be able to pattern match on the schema, but I don’t want to expose the schema to the outside world. (edit: by outside world, I just mean the code outside of the lib/app_name folder. So that the contexts are the public api for the app)

For example I want to be able to do this

# lib/some_module.ex
defmodule SomeModule do
  alias BusinessContext

  def some_function(%BusinessSchema{} = business) do # <---- I want to be able to do this
    # do something with the business
  end
end

This seems to work but feels clunky.

# lib/businesses_context.ex
defmodule BusinessContext do
  alias Business.BusinessSchema

  def business_schema_type, do: BusinessSchema
  
  # Context functions
  def get_business(id), do: # implementation...
  def list_businesses(), do: # implementation...
  # other functions...
end

# lib/some_module.ex
defmodule SomeModule do
  alias BusinessContext
  @business_schema_type BusinessContext.business_schema_type()

  def some_function(%@business_schema_type{} = business) do # <---- This works
    # do something with the business
  end
end

Is there a better way to do this?

What specifically are you aiming to achieve by “hiding” the schema?

Phrased another way, what’s some code you want to prohibit being written in your application?

FWIW, the module-attribute approach you’ve shown will produce identical BEAM bytecode (to check __struct__ == Business.BusinessSchema) as one that directly references BusinessSchema :man_shrugging:

Thanks for the reply. I was thinking of the contexts as the public API for the app and that it could be nice to not allow LiveViews, for example, to alias the schema directly itself and instead have to get the schema struct from the context. That just feels like a cleaner boundary context boundary to me.

But I was looking at the new phoenix 1.8 gen.auth and saw that the live views just alias the struct directly, no I guess thats the way things are done.

There is just not that much of a benefit by trying to hide it or making an indirection layer – the latter approach is used in really big apps where the data flying around between many UIs and middle-end code starts to dramatically differ from the shape of the data in the database, so there these transformations and having authoritative “middle-manager structs” actually do make a lot of practical sense.

But in your cause it seems a bit like paranoia. You might want to just namespace your API with /v1 and leave yourself room to change your mind in the future with /v2 and /v3 etc.

Or, you could consider making your endpoints GraphQL and allow your client apps to pick and choose what pieces of data they want returned.

Haha, paranoia probably isn’t far off. I think I was going a bit overboard. Thanks.