Skip to content

Elixir naming conventions

These rules extend the general naming conventions. Where the general rules and these overlap, the general rule is canonical; this page adds the Elixir-specific cases.

We assume the official Elixir naming conventions and mix format as a baseline — snake_case for atoms / functions / variables / files, PascalCase for modules, trailing ? for boolean predicates, trailing ! for "raises on failure", @specs on public functions. This page only documents the points where the Bliss Framework constrains or extends those defaults.

Casing summary

Used for Casing Example
Filenames (modules, scripts) snake_case.ex / snake_case.exs users.ex, locations_mapper.ex, auth_get_user_by_id_model.ex
Folder names snake_case dhl_locations_factory_backend/, source_addresses/
Module names PascalCase, dotted MyApp.Users, MyApp.SourceAddresses.SourceAddressesMapper
Functions and macros snake_case search_users, get_my_profile_data, inspect_user_action
Predicates returning boolean snake_case? (trailing ?) is_active?, has_permission?, nil_or_empty?, sees_admin_page?
Raising variants snake_case! (trailing !) fetch_env!, decode!
Variables and parameters snake_case user_id, ctx, search_text, page_size
Atoms (incl. error reasons) :snake_case :ok, :not_found, :create_location_error
Module attributes @snake_case @common_expiration, @pubsub_mod, @repo
Compile-time constants @SCREAMING_SNAKEdon't. Use @snake_case @common_expiration, not @COMMON_EXPIRATION
Struct fields snake_case keys %UserContext{user_id: ..., request_id: ...}
ETS / cache / PubSub topic strings snake_case with : separators "user_preferred_language:#{user_id}", "permission_changes"
Database error atoms (domain-translated) :snake_case_error :search_locations_error, :ga_already_used

A filename is the snake_case of its module name. MyApp.Locations.LocationsMapper lives at lib/my_app/locations/locations_mapper.ex — always.

Top-level namespaces

Two namespaces per project, no more:

Namespace Role What lives here
MyApp.* Business application Phoenix contexts, mappers, business structs, supervision tree, Repo, Consts
MyAppWeb.* HTTP / Channels / I/O Endpoint, Router, Controllers, Plugs, Views, Channels, web-only helpers
Database.* Generated provider layer DbContext, Models.*, Processors.* (all autogenerated by db-gen)
Models.* Cross-cutting structs UserContext, ADUser, Ident.User, CSV row models
Helpers.* Cross-cutting utilities DateHelpers, StringHelpers, MapHelpers, LoggingHelpers, ...

Database.*, Models.*, and Helpers.* deliberately have no app-name prefix — they are shared infrastructure and would be hoisted into a separate library on the day we extract one. Resist the urge to "make them more specific" by renaming to MyApp.Helpers.*; the lack of prefix is the convention.

If you need a feature-specific helper that isn't generic enough for Helpers.*, scope it to the context: MyApp.Locations.LocationsMapper, MyApp.Users.UserMappers. Not Helpers.LocationHelpers.

Contexts — MyApp.<Subject>

The Management layer. One module per business subject, named after the subject in plural form (matching the general singular/plural rule):

Module Folder Subject
MyApp.Users lib/my_app/users/users.ex Users
MyApp.Locations lib/my_app/locations/locations.ex Locations
MyApp.SourceAddresses lib/my_app/source_addresses/source_addresses.ex Source addresses
MyApp.GlobalAddresses lib/my_app/global_addresses/global_addresses.ex Global addresses
MyApp.Consts lib/my_app/consts.ex Cached lookup data

A context module file has the same base name as the folder it lives in (locations/locations.ex). Supporting modules go alongside it:

lib/my_app/locations/
├── locations.ex                 # MyApp.Locations — the context
├── locations_mapper.ex          # MyApp.Locations.LocationsMapper
├── clusters.ex                  # MyApp.Locations.Clusters — sub-subject
├── clusters_mapper.ex
├── clusters_locations.ex        # MyApp.Locations.ClustersLocations — composite
└── clusters_locations_mapper.ex

When a subject grows a sub-subject

When a single context gets too big, split by the secondary concept the same way the general hierarchy rule does: MyApp.Locations.Clusters is allowed because Cluster is its own subject within Locations. The composite-relation table (clusters_locations) gets its own module too: MyApp.Locations.ClustersLocations.

What is not allowed: splitting one context into "Reads" and "Writes" modules, or by HTTP verb, or by "internal" vs "public". A context's public API is the set of functions in the module; if it's getting unwieldy, the answer is a sub-subject, not a horizontal split.

Controllers — MyAppWeb.<Subject>Controller

Pattern Example
MyAppWeb.<SubjectPlural>Controller for collection endpoints MyAppWeb.GlobalAddressController (matches /api/global-addresses)
MyAppWeb.<DomainAction>Controller for verb-shaped endpoints MyAppWeb.AddressesExportController, MyAppWeb.AuthController
Always action_fallback MyAppWeb.FallbackController translates {:error, _} to HTTP responses in one place
One controller file per route scope me_controller.ex, auth_controller.ex, system_controller.ex

Standard action names follow Phoenix defaults: index, show, create, update, delete. For non-CRUD verbs use a snake_case action name that matches the route: def export_global_addresses(conn, params), def set_current_locale(conn, params). Don't invent do_* / handle_* prefixes.

The body of an action is ctx = user_ctx(conn) + one with chain + render(...). If a controller action is more than ~15 lines, the missing work belongs in the context.

Plugs — MyAppWeb.Plugs.<Purpose> or top-level MyAppWeb.<Purpose>Plug

Two acceptable shapes, pick one project-wide:

  • MyAppWeb.Plugs.Authentication, MyAppWeb.Plugs.Authorization, MyAppWeb.Plugs.RateLimiter — grouped under Plugs.*.
  • MyAppWeb.RequestIdPlug, MyAppWeb.LocalePlug, MyAppWeb.LanguagesPlug — bare with a Plug suffix.

Each plug does one thing — the name is the thing. No MyAppWeb.RequestPlug that does five different things.

Structs and types

A struct module defines defstruct, @enforce_keys, and a t() typespec:

defmodule Models.UserContext do
  @enforce_keys [:user_id, :username]
  defstruct [
    :user_id, :user_oid, :username, :user,
    :ip, :user_agent, :origin,
    :locale, :request_id
  ]

  @type t() :: %__MODULE__{
          user_id: pos_integer(),
          user_oid: binary(),
          username: binary(),
          user: Ident.User.t(),
          ip: binary(),
          user_agent: binary(),
          origin: binary(),
          locale: binary(),
          request_id: binary()
        }
end
Rule Why
@enforce_keys lists every field that has no sensible default Catches partial construction at compile time
@type t() :: %__MODULE__{...} is mandatory on cross-module structs Dialyzer / @spec callers reference it
@derive Jason.Encoder on structs returned to a JSON view View can encode without manual mapping
use Accessible on DB result models Lets callers use model[:field] access uniformly
No :default for unrequired fields unless the default is nil Surprises down the line; prefer explicit construction

Generated DB models follow the same shape but with @fields factored out:

defmodule Database.Models.AuthGetUserByIdModel do
  @fields [:user_id, :code, :uuid, :username, :email, :display_name]
  @enforce_keys @fields

  @derive Jason.Encoder
  defstruct @fields

  @type t() :: %__MODULE__{...}

  use Accessible
end

The @fields-then-@enforce_keys-then-defstruct triple is the project standard — don't break the pattern in hand-written models either.

Functions — verbs and shapes

The general Bliss verb registry applies. The Elixir shapes:

Verb Returns Example
get_* One row as a struct/map, or nil if absent (or the tuple shape) get_user(ctx, id), get_location_detail_by_id(ctx, id)
get_*! One row, raises if absent Repo.get!, Application.fetch_env!
search_* Paged list, takes filters + page + page_size search_users(ctx, text, page, page_size), search_locations(ctx, ...)
list_* Unpaged collection (small, bounded) list_source_addresses(ctx, params), list_locations_geo(ctx, ...)
create_* Insert; returns the new row (or :ok) create_location(ctx, item), create_manual_validation(ctx, ...)
update_* Update; returns the updated row (or :ok) update_location(ctx, id, data)
delete_* Delete; returns :ok or the deleted row delete_location(ctx, id)
ensure_* Idempotent upsert ensure_user_groups_and_permissions(user_id, uuids)
set_*_as_* Categorical state change set_location_address_as_primary(ctx, id, la_id)
enable_* / disable_* Flip a is_active-shaped field enable_user, disable_user_group
is_*? / has_*? / can_*? / should_*? Pure boolean predicate has_all_roles?, sees_admin_page?, nil_or_empty?
validate_* Boundary check; returns tuple, never raises across context boundary validate_provider_is_active, validate_token
verify_* Postcondition / claim assertion verify_user_identity
process_* Multi-step batch process_external_group_members
parse_* Input parsing parse_date, parse_socket_auth_token
map_* Side-layer mapper function map_items, map_item_detail, map_user_from_ad
inspect_* Returns formatted log payload inspect_user_action(:search_users)

The shape rules:

  • Boolean-returning functions end in ?. Always. has_permission?, not has_permission. The general Bliss rule says "predicates start with is/has/can/should"; the Elixir version is "and they end in ?".
  • Functions that raise on failure end in !. Reserve ! for the actual raising variant. Don't decorate every function with ! to mean "important".
  • map_* is reserved for mappers (data transformation, no I/O). Don't name a "fetch + transform" function map_* just because it ends in a transformation.
  • No do_* / _helper suffix on private functions. Use defp and a clear name. The privacy is communicated by defp, not by a _ prefix on the name.

Two-arity variants

When a function operates on the calling user by default but can be called for another user, define both arities:

def get_user_assigned_groups(ctx),        do: DbContext.auth_get_user_assigned_groups(ctx.user_id, ctx.user_id)
def get_user_assigned_groups(ctx, user_id), do: DbContext.auth_get_user_assigned_groups(ctx.user_id, user_id)

Same name, different arity, related semantics. Don't introduce get_my_assigned_groups + get_user_assigned_groups for the same operation.

"My" prefix

Use get_my_* / update_my_* when the function is specifically and only about the calling user (and the caller cannot vary it): get_my_profile_data(ctx), get_my_global_id(access_token, request_id). Don't decorate every context call with my_ just because it takes a ctx.

Parameters

ctx is the first argument

UserContext.t() (aliased everywhere as ctx) is the first parameter of every context function. Always. Even if today's body doesn't read every field, the next variant will, and consistency at the call site matters more than parameter parsimony.

def search_locations(ctx, search_text, address_filters, search_filters, page, page_size)
def get_location_detail_by_id(ctx, item_id)
def update_location(ctx, item_id, update_data)

Parameter order

After ctx, parameters go in this order:

  1. Identifier of the entity being acted uponitem_id, user_id, location_id.
  2. Required datasearch_text, update_data, the entity to insert.
  3. Filters and optionsaddress_filters, search_filters, filters.
  4. Pagination, lastpage, page_size.

This mirrors the PostgreSQL parameter order rule. When passing through to DbContext, the SQL function's parameter order already follows the same convention, so the wrapper is a direct passthrough.

Default arguments

Use \\ default for optional parameters with a sensible default. Use the :eg_value_not_provided sentinel only in autogenerated DbContext wrappers — never in hand-written context code. In a context, the right shape is:

def search_notifications(ctx, filters \\ %{}, page \\ nil, page_size \\ nil) do
  DbContext.search_notifications(ctx.user_id, ctx.locale, filters[:search_text], page, page_size)
end

nil means "not provided" for optional database arguments; let the SQL function decide what to do with it.

Standard parameter names

A small ubiquitous vocabulary:

Parameter Type Meaning
ctx UserContext.t() The actor / request context
item_id, user_id, <entity>_id pos_integer() The thing being acted on
params map() Raw controller params (only at the controller / context boundary)
filters map() Search filters bundle
page, page_size pos_integer() \| nil Pagination
search_text binary() \| nil Whisper / search input
correlation_id / request_id binary() Threaded into logs and outbound headers
access_token binary() For calls to external identity providers
locale binary() Two-letter language code

Aliases and imports

At the top of every module, in this order, blank line between groups:

defmodule MyApp.Users do
  alias Helpers.LoggingHelpers
  alias Models.{ADUser, UserContext}
  alias Database.DbContext
  alias MyApp.Users.{GraphApi, UserMappers}

  require Logger

  use LoggingHelpers
end

Result (from the :result package) is used so often it doesn't need a project-level aliasResult.ok/1, Result.error/1, Result.map/2, Result.and_then/2 read fine fully-qualified. Add alias only if you import a submodule like Result.Ok.

  1. alias lines first, grouped by origin (external libs, then Models.*, then Database.*, then same-app modules).
  2. import and require next (usually only require Logger).
  3. use last (macros that expand into definitions).

Prefer alias over fully-qualified module names in the body — but only for modules you actually use more than once. A single-use module stays fully qualified.

alias Models.{ADUser, UserContext} is the project-preferred shape when you need three or more aliases from the same parent. For two or fewer, write them on separate lines.

Result type — naming side of the contract

The architectural rules for the result type live in Result type and error handling in the index — the Bliss default is the :result package: tagged tuples {:ok, value} / {:error, reason} always constructed with Result.ok/1 / Result.error/1 and composed with Result.map/2 / Result.and_then/2 / Result.map_error/2. Don't write raw tuple literals as the output of a function in your own code.

The naming-side rules:

Position Type Naming rule
Success payload (inside {:ok, _}) Operation-class shape Mapped entity, list of entities, paged map %{items, count, pages, page_size}, nil, or boolean() — see the shape table. Never a :no_data / :nothing / :empty sentinel atom.
Error reason (inside {:error, _}) Atom (default) or {atom, map} Call-site-specific, snake_case, named in the caller's vocabulary. :ga_already_used, :location_not_found, :create_location_error. Never a string, raw library struct, or unstructured nested tuple. Use {:reason_atom, %{metadata_map}} only when a caller actually destructures the metadata.
Constructor at call site Result.ok/1 / Result.error/1 Use the constructors, not raw {:ok, _} / {:error, _} literals, in your own code. Constructors are greppable and let :result's combinators thread through.
Combinator at call site Result.map/2 / Result.and_then/2 / Result.map_error/2 / Result.with_default/2 / Result.catch_error/3 / Result.perform/2 Reach for the named combinator before reaching for case. A case on a result is fine as a leaf operation; a case ladder is a sign that combinators would read better.
Function @spec return type Result.t(error, value) The :result package exports Result.t(error_type, value_type). Use it. Don't expand it into {:ok, value} \| {:error, reason} at every call site.
Constructor as function reference &Result.ok/1 or &Result.Ok.of/1 Both work; both produce {:ok, value}. The flat form is shorter; the submodule form reads as "lift via the Ok constructor" for FP-style readers and for code that takes a constructor module as a parameter.

The Bliss verb registry maps directly onto success payload shapes:

Function name shape Returns inside {:ok, _}
get_<entity>(ctx, id) One entity (struct/map). Missing → Result.error(:<entity>_not_found).
get_<entities>(ctx, ...) / list_<entities>(...) List of entities. Empty list is success: Result.ok([]).
search_<entities>(ctx, ..., page, page_size) A map: %{items: [...], count: n, pages: p, page_size: s}.
create_<entity>(ctx, ...) The new entity, or %{<entity>_id: id} when only the id matters.
update_<entity> / delete_<entity> / set_<entity>_as_<state> / enable_<entity> / disable_<entity> nil (use Result.ok(nil)). Pick one project-wide convention if you instead want bare :ok for no-payload success.
ensure_<entity>(ctx, ...) The (existing or newly created) entity.
is_<thing>? / has_<thing>? returning Result.t() instead of bare boolean() Result.ok(true) / Result.ok(false). Most predicates should stay bare boolean(); only wrap when the operation can also fail (e.g. DB error during the check).

Logging

One shape, project-wide, via Helpers.LoggingHelpers:

{:error, reason} ->
  Logger.error("Error occurred while searching locations",
    reason: inspect(reason),
    detail: inspect_user_action(:search_locations)
  )

  Result.error(:search_locations_error)
  • Message starts with "Error occurred while …" / "Could not …" / a present-tense gerund clause describing the failed operation. No exclamation marks, no stack traces in the message text.
  • reason: inspect(reason)inspect/1 because reasons can be tuples, structs, or nested.
  • detail: inspect_user_action(:atom) — the macro from Helpers.LoggingHelpers injects user_id, username, and the action atom. Don't hand-construct the equivalent map.
  • Add metadata: inspect(metadata) only when there's structured context worth keeping (HTTP response metadata, batch counters). Don't pad logs with empty maps.

Log levels: Logger.error for "the operation failed", Logger.warning for "degraded but proceeding", Logger.info for state transitions worth recording, Logger.debug for tracing during development. Production runs at :info by default.

PubSub topic strings

Topic naming follows the format <concept>:<id> with optional :<sub-concept> segments:

"user_preferred_language:#{user_id}"
"permission_changes"
"change_request:#{change_request_id}:vote"

Topics live in a <Subject>.PubSub module that owns the topic string construction and the broadcast functions:

defmodule MyApp.Users.PubSub do
  alias Phoenix.PubSub
  @pubsub_mod MyApp.PubSub

  def broadcast_user_preferred_language_changed(user_id, language_code) do
    PubSub.broadcast(@pubsub_mod, user_preferred_language_topic(user_id),
      {:user_preferred_language_changed, user_id, language_code})
  end

  def user_preferred_language_topic(user_id), do: "user_preferred_language:#{user_id}"
end

The topic-string function is named <event>_topic/N and is part of the module's public API so subscribers don't reach for string interpolation.

Behaviour and protocol naming

Pattern Use
<Concept>Behaviour (suffix Behaviour) Defines a @callback-driven contract
<Concept>Adapter / <Concept>Mock Implementations of a Behaviour
<Concept> (a defprotocol) Polymorphic dispatch on the first argument's type

Don't introduce a Behaviour for "we might want to swap this someday". Introduce it when there is a second implementation, or when the test suite needs an explicit mock contract.

File header

Every module has at most a brief @moduledoc:

defmodule MyApp.Locations do
  @moduledoc """
  Context for making basic CRUD operations for Locations entries
  """
  ...
end
  • One sentence. Reads like "Context for …", "Helper for …", "Mapper for …", "Provides …".
  • @moduledoc false when the module is internal (e.g. Application, autogenerated processors). Don't leave the placeholder triple-quoted string from mix phx.new.
  • No "this file is part of MyApp" boilerplate. The module name says it.

Comments

Default to writing no comment. Add one only when the why is non-obvious — a hidden constraint, a workaround for a quirk in a third-party library, the rationale for a particular default. Don't restate the function body in English. The autogenerated @doc "Calls database function auth.create_api_key" strings are fine because they survive code generation; don't paraphrase them into a context module.

Commented-out code blocks (TODO scaffolding, abandoned alternatives) are noise. Either ship the code, file an issue, or delete it.

Migrations and database wrappers — see PostgreSQL

The Database.DbContext, Database.Models.*, and Database.Processors.* modules are autogenerated from the SQL function catalogue. Their naming is driven entirely by the PostgreSQL naming conventions:

SQL Elixir
Function auth.get_user_by_id(_user_id, _target_user_id) DbContext.auth_get_user_by_id(user_id, target_user_id) returning {:ok, [%Database.Models.AuthGetUserByIdModel{}]}
Function public.create_journal_message_for_entity(...) DbContext.create_journal_message_for_entity(...)
Function const.get_business_units(_locale) DbContext.const_get_business_units(locale)
Schema prefix in SQL (auth., unsecure., const.) Underscore prefix on the Elixir function (auth_*, unsecure_*, const_*)

Don't rename or wrap a generated function just because you don't like its name. If the name is wrong, fix the SQL function and regenerate.

Anti-patterns

Anti-pattern Why Use instead
Naming a boolean predicate without ? (is_active, has_permission) Loses the Elixir convention for boolean-shape is_active?, has_permission?
do_create_location, internal_create_location for a defp The defp already says private defp create_location_row with a real name
Per-context invented log shape Inconsistent grep / parsing Logger.error("...", reason: inspect(reason), detail: inspect_user_action(:atom))
Raw {:ok, value} / {:error, reason} literals as your own function's output Bypasses the grep index Result.ok / Result.error give; encourages drift Result.ok(value) / Result.error(reason) from :result. Raw tuples are fine when destructuring or inside a provider that wraps a third-party library.
Adopting Simplificator3000.Result in new projects Deprecated as of simplificator_3000 v1.0; struct-based model is no longer the Bliss default :result package. Migrate existing code at the team's pace; don't introduce new dependents.
:error / :db_error / {:error, "string"} as the reason Carries no information for the fallback Result.error(:search_locations_error), Result.error(:ga_already_used), call-site-specific atoms
{:ok, nil} from a get_<entity> to mean "not found" Forces every caller to add is_nil(value) Result.error(:<entity>_not_found), optionally {:error, {:<entity>_not_found, %{<entity>_id: id}}} when a caller destructures
{:ok, :no_data} / {:ok, :nothing} / {:ok, :empty} Sentinel atoms inside the success payload re-invent error reasons Result.error(:<thing>_not_found) for missing; Result.ok([]) for empty collections; Result.ok(nil) for no-payload success
Bare :ok from one path, {:ok, _} from another, for the same function Caller has to handle both shapes Pick the shape the operation class demands and use it on every clause
{:ok} (one-element tuple as success) Pointless — :ok or Result.ok(nil) already say "success, no value" Result.ok(nil) (or bare :ok if that's the project's no-payload convention)
Leaking %Postgrex.Error{} / %Ecto.Changeset{} / %HTTPoison.Error{} as the error reason Controller and view see DB/HTTP types Translate at the context: Result.error(:<domain_atom>) or Result.error({:<domain_atom>, %{...}})
{:error, "Permission denied"} (string reason) Strings can't be pattern-matched safely Result.error(:permission_denied) (put the prose in a translation table or a log line)
{:error, {:validation, [errs]}} (ad-hoc nested-tuple reason) Unstructured, drifts across call sites Result.error({:validation_failed, %{errors: [...]}}) with the metadata under a known map shape
Controller pattern-matching on a %Postgrex.Error{} or other library struct The context is leaking provider shapes past the boundary Fix the context to translate library errors to domain atoms; controller only sees {:ok, _} / {:error, atom}
Hand-written case ladder where two combinators would do Same logic, more places to get the tuple shape wrong Result.map/2 for a pure transform, Result.and_then/2 for the next fallible step, Result.map_error/2 to rewrite the reason
raise across a context boundary Controller must try/rescue Return {:error, reason}
Calling Repo.query directly from a context Bypasses DbContext and the processors Add the SQL function, regenerate, call DbContext.*
Calling another context's private helpers Couples two contexts Extract a Side layer module (Helpers.* or a struct in Models.*)
Storing the current user in the process dictionary Hidden state, hard to test Pass ctx as the first argument
Building UserContext inside a context The context shouldn't know about Plug.Conn Build it in MyAppWeb.ConnHelpers.user_ctx/1
Bare GenServer.start_link outside the supervision tree Orphan processes, no restart Add it as a child of MyApp.Application (or a nested supervisor)
One shared Phoenix.PubSub for everything Topic collisions across domains One PubSub per domain (MyApp.PubSub, Notifications.PubSub, ...)
Hand-editing Database.DbContext / Database.Models.* Lost on next regeneration Fix the SQL function and regenerate
MyApp.Helpers.LocationHelpers for feature-specific code Helpers.* is for generic utilities MyApp.Locations.LocationsMapper or a private function in the context
MyAppWeb modules calling other MyAppWeb modules across feature boundaries Web layer becomes its own dependency graph Move shared logic into MyApp.* (Management) or MyAppWeb.*Helpers (Side)
@SCREAMING_SNAKE module attributes Not the Elixir convention @snake_case
Action name not matching the route verb Reader has to cross-reference router and controller def export_global_addresses for get "/export/global-addresses"
Committing config/.local.exs or config/<env>.local.exs Leaks personal overrides; defeats the purpose Add both patterns to .gitignore; keep checked-in defaults in config.exs / <env>.exs
Hardcoding production secrets in prod.exs Secrets in the binary / VCS Read with System.fetch_env! / System.get_env \|\| raise in runtime.exs
runtime.exs without the if config_env() == :prod do guard Dev/test boots fail when a prod-only env var is absent Wrap the prod-specific block; let dev/test use <env>.exs + .local.exs
System.get_env("FOO") for a value the app cannot run without Silent nil, failure at first use, no hint to the operator System.fetch_env!("FOO") or System.get_env("FOO") \|\| raise """...""" with an example value
Reading config with Application.get_env at compile time via @var Survives compile-time, ignores runtime.exs overrides Read at the point of use; or use Application.compile_env/2 only when truly needed (macros, guards)
Hand-checked sentinel like "CHANGE_ME", "TODO", "XXX" in checked-in config Inconsistent grep, easy to ship Project-wide single sentinel ("FILL_ME_UP")

Worked example — a complete context function

The shape every context function follows:

defmodule MyApp.Locations do
  @moduledoc "Context for making basic CRUD operations for Locations entries"

  alias MyApp.Locations.LocationsMapper
  alias Models.UserContext
  alias Database.DbContext

  use Helpers.LoggingHelpers
  require Logger

  @spec create(UserContext.t(), map()) :: Result.t(atom(), %{location_id: pos_integer()})
  def create(ctx, item) do
    DbContext.create_location(
      ctx.username, ctx.user_id,
      item.primary_address.global_address_id,
      item.location_type_code,
      item.business_unit_code,
      item.primary_address.title,
      item.title,
      item.location_geom,
      nil
    )
    |> case do
      {:ok, [row]} ->
        Result.ok(%{location_id: row.location_id})

      {:error, %Postgrex.Error{postgres: %{pg_code: "69101"}}} ->
        Result.error(:ga_already_used)

      {:error, reason} ->
        Logger.error("Error occurred while creating location",
          reason: inspect(reason),
          detail: inspect_user_action(:create_location)
        )
        Result.error(:create_location_error)
    end
  end
end

And its matching controller action:

defmodule MyAppWeb.LocationController do
  use MyAppWeb, :controller

  alias MyApp.Locations

  action_fallback MyAppWeb.FallbackController

  def create(conn, %{"location" => params}) do
    ctx = user_ctx(conn)

    with {:ok, data} <- Locations.create(ctx, params) do
      conn
      |> put_status(:created)
      |> render("show.json", location: data)
    end
  end
end

Five things in five places: the context decides; the mapper shapes; the provider does; the controller binds it to HTTP; the fallback translates failure into an HTTP response. Each module owns its slice.

See also