<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Byron Salty</title>
    <description>The latest articles on DEV Community by Byron Salty (@byronsalty).</description>
    <link>https://dev.to/byronsalty</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F976635%2Fcbd32d54-eb39-4cbe-81f7-000df3554a8c.jpeg</url>
      <title>DEV Community: Byron Salty</title>
      <link>https://dev.to/byronsalty</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/byronsalty"/>
    <language>en</language>
    <item>
      <title>A little Rust proxy for Ollama</title>
      <dc:creator>Byron Salty</dc:creator>
      <pubDate>Sun, 30 Mar 2025 23:19:18 +0000</pubDate>
      <link>https://dev.to/byronsalty/a-little-rust-proxy-for-ollama-4kj3</link>
      <guid>https://dev.to/byronsalty/a-little-rust-proxy-for-ollama-4kj3</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Recently, while exploring the new Zed IDE, I encountered a need to integrate it with a self-hosted Ollama model. Zed allows for custom LLM integrations, which is great, but I ran into a small hiccup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem Description
&lt;/h2&gt;

&lt;p&gt;The challenge arose because Ollama's configuration in Zed requires a connection to &lt;code&gt;localhost&lt;/code&gt;. However, I have Ollama running on my GPU-enabled home server, not on my local machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution Approach
&lt;/h2&gt;

&lt;p&gt;To bridge this gap, I devised a simple solution: a Rust script that acts as a proxy. This script forwards the Ollama port from my server to the local machine, allowing Zed to connect seamlessly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation Details
&lt;/h2&gt;

&lt;p&gt;Here's a brief walkthrough of the Rust script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;hyper&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;service&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;make_service_fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;service_fn&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;hyper&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Infallible&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;net&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;SocketAddr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;hyper&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Create a client to send the forwarded request.&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Build a new URI using the hostname "ace" and the appropriate port.&lt;/span&gt;
    &lt;span class="c1"&gt;// Here, we assume your Ollama server is also listening on port 11434.&lt;/span&gt;
    &lt;span class="c1"&gt;// Adjust the port in the format string if it's different.&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;uri_string&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http://ace:11434{}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="nf"&gt;.uri&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;new_uri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;uri_string&lt;/span&gt;&lt;span class="nf"&gt;.parse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Failed to parse URI"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Replace the URI in the incoming request with the new one.&lt;/span&gt;
    &lt;span class="c1"&gt;// This effectively "redirects" the request to your remote server.&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;proxied_request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;proxied_request&lt;/span&gt;&lt;span class="nf"&gt;.uri_mut&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_uri&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Forward the request to the remote server.&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="nf"&gt;.request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proxied_request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;#[tokio::main]&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nb"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;dyn&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;error&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Send&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Sync&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Bind to local port 11434.&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;addr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;SocketAddr&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="mi"&gt;127&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;11434&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;make_svc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;make_service_fn&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;_conn&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// For each connection, we create a service to handle the request.&lt;/span&gt;
        &lt;span class="nn"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Infallible&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;service_fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;addr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.serve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;make_svc&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Reverse proxy running on http://{}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;addr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Run the server until it's stopped.&lt;/span&gt;
    &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script efficiently handles the port forwarding with no noticeable performance penalty, making the integration smooth and reliable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This quick Rust script solved the integration challenge, enabling me to use Zed with my self-hosted Ollama model effortlessly. It's a testament to how small, targeted scripts can effectively solve specific problems without overcomplicating the setup.&lt;/p&gt;

&lt;p&gt;To me this is a very interesting implication of the new world of AI tooling. I just used some simple prompts and had a working solution to a problem in a few minutes. I'm sure I could have taken the time to make &lt;code&gt;caddy&lt;/code&gt; or &lt;code&gt;nginx&lt;/code&gt; or something else work, but custom built software for a specific need is also an option nowadays. &lt;/p&gt;

</description>
      <category>rust</category>
      <category>zed</category>
      <category>llm</category>
    </item>
    <item>
      <title>A Simple Test HTTP server from Flask</title>
      <dc:creator>Byron Salty</dc:creator>
      <pubDate>Fri, 16 Aug 2024 04:26:33 +0000</pubDate>
      <link>https://dev.to/byronsalty/a-simple-test-http-server-from-flask-3lf9</link>
      <guid>https://dev.to/byronsalty/a-simple-test-http-server-from-flask-3lf9</guid>
      <description>&lt;p&gt;Last night I found myself needing to update some app code to switch from a synchronous http call to an async one. This is not a particularly hard problem but it can be tricky to get correct. &lt;/p&gt;

&lt;p&gt;What I needed therefore was a way to test these http calls, and the machine I was using didn't already have any http servers running to use as the target. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;No problem: it has Python - and that's all you need.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't Install - Just create
&lt;/h2&gt;

&lt;p&gt;This pattern has been forming for me. For simple tasks, it's often easier to just write a little utility script instead of installing some full blown software to do a job.&lt;/p&gt;

&lt;p&gt;If you need a real server, by all means go for it (but even then I'd suggest Docker if possible). But if you just need a quick endpoint to test with or to solve a single use case then Python is probably your friend. &lt;/p&gt;

&lt;p&gt;I'm saying Python specifically because it's rather ubiquitous with a rich standard library and ecosystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Server code
&lt;/h2&gt;

&lt;p&gt;The idea with this simple server was to help me test the async client calls so I wanted it to wait 5 seconds and then respond. I just wanted it to respond to any standard call with a 200 after 5 seconds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;defaults&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;PUT&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;DELETE&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;PATCH&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/&amp;lt;path:path&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;PUT&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;DELETE&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;PATCH&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;catch_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Path: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Headers: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Params: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Data: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Wait for 5 seconds
&lt;/span&gt;    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;yessir&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.0.0.0&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>python</category>
      <category>flask</category>
      <category>testing</category>
      <category>http</category>
    </item>
    <item>
      <title>Adding Embeddings to a Phoenix App</title>
      <dc:creator>Byron Salty</dc:creator>
      <pubDate>Mon, 19 Feb 2024 05:35:50 +0000</pubDate>
      <link>https://dev.to/byronsalty/adding-embeddings-to-a-phoenix-app-120a</link>
      <guid>https://dev.to/byronsalty/adding-embeddings-to-a-phoenix-app-120a</guid>
      <description>&lt;p&gt;Let's add OpenAI based Embeddings to a Phoenix app and store the vector in the database so we can use it for similarity searches.&lt;/p&gt;

&lt;p&gt;This resulting code for this article is captured in this sample project:&lt;br&gt;
&lt;a href="https://github.com/byronsalty/questify" rel="noopener noreferrer"&gt;https://github.com/byronsalty/questify&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Specifically this commit:&lt;br&gt;
&lt;a href="https://github.com/byronsalty/questify/commit/cba277ce8104c6f3b29dc2a6f602158ba4d1f62a" rel="noopener noreferrer"&gt;https://github.com/byronsalty/questify/commit/cba277ce8104c6f3b29dc2a6f602158ba4d1f62a&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;Steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add PG Vector to deps&lt;/li&gt;
&lt;li&gt;Add a migration to install Vector db extension&lt;/li&gt;
&lt;li&gt;Add Postgrex.Types&lt;/li&gt;
&lt;li&gt;Configure OpenAI embedding endpoints&lt;/li&gt;
&lt;li&gt;Hit the API endpoint&lt;/li&gt;
&lt;li&gt;Setup your schema for vector types&lt;/li&gt;
&lt;li&gt;Incorporate vectors into your queries&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Add PG Vector to deps
&lt;/h2&gt;

&lt;p&gt;In &lt;code&gt;mix.exs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;      {:pgvector, "~&amp;gt; 0.2.0"},
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Add Migration to install the vector extension
&lt;/h2&gt;

&lt;p&gt;Create a migration to install the PGVector extension:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;defmodule Questify.Repo.Migrations.AddPgVector do
  use Ecto.Migration

  def up do
    execute "CREATE EXTENSION IF NOT EXISTS vector"
  end

  def down do
    execute "DROP EXTENSION vector"
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Add Postgrex.Types
&lt;/h2&gt;

&lt;p&gt;This is so you can use the term &lt;code&gt;vector&lt;/code&gt; in your type definitions. &lt;/p&gt;

&lt;p&gt;Add a file &lt;code&gt;postgrex_types.ex&lt;/code&gt; in your /lib folder with the following contents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Postgrex.Types.define(
  Questify.PostgrexTypes,
  [Pgvector.Extensions.Vector] ++ Ecto.Adapters.Postgres.extensions(),
  []
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add this to your &lt;code&gt;config.exs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;config :questify, Questify.Repo, types: Questify.PostgrexTypes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Configure your app with the OpenAI endpoints
&lt;/h2&gt;

&lt;p&gt;Here are the relevant lines to add to your configuration. I put this into the &lt;code&gt;runtime.exs&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;openai_api_key = System.get_env("OPENAI_API_KEY") ||
  raise """
  environment variable OPENAI_API_KEY is missing
  """

config :questify, :openai,
  openai_api_key: openai_api_key,
  embedding_url: "https://api.openai.com/v1/embeddings",
  embedding_model: "text-embedding-ada-002"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Hit the API
&lt;/h2&gt;

&lt;p&gt;The embedding API will simply take in a text string and will return a JSON response with the embedding information.&lt;/p&gt;

&lt;p&gt;You can hit the API with HTTPoison like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    response =
      HTTPoison.post(
        embedding_url,
        Jason.encode!(%{
          input: text,
          model: Keyword.get(opts, :model, embedding_model)
        }),
        [
          {"Content-Type", "application/json"},
          {"Authorization", "Bearer #{openai_api_key}"}
        ]
      )
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See the whole file here: &lt;a href="https://github.com/byronsalty/questify/blob/cba277ce8104c6f3b29dc2a6f602158ba4d1f62a/lib/questify/embeddings.ex" rel="noopener noreferrer"&gt;https://github.com/byronsalty/questify/blob/cba277ce8104c6f3b29dc2a6f602158ba4d1f62a/lib/questify/embeddings.ex&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Add a vector to one of your schemas
&lt;/h2&gt;

&lt;p&gt;Add a migration like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;defmodule Questify.Repo.Migrations.AddCommandEmbeddings do
  use Ecto.Migration

  def change do
    alter table(:actions) do
      add :embedding, :vector, size: 1536
    end
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then update your schema with a new vector type field like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    field :embedding, Pgvector.Ecto.Vector

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Incorporate Vectors into Queries
&lt;/h2&gt;

&lt;p&gt;You'll need to add this line at the top of an model where you plan to query with PGVector specific functions - importantly including the "distance" ones that we're looking to use.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  import Pgvector.Ecto.Query
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can query like the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  def get_action_by_text(location, text) do
    embedding = Questify.Embeddings.embed!(text)
    min_distance = 1.0

    Repo.all(
      from a in Action,
        order_by: cosine_distance(a.embedding, ^embedding),
        limit: 1,
        where: cosine_distance(a.embedding, ^embedding) &amp;lt; ^min_distance,
        where: a.from_id == ^location.id
    )
  end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Follow along for more ways to add LLM related capabilities into your Phoenix apps.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://byronsalty.com/questify" rel="noopener noreferrer"&gt;See more related content here.&lt;/a&gt;&lt;/p&gt;

</description>
      <category>phoenix</category>
      <category>llm</category>
      <category>embedding</category>
      <category>elixir</category>
    </item>
    <item>
      <title>Adding /blog proxy to a Phoenix app</title>
      <dc:creator>Byron Salty</dc:creator>
      <pubDate>Tue, 06 Feb 2024 05:46:51 +0000</pubDate>
      <link>https://dev.to/byronsalty/adding-blog-proxy-to-a-phoenix-app-1o88</link>
      <guid>https://dev.to/byronsalty/adding-blog-proxy-to-a-phoenix-app-1o88</guid>
      <description>&lt;p&gt;Let's say you have a product you are creating using Elixir/Phoenix but for marketing purposes you also setup a blog on a separate site like Wordpress. &lt;/p&gt;

&lt;p&gt;It makes sense to keep this separation of concerns and let some off the shelf software worry about creating a blog experience, while you work on your primary product. &lt;/p&gt;

&lt;p&gt;However, for user experience and SEO purposes you may want to have your applications appear to come from a single domain. I did a quick search for the importance of a single domain vs subdomains &lt;a href="https://www.ecenica.com/blog/seo-benefits-of-using-one-domain-vs-subdomains/" rel="noopener noreferrer"&gt;and the first article I found&lt;/a&gt; also uses this exact blog use case as their example.&lt;/p&gt;

&lt;p&gt;Today I'll use just a couple bits of code and configuration to make to solve this problem directly in Phoenix. No access to your https proxy (nginx, caddy, etc) required!&lt;/p&gt;




&lt;h1&gt;
  
  
  Goal:
&lt;/h1&gt;

&lt;p&gt;Create a proxy endpoint that will take all traffic to my primary domain mydomain.com/blog and route it over to the actual host of blog.mydomain.com&lt;/p&gt;

&lt;p&gt;One reason I wanted to do this all in Elixir/Phoenix was I like to use Fly.io and their setup is super simple and effective... but I don't have direct access to their load balancer where I might otherwise setup a proxy like this.&lt;/p&gt;

&lt;p&gt;The other thing that is nice about this setup is I will be able to use Elixir to manipulate the upstream request and response, which will come in handy as I make the experience seamless.&lt;/p&gt;




&lt;h1&gt;
  
  
  Step 1: The Proxy
&lt;/h1&gt;

&lt;p&gt;I used the Elixir library &lt;code&gt;reverse_proxy_plug&lt;/code&gt; which has the very nice benefit of not assuming anything about the hosting for the upstream service. For instance, it didn't need to be another Phoenix app or a service running on the same host.&lt;/p&gt;

&lt;p&gt;Add this to your deps in &lt;code&gt;mix.exs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;      {:reverse_proxy_plug, "~&amp;gt; 2.3"},
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you can add this to your &lt;code&gt;router.ex&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  scope "/blog" do
    pipe_through [:browser]
    forward "/", ReverseProxyPlug, upstream: "https://blog.mydomain.com", response_mode: :buffer
  end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that my use case is not high volume so I didn't really worry about different response modes. I believe the &lt;code&gt;:buffer&lt;/code&gt; option works well enough for me here.&lt;/p&gt;

&lt;p&gt;At this point, you should be able to hit your project at /blog and get some html that really came from your upstream service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But there's a problem...&lt;/strong&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  Step 2: Rewrite the content
&lt;/h1&gt;

&lt;p&gt;The problem now is that the html that you will get will not know that you're trying to serve from &lt;code&gt;mydomain.com/blog&lt;/code&gt; and will still have all links etc pointing to &lt;code&gt;blog.mydomain.com&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;No problem, we just need to alter the html response on the way out so that all of these references are updated.&lt;/p&gt;

&lt;p&gt;This was a bit tricky because the &lt;code&gt;conn&lt;/code&gt; struct is very particular about how it can be interacted with, stemming from the fact that HTTP is similarly and correctly picky. It wouldn't make sense if you could alter a response after you've already sent or even began sending it back to the client. So we need to pinpoint the moment where we have the response body but we are allowed to alter it. &lt;/p&gt;

&lt;p&gt;Luckily, Plugs give us this exact ability in the standard function &lt;code&gt;Plug.Conn.register_before_send/2&lt;/code&gt;. This gives us the opportunity to define a function that will be called with the Conn after the response is ready but before it's actually given back to the client.&lt;/p&gt;

&lt;p&gt;We just need to define a custom plug.&lt;/p&gt;

&lt;p&gt;Update the scope and define a new pipeline in &lt;code&gt;router.ex&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  pipeline :transform_blog do
    plug BuddyWeb.Plugs.TransformBlog
  end

  scope "/blog" do
    pipe_through [:transform_blog]
    forward "/", ReverseProxyPlug, upstream: "https://blog.mydomain.com", response_mode: :buffer
  end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here's the entire &lt;code&gt;TransformBlog&lt;/code&gt; plug:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;defmodule BuddyWeb.Plugs.TransformBlog do

  def init(options), do: options

  def call(%Plug.Conn{} = conn, _ \\ []) do
    Plug.Conn.register_before_send(conn, &amp;amp;BuddyWeb.Plugs.TransformBlog.transform_body/1)
  end

  def transform_body(%Plug.Conn{} = conn) do
    case List.keyfind(conn.resp_headers, "content-type", 0) do
      {_, "text/html" &amp;lt;&amp;gt; _} -&amp;gt;
        body =
          conn.resp_body
          |&amp;gt; String.replace("blog.mydomain.com", "mydomain.com/blog")

        %Plug.Conn{conn | resp_body: body}

      type -&amp;gt;
        IO.inspect(type, label: "content type header")
        conn
    end
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Thanks to Curiosum &lt;a href="https://curiosum.com/til/process-phoenix-conn-after-render-response" rel="noopener noreferrer"&gt;for their article&lt;/a&gt; on this using Plugs in this way. My code borrows heavily from their example.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But there's one more problem...&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note:&lt;/em&gt; If you test the code at this point you may notice that you get expected results in &lt;code&gt;curl&lt;/code&gt; or &lt;code&gt;wget&lt;/code&gt; but not in a browser. &lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Dealing with encodings
&lt;/h2&gt;

&lt;p&gt;The reason why browsers give a different response than the command line tools are the &lt;code&gt;accept-encoding&lt;/code&gt; headers are different.&lt;/p&gt;

&lt;p&gt;The command line tools are essentially asking for a response in a text format so our string replace is working and we get the result we expect.&lt;/p&gt;

&lt;p&gt;But browsers will ask for a response in gzip or br encoding. Binary encodings that will only get turned back into text on the end-user's client so our string replace will not work. &lt;/p&gt;

&lt;p&gt;The fix I employed here was to override the &lt;code&gt;accept-encoding&lt;/code&gt; header to force a non-binary response from our upstream service. This will be less efficient but fine for my purposes and scale. &lt;/p&gt;

&lt;p&gt;Update the same &lt;code&gt;TransformBlog&lt;/code&gt; plug to change the request headers on the way in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  def call(%Plug.Conn{} = conn, _ \\ []) do
    Plug.Conn.register_before_send(conn, &amp;amp;BuddyWeb.Plugs.TransformBlog.transform_body/1)
    |&amp;gt; Plug.Conn.update_req_header(
      "accept-encoding",
      "identity",
      fn _ -&amp;gt; "identity" end
    )
  end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;update_req_header()&lt;/code&gt; function asks you to specify both the default value (in case the header doesn't already exist) and a function to manipulate the existing header. In my case, I don't care what the previous header was - I'm just overriding to use &lt;code&gt;identity&lt;/code&gt; which means &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding" rel="noopener noreferrer"&gt;no modification or compression&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;That's all there is to it - one dependency to configure and one plug.&lt;/p&gt;

&lt;p&gt;Happy Proxying~!&lt;/p&gt;

</description>
      <category>seo</category>
      <category>phoenix</category>
      <category>proxy</category>
    </item>
    <item>
      <title>Techniques for Idempotency in seeds.exs</title>
      <dc:creator>Byron Salty</dc:creator>
      <pubDate>Tue, 09 Jan 2024 05:21:19 +0000</pubDate>
      <link>https://dev.to/byronsalty/techniques-for-idempotency-in-seedsexs-591l</link>
      <guid>https://dev.to/byronsalty/techniques-for-idempotency-in-seedsexs-591l</guid>
      <description>&lt;p&gt;Idempotency is an annoying word that makes people think you are an asshole when you use it, at least if they aren't exactly sure what it means. &lt;/p&gt;

&lt;p&gt;But it's such a useful concept that I use it anyway and will just suffer the consequences. &lt;/p&gt;

&lt;p&gt;For the uninitiated, it simply means that I can run the same code over and over and over but the result will always be the same. &lt;/p&gt;

&lt;p&gt;Consider these two versions of a function trying to add tax to a bill:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;defmodule Bill do
  defstruct total: 0.0, subtotal: 0.0
end

defmodule Tax do
  def calc_tax(bill) do
    %Bill{bill | total: bill.total * 1.07}
  end

  def calc_tax_w_subtotal(bill) do
    tax = bill.subtotal * 0.07
    %Bill{bill | total: bill.subtotal + tax}
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both would work fine, if they are only called one time. In both cases, the current total is increased by 7%. The second version is more complex because we have the concept of both a subtotal and a total. &lt;/p&gt;

&lt;p&gt;But the first version is not idempotent. &lt;/p&gt;

&lt;p&gt;A race condition, bad calling code, or any number of other reasons could result in something like the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bill1 = %Bill{total: 10.0}
bill1 = Tax.calc_tax(bill1)
bill1 = Tax.calc_tax(bill1)
bill1.total # 11.449
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;vs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bill2 = %Bill{subtotal: 10.0}
bill2 = Tax.calc_tax_w_subtotal(bill2)
bill2 = Tax.calc_tax_w_subtotal(bill2)
bill2.total # 10.7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the case of the first, the tax compounds because we're only working with a single total and we can't tell if that total already includes tax. In the second case, we can calculate tax as much as we want but it doesn't effect the result after the first time. &lt;/p&gt;




&lt;h1&gt;
  
  
  Where it matters - Seeds
&lt;/h1&gt;

&lt;p&gt;I see the need for idempotency in a lot of places but a very likely place you'll run into and need to care about it is with your &lt;code&gt;seeds&lt;/code&gt; code. &lt;/p&gt;

&lt;p&gt;If you run your seeds multiple times you probably don't want your default users or system categories or whatever created more than once.&lt;/p&gt;

&lt;p&gt;I tried out a new form today to achieve idempotency in my &lt;code&gt;seeds.exs&lt;/code&gt; and I found it cleaner than a simple &lt;code&gt;if&lt;/code&gt; based approach so I thought I'd share. &lt;em&gt;I'd love to hear if others have found better forms.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach one - &lt;code&gt;ecto.reset&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;I suppose should start with the brute force method. If you always run your seeds as part of &lt;code&gt;mix ecto.reset&lt;/code&gt; then you are achieving idempotency by always starting with a blank slate. &lt;/p&gt;

&lt;p&gt;Similarly, you could create another approach in which you try to delete specific entities if they exist, before creating them in the seeds. (We can call that 1B)&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach two - &lt;code&gt;try&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;We could rely on database unique constraints to prevent multiple similar entries. We just try to create every time and ignore any errors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;try do
  Repo.insert!(%Category{
    name: "Books",
  })
rescue
  _ -&amp;gt;
    IO.puts("Already seeded")
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Approach three - &lt;code&gt;if&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;I've done this one a fair amount. It's not too bad, and rather readable, especially if all you want to do is create the entry (but not use it later in the seeds). &lt;/p&gt;

&lt;p&gt;For example, this doesn't look horrible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if Repo.get_by(Category, name: "Books") == nil do
  Repo.insert!(%Category{
    name: "Books",
  })
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But it looks worse if you want to use that entity later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;book_category = 
  if Repo.get_by(Category, name: "Books") == nil do
    Repo.insert!(%Category{
      name: "Books",
    })
  else
    Repo.get_by(Category, name: "Books")
  end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Yes, you could keep the result of the first &lt;code&gt;get_by&lt;/code&gt; but the form doesn't really get much cleaner.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lookup = Repo.get_by(Category, name: "Books")
book_category = 
  if lookup == nil do
    Repo.insert!(%Category{
      name: "Books",
    })
  else
    lookup
  end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Approach four - &lt;code&gt;case&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This is my current favorite way of achieving idempotency and capturing the value for later use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;book_category =
  case Repo.get_by(Category, name: "Books") do
    nil -&amp;gt; Repo.insert!(%Category{
      name: "Books",
    })
    category -&amp;gt; category
  end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Is there something even better that you use?&lt;/p&gt;

&lt;p&gt;Please share!&lt;/p&gt;




&lt;p&gt;If you found this article helpful, show your support with a Like, Comment, or Follow.&lt;/p&gt;

&lt;p&gt;Read more of Byron’s articles about &lt;a href="https://byronsalty.medium.com/list/leadership-37d7992c55ba" rel="noopener noreferrer"&gt;Leadership&lt;/a&gt; and &lt;a href="https://byronsalty.medium.com/list/my-ai-dc4873d307bf" rel="noopener noreferrer"&gt;AI&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Development articles &lt;a href="https://dev.to/byronsalty"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Follow on &lt;a href="https://byronsalty.medium.com/" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;, &lt;a href="https://dev.to/byronsalty"&gt;Dev.to&lt;/a&gt;, &lt;a href="https://twitter.com/byronsalty" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt;, or &lt;a href="https://www.linkedin.com/in/byronsalty/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Elegantly Running multiple validators in Elixir</title>
      <dc:creator>Byron Salty</dc:creator>
      <pubDate>Sun, 07 Jan 2024 23:33:32 +0000</pubDate>
      <link>https://dev.to/byronsalty/elegantly-running-multiple-validators-in-elixir-n1k</link>
      <guid>https://dev.to/byronsalty/elegantly-running-multiple-validators-in-elixir-n1k</guid>
      <description>&lt;p&gt;I need to update some validation code to be elegant and not this chained set of &lt;code&gt;if&lt;/code&gt; statements.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  def validate_guess(prompt, words) do
    if not Enum.all?(words, fn w -&amp;gt; String.trim(w) != "" end) do
      "Please fill in all the blanks"
    else
      if Enum.any?(words, fn w -&amp;gt; String.trim(w) |&amp;gt; String.length() &amp;lt; 3 end) do
        "No words less than 3 letters"
      else
        if Enum.any?(words, fn w -&amp;gt; Regex.match?(~r/\W/, w) end) do
          "Words can only contain letters"
        else
  ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How can we write this better?&lt;/p&gt;




&lt;h1&gt;
  
  
  Step 1 - write some tests
&lt;/h1&gt;

&lt;p&gt;Despite how the code looks, it works. Let's make sure we have tests in place before we refactor so that it continues to work. &lt;/p&gt;

&lt;p&gt;Writing tests will also help refactor the code, because we need to make the interfaces easily testable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  describe "test validations" do
    test "test for blank words" do
      prompt = nil
      words = ["", "dog", "walking", "around", "tree"]
      assert TextHelper.validate_guess(prompt, words) == "Please fill in all the blanks"
    end

    test "test for only letters" do
      prompt = nil
      words = ["some words", "dog", "walking", "around", "tree"]
      assert TextHelper.validate_guess(prompt, words) == "Words can only contain letters"

      words = ["brown1123", "dog", "walking", "around", "tree"]
      assert TextHelper.validate_guess(prompt, words) == "Words can only contain letters"

      words = ["brown!", "dog", "walking", "around", "tree"]
      assert TextHelper.validate_guess(prompt, words) == "Words can only contain letters"
    end

    test "test for no words less than 3 letters" do
      prompt = nil
      words = ["br", "dog", "walking", "around", "tree"]
      assert TextHelper.validate_guess(prompt, words) == "No words less than 3 letters"
    end
  end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Note:&lt;/em&gt; I did end up finding an error where my former code was not detecting (rejecting) words with digits, so a win for unit tests!&lt;/p&gt;




&lt;h1&gt;
  
  
  Step 2 - Rewrite using &lt;code&gt;with&lt;/code&gt; statement
&lt;/h1&gt;

&lt;p&gt;I saw this structure in this project: &lt;br&gt;
&lt;a href="https://github.com/fireproofsocks/spelling_bee/blob/main/lib/spelling_bee.ex" rel="noopener noreferrer"&gt;SpellingBee project&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And here is &lt;a href="https://dev.to/martinthenth/using-elixirs-with-statement-5e36"&gt;another article&lt;/a&gt; talking specifically about using the &lt;code&gt;with&lt;/code&gt; statement for this reason.&lt;/p&gt;

&lt;p&gt;A much cleaner way to combine all of the validations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  def validate_guess(prompt, words) do
    with "" &amp;lt;- validate_blank_words(words),
         "" &amp;lt;- validate_word_characters(words),
         "" &amp;lt;- validate_word_length(words),
         "" &amp;lt;- validate_grammar(prompt, words) do
      ""
    end
  end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Individual validators:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  defp validate_blank_words(words) do
    case Enum.any?(words, fn w -&amp;gt; String.trim(w) == "" end) do
      true -&amp;gt; "Please fill in all the blanks"
      false -&amp;gt; ""
    end
  end
  defp validate_word_length(words) do
    case Enum.any?(words, fn w -&amp;gt; String.trim(w) |&amp;gt; String.length() &amp;lt; 3 end) do
      true -&amp;gt; "No words less than 3 letters"
      false -&amp;gt; ""
    end
  end
  defp validate_word_characters(words) do
    case Enum.any?(words, fn w -&amp;gt; Regex.match?(~r/[\W\d]/, w) end) do
      true -&amp;gt; "Words can only contain letters"
      false -&amp;gt; ""
    end
  end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And all the tests pass:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Finished in 0.08 seconds (0.00s async, 0.08s sync)
3 tests, 0 failures
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Woot!&lt;/p&gt;




&lt;p&gt;If you found this article helpful, show your support with a Like, Comment, or Follow.&lt;/p&gt;

&lt;p&gt;Read more of Byron’s articles about &lt;a href="https://byronsalty.medium.com/list/leadership-37d7992c55ba" rel="noopener noreferrer"&gt;Leadership&lt;/a&gt; and &lt;a href="https://byronsalty.medium.com/list/my-ai-dc4873d307bf" rel="noopener noreferrer"&gt;AI&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Development articles &lt;a href="https://dev.to/byronsalty"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Follow on &lt;a href="https://byronsalty.medium.com/" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;, &lt;a href="https://dev.to/byronsalty"&gt;Dev.to&lt;/a&gt;, &lt;a href="https://twitter.com/byronsalty" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt;, or &lt;a href="https://www.linkedin.com/in/byronsalty/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Creating an Elixir library for Spell Checking</title>
      <dc:creator>Byron Salty</dc:creator>
      <pubDate>Sun, 07 Jan 2024 19:48:24 +0000</pubDate>
      <link>https://dev.to/byronsalty/creating-an-elixir-library-for-spell-checking-feo</link>
      <guid>https://dev.to/byronsalty/creating-an-elixir-library-for-spell-checking-feo</guid>
      <description>&lt;p&gt;Instead of directly incorporating my &lt;a href="https://dev.to/byronsalty/creating-an-efficient-spellchecker-in-elixir-5a8d"&gt;simple spell checking code&lt;/a&gt; into my project, why not turn this into a library that I could more easily use in other places as well as share publicly.&lt;/p&gt;

&lt;p&gt;I know that some other libraries exist but none appear to be a standard, and since I'm really doing this as an experiment forgive me for re-inventing the wheel a bit.&lt;/p&gt;

&lt;p&gt;I'm following this &lt;a href="https://hexdocs.pm/elixir/main/library-guidelines.html#content" rel="noopener noreferrer"&gt;official guide for creating a library&lt;/a&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  Create a Project
&lt;/h1&gt;

&lt;p&gt;Let's start with simply creating a new library.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ mix new spell_chex
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I prefer an evolutionary and iterative style of development so before I worry about the complexity of a &lt;code&gt;GenServer&lt;/code&gt; in my final spell checker build, I'll create the project with the naive implementation (&lt;a href="https://dev.to/byronsalty/creating-an-efficient-spellchecker-in-elixir-5a8d"&gt;discussed previously&lt;/a&gt;) and make sure I can create the library and then use the library in my host application.&lt;/p&gt;

&lt;p&gt;Let's be good citizens and add some docs too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;defmodule SpellChex do
  @moduledoc """
  Module used to invoke SpellChex.
  """
  @moduledoc since: "1.0.0"

  @sample_words ["hello", "world", "elixir", "phoenix", "spell", "check", "dog", "cat"]


  @doc """
  Determines if a given `word` is in the list of known words.

  Returns `true` or `false`.

  ## Examples

      iex&amp;gt; SpellChex.exists?("dog")
      true

      iex&amp;gt; SpellChex.exists?("asdfas")
      false

  """
  @doc since: "1.3.0"
  def exists?(word) do
    # GenServer.call(__MODULE__, {:check_exists, word})
    word in @sample_words
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In order to get our docs to create we need at add the &lt;code&gt;ex_doc&lt;/code&gt; dependency to our &lt;code&gt;mix.exs&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  defp deps do
    [
      {:ex_doc, "~&amp;gt; 0.31", only: :dev, runtime: false}
    ]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And let's test out those docs. (More on running &lt;a href="https://dev.to/byronsalty/use-hexdocs-locally-19m3"&gt;docs locally here&lt;/a&gt;.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mix deps.get
mix docs
cd doc
caddy file-server --browse --listen :5051
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That should be it for the naive implementation. Now let's figure out how to build it locally and use it in our host project.&lt;/p&gt;




&lt;h1&gt;
  
  
  Consume the library locally
&lt;/h1&gt;

&lt;p&gt;Eventually we will want to push our local code to github and publish on Hex, but why worry about publishing every time you want to test something out locally?&lt;/p&gt;

&lt;p&gt;First thing to do is to update your host project to reference your new library with the &lt;code&gt;path&lt;/code&gt; option:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  defp deps do
    [
      ...
      {:spell_chex, path: "../spell_chex"},
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Grab your deps again (&lt;code&gt;mix deps.get&lt;/code&gt;) and now you can start using the library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;iex(1)&amp;gt; SpellChex.exists?("dog")
true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F17zliuip141r1g4tic4z.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F17zliuip141r1g4tic4z.gif" alt="That was almost too easy" width="400" height="225"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  Configure the Library
&lt;/h1&gt;

&lt;p&gt;Since the library relies on a &lt;code&gt;GenServer&lt;/code&gt; to create a dictionary of words we need to add our dictionary to the host application's start-up.&lt;/p&gt;

&lt;p&gt;Update &lt;code&gt;children&lt;/code&gt; in &lt;code&gt;lib/&amp;lt;host&amp;gt;/application.ex&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; def start(_type, _args) do

    children = [
      ...
      SpellChex.Dictionary,
      ...
    ]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you should be able to start your host mix app and test it out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;iex -S mix
...

iex(1)&amp;gt; SpellChex.exists?("dog")
true
iex(2)&amp;gt; SpellChex.exists?("blahblah")
false
iex(3)&amp;gt; SpellChex.exists?("bonsai")  
true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  Publish via Github
&lt;/h1&gt;

&lt;p&gt;You could simply push your latest to main and then reference your library from the host app's &lt;code&gt;mix.exs&lt;/code&gt; like this&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;      {:spell_chex, git: "https://github.com/byronsalty/spell_chex"},
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But you may want to still utilize versioning. If so you'd tag in get like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git tag -a "0.0.1"
git push origin 0.0.1
# or
git push origin --tags
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you can use the version in the &lt;code&gt;mix.exs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;      {:spell_chex, git: "https://github.com/byronsalty/spell_chex", tag: "0.0.1"},
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  Publish to Hex
&lt;/h1&gt;

&lt;p&gt;But maybe you want to make your library truly global...&lt;/p&gt;

&lt;p&gt;Following this guide to publishing to Hex:&lt;br&gt;
&lt;a href="https://hex.pm/docs/publish" rel="noopener noreferrer"&gt;https://hex.pm/docs/publish&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Highlights: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Setup your &lt;a href="https://hex.pm/docs/publish#registering-a-hex-user" rel="noopener noreferrer"&gt;hex account&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Add all of the &lt;a href="https://hex.pm/docs/publish#adding-metadata-to-code-classinlinemixexscode" rel="noopener noreferrer"&gt;required metadata&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Make sure your docs build.&lt;/li&gt;
&lt;li&gt;Publish! &lt;code&gt;mix hex.publish&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now you can use the standard &lt;code&gt;mix.exs&lt;/code&gt; dependency style:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;      {:spell_chex, "~&amp;gt; 0.0.1"},
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Happy Coding!&lt;/p&gt;




&lt;p&gt;If you found this article helpful, show your support with a Like, Comment, or Follow.&lt;/p&gt;

&lt;p&gt;Read more of Byron’s articles about &lt;a href="https://byronsalty.medium.com/list/leadership-37d7992c55ba" rel="noopener noreferrer"&gt;Leadership&lt;/a&gt; and &lt;a href="https://byronsalty.medium.com/list/my-ai-dc4873d307bf" rel="noopener noreferrer"&gt;AI&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Development articles &lt;a href="https://dev.to/byronsalty"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Follow on &lt;a href="https://byronsalty.medium.com/" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;, &lt;a href="https://dev.to/byronsalty"&gt;Dev.to&lt;/a&gt;, &lt;a href="https://twitter.com/byronsalty" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt;, or &lt;a href="https://www.linkedin.com/in/byronsalty/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Creating an efficient SpellChecker in Elixir</title>
      <dc:creator>Byron Salty</dc:creator>
      <pubDate>Fri, 05 Jan 2024 06:05:53 +0000</pubDate>
      <link>https://dev.to/byronsalty/creating-an-efficient-spellchecker-in-elixir-5a8d</link>
      <guid>https://dev.to/byronsalty/creating-an-efficient-spellchecker-in-elixir-5a8d</guid>
      <description>&lt;h1&gt;
  
  
  Goal
&lt;/h1&gt;

&lt;p&gt;To efficiently use a large list of known English words to create a spell checker.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;NOTE&lt;/em&gt; This is a work in progress. Please respond with comments on how it could be done better / if there are existing frameworks or libraries that I should use instead.&lt;/p&gt;




&lt;h1&gt;
  
  
  Plan
&lt;/h1&gt;

&lt;p&gt;In this article I'll create two versions of a simple spell checker which really will just take a word and see if it's in a large list of known words. I'm using &lt;a href="https://gist.github.com/eyturner/3d56f6a194f411af9f29df4c9d4a4e6e" rel="noopener noreferrer"&gt;this list of 20,000 words&lt;/a&gt;, which is supposed to be a list of the most frequent ones.&lt;/p&gt;

&lt;p&gt;I plan to use this in a Phoenix app so I wanted to do some measurements and determine the performance characteristics. My first thought was to use a GenServer so that I could have a single process load up the list and answer check requests, but I wanted to make sure overhead of a GenServer is worth it.&lt;/p&gt;




&lt;h1&gt;
  
  
  Code Structure
&lt;/h1&gt;

&lt;p&gt;I'm creating two different SpellCheckers - a Naive one and a GenServer one.&lt;/p&gt;

&lt;p&gt;I'm going to use a page from &lt;a href="https://www.gutenberg.org/ebooks/345" rel="noopener noreferrer"&gt;Bram Stoker's Dracula&lt;/a&gt; as test data.&lt;/p&gt;

&lt;p&gt;And then I'll use &lt;code&gt;Benchee&lt;/code&gt; to measure results.&lt;/p&gt;




&lt;h1&gt;
  
  
  Implementations
&lt;/h1&gt;

&lt;p&gt;Helper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;defmodule FileReader do
  def read_words_from_file(file_path) do
    file_path
    |&amp;gt; File.stream!()
    |&amp;gt; Enum.map(&amp;amp;String.trim/1)
    |&amp;gt; Enum.reject(&amp;amp;(&amp;amp;1 == ""))
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Naive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;defmodule SpellCheck.Naive do

  #@all_words ["these", "are", "some", "words"]

  def exists?(word) do
    all_words = FileReader.read_words_from_file("/tmp/20k.txt")

    word in all_words
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GenServer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;defmodule SpellCheck.GenServer do
  use GenServer

  #@all_words ["these", "are", "some", "words"]

  def start_link(_opts \\ []) do
    GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
  end

  def exists?(word) do
    GenServer.call(__MODULE__, {:check_exists, word})
  end

  @impl true
  def init(_) do
    all_words = FileReader.read_words_from_file("/tmp/20k.txt")
    {:ok, all_words}
  end


  @impl true
  def handle_call({:check_exists, word}, _from, all_words) do
    {:reply, word in all_words, all_words}
  end
end

SpellCheck.GenServer.start_link()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test words:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sample_text = """
I soon lost sight and recollection of ghostly fears
&amp;lt;snip&amp;gt;
"""

eval_words = String.split(sample_text, " ", trim: true)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Benchee:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Benchee.run(%{
    "Naive" =&amp;gt; fn -&amp;gt; Enum.map(eval_words, fn w -&amp;gt; SpellCheck.Naive.exists?(w) end) end,
    "GenServer" =&amp;gt; fn -&amp;gt; Enum.map(eval_words, fn w -&amp;gt; SpellCheck.GenServer.exists?(w) end) end
  },
  time: 5, 
  memory_time: 2
  )
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  Test 1 - Hard coded dictionary
&lt;/h1&gt;

&lt;p&gt;First, I actually tested the code without reading the 20,000 words from a file and just used an array set as module attributes.&lt;/p&gt;

&lt;p&gt;Perhaps not surprisingly, when using a hard coded list of words as a dictionary the extra overhead of a GenServer was noticable and the Naive implementation performed better:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Name                ips        average  deviation         median         99th %
Naive           14.17 K       70.57 μs    ±37.68%       69.79 μs       87.25 μs
GenServer        2.99 K      333.95 μs     ±3.30%      330.88 μs      374.93 μs

Comparison: 
Naive           14.17 K
GenServer        2.99 K - 4.73x slower +263.38 μs

Memory usage statistics:

Name         Memory usage
Naive           171.97 KB
GenServer       246.55 KB - 1.43x memory usage +74.58 KB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Interpreting the results: &lt;em&gt;Naive was 5x faster and used ~60% less memory.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;But this is not a viable way to check spelling because we need to load a large set of words. &lt;/p&gt;

&lt;p&gt;How will it perform at higher scale?&lt;/p&gt;




&lt;h1&gt;
  
  
  Test 2 - Reading dictionary from a file
&lt;/h1&gt;

&lt;p&gt;You won't be surprised to see that the &lt;code&gt;GenServer&lt;/code&gt; performs much better, considering that their purpose is maintaining state so we don't need to load the state over and over.&lt;/p&gt;

&lt;p&gt;However, I was surprised to see HOW MUCH better it was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Name                ips        average  deviation         median         99th %
GenServer        110.18      0.00908 s    ±55.16%      0.00876 s      0.00981 s
Naive              0.68         1.46 s     ±6.78%         1.41 s         1.61 s

Comparison: 
GenServer        110.18
Naive              0.68 - 160.88x slower +1.45 s

Memory usage statistics:

Name         Memory usage
GenServer      0.00024 GB
Naive             1.98 GB - 8419.39x memory usage +1.98 GB

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Interpreting the results: &lt;em&gt;GenServer was 160x faster and a tiny fraction of the memory that Naive used.&lt;/em&gt; In fact, it looks like GenServer's memory usage didn't change much between tests. &lt;/p&gt;

&lt;p&gt;Though the overall throughput between the two tests varies significantly. My method of checking if a word is in the dictionary could certainly be improved with a data structure that supports a binary search instead of a linear search. But this is good enough for my use case and I'll need to explore faster searching another time.&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>bash</category>
      <category>benchee</category>
      <category>genserver</category>
    </item>
    <item>
      <title>Easy LLM based text classification with instructor_ex</title>
      <dc:creator>Byron Salty</dc:creator>
      <pubDate>Thu, 04 Jan 2024 05:38:06 +0000</pubDate>
      <link>https://dev.to/byronsalty/easy-llm-based-text-classification-with-instructorex-29b4</link>
      <guid>https://dev.to/byronsalty/easy-llm-based-text-classification-with-instructorex-29b4</guid>
      <description>&lt;p&gt;I got lucky today.&lt;/p&gt;

&lt;p&gt;Just as I was contemplating how to add grammar and spell checking to my &lt;a href="https://telepromptgame.com/" rel="noopener noreferrer"&gt;Text-to-Image game&lt;/a&gt;, I read about a new library that I could use to solve both problems at the same time. &lt;/p&gt;

&lt;p&gt;My game involves trying to find 5 missing words needed to complete a prompt phrase which was used to generate the original image.  Every time there is a unique guess, I generate a new image to show the user if they were close. But if a user guesses a phrase that someone else guessed then I can pull the image from cache and don't need to pay for a new generation.&lt;/p&gt;

&lt;p&gt;This means that spelling errors cost me money.&lt;/p&gt;

&lt;p&gt;And since the target phrase is grammatically correct, any bad grammar is also a waste.&lt;/p&gt;

&lt;p&gt;I was considering adding a dictionary to check words against, and I wasn't sure what to do about grammar checking...&lt;/p&gt;




&lt;h2&gt;
  
  
  Enter instructor_ex and LLMs
&lt;/h2&gt;

&lt;p&gt;I'm a bit ashamed to admit that I didn't first jump to using an LLM to solve this problem - because the answer is pretty obvious now.&lt;/p&gt;

&lt;p&gt;Why not just ask an LLM, which understands both grammar and spelling VERY well: &lt;br&gt;
"Is the following phrase grammatically correct and spelled correctly?"&lt;/p&gt;

&lt;p&gt;This is the exact type of question and use case that &lt;a href="https://twitter.com/thmsmlr" rel="noopener noreferrer"&gt;Thomas Millar&lt;/a&gt;'s new library &lt;a href="https://github.com/thmsmlr/instructor_ex" rel="noopener noreferrer"&gt;instructor_ex&lt;/a&gt; is designed to make easy.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 0 - Add Instructor to your project
&lt;/h2&gt;

&lt;p&gt;Update your mix.exs deps&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; defp deps do
    [
      ...
      {:instructor, "~&amp;gt; 0.0.2"},
      ...
    ]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;instructor&lt;/code&gt; and &lt;code&gt;openai&lt;/code&gt; to your config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;config :instructor,
  adapter: Instructor.Adapters.OpenAI

config :openai,
  api_key: "sk-...8pj",
  http_options: [recv_timeout: 10 * 60 * 1000]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Note&lt;/em&gt; You can tell from this config that OpenAI is not the only adapter and LLM you can use.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1 - Create a Grammar classifier
&lt;/h2&gt;

&lt;p&gt;Instructor follows the ecto schema pattern, which I believe could be extremely helpful for more complex use cases. In my use case, the effort was minimal and definition was pretty clear. &lt;/p&gt;

&lt;p&gt;The one part that was a bit confusing was that the question being asked of the LLM is embedded in the &lt;code&gt;@doc&lt;/code&gt; string for the schema. Here's my classifier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;defmodule GrammarClassification do
  use Ecto.Schema

  @doc """
  A classification of whether or not a provided phrase is grammatically correct and has correct spelling
  """
  @primary_key false
  embedded_schema do
    field(:is_correct?, :boolean)
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 2 - Classify some text and get a result
&lt;/h2&gt;

&lt;p&gt;I then wrapped the calling of the classifier in a simple helper function which follows the exact format from the &lt;a href="https://github.com/thmsmlr/instructor_ex/tree/main/notebooks" rel="noopener noreferrer"&gt;example notebooks&lt;/a&gt; found in project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;defmodule Teleprompt.TextHelper do
  def is_correct?(phrase) do
    {:ok, %{is_correct?: result}} =
      Instructor.chat_completion(
        model: "gpt-3.5-turbo",
        response_model: GrammarClassification,
        messages: [
          %{
            role: "user",
            content: "Classify the following text: #{phrase}"
          }
        ]
      )

    result
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;Now I can simply call a single function, pass in a string of words, and receive a true/false response on whether or not it is spelled correctly and grammatically correct. &lt;/p&gt;

&lt;p&gt;Hell yeah!&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>llm</category>
      <category>classification</category>
      <category>openai</category>
    </item>
    <item>
      <title>Adding DALL-E to your Elixir app</title>
      <dc:creator>Byron Salty</dc:creator>
      <pubDate>Wed, 03 Jan 2024 05:54:24 +0000</pubDate>
      <link>https://dev.to/byronsalty/adding-dall-e-to-your-elixir-app-16b5</link>
      <guid>https://dev.to/byronsalty/adding-dall-e-to-your-elixir-app-16b5</guid>
      <description>&lt;p&gt;I &lt;a href="https://byronsalty.medium.com/teleprompt-a70a3b663d67" rel="noopener noreferrer"&gt;recently updated&lt;/a&gt; my little image generation game, &lt;a href="https://telepromptgame.com/" rel="noopener noreferrer"&gt;Teleprompt&lt;/a&gt;, to use DALL-E 3 instead of Stable Diffusion as the image engine. &lt;/p&gt;

&lt;p&gt;If you're curious about the reasoning, overall product and architecture, check out my &lt;a href="https://byronsalty.medium.com/teleprompt-a70a3b663d67" rel="noopener noreferrer"&gt;previous, high-level article with those details&lt;/a&gt; - as well as some cool pictures of dragons!&lt;/p&gt;

&lt;p&gt;In this article, I will give you the few pieces of code you need to create your own DALL-E 3 integration with Elixir.&lt;/p&gt;

&lt;p&gt;...&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 - Setup OpenAI
&lt;/h2&gt;

&lt;p&gt;DALL-E is part of the OpenAI product suite, under &lt;a href="https://platform.openai.com/docs/guides/images?context=node" rel="noopener noreferrer"&gt;Image Generation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Create an account with &lt;a href="https://platform.openai.com/" rel="noopener noreferrer"&gt;OpenAI&lt;/a&gt; and on the left nav bar you'll see an option for API Keys. &lt;/p&gt;

&lt;p&gt;From there you can select "Create new secret key" and copy the value that will look like &lt;code&gt;sk-...&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I like to add this to a &lt;code&gt;.env&lt;/code&gt; file to source and make available to my project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export OPENAI_API_KEY=sk-...u8pj
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2 - API request
&lt;/h2&gt;

&lt;p&gt;In Teleprompt, I setup a GenServer in order to asynchronously call the OpenAI API. Depending on your use case you might not need this complexity but I felt it was important for a webapp to be more event driven and then use &lt;a href="https://dev.to/byronsalty/clustering-with-phoenix-17-25o4"&gt;PubSub to inform listeners&lt;/a&gt; when the image generation was complete.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;defmodule Teleprompt.GenerationHandler do
  use GenServer

  ...

  def start_generating(prompt) do
    # create a post request to the server
    GenServer.cast(__MODULE__, {:generate, prompt, listener_code})
  end

  @impl true
  def handle_cast({:generate, prompt, listener_code}, _state) do
    endpoint = "https://api.openai.com/v1/images/generations"
    openai_api_key = @openai_key
    {model, size} = {"dall-e-3", "1024x1024"}
    # {model, size} = {"dall-e-2", "512x512"}

    data =
      %{
        "model" =&amp;gt; model,
        "size" =&amp;gt; size,
        "quality" =&amp;gt; "standard",
        "n" =&amp;gt; 1,
        "prompt" =&amp;gt; prompt
      }
      |&amp;gt; Jason.encode!()

    opts = [async: true, recv_timeout: 30_000, timeout: 30_000]

    response =
      HTTPoison.post!(
        endpoint,
        data,
        [
          {"Content-Type", "application/json"},
          {"Authorization", "Bearer #{openai_api_key}"}
        ],
        opts
      )

    response_body = 
      response
      |&amp;gt; Map.get(:body)
      |&amp;gt; Jason.decode!()

    # url in body: body["data"][0]["url"]
    url = response_body |&amp;gt; get_in(["data", Access.at(0), "url"])

  ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we are making the post with mostly default parameters. I did try out DALL-E 2 as well, which allows for different parameters and different options (like sizes). &lt;/p&gt;

&lt;p&gt;One parameter I was a bit surprised to see missing is a &lt;code&gt;seed&lt;/code&gt;, so I don't know if DALL-E allows you to create repeatable results.&lt;/p&gt;

&lt;p&gt;You can see the OpenAI API reference &lt;a href="https://platform.openai.com/docs/api-reference/images/create" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I also used the default response type, which is a url to a public image, however I could have also specified a different &lt;code&gt;response_format&lt;/code&gt; and received the file contents as base64 encoded json.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 - Use the file
&lt;/h2&gt;

&lt;p&gt;In my case I did want to download the file and manipulated it so I immediately take the URL and do some processing on it. Perhaps the &lt;code&gt;b64_json&lt;/code&gt; response would have made more sense but I was already setup to handle urls so I left that code in place.&lt;/p&gt;

&lt;p&gt;One question I have is how long the images would last on the OpenAI CDN if you wanted to directly use the url they give you in your app.&lt;/p&gt;

&lt;p&gt;I didn't trust that the image would last forever so I took the url, downloaded the file, uploaded it to AWS to serve myself.&lt;/p&gt;

&lt;p&gt;Here's how the end of my generate handler looks, after I get the generated image url:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    ...
    url = response_body |&amp;gt; get_in(["data", Access.at(0), "url"])
    {file_name, file_path} = download_and_resize_image(url)

    # upload to s3
    {:ok, file_binary} = File.read(file_path)

    write_file_to_s3(file_name, file_binary, "image/png")

    Teleprompt.Messaging.received_image(listener_code)

    {:noreply, nil}
  end

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'll save some of the small details around resizing but the download code is simply a &lt;code&gt;get&lt;/code&gt; and &lt;code&gt;File.write&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  ...
  file_path = "/tmp/#{file_name}"
  {:ok, response} = HTTPoison.get(image_url)

  File.write!(file_path, response.body)
  ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's really all there is to it. Now go make some images!&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>dalle</category>
      <category>genai</category>
      <category>openai</category>
    </item>
    <item>
      <title>Upgrading to Phoenix 1.7</title>
      <dc:creator>Byron Salty</dc:creator>
      <pubDate>Fri, 29 Dec 2023 05:20:27 +0000</pubDate>
      <link>https://dev.to/byronsalty/upgrading-to-phoenix-17-3l21</link>
      <guid>https://dev.to/byronsalty/upgrading-to-phoenix-17-3l21</guid>
      <description>&lt;p&gt;I consider this a work in progress. This is a guide that I'll use myself as I migrate all my projects to 1.7 so it will refine over time and please submit comments / suggestions for improvements.&lt;/p&gt;

&lt;h1&gt;
  
  
  The Problem
&lt;/h1&gt;

&lt;p&gt;I have several older projects that are in the Phoenix 1.6 style and structure, and lots of newer ones in 1.7. In order to share code and solutions between them I need to move them all to 1.7 and do it by making the 1.6 look exactly like a project started on 1.7.&lt;/p&gt;

&lt;p&gt;Therefore, I'm moving things and removing things resulting in changes that may not be strictly necessary if you are just trying to get 1.7 to run.&lt;/p&gt;

&lt;p&gt;Some other &lt;a href="https://elixircasts.io/upgrading-to-phoenix-1.7" rel="noopener noreferrer"&gt;good tutorials exist&lt;/a&gt; that can be helpful to just make a project work, for example by adding Views back into the project via an optional dependency.&lt;/p&gt;




&lt;h1&gt;
  
  
  Steps
&lt;/h1&gt;




&lt;h2&gt;
  
  
  0. Create a branch in git
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Disclaimer&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In order to make the app look like a true 1.7 app, we need to move, edit and delete a lot of files. &lt;/p&gt;

&lt;p&gt;Start with creating a branch in git in case you ever need to revert or reference the 1.6 code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git checkout -b phoenix_1_6
git checkout -b upgrading_to_1_7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also suggest that you keep a separate clone of the old version, or at least grab the list of generated routes for use later when you convert to path sigils:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mix phx.routes &amp;gt; routes.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(I'm saying store it because once you start making these changes your project may not compile again until all the issues are fixed. In my latest conversion, there were over 800 issues to be fixed to get the project to compile again.)&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Update dependencies
&lt;/h2&gt;

&lt;p&gt;List of deps that I changed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;defp deps do
 [
    {:phoenix, "~&amp;gt; 1.7.10"},
    {:phoenix_html, "~&amp;gt; 3.3"},
    {:phoenix_live_reload, "~&amp;gt; 1.2", only: [:dev]},
    {:phoenix_live_view, "~&amp;gt; 0.19"},
    {:phoenix_live_dashboard, "~&amp;gt; 0.8.0"},
    {:esbuild, "~&amp;gt; 0.7", runtime: Mix.env() == :dev},
    ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mix deps.clean --all
rm mix.lock
mix deps.get
mix deps.compile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  2. Fix issues related to Views
&lt;/h2&gt;

&lt;p&gt;Phoenix 1.7 removed the view files so things need to be updated. You'll see errors like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;error: module Phoenix.View is not loaded and could not be found. This may be happening because the module you are trying to load directly or indirectly depends on the current module 
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead, Phoenix 1.7 uses &lt;code&gt;html_template&lt;/code&gt; embedding modules. (See Step 5)&lt;/p&gt;

&lt;p&gt;Take note of any functionality that you may have added to your &lt;code&gt;*_view.ex&lt;/code&gt; modules. This will likely move into your &lt;code&gt;*_html.ex&lt;/code&gt; files that we'll create in Step 5.&lt;/p&gt;

&lt;p&gt;I suggest keeping those &lt;code&gt;View&lt;/code&gt; modules for now, but remove the &lt;code&gt;use &amp;lt;App&amp;gt;Web, :view&lt;/code&gt; so that it won't throw compilation errors. &lt;/p&gt;

&lt;p&gt;But if you only have boilerplate code in &lt;code&gt;/views&lt;/code&gt; then you can safely remove those files and that directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# NOTE: Only if you don't need to keep any of those files
#   Otherwise run this after Step 5.
rm -fr lib/appName_web/views
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  3. Create components dir
&lt;/h2&gt;

&lt;p&gt;This is where layouts and standard components will live now.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mkdir lib/&amp;lt;app&amp;gt;_web/components
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Move you layouts from the former &lt;code&gt;templates&lt;/code&gt; dir:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mv lib/&amp;lt;app&amp;gt;_web/templates/layouts lib/&amp;lt;app&amp;gt;_web/components/.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Note&lt;/em&gt;: If you have a &lt;code&gt;live.html.heex&lt;/code&gt; layout, you should be able to remove it. One of the major benefits of 1.7 is that the layouts are shared across live and dead views.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Move template dirs
&lt;/h2&gt;

&lt;p&gt;Instead of being in a separate &lt;code&gt;templates&lt;/code&gt; directory, the remaining templates folders with the &lt;code&gt;*.html.heex&lt;/code&gt; files move into the &lt;code&gt;controllers&lt;/code&gt; dir as subfolders:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mv lib/appName_web/templates/* lib/appName_web/controllers/.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Note:&lt;/em&gt; I then manually renamed them to include the standard &lt;code&gt;_html&lt;/code&gt; suffix.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd lib/appName_web/controllers
mv page page_html
mv post post_html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that you should be able to get rid of the templates dir:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rm -fr lib/appName_web/templates
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  5. Add the html embedding modules
&lt;/h2&gt;

&lt;p&gt;Let's say you still have a PageController. You'll want to add a companion file next to the &lt;code&gt;page_controller.ex&lt;/code&gt; conventionally called &lt;code&gt;page_html.ex&lt;/code&gt; which looks like this by default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;defmodule AppNameWeb.PageHTML do
    use AppNameWeb, :html
    embed_templates "page_html/*"
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will probably get an error that &lt;code&gt;:html&lt;/code&gt; is an unknown function in your app's web module. You'll need to find your base module at &lt;code&gt;lib/appName_web/appName_web.ex&lt;/code&gt; and change a few functions. &lt;/p&gt;

&lt;p&gt;Add: &lt;code&gt;html&lt;/code&gt;, &lt;code&gt;html_helpers&lt;/code&gt;, &lt;code&gt;verified_routes&lt;/code&gt;, and &lt;code&gt;static_paths&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def html do
    quote do
        use Phoenix.Component
        # Import convenience functions from controllers

        import Phoenix.Controller,
            only: [get_csrf_token: 0, view_module: 1, view_template: 1]

        # Include general helpers for rendering HTML
        unquote(html_helpers())
    end
end

defp html_helpers do
    quote do
        # HTML escaping functionality
        import Phoenix.HTML

        # Core UI components and translation
        import AppNameWeb.CoreComponents
        import AppNameWeb.Gettext

        # Shortcut for generating JS commands
        alias Phoenix.LiveView.JS

        # Routes generation with the ~p sigil
        unquote(verified_routes())
    end
end

def verified_routes do
    quote do
        use Phoenix.VerifiedRoutes,
            endpoint: AppNameWeb.Endpoint,
            router: AppNameWeb.Router,
            statics: AppNameWeb.static_paths()
    end
end

def static_paths, do: ~w(assets fonts images favicon.ico robots.txt)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remove &lt;code&gt;component&lt;/code&gt;, &lt;code&gt;view&lt;/code&gt; and &lt;code&gt;view_helpers&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Update references to &lt;code&gt;view_helpers&lt;/code&gt; to &lt;code&gt;html_helpers&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Also - update the &lt;code&gt;controller&lt;/code&gt;,&lt;code&gt;live_view&lt;/code&gt; and &lt;code&gt;live_component&lt;/code&gt; function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def controller do
    quote do
        use Phoenix.Controller,
            formats: [:html, :json],
            layouts: [html: AppNameWeb.Layouts]

        import Plug.Conn
        import AppNameWeb.Gettext

        unquote(verified_routes())
    end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  def live_view do
    quote do
      use Phoenix.LiveView,
        layout: {AppNameWeb.Layouts, :app}

      unquote(html_helpers())
    end
  end

  def live_component do
    quote do
      use Phoenix.LiveComponent

      unquote(html_helpers())
    end
  end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  6. Add standard files
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Add some components
&lt;/h3&gt;

&lt;p&gt;Grab the &lt;code&gt;core_components.ex&lt;/code&gt; and &lt;code&gt;layouts.ex&lt;/code&gt; from any 1.7 projects or clone &lt;a href="https://github.com/byronsalty/app_name" rel="noopener noreferrer"&gt;this example project&lt;/a&gt; and put it into &lt;code&gt;lib/appName_web/components&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Add Gettext
&lt;/h3&gt;

&lt;p&gt;If you don't have GetText in the base of your app then grab that too from &lt;code&gt;lib/appName_web/gettext.ex&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Make sure you updated "appName"
&lt;/h3&gt;

&lt;p&gt;Make sure this doesn't find anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;grep -ri lib appname
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  7. Small change to the csrf_token
&lt;/h2&gt;

&lt;p&gt;Your &lt;code&gt;root.html.heex&lt;/code&gt; layout probably has a function call like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;csrf_token_value()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This needs to change to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;get_csrf_token()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  8. Update router
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;:browser&lt;/code&gt; pipeline needs to reference the layouts with the new module name. Change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#from
plug :put_root_layout, {AppNameWeb.LayoutView, :root}

#to
plug :put_root_layout, html: {AppNameWeb.Layouts, :root}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  9. Update Routes to use &lt;code&gt;~p&lt;/code&gt; sigil
&lt;/h2&gt;

&lt;p&gt;Now you can use the much more convenient &lt;code&gt;~p&lt;/code&gt; sigil.&lt;/p&gt;

&lt;p&gt;Look in all of your html for references to &lt;code&gt;Routes.&lt;/code&gt; and change them similar to the following examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# from:
href={Routes.static_path(@conn, "/assets/app.css")}

# to:
href={~p"/assets/app.css"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# from
&amp;lt;img src={Routes.static_path(@conn, "/images/logo.png")}

# to
&amp;lt;img src={~p"/images/logo.png")}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# from
Routes.post_path(@conn, :index)

# to
~p"/posts"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# from
Routes.post_path(@conn, :update, @post)

# to
~p"/admin/prompts/#{@prompt}"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  10. Update heex to use components
&lt;/h2&gt;

&lt;p&gt;Instead of embedding elixir code into templates with &lt;code&gt;&amp;lt;%= ... %&amp;gt;&lt;/code&gt;, convert these snippets to components where appropriate.&lt;/p&gt;

&lt;p&gt;Some example replacements:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# from:
&amp;lt;%= live_title_tag assigns[:page_title] || "App" %&amp;gt;

# to:
&amp;lt;.live_title suffix=""&amp;gt;
    &amp;lt;%= assigns[:page_title] || "App" %&amp;gt;
&amp;lt;/.live_title&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# from:
&amp;lt;%= render 'form.html', ... %&amp;gt;

# to:
&amp;lt;.post_form changeset={@changeset} action={~p"/posts/update/#{@post}"} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# from:
&amp;lt;%= link "Edit", to: Routes.page_path(@conn, :edit, @page) %&amp;gt;

# to:
&amp;lt;.link navigate={~p"/pages/#{@page}"}&amp;gt;Edit&amp;lt;/.link&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# from:
&amp;lt;%= link "Delete", to: Routes.page_path(@conn, :delete, page), method: :delete, data: [confirm: "Are you sure you want to delete #{page.title}?"] %&amp;gt;

# to:
&amp;lt;.link href={~p"/mascots/#{mascot}"} method="delete" data-confirm="Are you sure?"&amp;gt;
  Delete
&amp;lt;/.link&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# from:
  &amp;lt;%= label f, :name %&amp;gt;
  &amp;lt;%= text_input f, :name %&amp;gt;
  &amp;lt;%= error_tag f, :name %&amp;gt;

# to:
  &amp;lt;.input field={f[:name]} type="text" label="Name" /&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# from:
  &amp;lt;%= inputs_for f, :children, fn c -&amp;gt; %&amp;gt;

    &amp;lt;%= label c, :baby_name %&amp;gt;
    &amp;lt;%= text_input c, :baby_name %&amp;gt;
    &amp;lt;%= error_tag c, :baby_name %&amp;gt;

# to:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# from:
  &amp;lt;%= if @changeset.action do %&amp;gt;
    &amp;lt;div class="alert alert-danger"&amp;gt;
      &amp;lt;p&amp;gt;Oops, something went wrong! Please check the errors below.&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;% end %&amp;gt;

# to:
  &amp;lt;.error :if={@changeset.action}&amp;gt;
    Oops, something went wrong! Please check the errors below.
  &amp;lt;/.error&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# from:
  &amp;lt;div class="buttons"&amp;gt;
    &amp;lt;%= submit "Save" %&amp;gt;
  &amp;lt;/div&amp;gt;

# to:
  &amp;lt;:actions&amp;gt;
    &amp;lt;.button&amp;gt;Save&amp;lt;/.button&amp;gt;
  &amp;lt;/:actions&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TIP: Keep a 1.7 generated app nearby with a some forms etc created with &lt;code&gt;mix phx.gen.html ...&lt;/code&gt; for comparisons during this phase.&lt;/p&gt;




&lt;h2&gt;
  
  
  10B - Check that you didn't miss anything
&lt;/h2&gt;

&lt;p&gt;Here are a few scripts to run against your lib dir to find missed replacements:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;grep -r lib "&amp;lt;%= link"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  11. Update various heex items
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;form&lt;/code&gt; component now needs &lt;code&gt;:let&lt;/code&gt; instead of &lt;code&gt;let&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You could try bulk updating like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# MacOS
find lib -type f -name "*.heex" -exec sed -i '' 's/form let/form :let/g' {} +

# Linux
find lib -type f -name "*.heex" -exec sed -i 's/form let/form :let/g' {} +
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;If this was helpful - give it a Like and Follow!&lt;/p&gt;

&lt;p&gt;Happy Coding.&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>phoenix</category>
      <category>phoenix17</category>
    </item>
    <item>
      <title>Clustering with Phoenix 1.7</title>
      <dc:creator>Byron Salty</dc:creator>
      <pubDate>Tue, 08 Aug 2023 08:17:55 +0000</pubDate>
      <link>https://dev.to/byronsalty/clustering-with-phoenix-17-25o4</link>
      <guid>https://dev.to/byronsalty/clustering-with-phoenix-17-25o4</guid>
      <description>&lt;p&gt;There several good articles about how to setup a Phoenix cluster with PubSub messaging through the nodes but I found them to be incomplete or slightly out of date so in this article I plan to give step by step instructions on how to create a clustered Phoenix 1.7 application in 2023.&lt;/p&gt;

&lt;p&gt;The goal of this article will be to run multiple instances of our Phoenix which seamlessly can do message passing between nodes.&lt;/p&gt;

&lt;p&gt;The target production environment will be Fly.io for this article, but I'll also show how to setup your project for local development as well so you can make sure you are subscribing and broadcasting correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Simplest Demo Project
&lt;/h2&gt;

&lt;p&gt;We're going to create a stripped down Phoenix app that does only two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Exposes a live view that displays messages received&lt;/li&gt;
&lt;li&gt;Has an API end-point to receive external messages via HTTP&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Create project
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Note&lt;/em&gt; - we will build this project step by step but feel free to check out the &lt;a href="https://github.com/byronsalty/talkie" rel="noopener noreferrer"&gt;completed project here&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup Phoenix app
&lt;/h2&gt;

&lt;p&gt;Phoenix new with most things removed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mix phx.new talkie --no-ecto --no-mailer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update the &lt;code&gt;config/dev.exs&lt;/code&gt; file to allow passing in a &lt;code&gt;PORT&lt;/code&gt; since we'll need to run two instances:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Before
config :talkie, TalkieWeb.Endpoint,
    http: [ip: {127, 0, 0, 1}, port: 4000],
    ...

# After
port = String.to_integer(System.get_env("PORT") || "5000")

config :talkie, TalkieWeb.Endpoint,
    http: [ip: {127, 0, 0, 1}, port: port],
    ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Create the &lt;code&gt;ping&lt;/code&gt; API endpoint:
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Add this controller
defmodule TalkieWeb.APIController do
    use TalkieWeb, :controller

    def ping(conn, _params) do
        # Not doing anything but responding so far
        json(conn, %{pong: true})
    end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the &lt;code&gt;ping&lt;/code&gt; route to your &lt;code&gt;router.ex&lt;/code&gt; inside the existing &lt;code&gt;/api&lt;/code&gt; scope:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;scope "/api", TalkieWeb do
    pipe_through :api
    get "/ping", APIController, :ping
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl http://127.0.0.1:5000/api/ping

# {"pong": true}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Create the Liveview viewer
&lt;/h2&gt;

&lt;p&gt;Add a LiveView module like this&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;defmodule TalkieWeb.ViewerLive.Index do
    use TalkieWeb, :live_view

    @impl true
    def render(assigns) do
        ~H"""
        &amp;lt;h1&amp;gt;Messages&amp;lt;/h1&amp;gt;
        &amp;lt;%= for msg &amp;lt;- @messages do %&amp;gt;
            &amp;lt;span&amp;gt;&amp;lt;%= msg %&amp;gt;&amp;lt;/span&amp;gt;
        &amp;lt;% end %&amp;gt;
        """
    end

    @impl true
    def mount(_params, _session, socket) do
        {:ok, assign(socket, :messages, [])}
    end

    @impl true
    def handle_info({:message, msg}, socket) do
        {:noreply, assign(socket, 
            :messages, [msg | socket.assigns.messages])}
    end

    @impl true
    def handle_info(
        %Phoenix.Socket.Broadcast{
            topic: "messages",
            event: "ping",
            payload: {:message, msg}
        }, socket) do
        handle_info({:message, msg}, socket)
    end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And add this line to your &lt;code&gt;router.ex&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    # in the browser scope
    live "/viewer", ViewerLive.Index
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test it by pointing your browser at (&lt;a href="http://127.0.0.1:5000/viewer" rel="noopener noreferrer"&gt;http://127.0.0.1:5000/viewer&lt;/a&gt;)&lt;/p&gt;

&lt;h1&gt;
  
  
  Listen for messages on One instance
&lt;/h1&gt;

&lt;p&gt;Now that we have those components. Let's wire them together with a single instance.&lt;/p&gt;

&lt;p&gt;We're going to have our Viewer listen for messages, and our API broadcast whenever the &lt;code&gt;/ping&lt;/code&gt; api is hit.&lt;/p&gt;

&lt;p&gt;You "listen" by &lt;em&gt;subscribing&lt;/em&gt; to a topic. Update the liveview mount to look like this now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def mount(_params, _session, socket) do
    TalkieWeb.Endpoint.subscribe("messages")
    {:ok, assign(socket, :messages, ["test message..."])}
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And you broadcast by adding the broadcast to the ping function this the API Controller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def ping(conn, _params) do
    Phoenix.PubSub.broadcast!(Talkie.PubSub, "messages",
        %Phoenix.Socket.Broadcast{
            topic: "messages",
            event: "ping",
            payload: {:message, "ping"}
        }
    )
    json(conn, %{pong: true})
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Test it
&lt;/h3&gt;

&lt;p&gt;Start the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PORT=5000 mix phx.server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open a browser to: (&lt;a href="http://127.0.0.1:5000/viewer" rel="noopener noreferrer"&gt;http://127.0.0.1:5000/viewer&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Hit the ping api again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl http://127.0.0.1:5000/api/ping
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This should cause the Viewer to display a "ping" message instantly.&lt;/p&gt;

&lt;h1&gt;
  
  
  Clustering for local dev
&lt;/h1&gt;

&lt;p&gt;Now let's see clustering NOT work first. Open up two instances like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PORT=5000 elixir --name a@127.0.0.1 -S mix phx.server
PORT=5001 elixir --name b@127.0.0.1 -S mix phx.server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bring up a Viewer pointed to both servers:&lt;br&gt;
(&lt;a href="http://127.0.0.1:5000/viewer" rel="noopener noreferrer"&gt;http://127.0.0.1:5000/viewer&lt;/a&gt;)&lt;br&gt;
(&lt;a href="http://127.0.0.1:5001/viewer" rel="noopener noreferrer"&gt;http://127.0.0.1:5001/viewer&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;And if you run the &lt;code&gt;curl&lt;/code&gt; on the ping API you'll see that only one viewer will update. &lt;/p&gt;
&lt;h3&gt;
  
  
  Add &lt;code&gt;libcluster&lt;/code&gt; to your dependencies in &lt;code&gt;mix.exs&lt;/code&gt; and rerun &lt;code&gt;mix deps.get&lt;/code&gt;
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{:libcluster, "~&amp;gt; 3.3"},
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Add the libcluster config to your &lt;code&gt;config/dev.exs&lt;/code&gt;:
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;config :libcluster,
    topologies: [
        example: [
            strategy: Cluster.Strategy.Epmd,
            config: [hosts: [:"a@127.0.0.1", :"b@127.0.0.1"]],
            connect: {:net_kernel, :connect_node, []},
            disconnect: {:erlang, :disconnect_node, []},
            list_nodes: {:erlang, :nodes, [:connected]},
        ]
    ]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Update your &lt;code&gt;application.ex&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;You will define a &lt;code&gt;topologies&lt;/code&gt; variable in your start() function and add a &lt;code&gt;Cluster.Supervisor&lt;/code&gt; to the list of children.&lt;/p&gt;

&lt;p&gt;It will look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def start(_type, _args) do
    topologies = Application.get_env(:libcluster, :topologies) || []

    children = [
        ...
        {Cluster.Supervisor, [topologies, [name: Talkie.ClusterSupervisor]]}
    ]

    ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now if you restart both of the instances and test again you SHOULD see both Viewers update when you ping either one of the instances. &lt;/p&gt;

&lt;p&gt;Pretty cool right!&lt;/p&gt;

&lt;h1&gt;
  
  
  Clustering with Fly.io
&lt;/h1&gt;

&lt;p&gt;First, deploy the app as-is to Fly and test that you see it not working as expected again.&lt;/p&gt;

&lt;p&gt;What you should see (assuming you default to 2 instances) is a 50/50 chance that you'll receive the ping message if you pull up a &lt;code&gt;/viewer&lt;/code&gt; page and then hit the ping API. A rough test is to bring up 2-4 separate windows and to the &lt;code&gt;/viewer&lt;/code&gt; path and test a ping to see how many of the windows update.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add &lt;code&gt;libcluster&lt;/code&gt; configuration to prod in the &lt;code&gt;config/runtime.exs&lt;/code&gt; file
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app_name = System.get_env("FLY_APP_NAME") || "talkie"

config :libcluster,
    debug: true,
    topologies: [
        fly6pn: [
            strategy: Cluster.Strategy.DNSPoll,
            config: [
                polling_interval: 5_000,
                query: "#{app_name}.internal",
                node_basename: app_name
            ]
        ]
    ]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Change how the RELEASE_NODE value is set in the &lt;code&gt;rel/env.sh.eex&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Note if you don't have this file, then you just haven't completed &lt;code&gt;fly deploy&lt;/code&gt; yet. Do that first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#Before
export RELEASE_NODE="${FLY_APP_NAME}-${FLY_IMAGE_REF##*-}@${FLY_PRIVATE_IP}"

#After
ip=$(grep fly-local-6pn /etc/hosts | cut -f 1)
export RELEASE_NODE="${FLY_APP_NAME}@${ip}"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See more from &lt;a href="https://fly.io/docs/elixir/the-basics/clustering/" rel="noopener noreferrer"&gt;Fly.io guide&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  See the full project
&lt;/h1&gt;

&lt;p&gt;Check out the &lt;a href="https://github.com/byronsalty/talkie" rel="noopener noreferrer"&gt;Github Repo&lt;/a&gt;for the project used to test this article.&lt;/p&gt;

&lt;h1&gt;
  
  
  Other Resources
&lt;/h1&gt;

&lt;p&gt;There are some good examples here:&lt;/p&gt;

&lt;p&gt;Well &lt;a href="https://www.poeticoding.com/distributed-phoenix-chat-with-pubsub-pg2-adapter/" rel="noopener noreferrer"&gt;written article&lt;/a&gt; from Alvise Susmel that helped me a lot&lt;/p&gt;

</description>
      <category>pubsub</category>
      <category>phoenix</category>
      <category>2023</category>
      <category>flyio</category>
    </item>
  </channel>
</rss>
