jonnifyelixir
F5 / F5.02 / api

F5.02 · part 3 of 3

A context's public API

A context is only as decoupled as its surface is small. Each one exposes a handful of public functions — a smart constructor that validates and returns {:ok, struct} or {:error, reason}, a command or two, a query — and keeps its structs, validations, and helpers defp-private. Callers depend on the API, not the data, so the context can rearrange its internals freely. This is the contract every caller leans on: the thin server today, Phoenix in F6, and the tests in between.

The public functions of the Learning context, and what each one promises. Select a function to see its shape.

Public surface, private core

Everything above the line is callable; everything below it is not. A command returns a tagged result so the caller can branch; a query returns data with no side effects; a smart constructor refuses to build an invalid entity in the first place. The structs and the validation that backs those promises stay private. Select a function on the surface.

The Learning API · select a function

PORTAL.LEARNING · PUBLIC API enroll/2 record_progress/2 courses_of/1 public ↑ private ↓ PRIVATE CORE %Enrollment{} · %Progress{} defp validate_ids/2 · helpers callers touch the surface; the core stays hidden

function: enroll/2

kind: command (constructor)

The API is the contract. Keep it small and tagged, keep the structs and validation private, and a caller can never put the context into a state its rules forbid.

In code

The public functions carry @specs and return tagged tuples; the smart constructor runs validation through a with chain and only then builds the struct; the validation itself is defp. A caller sees three functions and three contracts, not the data behind them.

defmodule Portal.Learning do
  alias Portal.Learning.Enrollment

  @spec enroll(String.t(), String.t()) :: {:ok, Enrollment.t()} | {:error, atom()}
  def enroll(user_id, course_id) do
    with :ok <- validate_ids(user_id, course_id) do
      {:ok, %Enrollment{id: Portal.ID.new("ENR"), user_id: user_id, course_id: course_id}}
    end
  end

  @spec courses_of(String.t()) :: [Enrollment.t()]
  def courses_of(user_id), do: # a query — no side effects

  # private — not part of the API surface
  defp validate_ids("USR" <> _ = _u, "CRS" <> _ = _c), do: :ok
  defp validate_ids(_, _), do: {:error, :bad_reference}
end

a small surface

A few public functions, each with a contract — the only way into the context.

a hidden core

Structs and defp validation stay private, free to change.

That closes F5.02: the Portal’s entities are typed structs, grouped into decoupled contexts, each behind a small API. The next module, F5.03 — Tracer bullets, drives one use case through this model end to end. Back to the module overview or the chapter.

References

Primary sources for this lesson, and where it connects in the course.

Sources

Related in this course