<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Phoenix-Pubsub on Buro notes</title><link>https://notes.burelli.xyz/tags/phoenix-pubsub/</link><description>Recent content in Phoenix-Pubsub on Buro notes</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Sun, 25 Oct 2026 18:50:18 +0100</lastBuildDate><atom:link href="https://notes.burelli.xyz/tags/phoenix-pubsub/index.xml" rel="self" type="application/rss+xml"/><item><title>How I design a CRUD module in Elixir</title><link>https://notes.burelli.xyz/posts/engineering/elixir/crud/</link><pubDate>Sun, 25 Oct 2026 15:21:12 +0100</pubDate><guid>https://notes.burelli.xyz/posts/engineering/elixir/crud/</guid><description>&lt;h1 id="how-i-design-a-crud-module-in-elixir"&gt;How I design a CRUD module in Elixir&lt;/h1&gt;&#10;&lt;p&gt;Most resources need the same operations: get, list, count, create, update, delete. The pattern I use:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;a &lt;strong&gt;behaviour&lt;/strong&gt; that every CRUD module implements;&lt;/li&gt;&#10;&lt;li&gt;a set of &lt;strong&gt;macros&lt;/strong&gt; with the default implementation of each operation;&lt;/li&gt;&#10;&lt;li&gt;one &lt;strong&gt;standalone CRUD module per resource&lt;/strong&gt;, mixing default and custom functions;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;application errors&lt;/strong&gt; only, never a &lt;code&gt;nil&lt;/code&gt; or a raw changeset;&lt;/li&gt;&#10;&lt;li&gt;an &lt;strong&gt;event&lt;/strong&gt; for every operation that changes the data.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="the-behaviour"&gt;The behaviour&lt;/h2&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-elixir" data-lang="elixir"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;defmodule&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;MyApp.Crud&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;do&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;alias&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;MyApp.Errors.Error&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@callback&lt;/span&gt; get(id &lt;span style="color:#f92672"&gt;::&lt;/span&gt; term(), opts &lt;span style="color:#f92672"&gt;::&lt;/span&gt; keyword()) &lt;span style="color:#f92672"&gt;::&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#e6db74"&gt;:ok&lt;/span&gt;, struct()} &lt;span style="color:#f92672"&gt;|&lt;/span&gt; {&lt;span style="color:#e6db74"&gt;:error&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Error&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;t()}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@callback&lt;/span&gt; list(params &lt;span style="color:#f92672"&gt;::&lt;/span&gt; map(), opts &lt;span style="color:#f92672"&gt;::&lt;/span&gt; keyword()) &lt;span style="color:#f92672"&gt;::&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#e6db74"&gt;:ok&lt;/span&gt;, [struct()]} &lt;span style="color:#f92672"&gt;|&lt;/span&gt; {&lt;span style="color:#e6db74"&gt;:error&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Error&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;t()}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@callback&lt;/span&gt; list_query(query &lt;span style="color:#f92672"&gt;::&lt;/span&gt; struct()) &lt;span style="color:#f92672"&gt;::&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Ecto.Query&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;t()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@callback&lt;/span&gt; count(params &lt;span style="color:#f92672"&gt;::&lt;/span&gt; map()) &lt;span style="color:#f92672"&gt;::&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#e6db74"&gt;:ok&lt;/span&gt;, non_neg_integer()} &lt;span style="color:#f92672"&gt;|&lt;/span&gt; {&lt;span style="color:#e6db74"&gt;:error&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Error&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;t()}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@callback&lt;/span&gt; count_query(query &lt;span style="color:#f92672"&gt;::&lt;/span&gt; struct()) &lt;span style="color:#f92672"&gt;::&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Ecto.Query&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;t()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@callback&lt;/span&gt; create(attrs &lt;span style="color:#f92672"&gt;::&lt;/span&gt; map()) &lt;span style="color:#f92672"&gt;::&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#e6db74"&gt;:ok&lt;/span&gt;, struct()} &lt;span style="color:#f92672"&gt;|&lt;/span&gt; {&lt;span style="color:#e6db74"&gt;:error&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Error&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;t()}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@callback&lt;/span&gt; update(entity_or_id &lt;span style="color:#f92672"&gt;::&lt;/span&gt; struct() &lt;span style="color:#f92672"&gt;|&lt;/span&gt; term(), attrs &lt;span style="color:#f92672"&gt;::&lt;/span&gt; map()) &lt;span style="color:#f92672"&gt;::&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#e6db74"&gt;:ok&lt;/span&gt;, struct()} &lt;span style="color:#f92672"&gt;|&lt;/span&gt; {&lt;span style="color:#e6db74"&gt;:error&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Error&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;t()}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@callback&lt;/span&gt; delete(entity_or_id &lt;span style="color:#f92672"&gt;::&lt;/span&gt; struct() &lt;span style="color:#f92672"&gt;|&lt;/span&gt; term()) &lt;span style="color:#f92672"&gt;::&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#e6db74"&gt;:ok&lt;/span&gt;, struct()} &lt;span style="color:#f92672"&gt;|&lt;/span&gt; {&lt;span style="color:#e6db74"&gt;:error&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Error&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;t()}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@callback&lt;/span&gt; topic(query &lt;span style="color:#f92672"&gt;::&lt;/span&gt; map()) &lt;span style="color:#f92672"&gt;::&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;String&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;t()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@callback&lt;/span&gt; topics(entity &lt;span style="color:#f92672"&gt;::&lt;/span&gt; struct()) &lt;span style="color:#f92672"&gt;::&lt;/span&gt; [&lt;span style="color:#a6e22e"&gt;String&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;t()]&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@callback&lt;/span&gt; publish_event(result, action &lt;span style="color:#f92672"&gt;::&lt;/span&gt; atom(), opts &lt;span style="color:#f92672"&gt;::&lt;/span&gt; keyword()) &lt;span style="color:#f92672"&gt;::&lt;/span&gt; result&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;when&lt;/span&gt; &lt;span style="color:#e6db74"&gt;result&lt;/span&gt;: {&lt;span style="color:#e6db74"&gt;:ok&lt;/span&gt;, struct()} &lt;span style="color:#f92672"&gt;|&lt;/span&gt; {&lt;span style="color:#e6db74"&gt;:error&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Error&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;t()} &lt;span style="color:#f92672"&gt;|&lt;/span&gt; struct()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;end&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;&#10;&lt;li&gt;Every CRUD module has the same API, whatever its implementation.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;update/2&lt;/code&gt; and &lt;code&gt;delete/1&lt;/code&gt; take the entity or its id.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;count_query/1&lt;/code&gt; turns a validated query into an &lt;code&gt;Ecto.Query&lt;/code&gt; with the filters, used by &lt;code&gt;count/1&lt;/code&gt;. &lt;code&gt;list_query/1&lt;/code&gt; adds what only the list needs (e.g. the order), used by &lt;code&gt;list/2&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;topic/1&lt;/code&gt;, &lt;code&gt;topics/1&lt;/code&gt; and &lt;code&gt;publish_event/3&lt;/code&gt; are for the &lt;a href="#events"&gt;events&lt;/a&gt;.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="the-default-implementation-one-macro-per-operation"&gt;The default implementation: one macro per operation&lt;/h2&gt;&#10;&lt;p&gt;&lt;code&gt;use MyApp.Crud&lt;/code&gt; stores the options in a &lt;code&gt;@crud_config&lt;/code&gt; map. Then each macro defines one function of the behaviour, as a call to a plain &lt;code&gt;do_*&lt;/code&gt; function with the config:&lt;/p&gt;</description></item></channel></rss>