<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Andrey Fadeev]]></title><description><![CDATA[My personal Substack]]></description><link>https://blog.andreyfadeev.com</link><image><url>https://substackcdn.com/image/fetch/$s_!KUxu!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa69ca129-bd71-45fd-af31-f8213181c181_685x685.png</url><title>Andrey Fadeev</title><link>https://blog.andreyfadeev.com</link></image><generator>Substack</generator><lastBuildDate>Thu, 16 Apr 2026 19:19:09 GMT</lastBuildDate><atom:link href="https://blog.andreyfadeev.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Andrey Fadeev]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[andreyfadeev@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[andreyfadeev@substack.com]]></itunes:email><itunes:name><![CDATA[Andrey Fadeev]]></itunes:name></itunes:owner><itunes:author><![CDATA[Andrey Fadeev]]></itunes:author><googleplay:owner><![CDATA[andreyfadeev@substack.com]]></googleplay:owner><googleplay:email><![CDATA[andreyfadeev@substack.com]]></googleplay:email><googleplay:author><![CDATA[Andrey Fadeev]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Bulletproof Type Safety in Gleam: From Database to Client]]></title><description><![CDATA[Building end-to-end type-safe applications with Gleam, PostgreSQL, and shared domain models for instant feedback and zero runtime surprises.]]></description><link>https://blog.andreyfadeev.com/p/bulletproof-type-safety-in-gleam</link><guid isPermaLink="false">https://blog.andreyfadeev.com/p/bulletproof-type-safety-in-gleam</guid><dc:creator><![CDATA[Andrey Fadeev]]></dc:creator><pubDate>Tue, 13 Jan 2026 20:42:19 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!DhXD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7983a521-a233-43f7-a966-e8a2cfb922a6_1280x720.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!DhXD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7983a521-a233-43f7-a966-e8a2cfb922a6_1280x720.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!DhXD!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7983a521-a233-43f7-a966-e8a2cfb922a6_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!DhXD!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7983a521-a233-43f7-a966-e8a2cfb922a6_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!DhXD!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7983a521-a233-43f7-a966-e8a2cfb922a6_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!DhXD!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7983a521-a233-43f7-a966-e8a2cfb922a6_1280x720.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!DhXD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7983a521-a233-43f7-a966-e8a2cfb922a6_1280x720.png" width="1280" height="720" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7983a521-a233-43f7-a966-e8a2cfb922a6_1280x720.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:720,&quot;width&quot;:1280,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:57925,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://blog.andreyfadeev.com/i/184116704?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7983a521-a233-43f7-a966-e8a2cfb922a6_1280x720.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!DhXD!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7983a521-a233-43f7-a966-e8a2cfb922a6_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!DhXD!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7983a521-a233-43f7-a966-e8a2cfb922a6_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!DhXD!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7983a521-a233-43f7-a966-e8a2cfb922a6_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!DhXD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7983a521-a233-43f7-a966-e8a2cfb922a6_1280x720.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Let&#8217;s learn how to build end-to-end, type-safe applications with Gleam.</p><p>We&#8217;ll use a PostgreSQL database to store our data and Gleam for both the frontend and backend.</p><h2>Project structure</h2><p>First, let&#8217;s setup the project structure. We will need 3 top level folders: <code>server</code>, <code>client</code> and <code>shared</code>. Each of them is a Gleam project:</p><pre><code>gleam new shared 
gleam new client 
gleam new server</code></pre><p>So as the result we will get this folder structure:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!2XcD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F636bb099-f216-42f4-9aff-616e90892888_2080x1652.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!2XcD!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F636bb099-f216-42f4-9aff-616e90892888_2080x1652.png 424w, https://substackcdn.com/image/fetch/$s_!2XcD!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F636bb099-f216-42f4-9aff-616e90892888_2080x1652.png 848w, https://substackcdn.com/image/fetch/$s_!2XcD!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F636bb099-f216-42f4-9aff-616e90892888_2080x1652.png 1272w, https://substackcdn.com/image/fetch/$s_!2XcD!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F636bb099-f216-42f4-9aff-616e90892888_2080x1652.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!2XcD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F636bb099-f216-42f4-9aff-616e90892888_2080x1652.png" width="1456" height="1156" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/636bb099-f216-42f4-9aff-616e90892888_2080x1652.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1156,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:164456,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.andreyfadeev.com/i/184116704?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F636bb099-f216-42f4-9aff-616e90892888_2080x1652.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!2XcD!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F636bb099-f216-42f4-9aff-616e90892888_2080x1652.png 424w, https://substackcdn.com/image/fetch/$s_!2XcD!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F636bb099-f216-42f4-9aff-616e90892888_2080x1652.png 848w, https://substackcdn.com/image/fetch/$s_!2XcD!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F636bb099-f216-42f4-9aff-616e90892888_2080x1652.png 1272w, https://substackcdn.com/image/fetch/$s_!2XcD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F636bb099-f216-42f4-9aff-616e90892888_2080x1652.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><pre><code>.
&#9500;&#9472;&#9472; client
&#9474;&nbsp;&nbsp; &#9500;&#9472;&#9472; gleam.toml
&#9474;&nbsp;&nbsp; &#9500;&#9472;&#9472; src
&#9474;&nbsp;&nbsp; &#9492;&#9472;&#9472; test
&#9500;&#9472;&#9472; server
&#9474;&nbsp;&nbsp; &#9500;&#9472;&#9472; gleam.toml
&#9474;&nbsp;&nbsp; &#9500;&#9472;&#9472; src
&#9474;&nbsp;&nbsp; &#9492;&#9472;&#9472; test
&#9492;&#9472;&#9472; shared
    &#9500;&#9472;&#9472; gleam.toml
    &#9500;&#9472;&#9472; src
    &#9492;&#9472;&#9472; test

10 directories, 3 files</code></pre><p>Note, that both <code>client</code> and <code>server</code> are using local dependency on the <code>shared</code> project. Aslo <code>client</code> is configured with <code>javascript</code> as the target. </p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!6fBf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3384100e-e70e-4d14-a13b-ad15dfcbc2dd_2080x752.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!6fBf!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3384100e-e70e-4d14-a13b-ad15dfcbc2dd_2080x752.png 424w, https://substackcdn.com/image/fetch/$s_!6fBf!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3384100e-e70e-4d14-a13b-ad15dfcbc2dd_2080x752.png 848w, https://substackcdn.com/image/fetch/$s_!6fBf!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3384100e-e70e-4d14-a13b-ad15dfcbc2dd_2080x752.png 1272w, https://substackcdn.com/image/fetch/$s_!6fBf!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3384100e-e70e-4d14-a13b-ad15dfcbc2dd_2080x752.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!6fBf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3384100e-e70e-4d14-a13b-ad15dfcbc2dd_2080x752.png" width="1456" height="526" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3384100e-e70e-4d14-a13b-ad15dfcbc2dd_2080x752.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:526,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:89550,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.andreyfadeev.com/i/184116704?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3384100e-e70e-4d14-a13b-ad15dfcbc2dd_2080x752.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!6fBf!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3384100e-e70e-4d14-a13b-ad15dfcbc2dd_2080x752.png 424w, https://substackcdn.com/image/fetch/$s_!6fBf!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3384100e-e70e-4d14-a13b-ad15dfcbc2dd_2080x752.png 848w, https://substackcdn.com/image/fetch/$s_!6fBf!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3384100e-e70e-4d14-a13b-ad15dfcbc2dd_2080x752.png 1272w, https://substackcdn.com/image/fetch/$s_!6fBf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3384100e-e70e-4d14-a13b-ad15dfcbc2dd_2080x752.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><pre><code><code>target = "javascript"

[dependencies]
shared = { path = "../shared" }</code></code></pre><h2>Preparing Postgres Database</h2><p>Let&#8217;s apply a pragmatic Domain-Driven Design (DDD) approach. Suppose we want to store and manage <code>User</code> entities. A sensible first step is to design the database schema, starting with a simple <code>users</code> table that contains a few core fields.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!VLqj!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F02627e16-4408-4995-89a3-5702b1ddf42a_2812x1112.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!VLqj!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F02627e16-4408-4995-89a3-5702b1ddf42a_2812x1112.png 424w, https://substackcdn.com/image/fetch/$s_!VLqj!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F02627e16-4408-4995-89a3-5702b1ddf42a_2812x1112.png 848w, https://substackcdn.com/image/fetch/$s_!VLqj!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F02627e16-4408-4995-89a3-5702b1ddf42a_2812x1112.png 1272w, https://substackcdn.com/image/fetch/$s_!VLqj!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F02627e16-4408-4995-89a3-5702b1ddf42a_2812x1112.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!VLqj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F02627e16-4408-4995-89a3-5702b1ddf42a_2812x1112.png" width="1456" height="576" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/02627e16-4408-4995-89a3-5702b1ddf42a_2812x1112.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:576,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:181386,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.andreyfadeev.com/i/184116704?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F02627e16-4408-4995-89a3-5702b1ddf42a_2812x1112.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!VLqj!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F02627e16-4408-4995-89a3-5702b1ddf42a_2812x1112.png 424w, https://substackcdn.com/image/fetch/$s_!VLqj!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F02627e16-4408-4995-89a3-5702b1ddf42a_2812x1112.png 848w, https://substackcdn.com/image/fetch/$s_!VLqj!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F02627e16-4408-4995-89a3-5702b1ddf42a_2812x1112.png 1272w, https://substackcdn.com/image/fetch/$s_!VLqj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F02627e16-4408-4995-89a3-5702b1ddf42a_2812x1112.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><pre><code>CREATE TABLE users (
    id          INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    name        TEXT NOT NULL,
    avatar_url  TEXT,
    email       TEXT NOT NULL,
    created_at  TIMESTAMP NOT NULL DEFAULT now(),
    updated_at  TIMESTAMP
);</code></pre><p>Next, let&#8217;s spin up a local database in a Docker container so we can use it for further development.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!IVtG!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa32de002-f8a7-4ff6-892b-ad7239e90e54_2696x1924.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!IVtG!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa32de002-f8a7-4ff6-892b-ad7239e90e54_2696x1924.png 424w, https://substackcdn.com/image/fetch/$s_!IVtG!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa32de002-f8a7-4ff6-892b-ad7239e90e54_2696x1924.png 848w, https://substackcdn.com/image/fetch/$s_!IVtG!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa32de002-f8a7-4ff6-892b-ad7239e90e54_2696x1924.png 1272w, https://substackcdn.com/image/fetch/$s_!IVtG!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa32de002-f8a7-4ff6-892b-ad7239e90e54_2696x1924.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!IVtG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa32de002-f8a7-4ff6-892b-ad7239e90e54_2696x1924.png" width="1456" height="1039" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a32de002-f8a7-4ff6-892b-ad7239e90e54_2696x1924.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1039,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:347878,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.andreyfadeev.com/i/184116704?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa32de002-f8a7-4ff6-892b-ad7239e90e54_2696x1924.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!IVtG!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa32de002-f8a7-4ff6-892b-ad7239e90e54_2696x1924.png 424w, https://substackcdn.com/image/fetch/$s_!IVtG!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa32de002-f8a7-4ff6-892b-ad7239e90e54_2696x1924.png 848w, https://substackcdn.com/image/fetch/$s_!IVtG!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa32de002-f8a7-4ff6-892b-ad7239e90e54_2696x1924.png 1272w, https://substackcdn.com/image/fetch/$s_!IVtG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa32de002-f8a7-4ff6-892b-ad7239e90e54_2696x1924.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><pre><code>services:
  postgres:
    image: postgres:17-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: gleam
      POSTGRES_PASSWORD: gleam
      POSTGRES_DB: gleam
    ports:
      - "5432:5432"
    volumes:
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres -d app_db"]
      interval: 10s
      timeout: 5s
      retries: 5</code></pre><h2>Gleam SQL queries</h2><p>How do we bring knowledge of the database schema into our Gleam code?</p><p>We don&#8217;t need a complex ORM or any magical framework. Instead, we can rely on plain SQL combined with a small amount of code generation.</p><p>To do this, let&#8217;s install the excellent <strong>squirrel</strong> library:</p><pre><code><code>gleam add --dev squirrel</code></code></pre><p>Notice the <code>--dev</code> flag &#8212; this isn&#8217;t even a runtime dependency.</p><p>Next, let&#8217;s add queries, written as plain SQL, to the <code>server/src/sql</code> directory.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!3dEM!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d9559ae-e89f-4f92-ab7b-e5d4c7be1e47_2080x1204.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!3dEM!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d9559ae-e89f-4f92-ab7b-e5d4c7be1e47_2080x1204.png 424w, https://substackcdn.com/image/fetch/$s_!3dEM!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d9559ae-e89f-4f92-ab7b-e5d4c7be1e47_2080x1204.png 848w, https://substackcdn.com/image/fetch/$s_!3dEM!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d9559ae-e89f-4f92-ab7b-e5d4c7be1e47_2080x1204.png 1272w, https://substackcdn.com/image/fetch/$s_!3dEM!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d9559ae-e89f-4f92-ab7b-e5d4c7be1e47_2080x1204.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!3dEM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d9559ae-e89f-4f92-ab7b-e5d4c7be1e47_2080x1204.png" width="1456" height="843" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2d9559ae-e89f-4f92-ab7b-e5d4c7be1e47_2080x1204.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:843,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:149454,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.andreyfadeev.com/i/184116704?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d9559ae-e89f-4f92-ab7b-e5d4c7be1e47_2080x1204.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!3dEM!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d9559ae-e89f-4f92-ab7b-e5d4c7be1e47_2080x1204.png 424w, https://substackcdn.com/image/fetch/$s_!3dEM!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d9559ae-e89f-4f92-ab7b-e5d4c7be1e47_2080x1204.png 848w, https://substackcdn.com/image/fetch/$s_!3dEM!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d9559ae-e89f-4f92-ab7b-e5d4c7be1e47_2080x1204.png 1272w, https://substackcdn.com/image/fetch/$s_!3dEM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d9559ae-e89f-4f92-ab7b-e5d4c7be1e47_2080x1204.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><pre><code>&#9492;&#9472;&#9472; server
    &#9500;&#9472;&#9472; gleam.toml
    &#9492;&#9472;&#9472; src
        &#9492;&#9472;&#9472; sql
            &#9500;&#9472;&#9472; insert_users.sql
            &#9500;&#9472;&#9472; search_users_by_name.sql
            &#9500;&#9472;&#9472; select_users_by_email.sql
            &#9500;&#9472;&#9472; select_users_by_id.sql
            &#9492;&#9472;&#9472; select_users.sql</code></pre><p>So we end up with the following SQL files: <code>select_users.sql, select_users_by_id.sql, select_users_by_email.sql, insert_users.sql</code></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!bOEW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6eeb7fcd-4108-4ac3-a1c9-df54a180ad94_2080x1564.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!bOEW!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6eeb7fcd-4108-4ac3-a1c9-df54a180ad94_2080x1564.png 424w, https://substackcdn.com/image/fetch/$s_!bOEW!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6eeb7fcd-4108-4ac3-a1c9-df54a180ad94_2080x1564.png 848w, https://substackcdn.com/image/fetch/$s_!bOEW!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6eeb7fcd-4108-4ac3-a1c9-df54a180ad94_2080x1564.png 1272w, https://substackcdn.com/image/fetch/$s_!bOEW!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6eeb7fcd-4108-4ac3-a1c9-df54a180ad94_2080x1564.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!bOEW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6eeb7fcd-4108-4ac3-a1c9-df54a180ad94_2080x1564.png" width="1456" height="1095" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6eeb7fcd-4108-4ac3-a1c9-df54a180ad94_2080x1564.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1095,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:235136,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.andreyfadeev.com/i/184116704?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6eeb7fcd-4108-4ac3-a1c9-df54a180ad94_2080x1564.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!bOEW!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6eeb7fcd-4108-4ac3-a1c9-df54a180ad94_2080x1564.png 424w, https://substackcdn.com/image/fetch/$s_!bOEW!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6eeb7fcd-4108-4ac3-a1c9-df54a180ad94_2080x1564.png 848w, https://substackcdn.com/image/fetch/$s_!bOEW!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6eeb7fcd-4108-4ac3-a1c9-df54a180ad94_2080x1564.png 1272w, https://substackcdn.com/image/fetch/$s_!bOEW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6eeb7fcd-4108-4ac3-a1c9-df54a180ad94_2080x1564.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><pre><code>-- select_users.sql
SELECT * FROM users;

-- insert_users.sql
INSERT INTO users(name, email)
VALUES ($1, $2)
RETURNING *;

-- select_users_by_email.sql
SELECT * FROM users WHERE email = $1;

-- select_users_by_id.sql
SELECT * FROM users WHERE id = $1;</code></pre><p>These queries are trivial for now, but we&#8217;re not limited in any way &#8212; each SQL file can contain <em>any</em> SQL that PostgreSQL understands. For example, we can start using full-text search right away:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!PryO!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2878ce7-0d35-479c-aeb0-9163c6b7551e_2888x572.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!PryO!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2878ce7-0d35-479c-aeb0-9163c6b7551e_2888x572.png 424w, https://substackcdn.com/image/fetch/$s_!PryO!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2878ce7-0d35-479c-aeb0-9163c6b7551e_2888x572.png 848w, https://substackcdn.com/image/fetch/$s_!PryO!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2878ce7-0d35-479c-aeb0-9163c6b7551e_2888x572.png 1272w, https://substackcdn.com/image/fetch/$s_!PryO!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2878ce7-0d35-479c-aeb0-9163c6b7551e_2888x572.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!PryO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2878ce7-0d35-479c-aeb0-9163c6b7551e_2888x572.png" width="1456" height="288" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e2878ce7-0d35-479c-aeb0-9163c6b7551e_2888x572.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:288,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:101385,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.andreyfadeev.com/i/184116704?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2878ce7-0d35-479c-aeb0-9163c6b7551e_2888x572.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!PryO!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2878ce7-0d35-479c-aeb0-9163c6b7551e_2888x572.png 424w, https://substackcdn.com/image/fetch/$s_!PryO!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2878ce7-0d35-479c-aeb0-9163c6b7551e_2888x572.png 848w, https://substackcdn.com/image/fetch/$s_!PryO!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2878ce7-0d35-479c-aeb0-9163c6b7551e_2888x572.png 1272w, https://substackcdn.com/image/fetch/$s_!PryO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2878ce7-0d35-479c-aeb0-9163c6b7551e_2888x572.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><pre><code>SELECT * FROM users
WHERE to_tsvector('simple', name) @@ plainto_tsquery('simple', $1);</code></pre><p>The SQL file names can be anything &#8212; it&#8217;s up to you to choose a convention. Keep in mind, however, that the generated code will use the file name both for the Gleam function name and for the generated record types.</p><p>I currently follow this pattern: the operation (e.g. <code>select</code>, <code>insert</code>, <code>update</code>, <code>delete</code>), followed by the table name, and then an optional condition.</p><p>Before we can use <code>squirrel</code> for code generation, we need to set the <code>DATABASE_URL</code> environment variable.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!6TSY!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F74110e2c-ca21-437f-b704-ee5aafa5be82_2740x932.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!6TSY!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F74110e2c-ca21-437f-b704-ee5aafa5be82_2740x932.png 424w, https://substackcdn.com/image/fetch/$s_!6TSY!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F74110e2c-ca21-437f-b704-ee5aafa5be82_2740x932.png 848w, https://substackcdn.com/image/fetch/$s_!6TSY!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F74110e2c-ca21-437f-b704-ee5aafa5be82_2740x932.png 1272w, https://substackcdn.com/image/fetch/$s_!6TSY!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F74110e2c-ca21-437f-b704-ee5aafa5be82_2740x932.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!6TSY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F74110e2c-ca21-437f-b704-ee5aafa5be82_2740x932.png" width="1456" height="495" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/74110e2c-ca21-437f-b704-ee5aafa5be82_2740x932.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:495,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:172021,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.andreyfadeev.com/i/184116704?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F74110e2c-ca21-437f-b704-ee5aafa5be82_2740x932.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!6TSY!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F74110e2c-ca21-437f-b704-ee5aafa5be82_2740x932.png 424w, https://substackcdn.com/image/fetch/$s_!6TSY!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F74110e2c-ca21-437f-b704-ee5aafa5be82_2740x932.png 848w, https://substackcdn.com/image/fetch/$s_!6TSY!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F74110e2c-ca21-437f-b704-ee5aafa5be82_2740x932.png 1272w, https://substackcdn.com/image/fetch/$s_!6TSY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F74110e2c-ca21-437f-b704-ee5aafa5be82_2740x932.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><pre><code>export DATABASE_URL=postgres://gleam:gleam@localhost:5432/gleam

&#10095; gleam run -m squirrel
   Compiled in 0.08s
    Running squirrel.main
&#128063;&#65039;  Generated 5 queries!</code></pre><p>The <code>squirrel</code> library detected SQL files and generated 5 queries, that looks promising! Now that we have the <code>server/src/sql.gleam</code> file, let&#8217;s take a look at what was generated.</p><p>For example, here is the <code>select_user_by_id</code> function (most of the decoding logic is omitted for simplicity):</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!KY02!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9d106d2-12ac-44d5-bb86-191af4cfd113_2772x1924.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!KY02!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9d106d2-12ac-44d5-bb86-191af4cfd113_2772x1924.png 424w, https://substackcdn.com/image/fetch/$s_!KY02!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9d106d2-12ac-44d5-bb86-191af4cfd113_2772x1924.png 848w, https://substackcdn.com/image/fetch/$s_!KY02!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9d106d2-12ac-44d5-bb86-191af4cfd113_2772x1924.png 1272w, https://substackcdn.com/image/fetch/$s_!KY02!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9d106d2-12ac-44d5-bb86-191af4cfd113_2772x1924.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!KY02!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9d106d2-12ac-44d5-bb86-191af4cfd113_2772x1924.png" width="1456" height="1011" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c9d106d2-12ac-44d5-bb86-191af4cfd113_2772x1924.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1011,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:377174,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.andreyfadeev.com/i/184116704?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9d106d2-12ac-44d5-bb86-191af4cfd113_2772x1924.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!KY02!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9d106d2-12ac-44d5-bb86-191af4cfd113_2772x1924.png 424w, https://substackcdn.com/image/fetch/$s_!KY02!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9d106d2-12ac-44d5-bb86-191af4cfd113_2772x1924.png 848w, https://substackcdn.com/image/fetch/$s_!KY02!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9d106d2-12ac-44d5-bb86-191af4cfd113_2772x1924.png 1272w, https://substackcdn.com/image/fetch/$s_!KY02!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9d106d2-12ac-44d5-bb86-191af4cfd113_2772x1924.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><pre><code>pub fn select_users_by_id(
  db: pog.Connection,
  arg_1: Int,
) -&gt; Result(pog.Returned(SelectUsersByIdRow), pog.QueryError) {
  let decoder = {
    use id &lt;- decode.field(0, decode.int)
    use name &lt;- decode.field(1, decode.string)

    decode.success(SelectUsersByIdRow(id:,name:,))
  }

  "SELECT * FROM users WHERE id = $1; "
  |&gt; pog.query
  |&gt; pog.parameter(pog.int(arg_1))
  |&gt; pog.returning(decoder)
  |&gt; pog.execute(db)
}</code></pre><p>And also the custom record type <code>SelectUsersByIdRow</code>:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!aKcj!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e855630-8f31-4968-8ea4-dd7043e499c0_2080x1292.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!aKcj!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e855630-8f31-4968-8ea4-dd7043e499c0_2080x1292.png 424w, https://substackcdn.com/image/fetch/$s_!aKcj!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e855630-8f31-4968-8ea4-dd7043e499c0_2080x1292.png 848w, https://substackcdn.com/image/fetch/$s_!aKcj!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e855630-8f31-4968-8ea4-dd7043e499c0_2080x1292.png 1272w, https://substackcdn.com/image/fetch/$s_!aKcj!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e855630-8f31-4968-8ea4-dd7043e499c0_2080x1292.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!aKcj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e855630-8f31-4968-8ea4-dd7043e499c0_2080x1292.png" width="1456" height="904" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3e855630-8f31-4968-8ea4-dd7043e499c0_2080x1292.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:904,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:186244,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.andreyfadeev.com/i/184116704?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e855630-8f31-4968-8ea4-dd7043e499c0_2080x1292.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!aKcj!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e855630-8f31-4968-8ea4-dd7043e499c0_2080x1292.png 424w, https://substackcdn.com/image/fetch/$s_!aKcj!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e855630-8f31-4968-8ea4-dd7043e499c0_2080x1292.png 848w, https://substackcdn.com/image/fetch/$s_!aKcj!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e855630-8f31-4968-8ea4-dd7043e499c0_2080x1292.png 1272w, https://substackcdn.com/image/fetch/$s_!aKcj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3e855630-8f31-4968-8ea4-dd7043e499c0_2080x1292.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><pre><code>pub type SelectUsersByIdRow {
  SelectUsersByIdRow(
    id: Int,
    name: String,
    avatar_url: Option(String),
    email: String,
    created_at: Timestamp,
    updated_at: Option(Timestamp),
  )
}</code></pre><p>The same code is generated for the other queries as well.</p><p>Next, let&#8217;s see how we can execute these queries from the <code>main</code> function of our application:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!PFlf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3f28378-1288-413e-97ef-e0b3f66d943f_3680x1472.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!PFlf!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3f28378-1288-413e-97ef-e0b3f66d943f_3680x1472.png 424w, https://substackcdn.com/image/fetch/$s_!PFlf!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3f28378-1288-413e-97ef-e0b3f66d943f_3680x1472.png 848w, https://substackcdn.com/image/fetch/$s_!PFlf!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3f28378-1288-413e-97ef-e0b3f66d943f_3680x1472.png 1272w, https://substackcdn.com/image/fetch/$s_!PFlf!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3f28378-1288-413e-97ef-e0b3f66d943f_3680x1472.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!PFlf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3f28378-1288-413e-97ef-e0b3f66d943f_3680x1472.png" width="1456" height="582" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e3f28378-1288-413e-97ef-e0b3f66d943f_3680x1472.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:582,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:309931,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.andreyfadeev.com/i/184116704?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3f28378-1288-413e-97ef-e0b3f66d943f_3680x1472.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!PFlf!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3f28378-1288-413e-97ef-e0b3f66d943f_3680x1472.png 424w, https://substackcdn.com/image/fetch/$s_!PFlf!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3f28378-1288-413e-97ef-e0b3f66d943f_3680x1472.png 848w, https://substackcdn.com/image/fetch/$s_!PFlf!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3f28378-1288-413e-97ef-e0b3f66d943f_3680x1472.png 1272w, https://substackcdn.com/image/fetch/$s_!PFlf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3f28378-1288-413e-97ef-e0b3f66d943f_3680x1472.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><pre><code>pub fn main() -&gt; Nil {
  let conn = conn()

  let assert Ok(query_result) = sql.insert_users(conn, "Andrey", "email@example.com")
  let assert Ok(inserted_user) = list.first(query_result.rows)

  // InsertUsersRow(2, "Andrey", None, "email@example.com", Timestamp(1768325274, 201229000), None)
}</code></pre><p>And also query by id:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!uHz5!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc67c7e53-4c4f-4941-9121-5a5d9266433c_3680x1472.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!uHz5!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc67c7e53-4c4f-4941-9121-5a5d9266433c_3680x1472.png 424w, https://substackcdn.com/image/fetch/$s_!uHz5!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc67c7e53-4c4f-4941-9121-5a5d9266433c_3680x1472.png 848w, https://substackcdn.com/image/fetch/$s_!uHz5!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc67c7e53-4c4f-4941-9121-5a5d9266433c_3680x1472.png 1272w, https://substackcdn.com/image/fetch/$s_!uHz5!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc67c7e53-4c4f-4941-9121-5a5d9266433c_3680x1472.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!uHz5!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc67c7e53-4c4f-4941-9121-5a5d9266433c_3680x1472.png" width="1456" height="582" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c67c7e53-4c4f-4941-9121-5a5d9266433c_3680x1472.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:582,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:311572,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.andreyfadeev.com/i/184116704?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc67c7e53-4c4f-4941-9121-5a5d9266433c_3680x1472.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!uHz5!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc67c7e53-4c4f-4941-9121-5a5d9266433c_3680x1472.png 424w, https://substackcdn.com/image/fetch/$s_!uHz5!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc67c7e53-4c4f-4941-9121-5a5d9266433c_3680x1472.png 848w, https://substackcdn.com/image/fetch/$s_!uHz5!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc67c7e53-4c4f-4941-9121-5a5d9266433c_3680x1472.png 1272w, https://substackcdn.com/image/fetch/$s_!uHz5!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc67c7e53-4c4f-4941-9121-5a5d9266433c_3680x1472.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><pre><code>pub fn main() -&gt; Nil {
  let conn = conn()

  let assert Ok(query_result) = sql.select_users_by_id(conn, 0)
  // Returned(0, [])

  let assert Ok(query_result) = sql.select_users_by_id(conn, 1)
  // Returned(1, [SelectUsersByIdRow(1, "Andrey", None, "email@example.com", Timestamp(1768303857, 745886000), None)])

  Nil
}</code></pre><p>This is already really handy &#8212; and pretty cool! It&#8217;s also type-safe, since the queries and record types were generated based on the actual database schema; the <code>squirrel</code> library performs schema introspection automatically.</p><p>The only downside is that we end up with multiple record types that actually represent the same logical type:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!KhwF!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecdbabf2-2285-41f6-abad-24618f343fe7_2080x1744.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!KhwF!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecdbabf2-2285-41f6-abad-24618f343fe7_2080x1744.png 424w, https://substackcdn.com/image/fetch/$s_!KhwF!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecdbabf2-2285-41f6-abad-24618f343fe7_2080x1744.png 848w, https://substackcdn.com/image/fetch/$s_!KhwF!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecdbabf2-2285-41f6-abad-24618f343fe7_2080x1744.png 1272w, https://substackcdn.com/image/fetch/$s_!KhwF!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecdbabf2-2285-41f6-abad-24618f343fe7_2080x1744.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!KhwF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecdbabf2-2285-41f6-abad-24618f343fe7_2080x1744.png" width="1456" height="1221" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ecdbabf2-2285-41f6-abad-24618f343fe7_2080x1744.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1221,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:289879,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.andreyfadeev.com/i/184116704?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecdbabf2-2285-41f6-abad-24618f343fe7_2080x1744.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!KhwF!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecdbabf2-2285-41f6-abad-24618f343fe7_2080x1744.png 424w, https://substackcdn.com/image/fetch/$s_!KhwF!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecdbabf2-2285-41f6-abad-24618f343fe7_2080x1744.png 848w, https://substackcdn.com/image/fetch/$s_!KhwF!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecdbabf2-2285-41f6-abad-24618f343fe7_2080x1744.png 1272w, https://substackcdn.com/image/fetch/$s_!KhwF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecdbabf2-2285-41f6-abad-24618f343fe7_2080x1744.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><pre><code>pub type SelectUsersRow {
  SelectUsersRow(
    id: Int,
    name: String,
    avatar_url: Option(String),
    email: String,
    created_at: Timestamp,
    updated_at: Option(Timestamp),
  )
}

pub type InsertUsersRow {}
pub type SearchUsersByNameRow {}
pub type SelectUsersByEmailRow {}
pub type SelectUsersByIdRow {}</code></pre><p>The solution to that is to create a domain model in the <code>shared</code> project:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!RLUH!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8368fc93-48c1-498e-b5eb-392002967af0_2080x1292.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!RLUH!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8368fc93-48c1-498e-b5eb-392002967af0_2080x1292.png 424w, https://substackcdn.com/image/fetch/$s_!RLUH!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8368fc93-48c1-498e-b5eb-392002967af0_2080x1292.png 848w, https://substackcdn.com/image/fetch/$s_!RLUH!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8368fc93-48c1-498e-b5eb-392002967af0_2080x1292.png 1272w, https://substackcdn.com/image/fetch/$s_!RLUH!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8368fc93-48c1-498e-b5eb-392002967af0_2080x1292.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!RLUH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8368fc93-48c1-498e-b5eb-392002967af0_2080x1292.png" width="1456" height="904" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8368fc93-48c1-498e-b5eb-392002967af0_2080x1292.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:904,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:174137,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.andreyfadeev.com/i/184116704?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8368fc93-48c1-498e-b5eb-392002967af0_2080x1292.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!RLUH!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8368fc93-48c1-498e-b5eb-392002967af0_2080x1292.png 424w, https://substackcdn.com/image/fetch/$s_!RLUH!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8368fc93-48c1-498e-b5eb-392002967af0_2080x1292.png 848w, https://substackcdn.com/image/fetch/$s_!RLUH!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8368fc93-48c1-498e-b5eb-392002967af0_2080x1292.png 1272w, https://substackcdn.com/image/fetch/$s_!RLUH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8368fc93-48c1-498e-b5eb-392002967af0_2080x1292.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><pre><code>pub type User {
  User(
    id: Int,
    name: String,
    avatar_url: Option(String),
    email: String,
    created_at: Timestamp,
    updated_at: Option(Timestamp),
  )
}</code></pre><p>Now we can create a set of mappers to convert the SQL-generated types into our domain types:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!tZ9j!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ee9aa75-ffdf-484e-8397-b0dff0293f11_3216x2012.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!tZ9j!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ee9aa75-ffdf-484e-8397-b0dff0293f11_3216x2012.png 424w, https://substackcdn.com/image/fetch/$s_!tZ9j!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ee9aa75-ffdf-484e-8397-b0dff0293f11_3216x2012.png 848w, https://substackcdn.com/image/fetch/$s_!tZ9j!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ee9aa75-ffdf-484e-8397-b0dff0293f11_3216x2012.png 1272w, https://substackcdn.com/image/fetch/$s_!tZ9j!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ee9aa75-ffdf-484e-8397-b0dff0293f11_3216x2012.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!tZ9j!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ee9aa75-ffdf-484e-8397-b0dff0293f11_3216x2012.png" width="1456" height="911" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7ee9aa75-ffdf-484e-8397-b0dff0293f11_3216x2012.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:911,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:424310,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.andreyfadeev.com/i/184116704?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ee9aa75-ffdf-484e-8397-b0dff0293f11_3216x2012.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!tZ9j!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ee9aa75-ffdf-484e-8397-b0dff0293f11_3216x2012.png 424w, https://substackcdn.com/image/fetch/$s_!tZ9j!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ee9aa75-ffdf-484e-8397-b0dff0293f11_3216x2012.png 848w, https://substackcdn.com/image/fetch/$s_!tZ9j!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ee9aa75-ffdf-484e-8397-b0dff0293f11_3216x2012.png 1272w, https://substackcdn.com/image/fetch/$s_!tZ9j!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ee9aa75-ffdf-484e-8397-b0dff0293f11_3216x2012.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><pre><code>pub fn select_users_row_to_user(row: sql.SelectUsersRow) -&gt; users.User {
  users.User(
    id: row.id,
    name: row.name,
    avatar_url: row.avatar_url,
    email: row.email,
    created_at: row.created_at,
    updated_at: row.updated_at,
  )
}

pub fn insert_users_row_to_user(row: sql.InsertUsersRow) {}

pub fn select_users_by_email_row_to_user(row: sql.SelectUsersByEmailRow) {}

pub fn select_users_by_id_row_to_user(row: sql.SelectUsersByIdRow) {}

pub fn search_users_by_name_row_to_user(row: sql.SearchUsersByNameRow) {}</code></pre><p>Yes, this approach introduces some code duplication, and when there&#8217;s a one-to-one mapping with the domain model it can feel redundant. But, in real-world applications it&#8217;s often much more useful, as it adds a layer of abstraction that can hide the underlying database structure, allow remapping of fields, and more.</p><p>Once we have a shared <code>User</code> type as our domain model, we can use it to send data over the wire &#8212; for example, in a JSON REST API.</p><p>There are also language server (LSP) commands that can automatically generate <code>_to_json</code> and <code>_from_json</code> functions for us:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!HOn2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8c59598-2108-4adc-9088-702b168d5310_3252x1924.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!HOn2!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8c59598-2108-4adc-9088-702b168d5310_3252x1924.png 424w, https://substackcdn.com/image/fetch/$s_!HOn2!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8c59598-2108-4adc-9088-702b168d5310_3252x1924.png 848w, https://substackcdn.com/image/fetch/$s_!HOn2!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8c59598-2108-4adc-9088-702b168d5310_3252x1924.png 1272w, https://substackcdn.com/image/fetch/$s_!HOn2!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8c59598-2108-4adc-9088-702b168d5310_3252x1924.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!HOn2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8c59598-2108-4adc-9088-702b168d5310_3252x1924.png" width="1456" height="861" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a8c59598-2108-4adc-9088-702b168d5310_3252x1924.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:861,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:442365,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.andreyfadeev.com/i/184116704?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8c59598-2108-4adc-9088-702b168d5310_3252x1924.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!HOn2!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8c59598-2108-4adc-9088-702b168d5310_3252x1924.png 424w, https://substackcdn.com/image/fetch/$s_!HOn2!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8c59598-2108-4adc-9088-702b168d5310_3252x1924.png 848w, https://substackcdn.com/image/fetch/$s_!HOn2!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8c59598-2108-4adc-9088-702b168d5310_3252x1924.png 1272w, https://substackcdn.com/image/fetch/$s_!HOn2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8c59598-2108-4adc-9088-702b168d5310_3252x1924.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><pre><code>pub fn user_to_json(user: User) -&gt; json.Json {
  let User(id:, name:, avatar_url:, email:, created_at:, updated_at:) = user
  json.object([
    #("id", json.int(id)),
    #("name", json.string(name)),
    #("avatar_url", case avatar_url {
      option.None -&gt; json.null()
      option.Some(value) -&gt; json.string(value)
    }),
    #("email", json.string(email)),
    #("created_at", timestamp_to_json(created_at)),
    #("updated_at", case updated_at {
      option.None -&gt; json.null()
      option.Some(value) -&gt; timestamp_to_json(value)
    }),
  ])
}</code></pre><p>And a decoder:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!GLV7!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4758398a-e92f-4d74-8770-a953bf691929_3440x1472.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!GLV7!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4758398a-e92f-4d74-8770-a953bf691929_3440x1472.png 424w, https://substackcdn.com/image/fetch/$s_!GLV7!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4758398a-e92f-4d74-8770-a953bf691929_3440x1472.png 848w, https://substackcdn.com/image/fetch/$s_!GLV7!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4758398a-e92f-4d74-8770-a953bf691929_3440x1472.png 1272w, https://substackcdn.com/image/fetch/$s_!GLV7!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4758398a-e92f-4d74-8770-a953bf691929_3440x1472.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!GLV7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4758398a-e92f-4d74-8770-a953bf691929_3440x1472.png" width="1456" height="623" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4758398a-e92f-4d74-8770-a953bf691929_3440x1472.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:623,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:406778,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.andreyfadeev.com/i/184116704?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4758398a-e92f-4d74-8770-a953bf691929_3440x1472.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!GLV7!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4758398a-e92f-4d74-8770-a953bf691929_3440x1472.png 424w, https://substackcdn.com/image/fetch/$s_!GLV7!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4758398a-e92f-4d74-8770-a953bf691929_3440x1472.png 848w, https://substackcdn.com/image/fetch/$s_!GLV7!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4758398a-e92f-4d74-8770-a953bf691929_3440x1472.png 1272w, https://substackcdn.com/image/fetch/$s_!GLV7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4758398a-e92f-4d74-8770-a953bf691929_3440x1472.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><pre><code>pub fn user_decoder() -&gt; decode.Decoder(User) {
  use id &lt;- decode.field("id", decode.int)
  use name &lt;- decode.field("name", decode.string)
  use avatar_url &lt;- decode.field("avatar_url", decode.optional(decode.string))
  use email &lt;- decode.field("email", decode.string)
  use created_at &lt;- decode.field("created_at", timestamp_decoder())
  use updated_at &lt;- decode.field(
    "updated_at",
    decode.optional(timestamp_decoder()),
  )
  decode.success(User(id:, name:, avatar_url:, email:, created_at:, updated_at:))
}</code></pre><p><br>Don&#8217;t forget that we place this domain type and its encode/decode functions into a shared project. This means both the server and client will work with the same entities, which reduces friction and the possibility of errors.</p><p>Let&#8217;s look at an example from the server, where we fetch users from the database, map them to the domain model type, and encode them to JSON &#8212; making them ready to be sent in an API response:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!TrYF!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68c318ec-0c1c-4576-8b98-8a831ac4674f_2772x1652.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!TrYF!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68c318ec-0c1c-4576-8b98-8a831ac4674f_2772x1652.png 424w, https://substackcdn.com/image/fetch/$s_!TrYF!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68c318ec-0c1c-4576-8b98-8a831ac4674f_2772x1652.png 848w, https://substackcdn.com/image/fetch/$s_!TrYF!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68c318ec-0c1c-4576-8b98-8a831ac4674f_2772x1652.png 1272w, https://substackcdn.com/image/fetch/$s_!TrYF!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68c318ec-0c1c-4576-8b98-8a831ac4674f_2772x1652.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!TrYF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68c318ec-0c1c-4576-8b98-8a831ac4674f_2772x1652.png" width="1456" height="868" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/68c318ec-0c1c-4576-8b98-8a831ac4674f_2772x1652.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:868,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:294559,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.andreyfadeev.com/i/184116704?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68c318ec-0c1c-4576-8b98-8a831ac4674f_2772x1652.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!TrYF!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68c318ec-0c1c-4576-8b98-8a831ac4674f_2772x1652.png 424w, https://substackcdn.com/image/fetch/$s_!TrYF!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68c318ec-0c1c-4576-8b98-8a831ac4674f_2772x1652.png 848w, https://substackcdn.com/image/fetch/$s_!TrYF!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68c318ec-0c1c-4576-8b98-8a831ac4674f_2772x1652.png 1272w, https://substackcdn.com/image/fetch/$s_!TrYF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68c318ec-0c1c-4576-8b98-8a831ac4674f_2772x1652.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><pre><code>pub fn main() -&gt; Nil {
  let assert Ok(query_result) = sql.select_users(conn())

  let users: List(users.User) =
    query_result.rows
    |&gt; list.map(mappers.select_users_row_to_user)

  let users_json: String =
    users
    |&gt; json.array(users.user_to_json)
    |&gt; json.to_string

  // "[{\"id\":1,\"name\":\"Andrey\",\"avatar_url\":null,...}]"
}</code></pre><h2>Moving to the client side</h2><p>Finally, it&#8217;s time to move to the frontend. Here&#8217;s a function that simulates the JSON response from our backend:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!nNFh!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb621c869-6917-4890-8ff6-9748465ad897_2772x2012.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!nNFh!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb621c869-6917-4890-8ff6-9748465ad897_2772x2012.png 424w, https://substackcdn.com/image/fetch/$s_!nNFh!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb621c869-6917-4890-8ff6-9748465ad897_2772x2012.png 848w, https://substackcdn.com/image/fetch/$s_!nNFh!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb621c869-6917-4890-8ff6-9748465ad897_2772x2012.png 1272w, https://substackcdn.com/image/fetch/$s_!nNFh!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb621c869-6917-4890-8ff6-9748465ad897_2772x2012.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!nNFh!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb621c869-6917-4890-8ff6-9748465ad897_2772x2012.png" width="1456" height="1057" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b621c869-6917-4890-8ff6-9748465ad897_2772x2012.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1057,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:387921,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.andreyfadeev.com/i/184116704?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb621c869-6917-4890-8ff6-9748465ad897_2772x2012.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!nNFh!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb621c869-6917-4890-8ff6-9748465ad897_2772x2012.png 424w, https://substackcdn.com/image/fetch/$s_!nNFh!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb621c869-6917-4890-8ff6-9748465ad897_2772x2012.png 848w, https://substackcdn.com/image/fetch/$s_!nNFh!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb621c869-6917-4890-8ff6-9748465ad897_2772x2012.png 1272w, https://substackcdn.com/image/fetch/$s_!nNFh!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb621c869-6917-4890-8ff6-9748465ad897_2772x2012.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><pre><code>fn random_users_json_from_api() {
  list.range(0, 10)
  |&gt; list.map(fn(id) {
    let name = "user_" &lt;&gt; int.to_string(id)
    users.User(
      id: id,
      name: name,
      email: name &lt;&gt; "@example.com",
      avatar_url: option.Some(
        "https://api.dicebear.com/9.x/glass/svg?seed=" &lt;&gt; name,
      ),
      created_at: timestamp.from_unix_seconds(0),
      updated_at: option.None,
    )
  })
  |&gt; json.array(users.user_to_json)
  |&gt; json.to_string
}</code></pre><p>We can define a <code>user_view</code> helper function in the client app, which specifies how to render our <code>User</code> type:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!7v7A!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F83bd8a52-3daf-4d08-b7b5-f09a7c7280aa_2992x1832.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!7v7A!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F83bd8a52-3daf-4d08-b7b5-f09a7c7280aa_2992x1832.png 424w, https://substackcdn.com/image/fetch/$s_!7v7A!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F83bd8a52-3daf-4d08-b7b5-f09a7c7280aa_2992x1832.png 848w, https://substackcdn.com/image/fetch/$s_!7v7A!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F83bd8a52-3daf-4d08-b7b5-f09a7c7280aa_2992x1832.png 1272w, https://substackcdn.com/image/fetch/$s_!7v7A!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F83bd8a52-3daf-4d08-b7b5-f09a7c7280aa_2992x1832.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!7v7A!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F83bd8a52-3daf-4d08-b7b5-f09a7c7280aa_2992x1832.png" width="1456" height="892" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/83bd8a52-3daf-4d08-b7b5-f09a7c7280aa_2992x1832.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:892,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:362991,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.andreyfadeev.com/i/184116704?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F83bd8a52-3daf-4d08-b7b5-f09a7c7280aa_2992x1832.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!7v7A!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F83bd8a52-3daf-4d08-b7b5-f09a7c7280aa_2992x1832.png 424w, https://substackcdn.com/image/fetch/$s_!7v7A!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F83bd8a52-3daf-4d08-b7b5-f09a7c7280aa_2992x1832.png 848w, https://substackcdn.com/image/fetch/$s_!7v7A!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F83bd8a52-3daf-4d08-b7b5-f09a7c7280aa_2992x1832.png 1272w, https://substackcdn.com/image/fetch/$s_!7v7A!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F83bd8a52-3daf-4d08-b7b5-f09a7c7280aa_2992x1832.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><pre><code>
fn user_view(user: users.User) {
  div([class("flex gap-2 border border-gray-200 p-2 rounded-3xl")], [
    div([class("flex gap-2 items-center")], [
      div([], [
        html.img([
          class("w-20 h-auto rounded-full"),
          src(option.unwrap(user.avatar_url, "")),
        ]),
      ]),
      div([], [
        p([class("font-bold")], [text(user.name)]),
        p([class("text-gray-700")], [text(user.email)]),
      ]),
    ]),
  ])
}</code></pre><p>So, if we put everything together, this is what our complete client app looks like:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!cfa_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf479e10-df9e-4061-bd15-62e8d6f90dda_2436x1832.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!cfa_!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf479e10-df9e-4061-bd15-62e8d6f90dda_2436x1832.png 424w, https://substackcdn.com/image/fetch/$s_!cfa_!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf479e10-df9e-4061-bd15-62e8d6f90dda_2436x1832.png 848w, https://substackcdn.com/image/fetch/$s_!cfa_!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf479e10-df9e-4061-bd15-62e8d6f90dda_2436x1832.png 1272w, https://substackcdn.com/image/fetch/$s_!cfa_!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf479e10-df9e-4061-bd15-62e8d6f90dda_2436x1832.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!cfa_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf479e10-df9e-4061-bd15-62e8d6f90dda_2436x1832.png" width="1456" height="1095" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/df479e10-df9e-4061-bd15-62e8d6f90dda_2436x1832.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1095,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:325580,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.andreyfadeev.com/i/184116704?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf479e10-df9e-4061-bd15-62e8d6f90dda_2436x1832.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!cfa_!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf479e10-df9e-4061-bd15-62e8d6f90dda_2436x1832.png 424w, https://substackcdn.com/image/fetch/$s_!cfa_!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf479e10-df9e-4061-bd15-62e8d6f90dda_2436x1832.png 848w, https://substackcdn.com/image/fetch/$s_!cfa_!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf479e10-df9e-4061-bd15-62e8d6f90dda_2436x1832.png 1272w, https://substackcdn.com/image/fetch/$s_!cfa_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdf479e10-df9e-4061-bd15-62e8d6f90dda_2436x1832.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><pre><code>fn random_users_json_from_api() {}
fn user_view(user: users.User) {}

pub fn main() {
  let assert Ok(users) =
    random_users_json_from_api()
    |&gt; json.parse(decode.list(users.user_decoder()))

  let app =
    lustre.element(div(
      [attribute.class("max-w-xl mx-auto space-y-2")],
      users |&gt; list.map(user_view),
    ))
    
  let assert Ok(_) = lustre.start(app, "#app", Nil)
}</code></pre><p>That&#8217;s it! We now have type-safety all the way from the database to the frontend, and everything lives in the same repository.</p><p>If the database schema changes in an incompatible way and we regenerate <code>sql.gleam</code>, the compiler will immediately show errors in the project that need to be fixed.</p><p>The same applies to field renames or similar changes. Since the shared module defines the types, the compiler checks both client and server code, so we avoid unnoticed issues and runtime exceptions.</p><p>Plus, the compiler is extremely fast, so running both server and client builds in watch mode gives us instant feedback!</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.andreyfadeev.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.andreyfadeev.com/subscribe?"><span>Subscribe now</span></a></p><p>If you want to see a full example, you can check out the project on my GitHub: <a href="https://github.com/andfadeev/learn_gleam/tree/master/type_safety">https://github.com/andfadeev/learn_gleam/tree/master/type_safety</a></p><p></p><p></p><p></p>]]></content:encoded></item><item><title><![CDATA[First Steps with Gleam: Building a Simple Web App (Rest API with PostgreSQL database)]]></title><description><![CDATA[A beginner-friendly walkthrough of creating a web app with Gleam, REST APIs, and PostgreSQL.]]></description><link>https://blog.andreyfadeev.com/p/gleam-web-application-development-tutorial</link><guid isPermaLink="false">https://blog.andreyfadeev.com/p/gleam-web-application-development-tutorial</guid><dc:creator><![CDATA[Andrey Fadeev]]></dc:creator><pubDate>Sat, 27 Dec 2025 10:19:15 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!HpPX!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff64c60c7-32dd-497c-9c4e-69a1fb8fcb3a_1280x720.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!HpPX!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff64c60c7-32dd-497c-9c4e-69a1fb8fcb3a_1280x720.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!HpPX!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff64c60c7-32dd-497c-9c4e-69a1fb8fcb3a_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!HpPX!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff64c60c7-32dd-497c-9c4e-69a1fb8fcb3a_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!HpPX!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff64c60c7-32dd-497c-9c4e-69a1fb8fcb3a_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!HpPX!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff64c60c7-32dd-497c-9c4e-69a1fb8fcb3a_1280x720.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!HpPX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff64c60c7-32dd-497c-9c4e-69a1fb8fcb3a_1280x720.png" width="1280" height="720" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f64c60c7-32dd-497c-9c4e-69a1fb8fcb3a_1280x720.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:720,&quot;width&quot;:1280,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:56898,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://blog.andreyfadeev.com/i/182533650?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff64c60c7-32dd-497c-9c4e-69a1fb8fcb3a_1280x720.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!HpPX!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff64c60c7-32dd-497c-9c4e-69a1fb8fcb3a_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!HpPX!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff64c60c7-32dd-497c-9c4e-69a1fb8fcb3a_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!HpPX!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff64c60c7-32dd-497c-9c4e-69a1fb8fcb3a_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!HpPX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff64c60c7-32dd-497c-9c4e-69a1fb8fcb3a_1280x720.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Not long ago, I started my Gleam learning journey and have been enjoying it a lot so far. In this article, I&#8217;d like to share what I&#8217;ve learned about web development with Gleam.</p><p>We&#8217;ll build a REST web application step by step, backed by a PostgreSQL database and complemented with a small HTML interface. Along the way, we&#8217;ll use the following packages:</p><ul><li><p><a href="https://github.com/gleam-wisp/wisp">wisp</a> and <a href="https://github.com/rawhat/mist">mist</a> for the web server </p></li><li><p><a href="https://github.com/giacomocavalieri/squirrel">squirrel</a> for the type-safe SQL queries</p></li><li><p>our app will talk JSON</p></li><li><p><a href="https://github.com/lustre-labs/lustre">lustre</a> for building server-side HTML</p></li></ul><p>The entire code for this blog post could be found here: <a href="https://github.com/andfadeev/learn_gleam_todo">https://github.com/andfadeev/learn_gleam_todo</a></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.andreyfadeev.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.andreyfadeev.com/subscribe?"><span>Subscribe now</span></a></p><h3>Setting up the development environment</h3><p>Setting up the development environment on macOS is simple. I was using <code>homebrew</code> but it&#8217;s also possible with version managers like <code>asdf</code> or <code>mise</code>. </p><pre><code>brew install gleam
brew install erlang
brew install rebar3</code></pre><p>Let&#8217;s confirm that everything is ready and good to go:</p><pre><code>&#10095; gleam --version
gleam 1.13.0

&#10095; erl --version
Erlang/OTP 28

&#10095; rebar3 --version
rebar 3.25.1 on Erlang/OTP 28 Erts 16.1.2</code></pre><p>Since I&#8217;m currently using my wife&#8217;s MacBook Air with only 8 GB of RAM, system resources are limited, and my usual IDE (JetBrains IntelliJ IDEA) is painfully slow. As a result, I decided to use Zed (<a href="https://zed.dev">https://zed.dev</a>) as the editor for this project.</p><p>Zed is extremely fast, and I haven&#8217;t experienced any delays or slowdowns on this machine. It&#8217;s also extensible, with support for language servers and Tree-sitter.</p><p>When you open a Gleam file, Zed automatically detects it and prompts you to install the appropriate extension. After that, it connects to the Gleam Language Server, so the entire setup was essentially automatic.</p><h3>Gleam language </h3><p>Gleam is a statically typed, functional programming language focused on simplicity, correctness, and developer experience. It features a strong type system with powerful type inference, immutable data, and an expressive standard library, and it compiles to both the Erlang VM (BEAM) and JavaScript.</p><p>I wanted to become more familiar with the BEAM ecosystem and was looking for a language to start with. Over the past decade, I&#8217;ve mostly worked with Clojure, which is dynamically typed. While Elixir is more mature, I was looking for a different experience&#8212;specifically one with stronger typing, which I&#8217;ve started to miss.</p><p>Here are some of the language features I&#8217;d like to highlight:</p><ul><li><p>Immutability</p></li><li><p>A concise yet powerful standard library</p></li><li><p>An emphasis on one clear way of doing things</p></li><li><p>No nulls and forced error handling</p></li><li><p>Built-in <code>Result</code> and <code>Option</code> types</p><p></p></li></ul><p>The goal of this blog is to provide a step-by-step guide to building a web application with a database backend. Because of that, I won&#8217;t go deep into the Gleam language itself. The best way to get familiar with Gleam is to follow the <a href="https://tour.gleam.run/">official language tour</a>.</p><h3>Gleam CLI tool</h3><p>Once Gleam is installed on your machine, you gain access to the <code>gleam</code> CLI tool. This tool will be our main entry point for all common tasks &#8212; creating a new project, building and running it, managing dependencies, formatting code, and running tests.</p><p>You can see the list of commands by running:</p><pre><code>gleam help</code></pre><p>Let&#8217;s start with creating a new project:</p><pre><code>gleam new learn_gleam_todo</code></pre><p>Now we can move to the newly created folder and run tests and the project itself:</p><pre><code>cd learn_gleam_todo

gleam test
1 passed, no failures

gleam run 
Hello from learn_gleam_todo!</code></pre><p>The CLI tool is also used to add dependencies (that are recorded in the <code>gleam.toml</code> file). Let&#8217;s add the dependency for the <code>wisp</code> framework (<a href="https://github.com/gleam-wisp/wisp">https://github.com/gleam-wisp/wisp</a>) that we are going to use later:</p><pre><code>gleam add wisp

cat gleam.toml
name = "learn_gleam_todo"
version = "1.0.0"

[dependencies]
gleam_stdlib = "&gt;= 0.44.0 and &lt; 2.0.0"
wisp = "&gt;= 2.1.1 and &lt; 3.0.0"

[dev-dependencies]
gleeunit = "&gt;= 1.0.0 and &lt; 2.0.0"</code></pre><p>Later, it could be used to keep our dependencies up-to-date by running:</p><pre><code>gleam update</code></pre><h3>Create a web server</h3><p>In this section, we will create a web server and a REST API for our Todo application. </p><p>Let&#8217;s add dependencies that will be needed to get started:</p><pre><code>gleam add wisp gleam_erlang mist envoy</code></pre><p>Let&#8217;s finally write some Gleam code and edit the <code>src/learn_gleam_todo.gleam</code> file:</p><pre><code>import envoy
import gleam/erlang/process
import gleam/result
import mist
import wisp.{type Request, type Response}
import wisp/wisp_mist

fn middleware(req: Request, handler: fn(Request) -&gt; Response) -&gt; Response {
  use &lt;- wisp.log_request(req)
  use &lt;- wisp.rescue_crashes
  use req &lt;- wisp.handle_head(req)
  use req &lt;- wisp.csrf_known_header_protection(req)

  handler(req)
}

fn handler(req: Request) -&gt; Response {
  use _ &lt;- middleware(req)

  wisp.ok()
  |&gt; wisp.string_body("Hellow from Wisp &amp; Gleam")
}

pub fn main() -&gt; Nil {
  wisp.configure_logger()

  let secret =
    result.unwrap(envoy.get("SECRET_KEY_BASE"), "wisp_secret_fallback")

  let assert Ok(_) =
    wisp_mist.handler(handler, secret)
    |&gt; mist.new
    |&gt; mist.port(8080)
    |&gt; mist.start

  process.sleep_forever()
}</code></pre><p>A lot is going on here, so let&#8217;s break it down. In the <code>main</code> function, we create a web server and configure it to listen on port 8080. Wisp requires a unique secret, so we use the <code>envoy</code> package to read it from an environment variable, with a fallback to a hardcoded value. If you prefer, you can instead require the environment variable to be set and let the application fail fast if it&#8217;s missing.</p><p>Our handler is just a function that accepts <code>wisp.Request</code> and returns <code>wisp.Response</code>. It will catch all the routes and all HTTP methods for now and will always return 200 with a text response. </p><p>We also used a subset of available middleware to have basic logging and error handling.</p><p>Now let&#8217;s run our application and validate that it&#8217;s working. I&#8217;m going to use <code>curl</code> but you can just open the URL in the browser:</p><pre><code>gleam run
Listening on http://127.0.0.1:8080

curl http://127.0.0.1:8080
Hellow from Wisp &amp; Gleam</code></pre><p>You can find the result of this step in this commit: <a href="https://github.com/andfadeev/learn_gleam_todo/commit/4e4dc6ce896dc6bda46f8cfb8641922df5132a65">https://github.com/andfadeev/learn_gleam_todo/commit/4e4dc6ce896dc6bda46f8cfb8641922df5132a65</a></p><h3>Adding REST CRUD API</h3><p>In this section, we will add some routes to our web handler, so we will need:</p><ul><li><p>GET /todos &#8212; to list all todo items </p></li><li><p>POST /todos &#8212; to create a new todo item </p></li><li><p>GET /todos/:id &#8212; get todo item by id </p></li><li><p>DELETE /todos/:id &#8212; delete todo item by id</p></li></ul><p>Interestingly, there are no abstractions or special syntax to define the route table. We will use Gleam functions and pattern matching to define the logic we need. </p><p>Let&#8217;s first add a couple of additional dependencies that we will need further:</p><pre><code>gleam add gleam_http gleam_json</code></pre><p>Now it&#8217;s time to define routes, not that we are just using pattern matching on the URL segment first and on the HTTP method (utilising some helper functions provided by <code>wisp</code>):</p><pre><code>fn delete_todo_handler(_id: String) {
  wisp.no_content()
}

fn get_todo_handler(id: String) {
  wisp.string_body(wisp.ok(), "Todo item " &lt;&gt; id)
}

fn todo_handler(req: Request, id: String) -&gt; Response {
  case req.method {
    http.Get -&gt; get_todo_handler(id)
    http.Delete -&gt; delete_todo_handler(id)
    _ -&gt; wisp.method_not_allowed([http.Get, http.Delete])
  }
}

fn get_todos_hander() {
  wisp.string_body(wisp.ok(), "Todo items")
}

fn post_todos_hander() {
  wisp.created()
}

fn todos_handler(req: Request) -&gt; Response {
  case req.method {
    http.Get -&gt; get_todos_hander()
    http.Post -&gt; post_todos_hander()
    _ -&gt; wisp.method_not_allowed([http.Get, http.Post])
  }
}

fn handler(req: Request) -&gt; Response {
  use req &lt;- middleware(req)

  case wisp.path_segments(req) {
    ["todos"] -&gt; todos_handler(req)
    ["todos", id] -&gt; todo_handler(req, id)
    _ -&gt; wisp.not_found()
  }
}</code></pre><p>Time to restart the application and do some testing:</p><pre><code>gleam run
Listening on http://127.0.0.1:8080

curl http://127.0.0.1:8080/todos
Todo items

curl -X POST http://127.0.0.1:8080/todos
Created

curl http://127.0.0.1:8080/todos/1
Todo item 1

curl -i -X DELETE http://127.0.0.1:8080/todos/1
HTTP/1.1 204 No Content</code></pre><p>Code changes could be found in this commit: <a href="https://github.com/andfadeev/learn_gleam_todo/commit/1bceaabf134befc0e7ba9013e82e983d30b5f01a">https://github.com/andfadeev/learn_gleam_todo/commit/1bceaabf134befc0e7ba9013e82e983d30b5f01a</a></p><h3>Adding JSON support </h3><p>Now let&#8217;s add a model for your todo items, support JSON in responses and requests. Again, we will need some extra packages:</p><pre><code>gleam add gleam_time youid</code></pre><p>So we are going to create the type for our todo items, JSON encoder and generate some random items for the GET /todos response:</p><pre><code>type TodoItem {
  TodoItem(
    id: uuid.Uuid,
    title: String,
    description: option.Option(String),
    status: String,
    created_at: timestamp.Timestamp,
    updated_at: timestamp.Timestamp,
  )
}

fn timestamp_to_json(ts: timestamp.Timestamp) {
  json.string(timestamp.to_rfc3339(ts, calendar.utc_offset))
}

fn todo_item_to_json(item: TodoItem) {
  json.object([
    #("id", json.string(uuid.to_string(item.id))),
    #("title", json.string(item.title)),
    #("description", json.string(option.unwrap(item.description, ""))),
    #("status", json.string(item.status)),
    #("created_at", timestamp_to_json(item.created_at)),
    #("updated_at", timestamp_to_json(item.updated_at)),
  ])
}

fn get_todos_hander() {
  let todo_items = [
    TodoItem(
      uuid.v4(),
      "todoitem1",
      option.Some("description 1"),
      "pending",
      timestamp.from_unix_seconds(1_766_689_000),
      timestamp.from_unix_seconds(1_766_689_000),
    ),
    TodoItem(
      uuid.v4(),
      "todoitem2",
      option.None,
      "completed",
      timestamp.from_unix_seconds(1_766_689_000),
      timestamp.from_unix_seconds(1_766_689_000),
    ),
  ]

  json.array(todo_items, todo_item_to_json)
  |&gt; json.to_string
  |&gt; wisp.json_response(200)
}</code></pre><p>So now everything should be in place, and we can test the endpoint. I&#8217;m going to use <code>jq</code> for pretty printing in the terminal:</p><pre><code>gleam run 

curl http://127.0.0.1:8080/todos | jq
[
  {
    "id": "2d2238c3-631b-49d9-962b-6aa251a5a34f",
    "title": "todoitem1",
    "description": "description 1",
    "status": "pending",
    "created_at": "2025-12-25T18:56:40Z",
    "updated_at": "2025-12-25T18:56:40Z"
  },
  {
    "id": "ec68c70f-0214-4fe6-84a0-b84535e682b0",
    "title": "todoitem2",
    "description": "",
    "status": "completed",
    "created_at": "2025-12-25T18:56:40Z",
    "updated_at": "2025-12-25T18:56:40Z"
  }
]</code></pre><p>Commit for this change could be found by this link: <a href="https://github.com/andfadeev/learn_gleam_todo/commit/5fccd22bc1694efb3f1af9e02c90c94a68c2351a">https://github.com/andfadeev/learn_gleam_todo/commit/5fccd22bc1694efb3f1af9e02c90c94a68c2351a</a></p><h3>Handling JSON request body</h3><p>Finally, we need to support parsing the incoming request body for the POST /todos route:</p><pre><code>import gleam/dynamic/decode

fn post_todos_handler(req: Request) {
  use json &lt;- wisp.require_json(req)

  let result = {
    let decoder = {
      use title &lt;- decode.field("title", decode.string)
      use description &lt;- decode.optional_field("description", "", decode.string)
      decode.success(#(title, description))
    }
    use #(title, description) &lt;- result.try(decode.run(json, decoder))

    let todo_item =
      TodoItem(
        uuid.v4(),
        title,
        option.Some(description),
        "pending",
        timestamp.from_unix_seconds(1_766_689_000),
        timestamp.from_unix_seconds(1_766_689_000),
      )

    Ok(
      todo_item_to_json(todo_item)
      |&gt; json.to_string
      |&gt; wisp.json_response(200),
    )
  }

  case result {
    Ok(resp) -&gt; resp
    Error(_) -&gt; wisp.unprocessable_content()
  }
}</code></pre><p>Let&#8217;s test that the POST endpoint is working now:</p><pre><code>curl -X POST http://127.0.0.1:8080/todos \
  -H "Content-Type: application/json" \
  -d '{
    "title": "mytodo",
    "description": "something here"
  }' | jq

{
  "id": "0390b077-4c9e-4cd9-982a-672250f4d707",
  "title": "mytodo",
  "description": "something here",
  "status": "pending",
  "created_at": "2025-12-25T18:56:40Z",
  "updated_at": "2025-12-25T18:56:40Z"
}</code></pre><p>Code change could be found here: <a href="https://github.com/andfadeev/learn_gleam_todo/commit/40fdef1445aeeaa3d08787e1e97b6a739947f2ea">https://github.com/andfadeev/learn_gleam_todo/commit/40fdef1445aeeaa3d08787e1e97b6a739947f2ea</a></p><h3>Adding a database layer</h3><p>In this section, we are going to add a PostgreSQL database backend to store our todo items.</p><p>Let&#8217;s define a simple Docker Compose file to have a way to start the database. We will also point it to an initialisation script so we will have a database table created automatically:</p><p>Create <code>docker-compose.yml</code> in the root of the project:</p><pre><code>services:
  postgres:
    image: postgres:17-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: gleam
      POSTGRES_PASSWORD: gleam
      POSTGRES_DB: gleamdb
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres -d app_db"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  postgres_data:</code></pre><p>And also the <code>init.sql</code> in the root as well:</p><pre><code>CREATE TABLE todo_items (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    title TEXT NOT NULL,
    description TEXT,
    status TEXT NOT NULL DEFAULT 'pending',
    created_at TIMESTAMP NOT NULL DEFAULT now(),
    updated_at TIMESTAMP NOT NULL DEFAULT now()
);</code></pre><p>Now we can start the database by running:</p><pre><code>docker compose up -d

psql -h localhost -p 5432 -U gleam -d gleamdb
Password: gleam

gleamdb=# select * from todo_items ;
 id | title | description | status | created_at | updated_at
----+-------+-------------+--------+------------+------------
(0 rows)</code></pre><p>We are going to use <code>squirrel</code> library (<a href="https://github.com/giacomocavalieri/squirrel">https://github.com/giacomocavalieri/squirrel</a>) to generate type-safe Gleam code from plain SQL queries. Let&#8217;s create a bunch of SQL files in the <code>src/sql</code> folder:</p><pre><code>&#10095; tree src/sql
src/sql
&#9500;&#9472;&#9472; delete_todo_item.sql
&#9500;&#9472;&#9472; find_todo_item.sql
&#9500;&#9472;&#9472; find_todo_items.sql
&#9492;&#9472;&#9472; insert_todo_item.sql

1 directory, 4 files</code></pre><p>Each file will contain an SQL query:</p><pre><code>-- delete_todo_item.sql

delete from todo_items
where id = $1;

-- find_todo_item.sql

select * from todo_items
where id = $1;

-- find_todo_items.sql

select * from todo_items;

-- insert_todo_item.sql

insert into todo_items (title, description, status)
values ($1, $2, $3)
returning *;</code></pre><p>Now let&#8217;s add a new package, note that we are using <code>--dev</code> as it will be only used for code generation:</p><pre><code>gleam add pog
gleam add squirrel --dev</code></pre><p>We also need to expose an env var so we can run a code generation command that will actually read our database table definition to generate type-safe Gleam code:</p><pre><code>export DATABASE_URL=postgres://gleam:gleam@localhost:5432/gleamdb

&#10095; gleam run -m squirrel
&#128063;&#65039;  Generated 4 queries!</code></pre><p>You can explore a new Gleam file <code>src/sql.gleam</code> with generated code, for example:</p><pre><code>/// A row you get from running the `find_todo_item` query
/// defined in `./src/sql/find_todo_item.sql`.
///
/// &gt; &#128063;&#65039; This type definition was generated automatically using v4.6.0 of the
/// &gt; [squirrel package](https://github.com/giacomocavalieri/squirrel).
///
pub type FindTodoItemRow {
  FindTodoItemRow(
    id: Uuid,
    title: String,
    description: Option(String),
    status: String,
    created_at: Timestamp,
    updated_at: Timestamp,
  )
}

/// Runs the `find_todo_item` query
/// defined in `./src/sql/find_todo_item.sql`.
///
/// &gt; &#128063;&#65039; This function was generated automatically using v4.6.0 of
/// &gt; the [squirrel package](https://github.com/giacomocavalieri/squirrel).
///
pub fn find_todo_item(
  db: pog.Connection,
  arg_1: Uuid,
) -&gt; Result(pog.Returned(FindTodoItemRow), pog.QueryError) {
  let decoder = {
    use id &lt;- decode.field(0, uuid_decoder())
    use title &lt;- decode.field(1, decode.string)
    use description &lt;- decode.field(2, decode.optional(decode.string))
    use status &lt;- decode.field(3, decode.string)
    use created_at &lt;- decode.field(4, pog.timestamp_decoder())
    use updated_at &lt;- decode.field(5, pog.timestamp_decoder())
    decode.success(FindTodoItemRow(
      id:,
      title:,
      description:,
      status:,
      created_at:,
      updated_at:,
    ))
  }

  "select * from todo_items
where id = $1;
"
  |&gt; pog.query
  |&gt; pog.parameter(pog.text(uuid.to_string(arg_1)))
  |&gt; pog.returning(decoder)
  |&gt; pog.execute(db)
}</code></pre><p>Now we can create a connection pool and execute some queries:</p><pre><code>import pog
import sql

pub fn main() -&gt; Nil {

  let db_pool_name = process.new_name("db_pool")
  let assert Ok(database_url) = envoy.get("DATABASE_URL")
  let assert Ok(pog_config) = pog.url_config(db_pool_name, database_url)
  let assert Ok(_) =
    pog_config
    |&gt; pog.pool_size(10)
    |&gt; pog.start

  let con = pog.named_connection(db_pool_name)

  case sql.find_todo_items(con) {
    Ok(todo_items) -&gt; {
      echo "Got todo items"
      echo todo_items
      echo "OK"
    }
    Error(_) -&gt; {
      echo "Failed to get todo items"
    }
  }
}</code></pre><p>Let&#8217;s test, don&#8217;t forget that we now need the <code>DATABASE_URL</code> env var:</p><pre><code>export DATABASE_URL=postgres://gleam:gleam@localhost:5432/gleamdb

gleam run
"Got todo items"
Returned(0, [])
"OK"</code></pre><p>Code change could be found here: <a href="https://github.com/andfadeev/learn_gleam_todo/commit/143fdc20a477cf0727ae0446772136f13db07582">https://github.com/andfadeev/learn_gleam_todo/commit/143fdc20a477cf0727ae0446772136f13db07582</a></p><h3>Integrating the database into the web application</h3><p>So we created the connection pool in the main function, but we need to have access to the database connection from handlers. The way of doing it is to create a new <code>Context</code> type that will hold a database connection and pass it to handlers:</p><pre><code>pub type Context {
  Context(db: pog.Connection)
}

fn handler(req: Request, ctx: Context) -&gt; Response {
  use req &lt;- middleware(req)

  case wisp.path_segments(req) {
    ["todos"] -&gt; todos_handler(req, ctx)
    ["todos", id] -&gt; todo_handler(req, ctx, id)
    _ -&gt; wisp.not_found()
  }
}

pub fn main() -&gt; Nil {

  // rest of the main fn

  let con = pog.named_connection(db_pool_name)

  let context = Context(con)
  let handler = handler(_, context)

  let assert Ok(_) =
    wisp_mist.handler(handler, secret)
    |&gt; mist.new
    |&gt; mist.port(8080)
    |&gt; mist.start

  process.sleep_forever()
}</code></pre><p>Note that we will also need to pass the new context object to all the handlers, see full change here: <a href="https://github.com/andfadeev/learn_gleam_todo/commit/1f4f578a3f48af39597c7b3b98c7b933c7aca8e2">https://github.com/andfadeev/learn_gleam_todo/commit/1f4f578a3f48af39597c7b3b98c7b933c7aca8e2</a></p><h3>Connecting the dots</h3><p>Now that we have database access and our REST handlers in place, it&#8217;s time to make things real. We&#8217;ll start persisting data by inserting records into the database in the <code>POST</code> request and using proper queries in the other handlers.</p><p>We&#8217;ll work with database types, map them to our custom <code>TodoItem</code> type, and JSON-encode the results. We&#8217;ll also make sure that the <code>id</code> passed in the request path is a valid UUID:</p><pre><code>fn delete_todo_handler(ctx: Context, id: String) {
  case uuid.from_string(id) {
    Ok(id) -&gt; {
      case sql.delete_todo_item(ctx.db, id) {
        Ok(_) -&gt; wisp.no_content()
        Error(_) -&gt; wisp.internal_server_error()
      }
    }
    Error(_) -&gt; wisp.bad_request("Invalid id")
  }
}

fn get_todo_handler(ctx: Context, id: String) {
  case uuid.from_string(id) {
    Ok(id) -&gt; {
      case sql.find_todo_item(ctx.db, id) {
        Ok(todo_item) -&gt; {
          case todo_item.rows {
            [] -&gt; wisp.not_found()
            [row] -&gt; {
              let todo_item =
                TodoItem(
                  row.id,
                  row.title,
                  row.description,
                  row.status,
                  row.created_at,
                  row.updated_at,
                )

              todo_item_to_json(todo_item)
              |&gt; json.to_string
              |&gt; wisp.json_response(200)
            }
            _ -&gt; {
              wisp.internal_server_error()
            }
          }
        }
        Error(_) -&gt; wisp.internal_server_error()
      }
    }
    Error(_) -&gt; wisp.bad_request("Invalid id")
  }
}

fn get_todos_hander(ctx: Context) {
  case sql.find_todo_items(ctx.db) {
    Ok(todo_items) -&gt; {
      todo_items.rows
      |&gt; list.map(fn(row: sql.FindTodoItemsRow) {
        TodoItem(
          row.id,
          row.title,
          row.description,
          row.status,
          row.created_at,
          row.updated_at,
        )
      })
      |&gt; json.array(todo_item_to_json)
      |&gt; json.to_string
      |&gt; wisp.json_response(200)
    }
    Error(_) -&gt; {
      wisp.internal_server_error()
    }
  }
}

fn post_todos_handler(req: Request, ctx: Context) {
  use json &lt;- wisp.require_json(req)

  let result = {
    let decoder = {
      use title &lt;- decode.field("title", decode.string)
      use description &lt;- decode.optional_field("description", "", decode.string)
      decode.success(#(title, description))
    }
    use #(title, description) &lt;- result.try(decode.run(json, decoder))

    case sql.insert_todo_item(ctx.db, title, description, "pending") {
      Ok(r) -&gt; {
        case r.rows {
          [row] -&gt; {
            TodoItem(
              row.id,
              row.title,
              row.description,
              row.status,
              row.created_at,
              row.updated_at,
            )
            |&gt; todo_item_to_json()
            |&gt; json.to_string
            |&gt; wisp.json_response(200)
            |&gt; Ok()
          }
          _ -&gt; Ok(wisp.internal_server_error())
        }
      }
      Error(_) -&gt; {
        Ok(wisp.internal_server_error())
      }
    }
  }

  case result {
    Ok(resp) -&gt; resp
    Error(_) -&gt; wisp.unprocessable_content()
  }
}</code></pre><p>Of course, error handling and error messaging could be better, but you got the idea. Full change for this section you can find in this commit: <a href="https://github.com/andfadeev/learn_gleam_todo/commit/5b06e5280d8450ff210afd176039c5733a1611bd">https://github.com/andfadeev/learn_gleam_todo/commit/5b06e5280d8450ff210afd176039c5733a1611bd</a></p><h3>Lustre</h3><p>Finally, let&#8217;s cover the UI. Gleam could be compiled to JavaScript, and there is a library to build complex frontend applications (Elm-inspired): <a href="https://github.com/lustre-labs/lustre">https://github.com/lustre-labs/lustre</a></p><p>In this example, we will just use it as an HTML DSL, so we will be able to create HTML views without templates, but directly with Gleam code. A bit more verbose compared to Clojure Hiccup, but it&#8217;s type-safe on the other hand. </p><pre><code>gleam add lustre</code></pre><p>The goal is to create an index handler for the GET / and show the list of todo items, but as an HTML page:</p><pre><code>import lustre/attribute as attr
import lustre/element
import lustre/element/html
import gleam/int

fn todo_item_component(item: TodoItem) {
  html.div([attr.class("rounded border mt-4 p-4")], [
    html.h2([], [html.text(item.title)]),
    html.p([], [html.text(option.unwrap(item.description, ""))]),
  ])
}

fn get_index_handler(req: Request, context: Context) -&gt; Response {
  use &lt;- wisp.require_method(req, http.Get)

  case sql.find_todo_items(context.db) {
    Ok(todo_items) -&gt; {
      let todo_items_html =
        todo_items.rows
        |&gt; list.map(fn(i: sql.FindTodoItemsRow) {
          TodoItem(
            i.id,
            i.title,
            i.description,
            i.status,
            i.created_at,
            i.updated_at,
          )
        })
        |&gt; list.map(todo_item_component)
      let html =
        html.html([], [
          html.head([], [
            html.title([], "Gleam todo items"),
            html.script([attr.src("https://cdn.tailwindcss.com")], ""),
          ]),
          html.body([attr.class("max-w-2xl mx-auto")], [
            html.h1([attr.class("text-red-800 font-bold")], [
              html.text("Todo: " &lt;&gt; int.to_string(list.length(todo_items_html))),
            ]),
            html.div([attr.class("text-blue-800")], todo_items_html),
          ]),
        ])

      wisp.ok()
      |&gt; wisp.html_body(element.to_document_string(html))
    }
    Error(_) -&gt; {
      wisp.internal_server_error()
    }
  }
}

fn handler(req: Request, ctx: Context) -&gt; Response {
  use req &lt;- middleware(req)

  case wisp.path_segments(req) {
    [] -&gt; get_index_handler(req, ctx)
    ["todos"] -&gt; todos_handler(req, ctx)
    ["todos", id] -&gt; todo_handler(req, ctx, id)
    _ -&gt; wisp.not_found()
  }
}</code></pre><p>You can open the index page in your browser and see the result:</p><pre><code>curl http://127.0.0.1:8080

&lt;!doctype html&gt;
&lt;html&gt;
   &lt;head&gt;
      &lt;title&gt;Gleam todo items&lt;/title&gt;
      &lt;script src="https://cdn.tailwindcss.com"&gt;&lt;/script&gt;
   &lt;/head&gt;
   &lt;body class="max-w-2xl mx-auto"&gt;
      &lt;h1 class="text-red-800 font-bold"&gt;Todo: 3&lt;/h1&gt;
      &lt;div class="text-blue-800"&gt;
         &lt;div class="rounded border mt-4 p-4"&gt;
            &lt;h2&gt;mytodo&lt;/h2&gt;
            &lt;p&gt;something here&lt;/p&gt;
         &lt;/div&gt;
         &lt;div class="rounded border mt-4 p-4"&gt;
            &lt;h2&gt;mytodo&lt;/h2&gt;
            &lt;p&gt;something here&lt;/p&gt;
         &lt;/div&gt;
         &lt;div class="rounded border mt-4 p-4"&gt;
            &lt;h2&gt;mytodo&lt;/h2&gt;
            &lt;p&gt;something here&lt;/p&gt;
         &lt;/div&gt;
      &lt;/div&gt;
   &lt;/body&gt;
&lt;/html&gt;</code></pre><p>Full commit for this change could be found by this link: <a href="https://github.com/andfadeev/learn_gleam_todo/commit/d00461ea30516fba5321d1b441b1bd98fb4a0d84">https://github.com/andfadeev/learn_gleam_todo/commit/d00461ea30516fba5321d1b441b1bd98fb4a0d84</a></p><p>I think that&#8217;s it, the whole example could be found by this link: <a href="https://github.com/andfadeev/learn_gleam_todo">https://github.com/andfadeev/learn_gleam_todo</a></p><h3>Final thoughts</h3><p>I&#8217;ve had a blast learning Gleam so far, and I&#8217;ll definitely continue exploring it. That said, I&#8217;ll need to work on a larger project to better understand where the rough edges are and what might be missing.</p><p>While Gleam has been gaining more traction recently, it&#8217;s still early days. I wouldn&#8217;t fully bet on it yet, but it&#8217;s absolutely worth learning if you want to broaden your horizons and explore a different part of the BEAM ecosystem.</p><p>A few thoughts on areas for improvement&#8212;please note that I&#8217;m still a Gleam beginner, so I may be missing existing or emerging solutions:</p><ul><li><p><strong>Proper live reload support</strong> for web development</p></li><li><p><strong>Stronger documentation</strong></p></li><li><p><strong>Ecosystem maturity</strong> (need to explore using external Elixir or Erlang libraries)</p></li></ul><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.andreyfadeev.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.andreyfadeev.com/subscribe?"><span>Subscribe now</span></a></p>]]></content:encoded></item><item><title><![CDATA[Building an open-source project as a solo developer]]></title><description><![CDATA[I'm building an open-source self-hosted Substack alternative focused on developers]]></description><link>https://blog.andreyfadeev.com/p/building-an-open-source-project-as</link><guid isPermaLink="false">https://blog.andreyfadeev.com/p/building-an-open-source-project-as</guid><dc:creator><![CDATA[Andrey Fadeev]]></dc:creator><pubDate>Thu, 28 Aug 2025 19:54:36 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!_7rA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b72d4b3-eb12-4245-a445-bd9b00e1e982_1527x1160.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!_7rA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b72d4b3-eb12-4245-a445-bd9b00e1e982_1527x1160.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!_7rA!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b72d4b3-eb12-4245-a445-bd9b00e1e982_1527x1160.jpeg 424w, https://substackcdn.com/image/fetch/$s_!_7rA!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b72d4b3-eb12-4245-a445-bd9b00e1e982_1527x1160.jpeg 848w, https://substackcdn.com/image/fetch/$s_!_7rA!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b72d4b3-eb12-4245-a445-bd9b00e1e982_1527x1160.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!_7rA!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b72d4b3-eb12-4245-a445-bd9b00e1e982_1527x1160.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!_7rA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b72d4b3-eb12-4245-a445-bd9b00e1e982_1527x1160.jpeg" width="1456" height="1106" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0b72d4b3-eb12-4245-a445-bd9b00e1e982_1527x1160.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1106,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="https://substackcdn.com/image/fetch/$s_!_7rA!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b72d4b3-eb12-4245-a445-bd9b00e1e982_1527x1160.jpeg 424w, https://substackcdn.com/image/fetch/$s_!_7rA!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b72d4b3-eb12-4245-a445-bd9b00e1e982_1527x1160.jpeg 848w, https://substackcdn.com/image/fetch/$s_!_7rA!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b72d4b3-eb12-4245-a445-bd9b00e1e982_1527x1160.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!_7rA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b72d4b3-eb12-4245-a445-bd9b00e1e982_1527x1160.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>I've been using Substack for my tech blog and newsletter for a while.</p><p>As a developer, I truly believe that sharing knowledge through a newsletter is one of the best ways to grow an audience &#8212; and eventually monetise your efforts.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.andreyfadeev.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.andreyfadeev.com/subscribe?"><span>Subscribe now</span></a></p><p>Substack is built for a general audience, not devs. I was missing code highlighting and minimalist design.</p><p>I wanted to build something dedicated for developers:</p><ul><li><p><strong>open-source</strong>, <strong>free </strong>to use, self-hosted</p></li><li><p>beautiful<strong> code highlighting</strong></p></li><li><p><strong>markdown-first</strong> writing</p></li><li><p>clean, minimalist design</p></li><li><p>dark mode support</p></li></ul><p>I believe that an open-source model is a great way to build a product. IndiePubStack is:</p><ul><li><p>fully open-source and free to use (<strong>MIT licence</strong>)</p></li><li><p>designed to be self-hosted (Docker image distribution)</p></li><li><p>built on top of reliable services (Kinde for auth, Resend for email delivery)</p></li></ul><p>All you need is an inexpensive VPS and a domain name, free Kinde and Resend accounts (that will be enough for a long time). For example with Resend you can grow your newsletter up to 1000 subscribers for free (with unlimited emails), Kinde free tier includes 10500 MAU (monthly active users). </p><p>Kinde also recently released billing feature, so I&#8217;m planning to add support for premium tiers soon, so you can start monetiziation of your newsletter with paid content.</p><p>I'm already using IndiePubStack for my personal blog and newsletter, I have a couple of blogs posts recently published here:</p><ul><li><p><a href="https://indiepubstack.andreyfadeev.com/posts/5/getting-started-with-leiningen-a-beginners-guide">https://indiepubstack.andreyfadeev.com/posts/5/getting-started-with-leiningen-a-beginners-guide</a></p></li><li><p><a href="https://indiepubstack.andreyfadeev.com/posts/3/expressive-clojure-testing-with-the-matcher-combinators-library">https://indiepubstack.andreyfadeev.com/posts/3/expressive-clojure-testing-with-the-matcher-combinators-library</a></p></li></ul><p>The future depends on the community feedback, if the open-source project gets some traction, the next logical step will be to build a SaaS platform on top of it.</p><p>If you want to support the project (or even contribute) please checkout the GitHub repository: <a href="https://github.com/IndiePubStack/IndiePubStack">https://github.com/IndiePubStack/IndiePubStack</a>, stars will help a lot! </p><p>There is dedicated X (Twitter) account for the project: <a href="https://x.com/IndiePubStack">https://x.com/IndiePubStack</a></p><p>Any form of feedback will be extremely helpful, I&#8217;m working hard my evenings preparing for the ProductHunt launched that planned on <strong>3rd September 2025</strong>: <a href="https://www.producthunt.com/products/indiepubstack">https://www.producthunt.com/products/indiepubstack</a></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.andreyfadeev.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.andreyfadeev.com/subscribe?"><span>Subscribe now</span></a></p>]]></content:encoded></item><item><title><![CDATA[Tech Stack for Indie Hackers: Keep It Simple and Iterate Fast]]></title><description><![CDATA[A no-nonsense guide to building, deploying, and scaling your solo project.]]></description><link>https://blog.andreyfadeev.com/p/tech-stack-for-indie-hackers-keep</link><guid isPermaLink="false">https://blog.andreyfadeev.com/p/tech-stack-for-indie-hackers-keep</guid><dc:creator><![CDATA[Andrey Fadeev]]></dc:creator><pubDate>Wed, 25 Jun 2025 13:16:31 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!DiFk!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde64200d-4cc3-4c05-a6aa-2807d774d1a6_1536x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!DiFk!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde64200d-4cc3-4c05-a6aa-2807d774d1a6_1536x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!DiFk!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde64200d-4cc3-4c05-a6aa-2807d774d1a6_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!DiFk!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde64200d-4cc3-4c05-a6aa-2807d774d1a6_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!DiFk!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde64200d-4cc3-4c05-a6aa-2807d774d1a6_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!DiFk!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde64200d-4cc3-4c05-a6aa-2807d774d1a6_1536x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!DiFk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde64200d-4cc3-4c05-a6aa-2807d774d1a6_1536x1024.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/de64200d-4cc3-4c05-a6aa-2807d774d1a6_1536x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2234265,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://blog.andreyfadeev.com/i/166804903?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde64200d-4cc3-4c05-a6aa-2807d774d1a6_1536x1024.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!DiFk!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde64200d-4cc3-4c05-a6aa-2807d774d1a6_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!DiFk!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde64200d-4cc3-4c05-a6aa-2807d774d1a6_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!DiFk!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde64200d-4cc3-4c05-a6aa-2807d774d1a6_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!DiFk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde64200d-4cc3-4c05-a6aa-2807d774d1a6_1536x1024.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>If you're a developer, there's a good chance you're thinking about launching a project of your own. But what&#8217;s the best tech stack? How do you deploy it? What should you focus on? In this article, I&#8217;ll give you some practical advice to help you get started quickly&#8212;and keep a clear path for scaling later.</p><div><hr></div><h3>Infrastructure and Deployment</h3><p>Keep it really simple. Get a cheap Hetzner instance and install <a href="https://coolify.io">Coolify</a> on it. Stick with a single-instance deployment. Provide a Dockerfile for your app&#8212;Coolify will build and deploy it for you. Once you&#8217;ve got your domain name, configure it in Coolify to get HTTPS with free SSL certificates.</p><p>For storage, just use Postgres and manage it with Coolify. It takes just a couple of clicks to get it running on the same Hetzner instance. One thing I&#8217;d still recommend: configure automatic database backups. Choose a frequency that works for you&#8212;it&#8217;s also handled by Coolify, and your snapshots will be stored in AWS S3 (or any compatible object storage).</p><p>Coolify also gives you built-in CI/CD. Push your code to a private GitHub repo, connect it with Coolify, and it will build and deploy from the <code>main</code> branch automatically.</p><div><hr></div><h3>Language Choice</h3><p>This one&#8217;s easy: pick what you're most comfortable with. You can package almost anything into a Docker image and ship it.</p><p>Personally, I&#8217;d pick Clojure for the backend. A few reasons why:</p><ul><li><p><strong>Dynamic types</strong> &#8211; Controversial, but for a solo developer it&#8217;s a no-brainer. You&#8217;ll iterate much faster.</p></li><li><p><strong>Hiccup</strong> &#8211; Hands-down the best way to work with HTML templates, in my opinion.</p></li><li><p><strong>JVM performance</strong> &#8211; Don&#8217;t stress about it. It&#8217;s more than good enough, and if your project takes off, you can optimize later.</p></li></ul><p>If I&#8217;m not planning rich frontend functionality, I stick with <strong>HTMX</strong> as long as possible. But if it starts leaning toward an SPA, I&#8217;d go with <strong>React + JavaScript</strong> to keep things straightforward.</p><div><hr></div><h3>Write High-Level Tests</h3><p>I highly recommend spending time writing tests. Be pragmatic&#8212;don&#8217;t go too deep. Focus on core scenarios: happy paths, failures, edge cases.</p><p>For web apps, API-level testing usually hits the sweet spot. Set up test data, call your API, and validate both the response and the resulting DB state. You'll thank yourself later&#8212;especially when it&#8217;s time to refactor.</p><div><hr></div><h3>Monitoring and Observability</h3><p>Coolify gives you basic log viewing, but it&#8217;s not enough. Set up host metrics alerts (CPU and memory usage). Also, add a simple health check that emails you if the app goes down. For exceptions, use something like <strong>Sentry</strong> to get visibility and alerts.</p><div><hr></div><h3><strong>Use Managed Services for the Hard Stuff</strong></h3><p>Some problems are deceptively hard to solve well&#8212;authentication and payments are at the top of that list. Don&#8217;t build your own auth system unless you absolutely have to. Use a managed service like <a href="https://clerk.dev">Clerk</a>, <a href="https://kinde.com">Kinde</a>, or <a href="https://auth0.com">Auth0</a>. They handle password security, social logins, multi-factor auth, and session management&#8212;saving you a <em>lot</em> of time and potential vulnerabilities.</p><p>Same goes for payments: just use <a href="https://stripe.com">Stripe</a>. It&#8217;s incredibly well-documented, widely supported, and gives you features like subscriptions, invoices, refunds, and webhook events out of the box. You&#8217;ll also learn a lot by integrating these services&#8212;it&#8217;s valuable experience, and it keeps your focus on building your product, not re-inventing the wheel.</p><div><hr></div><h3>Future Scaling Path</h3><p>Don&#8217;t worry about scaling too early. You can always scale vertically by upgrading your instance. But if things go well and your traffic grows, you still have a clear path forward:</p><ul><li><p>Move Postgres to a managed solution (AWS RDS, Aurora, Neon, etc.)</p></li><li><p>Use the same Dockerfile, but deploy it with a container orchestrator (AWS ECS Fargate, GCP, DigitalOcean, etc.)</p></li><li><p>Add a load balancer in front</p></li></ul><p>That&#8217;s it. Keep it simple. Avoid premature optimizations. Happy coding!</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.andreyfadeev.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.andreyfadeev.com/subscribe?"><span>Subscribe now</span></a></p><p></p>]]></content:encoded></item><item><title><![CDATA[How to Save Redeployment Time Using Clojure]]></title><description><![CDATA[Imagine you wrote a piece of software, it was deployed, and then it turned out there is a bug in it. You want to fix it, but there is a problem...]]></description><link>https://blog.andreyfadeev.com/p/how-to-save-redeployment-time-using</link><guid isPermaLink="false">https://blog.andreyfadeev.com/p/how-to-save-redeployment-time-using</guid><dc:creator><![CDATA[Pravles Redneckoff]]></dc:creator><pubDate>Tue, 18 Mar 2025 09:01:26 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!KNSH!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fafa2e1ca-ee4c-4d4c-b6e7-aa1fd6fc2f21_800x587.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!KNSH!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fafa2e1ca-ee4c-4d4c-b6e7-aa1fd6fc2f21_800x587.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!KNSH!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fafa2e1ca-ee4c-4d4c-b6e7-aa1fd6fc2f21_800x587.png 424w, https://substackcdn.com/image/fetch/$s_!KNSH!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fafa2e1ca-ee4c-4d4c-b6e7-aa1fd6fc2f21_800x587.png 848w, https://substackcdn.com/image/fetch/$s_!KNSH!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fafa2e1ca-ee4c-4d4c-b6e7-aa1fd6fc2f21_800x587.png 1272w, https://substackcdn.com/image/fetch/$s_!KNSH!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fafa2e1ca-ee4c-4d4c-b6e7-aa1fd6fc2f21_800x587.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!KNSH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fafa2e1ca-ee4c-4d4c-b6e7-aa1fd6fc2f21_800x587.png" width="800" height="587" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/afa2e1ca-ee4c-4d4c-b6e7-aa1fd6fc2f21_800x587.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:587,&quot;width&quot;:800,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;File:Deep Space 1 clean (PIA04242).png&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="File:Deep Space 1 clean (PIA04242).png" title="File:Deep Space 1 clean (PIA04242).png" srcset="https://substackcdn.com/image/fetch/$s_!KNSH!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fafa2e1ca-ee4c-4d4c-b6e7-aa1fd6fc2f21_800x587.png 424w, https://substackcdn.com/image/fetch/$s_!KNSH!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fafa2e1ca-ee4c-4d4c-b6e7-aa1fd6fc2f21_800x587.png 848w, https://substackcdn.com/image/fetch/$s_!KNSH!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fafa2e1ca-ee4c-4d4c-b6e7-aa1fd6fc2f21_800x587.png 1272w, https://substackcdn.com/image/fetch/$s_!KNSH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fafa2e1ca-ee4c-4d4c-b6e7-aa1fd6fc2f21_800x587.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><em>Artist concept of Deep Space 1 firing its ion thrusters. The image is in <a href="https://commons.wikimedia.org/wiki/File:Deep_Space_1_clean_(PIA04242).png">public domain</a>.</em></p><p>Imagine you wrote a piece of software, it was deployed, and then it turned out there is a bug in it. You want to fix it, but there is a problem: the hardware your software runs on is located at a distance of 60 million miles.</p><p>It would be a miracle if this bug could be fixed. Indeed, it was fixed, and the miracle is called Lisp. According to the developer of that software <a href="https://corecursive.com/lisp-in-space-with-ron-garret/">Ron Garrett</a>, it was the features of Lisp that allowed him to rescue the <a href="https://en.wikipedia.org/wiki/Deep_Space_1">Deep Space 1</a> probe.</p><p>I am a regular software developer in the banking and telecom sectors. But on several occasions, I faced a problem that seemed similar to that of Ron Garrett.</p><h2><strong>The problem</strong></h2><p>Sometimes you have a large application which takes a long time to build and deploy. In one bank, I had a situation where a Maven build took thirty minutes and a local deployment another twenty.</p><p>In a European telecom company, the full build and deployment took about two hours to complete.</p><p>This causes various problems.</p><p>One of them is this: If you have a large Spring-based application (tens of thousands of beans) and you need to figure out how something works, you need to:</p><ol><li><p>modify the code,</p></li><li><p>rebuild and launch the application,</p></li><li><p>look how it behaves,</p></li><li><p>modify the code again, and</p></li><li><p>repeat steps 2&#8211;4 until you've achieved your goal.</p></li></ol><p>If rebuilding and launching the application takes hours, it means you can make fewer experiments.</p><p>You could say that modifying a small piece of code in a running application takes as much time as sending a software update to a space probe at the other end of the solar system.</p><h2><strong>Alternative solutions</strong></h2><p>Sometimes you can isolate the problem in a JUnit test. Then you don't need to rebuild the entire application.</p><p>But in my case, this workaround was not feasible, because I needed to access the Spring context at runtime and inspect the data inside some of the beans.</p><p>Another approach is to use a tool like <a href="https://www.jrebel.com/">JRebel</a>. But</p><ul><li><p>in big companies it takes months to buy a licence, and you need to fix your issue yesterday, plus</p></li><li><p>there is no guarantee that a tool like this will work in the environment of the customer (e. g. because of firewalls and other security measures).</p></li></ul><p>If you only want to change one particular Java class (and the JAR files are not signed), you could also</p><ol><li><p>build the project locally, and</p></li><li><p>then replace the class file on the server.</p></li></ol><p>I know at least one company which did this in production. This was of the reasons I left it.</p><h2><strong>Clojure to the rescue</strong></h2><p>One day, I was waiting for the rebuild and redeployment to complete. I remembered the story about Lisp in space.</p><p>It got me thinking: Could it be that my problem was somewhat similar to Ron Garrett's? In both cases, there was the issue of waiting time. NASA developers needed to wait for the signal to reach the probe or come back to Earth, and I was waiting for builds.</p><p>They solved the problem using Lisp. So, couldn't I solve my problem using the modern incarnation of Lisp &#8211; Clojure?</p><p>It turned out that I could. The process looks like this:</p><ol><li><p>Create a Spring Bean with a Clojure REPL inside.</p></li><li><p>Put some initial code into a Clojure file inside the application.</p></li><li><p>Create a REST endpoint which calls the Clojure function whenever I call this endpoint in a browser.</p></li><li><p>Build and deploy the application.</p></li><li><p>Connect to the REPL from Visual Studio Code.</p></li><li><p>Call the endpoint and look at the results in the log file or its response.</p></li><li><p>Modify the Clojure code in Visual Studio Code and send the changes to the REPL.</p></li><li><p>Call the endpoint again.</p></li><li><p>Repeat steps 7 through 8 until the Clojure code behaves as you want it to.</p></li><li><p>Rewrite the Clojure code in Java.</p></li></ol><p>In this scenario, the lengthy operation of rebuilding and redeployment takes place only once (step 4).</p><p>The changes to the Clojure code (step 7) occur at fractions of a second.</p><p>Below you can find a step-by-step tutorial on how to do this.</p><h2><strong>Tutorial</strong></h2><h3><strong>Step 1</strong></h3><p>Create a simple Spring project using the <a href="https://start.spring.io/">Spring Initialzr</a>.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!6Vjf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F976bba49-049f-4682-acd6-6cb099d7a4d8_1000x536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!6Vjf!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F976bba49-049f-4682-acd6-6cb099d7a4d8_1000x536.png 424w, https://substackcdn.com/image/fetch/$s_!6Vjf!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F976bba49-049f-4682-acd6-6cb099d7a4d8_1000x536.png 848w, https://substackcdn.com/image/fetch/$s_!6Vjf!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F976bba49-049f-4682-acd6-6cb099d7a4d8_1000x536.png 1272w, https://substackcdn.com/image/fetch/$s_!6Vjf!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F976bba49-049f-4682-acd6-6cb099d7a4d8_1000x536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!6Vjf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F976bba49-049f-4682-acd6-6cb099d7a4d8_1000x536.png" width="1000" height="536" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/976bba49-049f-4682-acd6-6cb099d7a4d8_1000x536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:536,&quot;width&quot;:1000,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:117048,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.andreyfadeev.com/i/158993075?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F976bba49-049f-4682-acd6-6cb099d7a4d8_1000x536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!6Vjf!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F976bba49-049f-4682-acd6-6cb099d7a4d8_1000x536.png 424w, https://substackcdn.com/image/fetch/$s_!6Vjf!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F976bba49-049f-4682-acd6-6cb099d7a4d8_1000x536.png 848w, https://substackcdn.com/image/fetch/$s_!6Vjf!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F976bba49-049f-4682-acd6-6cb099d7a4d8_1000x536.png 1272w, https://substackcdn.com/image/fetch/$s_!6Vjf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F976bba49-049f-4682-acd6-6cb099d7a4d8_1000x536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>You can also git clone <a href="https://github.com/real-pravles/clojure-guest-post">my sample project</a>.</p><h3><strong>Step 2</strong></h3><p>Add Clojure and nREPL dependencies to the <a href="https://github.com/real-pravles/clojure-guest-post/blob/master/pom.xml">pom.xml</a> file:</p><pre><code>&lt;dependency&gt;
  &lt;groupId&gt;org.clojure&lt;/groupId&gt;
  &lt;artifactId&gt;clojure&lt;/artifactId&gt; 
  &lt;version&gt;1.10.3&lt;/version&gt;
&lt;/dependency&gt;

&lt;dependency&gt;
  &lt;groupId&gt;nrepl&lt;/groupId&gt;
  &lt;artifactId&gt;nrepl&lt;/artifactId&gt; 
  &lt;version&gt;1.0.0&lt;/version&gt;
&lt;/dependency&gt;</code></pre><h3><strong>Step 3</strong></h3><p>Add a <a href="https://github.com/real-pravles/clojure-guest-post/blob/master/src/main/java/com/pravles/clojureguestpost/ClojureRepl.java">Spring bean</a> which will launch the REPL.</p><pre><code>package com.pravles.guestpost;

import clojure.java.api.Clojure;
import clojure.lang.IFn;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

@Servicepublic
class ClojureRepl {
  @PostConstruct
  public void init() {
    final IFn require = Clojure.var("clojure.core", "require");
    require.invoke(Clojure.read("com.pravles.guestpost"));
    require.invoke(Clojure.read("nrepl.server"));
    final IFn start = Clojure.var("nrepl.server", "start-server");
    int port = 5555;
    start.invoke(Clojure.read(":port"), Clojure.read(Integer.toString(port)));
  }

  @PreDestroy
  public void shutDown() {
    Clojure.var("clojure.core.server", "stop-server").invoke(Clojure.read("{:name spring-repl}"));
  }
}</code></pre><h3><strong>Step 4</strong></h3><p>Create a <a href="https://github.com/real-pravles/clojure-guest-post/blob/master/src/main/java/com/pravles/clojureguestpost/DebugController.java">REST endpoint</a> which calls a Clojure function and passes the Spring context as an argument.</p><pre><code>package com.pravles.guestpost;

import clojure.java.api.Clojure;
import lombok.extern.sl4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.web.bind.annotations.GetMapping;
import org.springframework.web.bind.annotations.RequestMapping;
import org.springframework.web.bind.annotations.RestController;

@Slf4j
@RestController("newDebugController")
@RequestMapping("debug")
public class DebugController {
  @Autowired
  private ApplicationContext ctx;

  @GetMapping("run")
  public Object run() {
    return Clojure.var("com.pravles.guestpost", "debug").invoke(ctx);
  }
}</code></pre><h3><strong>Step 5</strong></h3><p>Create a file <a href="https://github.com/real-pravles/clojure-guest-post/blob/master/src/main/resources/com/pravles/clojureguestpost.clj">src/main/resources/com/pravles/clojureguestpost.clj</a> with the initial Clojure code.</p><pre><code>#_{:clj-kondo/ignore [:namespace-name-mismatch]}

(ns com.pravles.guestpost
  (:require [clojure.pprint :refer [pprint]])
  (:require [clojure.string])
  (:require [clojure.java.io]))

(import javax.servlet.http.HttpServletResponse)

(defn debug [ctx] "debug return value")</code></pre><h3><strong>Step 6</strong></h3><p>Add a <a href="https://github.com/real-pravles/clojure-guest-post/blob/master/src/main/java/com/pravles/clojureguestpost/UpperCaseBean.java">bean</a> with one method that capitalizes a string:</p><pre><code>@Componentpublic
class UpperCaseBean {
  public String upperCase(final String input) {
    if (input == null) {
      return "";
    }
    return input.toUpperCase();
  }
}</code></pre><h3><strong>Step 7</strong></h3><p>Build and run the <a href="https://github.com/real-pravles/clojure-guest-post/blob/master/src/main/java/com/pravles/clojureguestpost/ClojureGuestPostApplication.java">application</a>.</p><h3><strong>Step 8</strong></h3><p>Open the URL <a href="http://localhost:8080/debug/run">http://localhost:8080/debug/run</a> in the browser. You should see the text "debug return value" there.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!cvQ3!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92bbb252-5251-45fa-9bb5-5a30bcba002b_822x478.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!cvQ3!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92bbb252-5251-45fa-9bb5-5a30bcba002b_822x478.png 424w, https://substackcdn.com/image/fetch/$s_!cvQ3!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92bbb252-5251-45fa-9bb5-5a30bcba002b_822x478.png 848w, https://substackcdn.com/image/fetch/$s_!cvQ3!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92bbb252-5251-45fa-9bb5-5a30bcba002b_822x478.png 1272w, https://substackcdn.com/image/fetch/$s_!cvQ3!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92bbb252-5251-45fa-9bb5-5a30bcba002b_822x478.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!cvQ3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92bbb252-5251-45fa-9bb5-5a30bcba002b_822x478.png" width="822" height="478" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/92bbb252-5251-45fa-9bb5-5a30bcba002b_822x478.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:478,&quot;width&quot;:822,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:124860,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.andreyfadeev.com/i/158993075?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92bbb252-5251-45fa-9bb5-5a30bcba002b_822x478.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!cvQ3!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92bbb252-5251-45fa-9bb5-5a30bcba002b_822x478.png 424w, https://substackcdn.com/image/fetch/$s_!cvQ3!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92bbb252-5251-45fa-9bb5-5a30bcba002b_822x478.png 848w, https://substackcdn.com/image/fetch/$s_!cvQ3!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92bbb252-5251-45fa-9bb5-5a30bcba002b_822x478.png 1272w, https://substackcdn.com/image/fetch/$s_!cvQ3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92bbb252-5251-45fa-9bb5-5a30bcba002b_822x478.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3><strong>Step 9</strong></h3><p>Install VS Code, if necessary.</p><h3><strong>Step 10</strong></h3><p>Install the VS Code extension <a href="https://calva.io/">Calva</a>.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!zZYF!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c58445e-bc08-444e-a9a4-f00097dd0034_1000x575.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!zZYF!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c58445e-bc08-444e-a9a4-f00097dd0034_1000x575.png 424w, https://substackcdn.com/image/fetch/$s_!zZYF!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c58445e-bc08-444e-a9a4-f00097dd0034_1000x575.png 848w, https://substackcdn.com/image/fetch/$s_!zZYF!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c58445e-bc08-444e-a9a4-f00097dd0034_1000x575.png 1272w, https://substackcdn.com/image/fetch/$s_!zZYF!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c58445e-bc08-444e-a9a4-f00097dd0034_1000x575.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!zZYF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c58445e-bc08-444e-a9a4-f00097dd0034_1000x575.png" width="1000" height="575" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9c58445e-bc08-444e-a9a4-f00097dd0034_1000x575.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:575,&quot;width&quot;:1000,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:126622,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.andreyfadeev.com/i/158993075?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c58445e-bc08-444e-a9a4-f00097dd0034_1000x575.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!zZYF!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c58445e-bc08-444e-a9a4-f00097dd0034_1000x575.png 424w, https://substackcdn.com/image/fetch/$s_!zZYF!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c58445e-bc08-444e-a9a4-f00097dd0034_1000x575.png 848w, https://substackcdn.com/image/fetch/$s_!zZYF!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c58445e-bc08-444e-a9a4-f00097dd0034_1000x575.png 1272w, https://substackcdn.com/image/fetch/$s_!zZYF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c58445e-bc08-444e-a9a4-f00097dd0034_1000x575.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3><strong>Step 11</strong></h3><p>Create a new project using Leiningen (lein new app) and open it in VS Code.</p><p>To do, select the menu item File -&gt; Open folder.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Vjf9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41693970-00c1-48c3-bc3b-1ca5ea93de44_500x582.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Vjf9!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41693970-00c1-48c3-bc3b-1ca5ea93de44_500x582.png 424w, https://substackcdn.com/image/fetch/$s_!Vjf9!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41693970-00c1-48c3-bc3b-1ca5ea93de44_500x582.png 848w, https://substackcdn.com/image/fetch/$s_!Vjf9!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41693970-00c1-48c3-bc3b-1ca5ea93de44_500x582.png 1272w, https://substackcdn.com/image/fetch/$s_!Vjf9!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41693970-00c1-48c3-bc3b-1ca5ea93de44_500x582.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Vjf9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41693970-00c1-48c3-bc3b-1ca5ea93de44_500x582.png" width="500" height="582" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/41693970-00c1-48c3-bc3b-1ca5ea93de44_500x582.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:582,&quot;width&quot;:500,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:169537,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.andreyfadeev.com/i/158993075?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41693970-00c1-48c3-bc3b-1ca5ea93de44_500x582.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Vjf9!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41693970-00c1-48c3-bc3b-1ca5ea93de44_500x582.png 424w, https://substackcdn.com/image/fetch/$s_!Vjf9!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41693970-00c1-48c3-bc3b-1ca5ea93de44_500x582.png 848w, https://substackcdn.com/image/fetch/$s_!Vjf9!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41693970-00c1-48c3-bc3b-1ca5ea93de44_500x582.png 1272w, https://substackcdn.com/image/fetch/$s_!Vjf9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41693970-00c1-48c3-bc3b-1ca5ea93de44_500x582.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>There, select the directory with the application created by Leiningen.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!pSKW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc681d654-a19b-4103-89a1-1b792b25415f_700x393.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!pSKW!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc681d654-a19b-4103-89a1-1b792b25415f_700x393.png 424w, https://substackcdn.com/image/fetch/$s_!pSKW!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc681d654-a19b-4103-89a1-1b792b25415f_700x393.png 848w, https://substackcdn.com/image/fetch/$s_!pSKW!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc681d654-a19b-4103-89a1-1b792b25415f_700x393.png 1272w, https://substackcdn.com/image/fetch/$s_!pSKW!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc681d654-a19b-4103-89a1-1b792b25415f_700x393.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!pSKW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc681d654-a19b-4103-89a1-1b792b25415f_700x393.png" width="700" height="393" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c681d654-a19b-4103-89a1-1b792b25415f_700x393.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:393,&quot;width&quot;:700,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:83409,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.andreyfadeev.com/i/158993075?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc681d654-a19b-4103-89a1-1b792b25415f_700x393.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!pSKW!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc681d654-a19b-4103-89a1-1b792b25415f_700x393.png 424w, https://substackcdn.com/image/fetch/$s_!pSKW!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc681d654-a19b-4103-89a1-1b792b25415f_700x393.png 848w, https://substackcdn.com/image/fetch/$s_!pSKW!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc681d654-a19b-4103-89a1-1b792b25415f_700x393.png 1272w, https://substackcdn.com/image/fetch/$s_!pSKW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc681d654-a19b-4103-89a1-1b792b25415f_700x393.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3><strong>Step 12</strong></h3><p>Paste the contents of <a href="https://github.com/real-pravles/clojure-guest-post/blob/master/src/main/resources/com/pravles/clojureguestpost.clj">file</a> from step 5 into the file src/app/core.clj in VS Code.</p><p>Copy the file from step 4 into one of the folders of the project from the previous step.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!eC-y!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb12e0f68-3a38-4253-806e-fdc9e1629e2b_1000x565.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!eC-y!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb12e0f68-3a38-4253-806e-fdc9e1629e2b_1000x565.png 424w, https://substackcdn.com/image/fetch/$s_!eC-y!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb12e0f68-3a38-4253-806e-fdc9e1629e2b_1000x565.png 848w, https://substackcdn.com/image/fetch/$s_!eC-y!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb12e0f68-3a38-4253-806e-fdc9e1629e2b_1000x565.png 1272w, https://substackcdn.com/image/fetch/$s_!eC-y!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb12e0f68-3a38-4253-806e-fdc9e1629e2b_1000x565.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!eC-y!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb12e0f68-3a38-4253-806e-fdc9e1629e2b_1000x565.png" width="1000" height="565" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b12e0f68-3a38-4253-806e-fdc9e1629e2b_1000x565.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:565,&quot;width&quot;:1000,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:148730,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.andreyfadeev.com/i/158993075?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb12e0f68-3a38-4253-806e-fdc9e1629e2b_1000x565.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!eC-y!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb12e0f68-3a38-4253-806e-fdc9e1629e2b_1000x565.png 424w, https://substackcdn.com/image/fetch/$s_!eC-y!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb12e0f68-3a38-4253-806e-fdc9e1629e2b_1000x565.png 848w, https://substackcdn.com/image/fetch/$s_!eC-y!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb12e0f68-3a38-4253-806e-fdc9e1629e2b_1000x565.png 1272w, https://substackcdn.com/image/fetch/$s_!eC-y!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb12e0f68-3a38-4253-806e-fdc9e1629e2b_1000x565.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3><strong>Step 13</strong></h3><p>Connect VS Code to the REPL inside your Spring project.</p><p>To do, select the menu item View -&gt; Command Palette.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!0_zf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa08e374a-df01-41c7-853d-9faef72cac3b_700x142.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!0_zf!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa08e374a-df01-41c7-853d-9faef72cac3b_700x142.png 424w, https://substackcdn.com/image/fetch/$s_!0_zf!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa08e374a-df01-41c7-853d-9faef72cac3b_700x142.png 848w, https://substackcdn.com/image/fetch/$s_!0_zf!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa08e374a-df01-41c7-853d-9faef72cac3b_700x142.png 1272w, https://substackcdn.com/image/fetch/$s_!0_zf!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa08e374a-df01-41c7-853d-9faef72cac3b_700x142.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!0_zf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa08e374a-df01-41c7-853d-9faef72cac3b_700x142.png" width="700" height="142" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a08e374a-df01-41c7-853d-9faef72cac3b_700x142.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:142,&quot;width&quot;:700,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:81220,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.andreyfadeev.com/i/158993075?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa08e374a-df01-41c7-853d-9faef72cac3b_700x142.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!0_zf!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa08e374a-df01-41c7-853d-9faef72cac3b_700x142.png 424w, https://substackcdn.com/image/fetch/$s_!0_zf!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa08e374a-df01-41c7-853d-9faef72cac3b_700x142.png 848w, https://substackcdn.com/image/fetch/$s_!0_zf!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa08e374a-df01-41c7-853d-9faef72cac3b_700x142.png 1272w, https://substackcdn.com/image/fetch/$s_!0_zf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa08e374a-df01-41c7-853d-9faef72cac3b_700x142.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>A little text window will appear. Type Calva there, then select the item Connect to a running REPL server....</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!xU6-!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2eb64c33-ed42-449c-ad12-4ddd548905a9_1000x247.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!xU6-!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2eb64c33-ed42-449c-ad12-4ddd548905a9_1000x247.png 424w, https://substackcdn.com/image/fetch/$s_!xU6-!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2eb64c33-ed42-449c-ad12-4ddd548905a9_1000x247.png 848w, https://substackcdn.com/image/fetch/$s_!xU6-!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2eb64c33-ed42-449c-ad12-4ddd548905a9_1000x247.png 1272w, https://substackcdn.com/image/fetch/$s_!xU6-!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2eb64c33-ed42-449c-ad12-4ddd548905a9_1000x247.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!xU6-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2eb64c33-ed42-449c-ad12-4ddd548905a9_1000x247.png" width="1000" height="247" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2eb64c33-ed42-449c-ad12-4ddd548905a9_1000x247.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:247,&quot;width&quot;:1000,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:144126,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.andreyfadeev.com/i/158993075?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2eb64c33-ed42-449c-ad12-4ddd548905a9_1000x247.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!xU6-!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2eb64c33-ed42-449c-ad12-4ddd548905a9_1000x247.png 424w, https://substackcdn.com/image/fetch/$s_!xU6-!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2eb64c33-ed42-449c-ad12-4ddd548905a9_1000x247.png 848w, https://substackcdn.com/image/fetch/$s_!xU6-!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2eb64c33-ed42-449c-ad12-4ddd548905a9_1000x247.png 1272w, https://substackcdn.com/image/fetch/$s_!xU6-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2eb64c33-ed42-449c-ad12-4ddd548905a9_1000x247.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>In the next window, select the Generic REPL type.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!4fRo!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcf40fd8e-7f34-4108-aec8-dfff154e68fb_1000x328.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!4fRo!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcf40fd8e-7f34-4108-aec8-dfff154e68fb_1000x328.png 424w, https://substackcdn.com/image/fetch/$s_!4fRo!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcf40fd8e-7f34-4108-aec8-dfff154e68fb_1000x328.png 848w, https://substackcdn.com/image/fetch/$s_!4fRo!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcf40fd8e-7f34-4108-aec8-dfff154e68fb_1000x328.png 1272w, https://substackcdn.com/image/fetch/$s_!4fRo!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcf40fd8e-7f34-4108-aec8-dfff154e68fb_1000x328.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!4fRo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcf40fd8e-7f34-4108-aec8-dfff154e68fb_1000x328.png" width="1000" height="328" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/cf40fd8e-7f34-4108-aec8-dfff154e68fb_1000x328.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:328,&quot;width&quot;:1000,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:94697,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.andreyfadeev.com/i/158993075?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcf40fd8e-7f34-4108-aec8-dfff154e68fb_1000x328.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!4fRo!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcf40fd8e-7f34-4108-aec8-dfff154e68fb_1000x328.png 424w, https://substackcdn.com/image/fetch/$s_!4fRo!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcf40fd8e-7f34-4108-aec8-dfff154e68fb_1000x328.png 848w, https://substackcdn.com/image/fetch/$s_!4fRo!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcf40fd8e-7f34-4108-aec8-dfff154e68fb_1000x328.png 1272w, https://substackcdn.com/image/fetch/$s_!4fRo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcf40fd8e-7f34-4108-aec8-dfff154e68fb_1000x328.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>In the next window, enter localhost:5555 as the address of the REPL server.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!vwYJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99825249-af77-49f5-8ecd-0ca9af787676_1000x228.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!vwYJ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99825249-af77-49f5-8ecd-0ca9af787676_1000x228.png 424w, https://substackcdn.com/image/fetch/$s_!vwYJ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99825249-af77-49f5-8ecd-0ca9af787676_1000x228.png 848w, https://substackcdn.com/image/fetch/$s_!vwYJ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99825249-af77-49f5-8ecd-0ca9af787676_1000x228.png 1272w, https://substackcdn.com/image/fetch/$s_!vwYJ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99825249-af77-49f5-8ecd-0ca9af787676_1000x228.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!vwYJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99825249-af77-49f5-8ecd-0ca9af787676_1000x228.png" width="1000" height="228" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/99825249-af77-49f5-8ecd-0ca9af787676_1000x228.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:228,&quot;width&quot;:1000,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:97049,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.andreyfadeev.com/i/158993075?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99825249-af77-49f5-8ecd-0ca9af787676_1000x228.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!vwYJ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99825249-af77-49f5-8ecd-0ca9af787676_1000x228.png 424w, https://substackcdn.com/image/fetch/$s_!vwYJ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99825249-af77-49f5-8ecd-0ca9af787676_1000x228.png 848w, https://substackcdn.com/image/fetch/$s_!vwYJ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99825249-af77-49f5-8ecd-0ca9af787676_1000x228.png 1272w, https://substackcdn.com/image/fetch/$s_!vwYJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99825249-af77-49f5-8ecd-0ca9af787676_1000x228.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>After a few seconds you should see the message: Connected session: clj on the right side of the window.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!3TDj!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22b94274-3a73-4e25-98b7-6fccb97ece4f_1000x613.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!3TDj!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22b94274-3a73-4e25-98b7-6fccb97ece4f_1000x613.png 424w, https://substackcdn.com/image/fetch/$s_!3TDj!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22b94274-3a73-4e25-98b7-6fccb97ece4f_1000x613.png 848w, https://substackcdn.com/image/fetch/$s_!3TDj!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22b94274-3a73-4e25-98b7-6fccb97ece4f_1000x613.png 1272w, https://substackcdn.com/image/fetch/$s_!3TDj!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22b94274-3a73-4e25-98b7-6fccb97ece4f_1000x613.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!3TDj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22b94274-3a73-4e25-98b7-6fccb97ece4f_1000x613.png" width="1000" height="613" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/22b94274-3a73-4e25-98b7-6fccb97ece4f_1000x613.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:613,&quot;width&quot;:1000,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:269314,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.andreyfadeev.com/i/158993075?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22b94274-3a73-4e25-98b7-6fccb97ece4f_1000x613.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!3TDj!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22b94274-3a73-4e25-98b7-6fccb97ece4f_1000x613.png 424w, https://substackcdn.com/image/fetch/$s_!3TDj!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22b94274-3a73-4e25-98b7-6fccb97ece4f_1000x613.png 848w, https://substackcdn.com/image/fetch/$s_!3TDj!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22b94274-3a73-4e25-98b7-6fccb97ece4f_1000x613.png 1272w, https://substackcdn.com/image/fetch/$s_!3TDj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22b94274-3a73-4e25-98b7-6fccb97ece4f_1000x613.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3><strong>Step 14</strong></h3><p>Modify the file from step 12 as follows.</p><pre><code>#_{:clj-kondo/ignore [:namespace-name-mismatch]} 

(ns com.pravles.guestpost
  (:require [clojure.pprint :refer [pprint]])
  (:require [clojure.string])
  (:require [clojure.java.io]))

(import javax.servlet.http.HttpServletResponse)

(defn debug [ctx] (-&gt; java.util.Date (new) (.toString)))</code></pre><p>Press Ctrl-A to select the contents of the file. Press Ctrl-Enter. This will send the changes to the function debug to the REPL in your Spring application.</p><p>You should see a text like =&gt; #'com.pravles.clojureguestpost/debug at the bottom of the window with the code upon successful update.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!7tMv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3d5ddbc-6636-410b-a9de-271abe70b09b_944x626.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!7tMv!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3d5ddbc-6636-410b-a9de-271abe70b09b_944x626.png 424w, https://substackcdn.com/image/fetch/$s_!7tMv!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3d5ddbc-6636-410b-a9de-271abe70b09b_944x626.png 848w, https://substackcdn.com/image/fetch/$s_!7tMv!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3d5ddbc-6636-410b-a9de-271abe70b09b_944x626.png 1272w, https://substackcdn.com/image/fetch/$s_!7tMv!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3d5ddbc-6636-410b-a9de-271abe70b09b_944x626.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!7tMv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3d5ddbc-6636-410b-a9de-271abe70b09b_944x626.png" width="944" height="626" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e3d5ddbc-6636-410b-a9de-271abe70b09b_944x626.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:626,&quot;width&quot;:944,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:99926,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.andreyfadeev.com/i/158993075?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3d5ddbc-6636-410b-a9de-271abe70b09b_944x626.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!7tMv!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3d5ddbc-6636-410b-a9de-271abe70b09b_944x626.png 424w, https://substackcdn.com/image/fetch/$s_!7tMv!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3d5ddbc-6636-410b-a9de-271abe70b09b_944x626.png 848w, https://substackcdn.com/image/fetch/$s_!7tMv!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3d5ddbc-6636-410b-a9de-271abe70b09b_944x626.png 1272w, https://substackcdn.com/image/fetch/$s_!7tMv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3d5ddbc-6636-410b-a9de-271abe70b09b_944x626.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3><strong>Step 15</strong></h3><p>Reload the page from step 6. Now you should see the current date and time in your browser.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!7zqx!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d61c456-652f-4d1e-be27-6e8a4b2cd001_754x364.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!7zqx!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d61c456-652f-4d1e-be27-6e8a4b2cd001_754x364.png 424w, https://substackcdn.com/image/fetch/$s_!7zqx!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d61c456-652f-4d1e-be27-6e8a4b2cd001_754x364.png 848w, https://substackcdn.com/image/fetch/$s_!7zqx!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d61c456-652f-4d1e-be27-6e8a4b2cd001_754x364.png 1272w, https://substackcdn.com/image/fetch/$s_!7zqx!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d61c456-652f-4d1e-be27-6e8a4b2cd001_754x364.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!7zqx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d61c456-652f-4d1e-be27-6e8a4b2cd001_754x364.png" width="754" height="364" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8d61c456-652f-4d1e-be27-6e8a4b2cd001_754x364.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:364,&quot;width&quot;:754,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:102237,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.andreyfadeev.com/i/158993075?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d61c456-652f-4d1e-be27-6e8a4b2cd001_754x364.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!7zqx!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d61c456-652f-4d1e-be27-6e8a4b2cd001_754x364.png 424w, https://substackcdn.com/image/fetch/$s_!7zqx!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d61c456-652f-4d1e-be27-6e8a4b2cd001_754x364.png 848w, https://substackcdn.com/image/fetch/$s_!7zqx!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d61c456-652f-4d1e-be27-6e8a4b2cd001_754x364.png 1272w, https://substackcdn.com/image/fetch/$s_!7zqx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d61c456-652f-4d1e-be27-6e8a4b2cd001_754x364.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>This means that you've changed the code of your Spring application without rebuilding and redeploying it.</p><h3><strong>Step 16</strong></h3><p>Now let's retrieve a reference to our bean from step 6 from within Clojure. An instance of <code>org.springframework.context.ApplicationContext</code> is passed into our function. We can use its getBean method to retrieve the reference to the bean via its name.</p><pre><code>bean (-&gt; ctx (.getBean "upperCaseBean"))</code></pre><p>Then we can call its upperCase method and return the result:</p><pre><code>#_{:clj-kondo/ignore [:namespace-name-mismatch]}

(ns com.pravles.clojureguestpost
  (:require [clojure.pprint :refer [pprint]])
  (:require [clojure.string])
  (:require [clojure.java.io]))

(defn debug [ctx]
  (let [date (-&gt; java.util.Date (new) (.toString)) 
        bean (-&gt; ctx (.getBean "upperCaseBean"))]
    (str "bean: " (-&gt; bean (.upperCase "foo")))))</code></pre><p>Press Ctrl-A and Ctrl-Enter to send the update to the Spring application.</p><p>Then, reload the URL <a href="http://localhost:8080/debug/run">http://localhost:8080/debug/run</a> in the browser. You should see an output like this:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!z-wC!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe5eb5829-6b0f-427e-9e4d-910c7abfaac9_808x370.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!z-wC!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe5eb5829-6b0f-427e-9e4d-910c7abfaac9_808x370.png 424w, https://substackcdn.com/image/fetch/$s_!z-wC!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe5eb5829-6b0f-427e-9e4d-910c7abfaac9_808x370.png 848w, https://substackcdn.com/image/fetch/$s_!z-wC!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe5eb5829-6b0f-427e-9e4d-910c7abfaac9_808x370.png 1272w, https://substackcdn.com/image/fetch/$s_!z-wC!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe5eb5829-6b0f-427e-9e4d-910c7abfaac9_808x370.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!z-wC!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe5eb5829-6b0f-427e-9e4d-910c7abfaac9_808x370.png" width="808" height="370" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e5eb5829-6b0f-427e-9e4d-910c7abfaac9_808x370.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:370,&quot;width&quot;:808,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:100812,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.andreyfadeev.com/i/158993075?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe5eb5829-6b0f-427e-9e4d-910c7abfaac9_808x370.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!z-wC!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe5eb5829-6b0f-427e-9e4d-910c7abfaac9_808x370.png 424w, https://substackcdn.com/image/fetch/$s_!z-wC!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe5eb5829-6b0f-427e-9e4d-910c7abfaac9_808x370.png 848w, https://substackcdn.com/image/fetch/$s_!z-wC!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe5eb5829-6b0f-427e-9e4d-910c7abfaac9_808x370.png 1272w, https://substackcdn.com/image/fetch/$s_!z-wC!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe5eb5829-6b0f-427e-9e4d-910c7abfaac9_808x370.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2><strong>Conclusion</strong></h2><p>Now you know how to harness the power of Clojure in legacy projects. Did you use Clojure to work with legacy software in a better way? Share your thoughts in the comments.</p><h2><strong>About me</strong></h2><p>I work as a software developer during the day so I can work on my novel during the evenings. Visit <a href="https://pravles.substack.com">pravles.substack.com</a> to read completed parts of my novel for free. If you are interested in how writing fiction helped me become a better software developer, please leave a comment.</p>]]></content:encoded></item><item><title><![CDATA[How Clojure Atom and swap! internals work ]]></title><description><![CDATA[What is wrong with this code?]]></description><link>https://blog.andreyfadeev.com/p/how-clojure-atom-and-swap-internals</link><guid isPermaLink="false">https://blog.andreyfadeev.com/p/how-clojure-atom-and-swap-internals</guid><dc:creator><![CDATA[Andrey Fadeev]]></dc:creator><pubDate>Thu, 23 Jan 2025 08:01:46 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/98c2ef65-2cda-4d73-97c7-5188765b0b58_420x300.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>What is wrong with this code? </p><pre><code>class Example {
  
  private int x = 0;
  
  public int increment() {
    this.x = this.x + 1;
    return x;
  }
}</code></pre><p>It&#8217;s okay until you try to use it in concurrent environment, from different threads. In that case we have a classic race condition, increment is not atomic, two threads can read value at the same time and one one increment will be written. </p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.andreyfadeev.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Andrey Fadeev is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>There are ways to improve it, for example, we can use <code>synchronized</code> keyword on the method name, which will always attempt to acquire a lock on the object and force a single-thread execution of the method.</p><pre><code>public synchronized int increment() {
    this.x = this.x + 1;
    return x;
}</code></pre><p>Also, we can use explicit locks, like <code>ReentrantLock</code>:</p><pre><code> class X {
   private final ReentrantLock lock = new ReentrantLock();
   // ...

   public void m() {
     lock.lock();  // block until condition holds
     try {
       // ... method body
     } finally {
       lock.unlock()
     }
   }
 }</code></pre><p>Or alternatively, if we want a thread-safe way to work with an Integer we can completely swap our own implementation AtomicInteger:</p><pre><code>import java.util.concurrent.atomic.AtomicInteger;
  
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();</code></pre><p>There are other classes available in the  import java.util.concurrent.atomic.* package. The backbone of that is the Compare And Set (CAS) operation: Java will rely on the underlying platform processor instructions, for example, <code>CMPXCHG </code>on <code>x86/x64</code> to atomically swap the variable value. </p><p>So how is this applicable to our topic, Clojure Atoms?</p><p>Let&#8217;s check Atom semantics, we can define an atom containing any value type, from primitives to data structures like vectors, sets and maps, to update those values we use <code>swap!</code> function, it is guaranteed to be thread-safe. </p><pre><code>(def x (atom 0))
(swap! x inc) 
=&gt; 1
(deref x) 
=&gt; 1
@x 
=&gt; 1</code></pre><p>Note that to get value from an atom there is <code>deref</code> function, <code>@ </code>is the short way of writing <code>deref</code>.</p><p>As I&#8217;ve said we can use data structures inside atom container as well:</p><pre><code>(def x (atom [1 2 3]))

(swap! x (fn [v] (mapv inc v)))

=&gt; [2 3 4]</code></pre><p>Let&#8217;s look inside of the atom swap! implementation:</p><pre><code>(defn swap!
  "Atomically swaps the value of atom to be:
  (apply f current-value-of-atom args). Note that f may be called
  multiple times, and thus should be free of side effects.  Returns
  the value that was swapped in."
  {:added "1.0"
   :static true}
  ([^clojure.lang.IAtom atom f] (.swap atom f))
  ([^clojure.lang.IAtom atom f x] (.swap atom f x))
  ([^clojure.lang.IAtom atom f x y] (.swap atom f x y))
  ([^clojure.lang.IAtom atom f x y &amp; args] (.swap atom f x y args)))</code></pre><pre><code>import java.util.concurrent.atomic.AtomicReference;

public final class Atom extends ARef implements IAtom2 {
  final AtomicReference state;

  public Object deref() {
    return this.state.get();
  }

  public Object swap(IFn f) {
    Object v;
    Object newv;
    do {
      v = this.deref();
      newv = f.invoke(v);
      this.validate(newv);
    } while(!this.state.compareAndSet(v, newv));

    this.notifyWatches(v, newv);
    return newv;
  }</code></pre><p>We can see the usage of <code>AtomicReference, </code>that&#8217;s basically how our object is stored in the atom. Deref is simple, we just call <code>get</code> on the <code>AtomicReference:</code></p><pre><code>public Object deref() {
    return this.state.get();
}</code></pre><p>In the swap we see a common pattern: Compare And Set Loop. The idea is simple:</p><ul><li><p>get current value </p></li><li><p>calculate the next value (the invoke call of user-provided function with the current value as the input)</p></li><li><p>try to apply changes with compareAndSet</p></li></ul><p>But why do we need the loop? CompareAndSet will return true in case of success, but there is still a possibility that other threads changed the value in between! That&#8217;s why we need to retry that until we get lucky and our update is applied:</p><pre><code>    /**
     * Atomically sets the value to {@code newValue}
     * if the current value {@code == expectedValue},
     * with memory effects as specified by {@link VarHandle#compareAndSet}.
     *
     * @param expectedValue the expected value
     * @param newValue the new value
     * @return {@code true} if successful. False return indicates that
     * the actual value was not equal to the expected value.
     */
    public final boolean compareAndSet(V expectedValue, V newValue) {
        return VALUE.compareAndSet(this, expectedValue, newValue);
    }</code></pre><p>Due to this retry logic - it&#8217;s clear that you shouldn&#8217;t add side-effects into the swap fn, as you can see there is no guarantee that it will be executed once. It&#8217;s only guaranteed that the result will be updated in a thread-safe way.</p><p>Also, note that we have this line:</p><pre><code>this.notifyWatches(v, newv);</code></pre><p>It&#8217;s a watcher feature of the Atom, not commonly used or known but pretty cool. </p><pre><code>(def a (atom {}))

(add-watch a :watcher
  (fn [key atom old-state new-state]
    (println "Atom value changed)))</code></pre><p>Now the watcher fn will be triggered each time the value inside the atom is changed, it&#8217;s possible to add multiple watchers as well.</p><p>Hope that was useful!</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.andreyfadeev.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Andrey Fadeev is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Data modelling with Clojure (Clojure Book Chapter Draft)]]></title><description><![CDATA[Coming from Java, I was finally free from OOP, classes, interfaces and inheritance. It was like a miracle to realize that I only needed a vector, a map and a set to model everything.]]></description><link>https://blog.andreyfadeev.com/p/data-modelling-with-clojure-clojure</link><guid isPermaLink="false">https://blog.andreyfadeev.com/p/data-modelling-with-clojure-clojure</guid><dc:creator><![CDATA[Andrey Fadeev]]></dc:creator><pubDate>Tue, 21 Jan 2025 21:30:21 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/c368b871-04e6-4794-8664-7ed9aa4cf32a_120x120.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When I started my Clojure journey I was impressed by many things, one of which was the ease of expressing my thoughts regarding data modelling. </p><p>Coming from Java, I was finally free from OOP, classes, interfaces and inheritance. It was like a miracle to realize that I only needed a vector, a map and a set to model everything. </p><p>Lists are everywhere in Clojure, it&#8217;s basically a building block of the LISP program. They could be used as a data structure as well, under the hood it&#8217;s just a linked list, but usually, a vector is a better choice for data modelling.  </p><h3>Vector []</h3><p>In simple terms, you can think about a vector as an array. Same properties: fast access by index, but requires shifting elements if inserted in the middle. Technically all Clojure data structures are trees (to implement persistent immutable data structures), so instead of O(1) for index access it is O(log32n) but realistically could be neglected.</p><p>Here we have a simple vector with elements of the same type:</p><pre><code>[1 2 3]
["a" "b" "c"]</code></pre><p>It doesn&#8217;t have to be the case all the time, here is a vector with mixed types and also a nested vector:</p><pre><code>[1 "a" [2 3 nil]]</code></pre>
      <p>
          <a href="https://blog.andreyfadeev.com/p/data-modelling-with-clojure-clojure">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Clojure Hiccup with Tailwind CSS]]></title><description><![CDATA[Let's discover a production-ready way to use Tailwind CSS with Clojure backend and Hiccup HTML library]]></description><link>https://blog.andreyfadeev.com/p/clojure-hiccup-with-tailwind-css</link><guid isPermaLink="false">https://blog.andreyfadeev.com/p/clojure-hiccup-with-tailwind-css</guid><dc:creator><![CDATA[Andrey Fadeev]]></dc:creator><pubDate>Wed, 18 Dec 2024 22:14:13 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa69ca129-bd71-45fd-af31-f8213181c181_685x685.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!FFE0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff79bd4eb-1d7b-4400-bfc4-485267f038c4_1100x220.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!FFE0!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff79bd4eb-1d7b-4400-bfc4-485267f038c4_1100x220.png 424w, https://substackcdn.com/image/fetch/$s_!FFE0!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff79bd4eb-1d7b-4400-bfc4-485267f038c4_1100x220.png 848w, https://substackcdn.com/image/fetch/$s_!FFE0!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff79bd4eb-1d7b-4400-bfc4-485267f038c4_1100x220.png 1272w, https://substackcdn.com/image/fetch/$s_!FFE0!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff79bd4eb-1d7b-4400-bfc4-485267f038c4_1100x220.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!FFE0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff79bd4eb-1d7b-4400-bfc4-485267f038c4_1100x220.png" width="1100" height="220" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f79bd4eb-1d7b-4400-bfc4-485267f038c4_1100x220.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:220,&quot;width&quot;:1100,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:105403,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!FFE0!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff79bd4eb-1d7b-4400-bfc4-485267f038c4_1100x220.png 424w, https://substackcdn.com/image/fetch/$s_!FFE0!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff79bd4eb-1d7b-4400-bfc4-485267f038c4_1100x220.png 848w, https://substackcdn.com/image/fetch/$s_!FFE0!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff79bd4eb-1d7b-4400-bfc4-485267f038c4_1100x220.png 1272w, https://substackcdn.com/image/fetch/$s_!FFE0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff79bd4eb-1d7b-4400-bfc4-485267f038c4_1100x220.png 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a></figure></div><p>I&#8217;ve been using Tailwind for multiple years now and it&#8217;s my go-to way of styling web applications. Tiny, composable classes are great for both React components and for Clojure Hiccup components. The issue that in Clojure environment I was really lazy and just used CDN link to add Tailwind to my hobby projects. That&#8217;s worked but it&#8217;s not ideal, size of the file is large and it&#8217;s hard to configure. </p><p>I&#8217;ve recently explored a proper way of using Tailwind and was surprised, it&#8217;s not much harder to configure it to work with Clojure backend compared to React and Javascript. </p><p>First of all, as I usually don&#8217;t have any Node in my Clojure projects, I didn&#8217;t really want to install it just for Tailwind. Luckily, there is a standalone Tailwind CLI available: <a href="https://tailwindcss.com/blog/standalone-cli">https://tailwindcss.com/blog/standalone-cli</a></p><p>I&#8217;ve used Homebrew to install:</p><pre><code>brew install tailwindcss</code></pre><p>The next step is to run init command in the root of the project:</p><pre><code>tailwindcss init</code></pre><p>The result of this command is a new file `tailwind.config.js` that will be used for additional customisations.</p><p>The idea of Tailwind build step is to search source files for known Tailwind CSS classes and it&#8217;s language agnostic, it will basically search for a string like literals that match class names. </p><p>Let&#8217;s configure CLI to search for classes in Clojure source files:</p><pre><code>content: [
    "./src/**/*.{clj,cljs,cljc}"
]</code></pre><p>In addition to that we will need an `input.css` file with a couple of imports:</p><pre><code>@tailwind base;
@tailwind components;
@tailwind utilities;</code></pre><p>That&#8217;s basically all we need, let&#8217;s run the build:</p><pre><code>tailwindcss -i input.css -o ./resources/public/css/output.css</code></pre><p>It should discover all Tailwind classes and generate much smaller result CSS file. </p><p>I usually use Babashka as my task runner, so there is an example of adding tasks to build, minify or watch Tailwind, `bb.edn`:</p><pre><code>{:tasks
 {:init (do (defn sh [&amp; args]
              (binding [*out* *err*]
                (apply println "+" args))
              (apply shell args))
            (defn tailwindcss
              [cmd]
              (sh (str "tailwindcss -i input.css -o ./resources/public/css/output.css " cmd))))
  
  tailwind:watch {:doc "Build TailwindCSS in watch mode"
                  :task (tailwindcss "--watch")}
  tailwind:build {:doc "Build TailwindCSS output"
                  :task (tailwindcss nil)}
  tailwind:minify {:doc "Build &amp; minify TailwindCSS"
                   :task (tailwindcss "--minify")}}}</code></pre><p>Now, for example, in development we can just start watch mode:</p><pre><code>bb tailwind:watch</code></pre><h4>Other customisations</h4><p>I&#8217;ve tried a couple of things, as setting custom container width and adding other fonts.</p><p>For the font, the easiest is to add it as import into the `input.css` file, I&#8217;ve used one from Google Fonts. </p><pre><code>module.exports = {
  content: [
    "./src/**/*.{clj,cljs,cljc}"
  ],
  theme: {
    extend: {
      container: {
        center: true,
        padding: '1rem',
        screens: {
          DEFAULT: '600px',
        },
      },
      fontFamily: {
        sans: ['Titillium Web',  'sans-serif'],
      },
    },
  },
  plugins: [],
};</code></pre><h4>Dynamic classes </h4><p>I think it&#8217;s best to avoid dynamic class names, but if you have one of those, for example some string concatenation to generate class name &#8212; one way to solve it is to use `safelist` configuration:</p><pre><code>module.exports = {
  safelist: ["bg-red-500"]
};
</code></pre><p>If you are looking for a full example, I&#8217;ve added this Tailwind integration into my Clojure service template: <a href="https://github.com/andfadeev/clojure-service-template">https://github.com/andfadeev/clojure-service-template</a></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.andreyfadeev.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.andreyfadeev.com/subscribe?"><span>Subscribe now</span></a></p><p></p>]]></content:encoded></item><item><title><![CDATA[Coolify is actually pretty cool]]></title><description><![CDATA[Exploring a cheap single node VPS setup with Hetzner, Coolify (hosting Clojure application with PostgreSQL database)]]></description><link>https://blog.andreyfadeev.com/p/coolify-is-actually-pretty-cool</link><guid isPermaLink="false">https://blog.andreyfadeev.com/p/coolify-is-actually-pretty-cool</guid><dc:creator><![CDATA[Andrey Fadeev]]></dc:creator><pubDate>Mon, 09 Dec 2024 22:00:40 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!f4Of!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F524506e3-00de-4732-a2d2-872d7ae60761_1706x552.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!f4Of!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F524506e3-00de-4732-a2d2-872d7ae60761_1706x552.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!f4Of!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F524506e3-00de-4732-a2d2-872d7ae60761_1706x552.png 424w, https://substackcdn.com/image/fetch/$s_!f4Of!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F524506e3-00de-4732-a2d2-872d7ae60761_1706x552.png 848w, https://substackcdn.com/image/fetch/$s_!f4Of!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F524506e3-00de-4732-a2d2-872d7ae60761_1706x552.png 1272w, https://substackcdn.com/image/fetch/$s_!f4Of!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F524506e3-00de-4732-a2d2-872d7ae60761_1706x552.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!f4Of!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F524506e3-00de-4732-a2d2-872d7ae60761_1706x552.png" width="1456" height="471" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/524506e3-00de-4732-a2d2-872d7ae60761_1706x552.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:471,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:66132,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!f4Of!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F524506e3-00de-4732-a2d2-872d7ae60761_1706x552.png 424w, https://substackcdn.com/image/fetch/$s_!f4Of!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F524506e3-00de-4732-a2d2-872d7ae60761_1706x552.png 848w, https://substackcdn.com/image/fetch/$s_!f4Of!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F524506e3-00de-4732-a2d2-872d7ae60761_1706x552.png 1272w, https://substackcdn.com/image/fetch/$s_!f4Of!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F524506e3-00de-4732-a2d2-872d7ae60761_1706x552.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>I was looking for a cheap and convenient way to improve deployment for my hobby projects. My current setup is a DigitalOcean Droplet, which costs me ~8$/month. Inside I have Docker and Docker Compose that run: a Clojure application, a PostgreSQL database, an additional container that does database S3 backups on a schedule and Nginx as reverse proxy which also is configured to generate and renew LetsEncrypt certificates. That actually worked for me for many years, but there are a couple of things I don&#8217;t like:</p><ul><li><p>The cost of DigitalOcean Droplet is quite high for the resources.</p></li><li><p>My deployment process is quite manual: GitHub Actions pipeline is building a docker image for me and after I need to manually pull the repo on the host with `docker-compose.yaml` and restart the container.</p></li></ul><p>On the other hand, the Hetzner Cloud offering is appealing. They have the CX22 plan: 2 vCPU, 4GB RAM, 40TB for &#8364;3.79/month. Also, you will probably what to pay for IPv4 on top of that. </p><p>But I didn&#8217;t really want to replicate my poor deployment pipeline, so I&#8217;ve looked for alternatives. </p><p>Coolify was on my radar for quite a while, but the thing I didn&#8217;t understand is how many features it has. I saw it advertised as a self-hosted Vercel alternative and without looking into much detail I assumed that it&#8217;s Javascript-focused - I was wrong. </p><p>You can literally run anything - I was able to deploy a Clojure application without any modifications. </p><p>To build and deploy your application you can still use Docker, but by default, it&#8217;s using Nixpacks. It was new to me and better read official docs (<a href="https://nixpacks.com/docs">https://nixpacks.com/docs</a>), but It has Clojure support: <a href="https://nixpacks.com/docs/providers/clojure">https://nixpacks.com/docs/providers/clojure</a> by just detecting a `project.clj` file in your repo. </p><p>Let&#8217;s talk about installing Coolify - it is really simple and takes just a couple of minutes and a single command to run:</p><pre><code>curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash</code></pre><p>You need to run it from your Hetzner host and it&#8217;s probably the last time you need to SSH there. At the end of the installation, you&#8217;ll get a URL for the Coolify UI running on the host, everything else is done from there. </p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!qwl_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2b2ee2dc-dde2-471c-9bc5-6e2f1ca027ec_1706x961.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!qwl_!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2b2ee2dc-dde2-471c-9bc5-6e2f1ca027ec_1706x961.png 424w, https://substackcdn.com/image/fetch/$s_!qwl_!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2b2ee2dc-dde2-471c-9bc5-6e2f1ca027ec_1706x961.png 848w, https://substackcdn.com/image/fetch/$s_!qwl_!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2b2ee2dc-dde2-471c-9bc5-6e2f1ca027ec_1706x961.png 1272w, https://substackcdn.com/image/fetch/$s_!qwl_!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2b2ee2dc-dde2-471c-9bc5-6e2f1ca027ec_1706x961.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!qwl_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2b2ee2dc-dde2-471c-9bc5-6e2f1ca027ec_1706x961.png" width="1456" height="820" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2b2ee2dc-dde2-471c-9bc5-6e2f1ca027ec_1706x961.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:820,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:126727,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!qwl_!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2b2ee2dc-dde2-471c-9bc5-6e2f1ca027ec_1706x961.png 424w, https://substackcdn.com/image/fetch/$s_!qwl_!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2b2ee2dc-dde2-471c-9bc5-6e2f1ca027ec_1706x961.png 848w, https://substackcdn.com/image/fetch/$s_!qwl_!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2b2ee2dc-dde2-471c-9bc5-6e2f1ca027ec_1706x961.png 1272w, https://substackcdn.com/image/fetch/$s_!qwl_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2b2ee2dc-dde2-471c-9bc5-6e2f1ca027ec_1706x961.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>Running PostgreSQL</h3><p>My application requires a PostgreSQL database, so that was the first thing I&#8217;ve tried. You can basically deploy a PostgreSQL instance from the UI with a couple of clicks. </p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ahAH!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d9f2dce-60b2-4734-9e16-a75e638b390e_1715x961.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ahAH!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d9f2dce-60b2-4734-9e16-a75e638b390e_1715x961.png 424w, https://substackcdn.com/image/fetch/$s_!ahAH!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d9f2dce-60b2-4734-9e16-a75e638b390e_1715x961.png 848w, https://substackcdn.com/image/fetch/$s_!ahAH!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d9f2dce-60b2-4734-9e16-a75e638b390e_1715x961.png 1272w, https://substackcdn.com/image/fetch/$s_!ahAH!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d9f2dce-60b2-4734-9e16-a75e638b390e_1715x961.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ahAH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d9f2dce-60b2-4734-9e16-a75e638b390e_1715x961.png" width="1456" height="816" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3d9f2dce-60b2-4734-9e16-a75e638b390e_1715x961.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:816,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:160270,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!ahAH!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d9f2dce-60b2-4734-9e16-a75e638b390e_1715x961.png 424w, https://substackcdn.com/image/fetch/$s_!ahAH!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d9f2dce-60b2-4734-9e16-a75e638b390e_1715x961.png 848w, https://substackcdn.com/image/fetch/$s_!ahAH!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d9f2dce-60b2-4734-9e16-a75e638b390e_1715x961.png 1272w, https://substackcdn.com/image/fetch/$s_!ahAH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3d9f2dce-60b2-4734-9e16-a75e638b390e_1715x961.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!U4FY!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18b43222-adec-4a4a-9913-947bef50390b_1715x961.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!U4FY!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18b43222-adec-4a4a-9913-947bef50390b_1715x961.png 424w, https://substackcdn.com/image/fetch/$s_!U4FY!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18b43222-adec-4a4a-9913-947bef50390b_1715x961.png 848w, https://substackcdn.com/image/fetch/$s_!U4FY!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18b43222-adec-4a4a-9913-947bef50390b_1715x961.png 1272w, https://substackcdn.com/image/fetch/$s_!U4FY!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18b43222-adec-4a4a-9913-947bef50390b_1715x961.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!U4FY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18b43222-adec-4a4a-9913-947bef50390b_1715x961.png" width="1456" height="816" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/18b43222-adec-4a4a-9913-947bef50390b_1715x961.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:816,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:165894,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!U4FY!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18b43222-adec-4a4a-9913-947bef50390b_1715x961.png 424w, https://substackcdn.com/image/fetch/$s_!U4FY!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18b43222-adec-4a4a-9913-947bef50390b_1715x961.png 848w, https://substackcdn.com/image/fetch/$s_!U4FY!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18b43222-adec-4a4a-9913-947bef50390b_1715x961.png 1272w, https://substackcdn.com/image/fetch/$s_!U4FY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18b43222-adec-4a4a-9913-947bef50390b_1715x961.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>My instance was up and running in no time. Extra features I really liked:</p><ul><li><p>I was able to expose my database port to the outside (make it publically available for a short period), so I could easily transfer data from my old database with pg_dump and the psql restore.</p></li><li><p>You can configure database backups from Coolify, so you don&#8217;t need extra tools: the destination could be most of the S3-compatible object stores. </p></li></ul><h3>Deploying the Clojure application</h3><p>I have a private GitHub repo with my Clojure application. The way you configure the CI pipeline for it in Coolify is by creating a GitHub application with permission to read the repo, it will also automatically receive webhooks for new commits in the main branch and trigger new deployments. </p><p>The other killer feature is the preview branches on PRs, I don&#8217;t really use it in my project, but I definitely see the value. </p><p>So the build process as I&#8217;ve mentioned is using Nixpacks by default and it actually worked without problems for the Clojure application, it detected the environment like so: </p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Sddb!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff15a2814-d378-433b-b7d1-1d80b46a5651_1193x463.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Sddb!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff15a2814-d378-433b-b7d1-1d80b46a5651_1193x463.png 424w, https://substackcdn.com/image/fetch/$s_!Sddb!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff15a2814-d378-433b-b7d1-1d80b46a5651_1193x463.png 848w, https://substackcdn.com/image/fetch/$s_!Sddb!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff15a2814-d378-433b-b7d1-1d80b46a5651_1193x463.png 1272w, https://substackcdn.com/image/fetch/$s_!Sddb!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff15a2814-d378-433b-b7d1-1d80b46a5651_1193x463.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Sddb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff15a2814-d378-433b-b7d1-1d80b46a5651_1193x463.png" width="1193" height="463" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f15a2814-d378-433b-b7d1-1d80b46a5651_1193x463.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:463,&quot;width&quot;:1193,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:61050,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Sddb!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff15a2814-d378-433b-b7d1-1d80b46a5651_1193x463.png 424w, https://substackcdn.com/image/fetch/$s_!Sddb!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff15a2814-d378-433b-b7d1-1d80b46a5651_1193x463.png 848w, https://substackcdn.com/image/fetch/$s_!Sddb!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff15a2814-d378-433b-b7d1-1d80b46a5651_1193x463.png 1272w, https://substackcdn.com/image/fetch/$s_!Sddb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff15a2814-d378-433b-b7d1-1d80b46a5651_1193x463.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>One thing to mention, I got an issue with the Java runtime version in another app, but the fix was to set the build environment variable to configure Nixpack Clojure:</p><pre><code>NIXPACKS_JDK_VERSION=latest</code></pre><p>Coolify is also solving the problem of storing secrets, instead of setting them manually on the host or storing them in GitHub private repo (as I used to do) &#8212; you just set them in UI (similar to Vercel):</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!PJ6O!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ce1b2d7-1aec-4dc8-a4c5-ed66dc1f075f_1685x795.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!PJ6O!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ce1b2d7-1aec-4dc8-a4c5-ed66dc1f075f_1685x795.png 424w, https://substackcdn.com/image/fetch/$s_!PJ6O!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ce1b2d7-1aec-4dc8-a4c5-ed66dc1f075f_1685x795.png 848w, https://substackcdn.com/image/fetch/$s_!PJ6O!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ce1b2d7-1aec-4dc8-a4c5-ed66dc1f075f_1685x795.png 1272w, https://substackcdn.com/image/fetch/$s_!PJ6O!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ce1b2d7-1aec-4dc8-a4c5-ed66dc1f075f_1685x795.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!PJ6O!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ce1b2d7-1aec-4dc8-a4c5-ed66dc1f075f_1685x795.png" width="1456" height="687" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7ce1b2d7-1aec-4dc8-a4c5-ed66dc1f075f_1685x795.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:687,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:154673,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!PJ6O!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ce1b2d7-1aec-4dc8-a4c5-ed66dc1f075f_1685x795.png 424w, https://substackcdn.com/image/fetch/$s_!PJ6O!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ce1b2d7-1aec-4dc8-a4c5-ed66dc1f075f_1685x795.png 848w, https://substackcdn.com/image/fetch/$s_!PJ6O!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ce1b2d7-1aec-4dc8-a4c5-ed66dc1f075f_1685x795.png 1272w, https://substackcdn.com/image/fetch/$s_!PJ6O!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ce1b2d7-1aec-4dc8-a4c5-ed66dc1f075f_1685x795.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Also, you get a nice you to view logs and you can configure log drain to a 3rd party (like NewRelic):</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!BYji!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f2ca473-5417-402b-98b4-b1ca31a7aaff_1850x795.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!BYji!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f2ca473-5417-402b-98b4-b1ca31a7aaff_1850x795.png 424w, https://substackcdn.com/image/fetch/$s_!BYji!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f2ca473-5417-402b-98b4-b1ca31a7aaff_1850x795.png 848w, https://substackcdn.com/image/fetch/$s_!BYji!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f2ca473-5417-402b-98b4-b1ca31a7aaff_1850x795.png 1272w, https://substackcdn.com/image/fetch/$s_!BYji!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f2ca473-5417-402b-98b4-b1ca31a7aaff_1850x795.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!BYji!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f2ca473-5417-402b-98b4-b1ca31a7aaff_1850x795.png" width="1456" height="626" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3f2ca473-5417-402b-98b4-b1ca31a7aaff_1850x795.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:626,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:120877,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!BYji!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f2ca473-5417-402b-98b4-b1ca31a7aaff_1850x795.png 424w, https://substackcdn.com/image/fetch/$s_!BYji!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f2ca473-5417-402b-98b4-b1ca31a7aaff_1850x795.png 848w, https://substackcdn.com/image/fetch/$s_!BYji!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f2ca473-5417-402b-98b4-b1ca31a7aaff_1850x795.png 1272w, https://substackcdn.com/image/fetch/$s_!BYji!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f2ca473-5417-402b-98b4-b1ca31a7aaff_1850x795.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Other features I&#8217;ve tried are:</p><ul><li><p>Configuring custom domain</p></li><li><p>Basic built-in host monitoring (CPU, Memory, DiscUsage)</p></li><li><p>Email notifications (you need a Resend API key for that)</p></li><li><p>Push notification to Telegram Bot</p></li></ul><p>The most impressive thing really is that during my experiments I encountered <strong>0 issues </strong>(except that one with Java runtime version, which I solved in 10 minutes by reading Nixpacks docs and setting one extra var).</p><p><strong>Well done, Coolify!</strong></p><p>P.S.</p><p>I&#8217;ve not switched my project to run on the Coolify Hetzner host, but I&#8217;ll run a replica of it for a while, check that everything is stable &#8212; and hopefully switch! </p><p>That should save me a couple of USD per month and improve the developer experience :)</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.andreyfadeev.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.andreyfadeev.com/subscribe?"><span>Subscribe now</span></a></p><p></p><p></p>]]></content:encoded></item><item><title><![CDATA[The first year on YouTube as a software engineer (with stats)]]></title><description><![CDATA[It's time to celebrate my YouTube channel's first anniversary, share all the stats and discuss future plans.]]></description><link>https://blog.andreyfadeev.com/p/the-first-year-on-youtube-as-a-software</link><guid isPermaLink="false">https://blog.andreyfadeev.com/p/the-first-year-on-youtube-as-a-software</guid><dc:creator><![CDATA[Andrey Fadeev]]></dc:creator><pubDate>Thu, 12 Sep 2024 20:17:00 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!HR2O!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a6effdb-28cd-40fa-8d89-62f16fae4827_850x850.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!HR2O!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a6effdb-28cd-40fa-8d89-62f16fae4827_850x850.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!HR2O!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a6effdb-28cd-40fa-8d89-62f16fae4827_850x850.png 424w, https://substackcdn.com/image/fetch/$s_!HR2O!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a6effdb-28cd-40fa-8d89-62f16fae4827_850x850.png 848w, https://substackcdn.com/image/fetch/$s_!HR2O!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a6effdb-28cd-40fa-8d89-62f16fae4827_850x850.png 1272w, https://substackcdn.com/image/fetch/$s_!HR2O!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a6effdb-28cd-40fa-8d89-62f16fae4827_850x850.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!HR2O!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a6effdb-28cd-40fa-8d89-62f16fae4827_850x850.png" width="850" height="850" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0a6effdb-28cd-40fa-8d89-62f16fae4827_850x850.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:850,&quot;width&quot;:850,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:104814,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!HR2O!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a6effdb-28cd-40fa-8d89-62f16fae4827_850x850.png 424w, https://substackcdn.com/image/fetch/$s_!HR2O!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a6effdb-28cd-40fa-8d89-62f16fae4827_850x850.png 848w, https://substackcdn.com/image/fetch/$s_!HR2O!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a6effdb-28cd-40fa-8d89-62f16fae4827_850x850.png 1272w, https://substackcdn.com/image/fetch/$s_!HR2O!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a6effdb-28cd-40fa-8d89-62f16fae4827_850x850.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>In August 2023 I got an idea to start a YouTube channel. </p><p>Time flies, the whole year passed and it&#8217;s time to celebrate the first anniversary and sum up the results. </p><p><strong>During the year I&#8217;ve learned quite a lot of new skills:</strong></p><ul><li><p>how to plan and prepare videos </p></li><li><p>how to set the environment (recording tools like OBS), camera settings, lightning</p></li><li><p>basic video editing (thanks for a great free DaVinci Resolve software)</p></li><li><p>making video thumbnails with Canva</p></li><li><p>video recording process and talking to the camera (there is a lot to improve here)</p></li></ul><p>The journey was extremely exciting and I&#8217;ve learnt a lot. I think the most important lesson is to be consistent and optimize the process. Videos are obviously not my main job so I need to fit that somehow into my already quite busy life. The lesson I&#8217;ve learnt is not to pursue perfection, and not to spend a lot of time editing. It&#8217;s really hard to stay motivated when you put a lot of effort into a video and still get low numbers of views, but it&#8217;s a marathon, not a sprint. </p><p>In the end, I&#8217;ve really streamlined the recording process and spent minimum time editing &#8212; that really helped to stay consistent and release content. </p><p>To be honest, consistency could be better at some periods &#8212; that&#8217;s something I&#8217;ll try to fix this year.</p><p>Let&#8217;s move to current stats:</p><ul><li><p><strong>3012 </strong>subscribers </p></li><li><p><strong>154000 </strong>total views</p></li><li><p><strong>9500 </strong>watch hours</p></li><li><p><strong>75 </strong>uploads</p></li></ul><p>In addition, I&#8217;ve got a Substack publication with <strong>103 </strong>subscribers, grown my LinkedIn to <strong>900 </strong>followers and my Telegram channel is growing as well!</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.andreyfadeev.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.andreyfadeev.com/subscribe?"><span>Subscribe now</span></a></p><h2>Path to monetization </h2><p>One of the goals I had when started was to try to get to a YouTube partnership and achieve monetization on the channel. If you are not familiar with the terms &#8212; your channel needs to reach some milestones to be able to get part of the revenue from ads. Frankly speaking, the requirements are not easy:</p><ul><li><p>1000 subscribers </p></li><li><p>4000 watch hours (total time your video was watched on YouTube)</p></li></ul><p>So it was quite challenging. I was reading some stories about getting to monetization on the internet and depending on a niche and channel-specific both cases are valid: for some people, it&#8217;s easier to get 4000 watch hours (some entertainment or viral content) but it&#8217;s really hard to gain subscribers, for others it&#8217;s opposite. In my case, I&#8217;ve reached 1000 subscribers much faster than 4000 watch hours. </p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!nQt5!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd1362c0-5cae-452f-becd-b3acc640c820_1508x387.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!nQt5!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd1362c0-5cae-452f-becd-b3acc640c820_1508x387.png 424w, https://substackcdn.com/image/fetch/$s_!nQt5!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd1362c0-5cae-452f-becd-b3acc640c820_1508x387.png 848w, https://substackcdn.com/image/fetch/$s_!nQt5!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd1362c0-5cae-452f-becd-b3acc640c820_1508x387.png 1272w, https://substackcdn.com/image/fetch/$s_!nQt5!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd1362c0-5cae-452f-becd-b3acc640c820_1508x387.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!nQt5!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd1362c0-5cae-452f-becd-b3acc640c820_1508x387.png" width="1456" height="374" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fd1362c0-5cae-452f-becd-b3acc640c820_1508x387.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:374,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:33666,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!nQt5!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd1362c0-5cae-452f-becd-b3acc640c820_1508x387.png 424w, https://substackcdn.com/image/fetch/$s_!nQt5!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd1362c0-5cae-452f-becd-b3acc640c820_1508x387.png 848w, https://substackcdn.com/image/fetch/$s_!nQt5!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd1362c0-5cae-452f-becd-b3acc640c820_1508x387.png 1272w, https://substackcdn.com/image/fetch/$s_!nQt5!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd1362c0-5cae-452f-becd-b3acc640c820_1508x387.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>I got 1000 subscribers in November, but 4000 watch hours only in mid-February. So the entire journey to monetisation took 5 months (from mid-August to mid-February). </p><p>For most people, the total revenue will be an interesting topic I believe and I would say you won&#8217;t get rich with a small channel and videos that are not getting tons of views and not going viral. </p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!8CW1!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e83005d-a012-424c-94e6-d1ba36098f3c_1128x548.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!8CW1!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e83005d-a012-424c-94e6-d1ba36098f3c_1128x548.png 424w, https://substackcdn.com/image/fetch/$s_!8CW1!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e83005d-a012-424c-94e6-d1ba36098f3c_1128x548.png 848w, https://substackcdn.com/image/fetch/$s_!8CW1!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e83005d-a012-424c-94e6-d1ba36098f3c_1128x548.png 1272w, https://substackcdn.com/image/fetch/$s_!8CW1!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e83005d-a012-424c-94e6-d1ba36098f3c_1128x548.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!8CW1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e83005d-a012-424c-94e6-d1ba36098f3c_1128x548.png" width="1128" height="548" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9e83005d-a012-424c-94e6-d1ba36098f3c_1128x548.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:548,&quot;width&quot;:1128,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:65916,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!8CW1!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e83005d-a012-424c-94e6-d1ba36098f3c_1128x548.png 424w, https://substackcdn.com/image/fetch/$s_!8CW1!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e83005d-a012-424c-94e6-d1ba36098f3c_1128x548.png 848w, https://substackcdn.com/image/fetch/$s_!8CW1!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e83005d-a012-424c-94e6-d1ba36098f3c_1128x548.png 1272w, https://substackcdn.com/image/fetch/$s_!8CW1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e83005d-a012-424c-94e6-d1ba36098f3c_1128x548.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The total from ads is <strong>&#163;151</strong> since monetization was enabled and it&#8217;s less than &#163;1 a day. </p><p>To be honest, YouTube ads revenue wasn&#8217;t the only source of income from my blogging this year. I&#8217;ve got a bunch of donations on the <a href="https://buymeacoffee.com/andrey.fadeev">Buy Me A Coffee</a> resource and a paid subscription here on Substack. Those two together are a bit more successful compared to YouTube gains. It&#8217;s also a great feeling to receive donations with kind words and see that people are grateful for some videos. I really hope some of the topics were really useful!</p><p>The most unexpected experience was a request for <a href="https://www.youtube.com/watch?v=TmOumwzswyU&amp;ab_channel=AndreyFadeev">a sponsored video</a>. </p><h2>Final thoughts </h2><p>I&#8217;d like to say thank you to everyone who watched my videos, subscribed to the channel, left comments and made donations! That was a constant source of inspiration during the year.</p><p>Also if you were thinking of starting a blog or a channel, I highly recommend just starting it, it&#8217;s not that scary and you don&#8217;t need any planning or preparation! It definitely has a lot of benefits and is a constant source of motivation to learn new things.</p><p>Looking forward to the second year on YouTube!</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.andreyfadeev.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.andreyfadeev.com/subscribe?"><span>Subscribe now</span></a></p><p></p><p></p>]]></content:encoded></item><item><title><![CDATA[Exploring Java Virtual Threads in Clojure Applications]]></title><description><![CDATA[It feels like the right time to make yourself familiar with Java Virtual Threads and here are a couple of reasons:]]></description><link>https://blog.andreyfadeev.com/p/exploring-java-virtual-threads-in</link><guid isPermaLink="false">https://blog.andreyfadeev.com/p/exploring-java-virtual-threads-in</guid><dc:creator><![CDATA[Andrey Fadeev]]></dc:creator><pubDate>Mon, 12 Aug 2024 11:07:53 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!8zNy!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b760e59-e377-4bcc-a272-415672e484ba_1280x720.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!8zNy!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b760e59-e377-4bcc-a272-415672e484ba_1280x720.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!8zNy!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b760e59-e377-4bcc-a272-415672e484ba_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!8zNy!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b760e59-e377-4bcc-a272-415672e484ba_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!8zNy!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b760e59-e377-4bcc-a272-415672e484ba_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!8zNy!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b760e59-e377-4bcc-a272-415672e484ba_1280x720.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!8zNy!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b760e59-e377-4bcc-a272-415672e484ba_1280x720.png" width="1280" height="720" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9b760e59-e377-4bcc-a272-415672e484ba_1280x720.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:720,&quot;width&quot;:1280,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:655662,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!8zNy!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b760e59-e377-4bcc-a272-415672e484ba_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!8zNy!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b760e59-e377-4bcc-a272-415672e484ba_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!8zNy!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b760e59-e377-4bcc-a272-415672e484ba_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!8zNy!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b760e59-e377-4bcc-a272-415672e484ba_1280x720.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>It feels like the right time to make yourself familiar with Java Virtual Threads and here are a couple of reasons:</p><ul><li><p>In JDK 21, the Virtual Threads feature is out of preview and enabled by default</p></li><li><p>Looks like we will have a Clojure 1.12 release soon (let&#8217;s see why it is important later in this article)</p></li><li><p>Ring release supports Jetty 12 (which has support for Virtual Threads)</p></li></ul><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.andreyfadeev.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.andreyfadeev.com/subscribe?"><span>Subscribe now</span></a></p><h2>Virtual Threads vs Platform Threads</h2><p>If you want to learn more about virtual threads and all their whys and hows, I highly recommend this article: <a href="https://blog.rockthejvm.com/ultimate-guide-to-java-virtual-threads/">https://blog.rockthejvm.com/ultimate-guide-to-java-virtual-threads/</a></p><p>Virtual threads allow us to follow a simple pattern: <strong>to create a new thread for every concurrent task</strong>. This model is called <em>one task per thread</em>.</p><p>But why it&#8217;s not desired with old Java threads (platform threads) &#8212; the answer is that they are expensive in many ways. One way to see it is to try to create multiple platform threads in a loop:</p><pre><code>(defn with-platform-threads
  []
  (dotimes [i 100000]
    (.start (Thread. (fn []
                       (Thread/sleep 1000)))))
  :done)

(with-platform-threads)</code></pre><p>It&#8217;s quite easy to reach a message like this:</p><pre><code>Execution error (OutOfMemoryError) at java.lang.Thread/start0 (Thread.java:-2).
unable to create native thread: possibly out of memory or process/resource limits reached</code></pre><p>On JDK 21 (in previous releases we need to enable the preview feature) we can use virtual threads and try the same:</p><pre><code>(defn with-virtual-threads
  []
  (dotimes [i 100000]
    (.start (.name (Thread/ofVirtual) "virtual-thread-")
            (fn [] (Thread/sleep 1000))))
  :done)

(with-virtual-threads)
:done</code></pre><p>The other benefit is that if a virtual thread is blocked on IO, it will be unmounted from the platform (carrier) thread &#8212; and a pending virtual thread could reuse the same platform thread. As you can see we can efficiently reuse a limited pool of platform threads while keeping the virtual threads number high!</p><h2>Ring and Jetty 12</h2><p>Jetty 12 could be configured with a pool of virtual threads to handle requests, and it can significantly improve the server performance in case of lots of IO operations in handlers. </p><pre><code>;; project.clj 

(defproject jetty-ring-virtual-threads "0.1.0-SNAPSHOT"
  :dependencies [[org.clojure/clojure "1.11.1"]
                 [ring/ring "1.12.2"]]
  :main ^:skip-aot jetty-ring-virtual-threads.core
  :target-path "target/%s")

;; jetty-ring-virtual-threads.core

(defn handler [_]
  (Thread/sleep 50)
  {:status 200
   :headers {"Content-Type" "text/plain"}
   :body "Hello World"})

(defn -main []
  (println "Starting Jetty Server")
  (jetty/run-jetty handler {:port 3000}))

;; and let's run it!
(-main)</code></pre><p>To do a simple benchmarking of the server I&#8217;ll use `wrk` tool <a href="https://github.com/wg/wrk">https://github.com/wg/wrk</a>:</p><pre><code>&gt; wrk -t12 -c400 -d30s http://localhost:3000

Running 30s test @ http://localhost:3000
12 threads and 400 connections
25625 requests in 30.06s, 3.79MB read
Socket errors: connect 0, read 1303, write 0, timeout 0
Requests/sec: 852.39
Transfer/sec: 129.02KB</code></pre><p>We got 850 Request per second with Jetty 12 on platform threads, now let&#8217;s configure it to use virtual threads:</p><pre><code>(defn -main
  []
  (println "Starting Jetty Server (Virtual Threads)")
  (let [thread-pool (QueuedThreadPool.)]
    (.setVirtualThreadsExecutor
      thread-pool (Executors/newVirtualThreadPerTaskExecutor))
    (jetty/run-jetty handler {:port 3000
                              :thread-pool thread-pool})))</code></pre><p>Everything else stays the same &#8212; time to rerun the benchmark:</p><pre><code>&gt; wrk -t12 -c400 -d30s http://localhost:3000

Running 30s test @ http://localhost:3000
12 threads and 400 connections
214960 requests in 30.10s, 31.78MB read
Socket errors: connect 0, read 1251, write 0, timeout 0
Requests/sec: 7140.46
Transfer/sec: 1.06MB</code></pre><p>So we have almost 10x improvement &#8212; 7100 RPS is impressive!</p><h2>Pinned virtual threads </h2><p>So now let&#8217;s understand why future Clojure 1.12 release is important &#8212; and the reason is to avoid virtual threads pinning. </p><blockquote><p><strong>There are some cases where a blocking operation doesn&#8217;t unmount the virtual thread from the carrier thread</strong>, blocking the underlying carrier thread. In such cases, we say the virtual is <em>pinned</em> to the carrier thread. It&#8217;s not an error but a behavior that limits the application&#8217;s scalability. Note that if a carrier thread is pinned, the JVM can always add a new platform thread to the carrier pool if the configurations of the carrier pool allow it.</p><p>Fortunately, there are only two cases in which a virtual thread is pinned to the carrier thread:</p><ul><li><p>When it executes code inside a <code>synchronized</code> block or method;</p></li><li><p>When it calls a native method or a foreign function (i.e., a call to a native library using JNI).</p></li></ul></blockquote><p>In Clojure delay and lazy-seq are using synchronized blocks, let&#8217;s check <code>clojure.lang.Delay</code> internals:</p><pre><code>  public Object deref() {
    if (this.fn != null) {
      // this is the problem
      synchronized(this) {
        if (this.fn != null) {
          try {
            this.val = this.fn.invoke();
          } catch (Throwable var4) {
            Throwable t = var4;
            this.exception = t;
          }

          this.fn = null;
        }
      }
    }

    if (this.exception != null) {
      throw Util.sneakyThrow(this.exception);
    } else {
      return this.val;
    }
  }</code></pre><p>Let&#8217;s change our handler to demonstrate the problem:</p><pre><code>(defn handler [_]
  @(delay
    (Thread/sleep 50))
  {:status 200
   :headers {"Content-Type" "text/plain"}
   :body "Hello World"})</code></pre><p>Technically nothing changed, we still sleep for 50ms but it&#8217;s wrapped in a delay that immediately derefed, but now we have a blocking call inside synchronized block &#8212; let&#8217;s run the benchmark:</p><pre><code>&gt; wrk -t12 -c400 -d30s http://localhost:3000

Running 30s test @ http://localhost:3000
12 threads and 400 connections
12534 requests in 30.06s, 1.85MB read
Socket errors: connect 0, read 1272, write 0, timeout 2844
Requests/sec: 416.90
Transfer/sec: 63.10KB</code></pre><p>As you can see the result is much worse, but it&#8217;s on a first run after the server start, the sequential result is giving similar results (7000 RPS). I think it&#8217;s because the internal platform pool is scaled to max allowed size during the first test run &#8212; but I need to investigate more to be sure.</p><p>Also by enabling <code>-Djdk.tracePinnedThreads=full</code> I was able to get some errors about virtual thread pinning:</p><pre><code>Thread[#148,ForkJoinPool-1-worker-3,5,CarrierThreads]
    java.base/java.lang.VirtualThread$VThreadContinuation.onPinned(VirtualThread.java:183)
    java.base/jdk.internal.vm.Continuation.onPinned0(Continuation.java:393)
    java.base/java.lang.VirtualThread.parkNanos(VirtualThread.java:621)
    java.base/java.lang.VirtualThread.sleepNanos(VirtualThread.java:791)
    java.base/java.lang.Thread.sleep(Thread.java:507)</code></pre><h2>Clojure 1.12</h2><p>In future Clojure release there will be changes related to delays and lazy-seqs: <a href="https://github.com/clojure/clojure/blob/master/changes.md">https://github.com/clojure/clojure/blob/master/changes.md</a></p><blockquote><h3>1.2 Java 21 - Virtual thread pinning from user code under <code>synchronized</code></h3><p>Clojure users want to use virtual threads on JDK 21. Prior to 1.12, Clojure lazy-seqs and delays, in order to enforce run-once behavior, ran user code under synchronized blocks, which as of JDK 21 don't yet participate in cooperative blocking. Thus if that code did e.g. blocking I/O it would pin a real thread. JDK 21 may emit warnings for this when using <code>-Djdk.tracePinnedThreads=full</code>.</p><p>To avoid this pinning, in 1.12 <code>lazy-seq</code> and <code>delay</code> use locks instead of synchronized blocks.</p></blockquote><p>Let&#8217;s update the version in the project.clj:</p><pre><code>[org.clojure/clojure "1.12.0-rc1"]</code></pre><p>Now if we go to Delay implementation &#8212; we will see the usage of locks instead of synchronized blocks:</p><pre><code>this.lock = new ReentrantLock();</code></pre><p>And let&#8217;s rerun our benchmark again:</p><pre><code>&gt; wrk -t12 -c400 -d30s http://localhost:3000

Running 30s test @ http://localhost:3000
12 threads and 400 connections
214040 requests in 30.11s, 31.64MB read
Socket errors: connect 0, read 1240, write 0, timeout 0
Requests/sec: 7109.61
Transfer/sec: 1.05MB</code></pre><p>Alghtouth I still have some concerns about the benchmarking results (especially in the case when pinning should occur), clearly virtual threads in Jetty are definitely worth exploring &#8212; I&#8217;m looking forward to try that on some real applications soon!</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.andreyfadeev.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.andreyfadeev.com/subscribe?"><span>Subscribe now</span></a></p>]]></content:encoded></item><item><title><![CDATA[Clojure Getting Started Roadmap 2024: Configure Development Environment]]></title><description><![CDATA[In this chapter, I&#8217;ll cover the essential steps needed to flatten the learning curve of Clojure language.]]></description><link>https://blog.andreyfadeev.com/p/clojure-getting-started-roadmap-2024-f92</link><guid isPermaLink="false">https://blog.andreyfadeev.com/p/clojure-getting-started-roadmap-2024-f92</guid><dc:creator><![CDATA[Andrey Fadeev]]></dc:creator><pubDate>Fri, 09 Aug 2024 20:22:47 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/2af345a8-9f0e-4982-adb9-8c1ab84a2ab4_420x300.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this chapter, I&#8217;ll cover the essential steps needed to flatten the learning curve of Clojure language. A key to success is to configure your development environments and REPL-driven development, so the feedback cycle is extremely fast!</p>
      <p>
          <a href="https://blog.andreyfadeev.com/p/clojure-getting-started-roadmap-2024-f92">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Clojure Getting Started Roadmap 2024: Why Clojure?]]></title><description><![CDATA[Hello there!]]></description><link>https://blog.andreyfadeev.com/p/clojure-getting-started-roadmap-2024-cb2</link><guid isPermaLink="false">https://blog.andreyfadeev.com/p/clojure-getting-started-roadmap-2024-cb2</guid><dc:creator><![CDATA[Andrey Fadeev]]></dc:creator><pubDate>Fri, 28 Jun 2024 22:28:11 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/2a2a3c30-65ae-4516-ae60-c2ca59d47b04_420x300.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hello there!</p><p>Welcome to Chapter II of the Clojure Getting Started Roadmap series. Before diving into technical details, the core of the language, and libraries, let&#8217;s discuss why you might choose Clojure as your next programming language and consider any potential downsides.</p><p>It's not just about the technology; we'll also explore the impact of picking a niche language like Clojure on your software career progression.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.andreyfadeev.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.andreyfadeev.com/subscribe?"><span>Subscribe now</span></a></p><p>As of 2024, Clojure remains a niche language, but it's far from new. Its first public release was in 2007, marking 17 years since its debut. It's also not just a "hipster" language; it has proven its worth across many domains and attracts developers seeking better alternatives to mainstream languages. The Nubank story is impressive&#8212;Clojure-based neo-bank that acquired Cognitect, the company behind Clojure.</p><h1>Why Learn Clojure in 2024?</h1>
      <p>
          <a href="https://blog.andreyfadeev.com/p/clojure-getting-started-roadmap-2024-cb2">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Clojure Getting Started Roadmap 2024: Introduction]]></title><description><![CDATA[Step-by-step instructions focusing on key areas of Clojure, its core aspects, essential tools, and libraries.]]></description><link>https://blog.andreyfadeev.com/p/clojure-getting-started-roadmap-2024</link><guid isPermaLink="false">https://blog.andreyfadeev.com/p/clojure-getting-started-roadmap-2024</guid><dc:creator><![CDATA[Andrey Fadeev]]></dc:creator><pubDate>Mon, 24 Jun 2024 17:01:05 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/aa09d713-1e23-484a-a61e-fe7cf066a319_420x300.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hello everyone!</p><p>The goal of this roadmap is to provide step-by-step instructions on how to start with Clojure in 2024, highlighting the most important areas to focus on.</p><p>We&#8217;ll cover the core aspects of the language as well as the essential tools and libraries you need to become familiar with. Clojure's unique nature requires a mindset shift to be productive and to work harmoniously with the language, rather than against it.</p><p>Clojure is a hosted language that can run on multiple platforms. While we&#8217;ll primarily focus on the JVM and scripting with Babashka, much of the content will also be applicable to front-end development with ClojureScript.</p><p>To clarify, this is not intended to be an exhaustive book about Clojure. Instead, it&#8217;s a practical guide on what to focus on when starting out. With seven years of full-time professional experience in Clojure, I've gained extensive hands-on knowledge of the language and its ecosystem.</p><p><strong>This guide is what I wish I had when I was starting out, and I hope it will be incredibly useful to everyone who reads it!</strong></p><p>This is what to expect in the following chapters:</p><ul><li><p>Why Clojure?</p></li></ul><ul><li><p>Getting Started with Clojure</p><ul><li><p>Setting Up Your Development Environment</p></li><li><p> Your First Clojure Program</p></li></ul></li><li><p>Clojure Fundamentals</p><ul><li><p>Basic Syntax</p></li><li><p>Data Types (Numbers, Strings, Collections)</p></li><li><p>Functions</p></li></ul></li></ul><p>Most of the chapters will be behind a paywall, accessible to all paid subscribers on Substack. Additionally, all paid subscribers will receive the final version of the book in EPUB or PDF format once it&#8217;s completed.</p><p>The final version will also be available as a paid digital download on various platforms, so stay tuned for updates!</p><p>I plan to release a chapter each week, though it&#8217;s challenging to predict the exact schedule. I promise to do my best!</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.andreyfadeev.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption"></p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[My approach to learning a new language as an experienced developer (Go)]]></title><description><![CDATA[I&#8217;d like to share my thoughts on learning a new language in this article.]]></description><link>https://blog.andreyfadeev.com/p/my-approach-to-learning-a-new-language</link><guid isPermaLink="false">https://blog.andreyfadeev.com/p/my-approach-to-learning-a-new-language</guid><dc:creator><![CDATA[Andrey Fadeev]]></dc:creator><pubDate>Mon, 13 May 2024 20:16:03 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/fd797c02-2ed6-47f6-bb11-c3447b7edcf1_1920x1080.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I&#8217;d like to share my thoughts on learning a new language in this article. Recently I&#8217;ve decided to give Go a try. It will be my experience, so take the advice with a pinch of salt. It won&#8217;t work if that is the first language you are learning. You should be familiar with software development and know one or two languages. </p><h1>Starting with basics</h1><p>It&#8217;s important to start with the basics and make yourself comfortable with the language and most importantly with the workflow. The goal is to create an environment where you can quickly iterate and try new code.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.andreyfadeev.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.andreyfadeev.com/subscribe?"><span>Subscribe now</span></a></p><p>To be clear, I&#8217;m not suggesting spending much time  or reading a huge book. I think it should be the opposite, you should minimize the time until you write your first line of code and make sure you can easily iterate and learn more.</p><h3>Steps here:</h3><ul><li><p>Setup the editor so it&#8217;s helpful (for most people it will probably be VSCode, but I was exploring Zed recently and found it&#8217;s good enough to work with Go with `gopls` LSP)</p></li><li><p>Create your first module and make sure you can run it. Classic &#8220;Hello World&#8221; from the main function is what we need.</p></li><li><p> Trying code from `main` could be challenging as you go, so we need a better way. So here we should learn how to create unit tests, so we can test any function that we write. </p></li></ul><p>And here was the first discovery, built-in features of the language regarding tests and not that great, so you probably will need a 3rd party library for assertions. <a href="https://github.com/stretchr/testify">Feels like the `testify` package is a popular one.</a> </p><pre><code>import (
  "testing"
  "github.com/stretchr/testify/assert"
)

func TestSomething(t *testing.T) {
  assert.Equal(t, "expected", YourFnToTest(1), "should work!")
}</code></pre><p>Once we are here, you can spend a bit of time trying different things, solving some problems, etc. But to get a real feel of the language we need to check those areas:</p><ul><li><p>create a web server</p></li><li><p>define routes </p></li><li><p>working with JSON (serialize and deserialize) </p></li><li><p>querying relational database</p></li><li><p>making HTTP requests </p></li></ul><p>Of course in reality there is more, but those are the most commonly used things in back-end development, so by exploring all those areas - you&#8217;ll get a good understanding of the language ecosystem. </p><h2>Web server </h2><p>Being able to create a web server, and register some endpoints will be a huge leap forward. Now our application will start to feel real!</p><p>The first step is to check how it looks to create a web server without any 3rd party libraries, and due to the `net/http` package, it&#8217;s really easy in Go!</p><pre><code>package main

import (
    "fmt"
    "net/http"
)

func hello(w http.ResponseWriter, req *http.Request) {
    fmt.Fprintf(w, "hello\n")
}

func main() {
    http.HandleFunc("/hello", hello)
    http.ListenAndServe(":8090", nil)
}</code></pre><p>It&#8217;s already something! The next question is how we create a more complex application, here we will need a router. </p><p>I&#8217;ve done a bit of research on what are popular libraries and frameworks in this area, here are some: Gin, Echo, Fiber, Chi, Gorilla. </p><p>As I come from a Clojure background, I really hate heavy and overcomplicated frameworks with lots of magic. </p><p>So my choice here so far (and remember I&#8217;m only starting my Go journey) is Chi: <a href="https://github.com/go-chi/chi">https://github.com/go-chi/chi</a>. It is small, basically just a router, but at the same time is flexible, by allowing different sets of middleware for groups of routes. </p><pre><code>package main

import (
    "net/http"

    "github.com/go-chi/chi/v5"
    "github.com/go-chi/chi/v5/middleware"
)

func main() {
    r := chi.NewRouter()
    r.Use(middleware.Logger)
    r.Get("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello World!"))
    })
    http.ListenAndServe(":3000", r)
}</code></pre><p>OK, that&#8217;s something! Unfortunately, nowadays we don&#8217;t talk plain text, so the next area to focus on is working with JSON, how to serialize and deserialize data. </p><h3>Working with JSON</h3><p>Again, Go has a standard package: <code>encoding/json</code> (<a href="https://gobyexample.com/json">https://gobyexample.com/json</a>)</p><pre><code>type User struct {
  ID   int64  `json:"id"`
  Name string `json:"name"`
}

user := User{
  ID: 1, 
  Name: "Andrey",
}

jsonText, _ := json.Marshal(user)

fmt.Println("As JSON", string(jsonText))</code></pre><p>As I&#8217;ve picked Chi as a web component library, it makes sense, what is the recommended way to return JSON from handlers, there is a render package available: <a href="https://github.com/go-chi/render">https://github.com/go-chi/render</a>. </p><pre><code>func (u *User) Render(w http.ResponseWriter, r *http.Request) error {
 return nil
}

func GetUser(w http.ResponseWriter, r *http.Request) {
  user := User{
  ID:   1,
  Name: "Andrey",
}
  render.Status(r, http.StatusOK)
  render.Render(w, r, &amp;user)
}

func main() {
  r := chi.NewRouter()
  r.Use(middleware.Logger)

  r.Get("/user", GetUser)
  
  http.ListenAndServe(":8787", r)
}</code></pre><h3>Working with SQL</h3><p>SQL and relational databases are still the most commonly used persistent storage nowadays, so it&#8217;s crucial to know how to work with them from any language. </p><p>I&#8217;ll be using PostgreSQL for all examples. To work with it we will need an SQL package <a href="https://pkg.go.dev/database/sql">https://pkg.go.dev/database/sql</a> and a driver <a href="https://github.com/lib/pq">https://github.com/lib/pq</a>. </p><p>And while this should work for simple applications: </p><pre><code>err := pool.QueryRowContext(ctx, "select p.name from people as p where p.id = :id;", sql.Named("id", id)).Scan(&amp;name)</code></pre><p>We really need a better way. <strong>But, I hate ORMs! </strong>I think this is a trauma from my old Java days with Hibernate. In my opinion, ORMs just bring so much magic and complexity without much value, it&#8217;s usually an application that is too small and simple for an ORM or already too complex. </p><p>I was looking for some kind of SQL query builder library and found <a href="https://github.com/go-jet/jet">https://github.com/go-jet/jet</a>. It really reminded me of a type-safe Java library jOOQ, which I really enjoyed many years ago!</p><p>It&#8217;s not an ORM but it relies on code generation, it will generate tables and model definitions in Go based on your database schema. After that, you can write type-safe queries and benefit from auto-completion and additional checks from the compiler. </p><pre><code>stmt := SELECT(
    Actor.ActorID, Actor.FirstName, Actor.LastName, Actor.LastUpdate,  
    Film.AllColumns,                                                  
    Language.AllColumns.Except(Language.LastUpdate),
    Category.AllColumns,
).FROM(
    Actor.
        INNER_JOIN(FilmActor, Actor.ActorID.EQ(FilmActor.ActorID)).  
        INNER_JOIN(Film, Film.FilmID.EQ(FilmActor.FilmID)).          
        INNER_JOIN(Language, Language.LanguageID.EQ(Film.LanguageID)).
        INNER_JOIN(FilmCategory, FilmCategory.FilmID.EQ(Film.FilmID)).
        INNER_JOIN(Category, Category.CategoryID.EQ(FilmCategory.CategoryID)),
).WHERE(
    Language.Name.EQ(String("English")).             
        AND(Category.Name.NOT_EQ(String("Action"))).  
        AND(Film.Length.GT(Int(180))),               
).ORDER_BY(
    Actor.ActorID.ASC(),
    Film.FilmID.ASC(),
)</code></pre><p>Looks nice!</p><p>I&#8217;ve used a database schema provided (<a href="https://github.com/go-jet/jet-test-data/blob/master/init/postgres/dvds.sql">https://github.com/go-jet/jet-test-data/blob/master/init/postgres/dvds.sql</a>) to create my local database. And generated Go code from it:</p><pre><code>jet -dsn=postgresql://user:pass@localhost:5432/jetdb?sslmode=disable -schema=dvds -path=./.gen</code></pre><p>Finally, it&#8217;s time to link our web application with the database, so a handler should make a call to the database and return data as JSON. </p><p>A question I usually have at this point is how to pass dependencies to handlers, it is just global state or any dependency injection patterns. I&#8217;ve picked in my example just a global var to store my database connection handler, but I&#8217;ve found this link really useful to get the feel of other approaches: <a href="https://www.alexedwards.net/blog/organising-database-access">https://www.alexedwards.net/blog/organising-database-access</a></p><pre><code>var DB *sql.DB

type FilmResponse struct {
&#9;*model.Film
}

func (f *FilmResponse) Render(w http.ResponseWriter, r *http.Request) error {
&#9;return nil
}

func GetFilm(w http.ResponseWriter, r *http.Request) {
&#9;stmt := SELECT(
&#9;&#9;Film.AllColumns,
&#9;).FROM(
&#9;&#9;Film,
&#9;).WHERE(
&#9;&#9;Film.Length.GT(Int(150)),
&#9;).ORDER_BY(
&#9;&#9;Film.Title.ASC(),
&#9;)

&#9;var films []struct {
&#9;&#9;model.Film
&#9;}

&#9;err := stmt.Query(DB, &amp;films)
&#9;if err != nil {
&#9;&#9;log.Fatal(err)
&#9;}

&#9;firstFilm := films[1]

&#9;render.Status(r, http.StatusOK)
&#9;render.Render(w, r, &amp;FilmResponse{&amp;firstFilm.Film})
}


func main() {

         // I'm using TestContainers to start PostgreSQL
         // see the code snippet at the end of the blog post

&#9;DB, err = sql.Open("postgres", connStr)

&#9;if err != nil {
&#9;&#9;log.Fatal(err)
&#9;}

&#9;r := chi.NewRouter()
&#9;r.Use(middleware.Logger)

&#9;r.Get("/film", GetFilm)

&#9;http.ListenAndServe(":8787", r)
}</code></pre><h3>Making HTTP requests</h3><p>The final bit for the first round of learning is to figure out how to make HTTP requests. Again, looks like we have a standard package, so it&#8217;s simple!</p><p>Also, I&#8217;ve decided to wrap that in a new handler:</p><pre><code>r.Get("/proxy", func(w http.ResponseWriter, r *http.Request) {
&#9;resp, err := http.Get("https://pokeapi.co/api/v2/pokemon/ditto")
&#9;if err != nil {
&#9;&#9;log.Fatal(err)
&#9;}

&#9;body, err := io.ReadAll(resp.Body)
&#9;if err != nil {
&#9;&#9;log.Fatal(err)
&#9;}

&#9;w.Write([]byte(string(body)))
})</code></pre><h3>Final thoughts </h3><p>I think an exercise like this will give you a good overview of the language and the ecosystem as well as hands-on experience. Of course, there is more to learn: concurrency, non-relational databases, working with AWS, etc. Also usually I prefer to read one or two books just to have more structured knowledge. </p><h3>Bonus</h3><p>I&#8217;m really enjoying working with TestContainers and Go is supported (<a href="https://golang.testcontainers.org/">https://golang.testcontainers.org/</a>). I wasn&#8217;t using TestContainers for tests here, but just to avoid writing Docker Compose files to start the local database, I&#8217;ve done it from code in the main function:</p><pre><code>ctx := context.Background()

dbName := "jetdb"
dbUser := "jet"
dbPassword := "jet"

postgresContainer, err := postgres.RunContainer(ctx, testcontainers.WithImage("docker.io/postgres:16-alpine"),
&#9;postgres.WithInitScripts("dvds.sql"),
&#9;postgres.WithDatabase(dbName),
&#9;postgres.WithUsername(dbUser),
&#9;postgres.WithPassword(dbPassword),
&#9;testcontainers.WithWaitStrategy(
&#9;&#9;wait.ForLog("database system is ready to accept connections").
&#9;&#9;WithOccurrence(2).
&#9;&#9;WithStartupTimeout(30*time.Second)),
&#9;)

if err != nil {
&#9;log.Fatalf("failed to start container: %s", err)
}

connStr, err := postgresContainer.ConnectionString(ctx, "sslmode=disable", "application_name=test")
fmt.Println("PostgreSQL connection string", connStr)

DB, err = sql.Open("postgres", connStr)</code></pre><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.andreyfadeev.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Hope you enjoyed it!</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Manage all your runtime versions with one tool (asdf, mise)]]></title><description><![CDATA[I&#8217;ve recently seen some posts on LinkedIn about managing multiple Java versions with things like sdkman or jenv. Don&#8217;t get me wrong, those tools are doing the job well and really useful. But I just thought about how many people are still using different version managers from different tools (sdkman, nvm, rbenv, that list can go on and on).]]></description><link>https://blog.andreyfadeev.com/p/manage-all-your-runtime-versions</link><guid isPermaLink="false">https://blog.andreyfadeev.com/p/manage-all-your-runtime-versions</guid><dc:creator><![CDATA[Andrey Fadeev]]></dc:creator><pubDate>Sun, 14 Jan 2024 17:52:19 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!d8PZ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff19e4486-3a9f-4ad9-9694-a51a7463f74a_1988x1626.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!d8PZ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff19e4486-3a9f-4ad9-9694-a51a7463f74a_1988x1626.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!d8PZ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff19e4486-3a9f-4ad9-9694-a51a7463f74a_1988x1626.png 424w, https://substackcdn.com/image/fetch/$s_!d8PZ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff19e4486-3a9f-4ad9-9694-a51a7463f74a_1988x1626.png 848w, https://substackcdn.com/image/fetch/$s_!d8PZ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff19e4486-3a9f-4ad9-9694-a51a7463f74a_1988x1626.png 1272w, https://substackcdn.com/image/fetch/$s_!d8PZ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff19e4486-3a9f-4ad9-9694-a51a7463f74a_1988x1626.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!d8PZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff19e4486-3a9f-4ad9-9694-a51a7463f74a_1988x1626.png" width="1456" height="1191" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f19e4486-3a9f-4ad9-9694-a51a7463f74a_1988x1626.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1191,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:605893,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!d8PZ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff19e4486-3a9f-4ad9-9694-a51a7463f74a_1988x1626.png 424w, https://substackcdn.com/image/fetch/$s_!d8PZ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff19e4486-3a9f-4ad9-9694-a51a7463f74a_1988x1626.png 848w, https://substackcdn.com/image/fetch/$s_!d8PZ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff19e4486-3a9f-4ad9-9694-a51a7463f74a_1988x1626.png 1272w, https://substackcdn.com/image/fetch/$s_!d8PZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff19e4486-3a9f-4ad9-9694-a51a7463f74a_1988x1626.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>I&#8217;ve recently seen some posts on LinkedIn about managing multiple Java versions with things like <strong>sdkman</strong> or <strong>jenv</strong>. Don&#8217;t get me wrong, those tools are doing the job well and really useful. But I just thought about how many people are still using different version managers from different tools (sdkman, nvm, rbenv, that list can go on and on).</p><h1>asdf</h1><p><a href="https://asdf-vm.com/">https://asdf-vm.com/</a></p><p>This is just a reminder that things like <strong>asdf</strong> exist and it&#8217;s doing what it&#8217;s promising: a single tool to manage all runtimes. It is also really extensible as you can easily add your tool as well by writing a simple plugin, nothing too scary, it&#8217;s just a bit of scripts that define how you can list versions of your tool and how you can download it to install. In reality, it&#8217;s a rare case when you need to write your own plugin due to a huge list of community plugins available. </p><p>I have a video about using <strong>asdf</strong> on the channel, so it&#8217;s a good starting point: </p><div id="youtube2-4-tYwl12uyE" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;4-tYwl12uyE&quot;,&quot;startTime&quot;:null,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/4-tYwl12uyE?rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div><h1>mise (rtx previously)</h1><p><a href="https://mise.jdx.dev/">https://mise.jdx.dev/</a></p><p>Also in the comments for that video, there was a suggestion to try a different tool called <strong>mise</strong> (known as <strong>rtx</strong> before).</p><p>It is the same idea (fully compatible with <strong>asdf</strong>) but written in Rust and it is trying to solve some known issues of <strong>asdf</strong> (performance, CLI UX, etc). More information can be found here: <a href="https://mise.jdx.dev/dev-tools/comparison-to-asdf.html">https://mise.jdx.dev/dev-tools/comparison-to-asdf.html</a></p><p>A short demo of using mise:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!n7c0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1d8d700-690f-40f6-8ce3-40ca0af484cb_1200x600.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!n7c0!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1d8d700-690f-40f6-8ce3-40ca0af484cb_1200x600.gif 424w, https://substackcdn.com/image/fetch/$s_!n7c0!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1d8d700-690f-40f6-8ce3-40ca0af484cb_1200x600.gif 848w, https://substackcdn.com/image/fetch/$s_!n7c0!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1d8d700-690f-40f6-8ce3-40ca0af484cb_1200x600.gif 1272w, https://substackcdn.com/image/fetch/$s_!n7c0!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1d8d700-690f-40f6-8ce3-40ca0af484cb_1200x600.gif 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!n7c0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1d8d700-690f-40f6-8ce3-40ca0af484cb_1200x600.gif" width="1200" height="600" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a1d8d700-690f-40f6-8ce3-40ca0af484cb_1200x600.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:600,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2484757,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!n7c0!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1d8d700-690f-40f6-8ce3-40ca0af484cb_1200x600.gif 424w, https://substackcdn.com/image/fetch/$s_!n7c0!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1d8d700-690f-40f6-8ce3-40ca0af484cb_1200x600.gif 848w, https://substackcdn.com/image/fetch/$s_!n7c0!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1d8d700-690f-40f6-8ce3-40ca0af484cb_1200x600.gif 1272w, https://substackcdn.com/image/fetch/$s_!n7c0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1d8d700-690f-40f6-8ce3-40ca0af484cb_1200x600.gif 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>If you were using <strong>rtx</strong> previously, check this link: <a href="https://mise.jdx.dev/rtx.html">https://mise.jdx.dev/rtx.html</a></p><p>The cool thing is that you can also manage your global versions of tools. I already see a great use case for the future. Next time when I have to set up a new laptop, I can just install <strong>mise</strong>, copy <code>~/.config/mise/config.toml</code> file and run <code>mise install</code>. I&#8217;ll instantly get the same working environment as before. If a project requires different versions, they will be defined in a local <code>.mise.toml</code>  file, and <strong>mise</strong> will automatically install the required versions when you go into those directories.</p><p>I highly recommend you at least try those tools as they are convenient in practice (not just on paper), <strong>mise</strong> has great docs and it&#8217;s a great starting point!</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.andreyfadeev.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.andreyfadeev.com/subscribe?"><span>Subscribe now</span></a></p><p></p>]]></content:encoded></item><item><title><![CDATA[Background image in IntelliJ IDEA New UI]]></title><description><![CDATA[I&#8217;ve just realized that the New UI in IntelliJ supports a background image - so I decided to give it a try.]]></description><link>https://blog.andreyfadeev.com/p/background-image-in-intellij-idea</link><guid isPermaLink="false">https://blog.andreyfadeev.com/p/background-image-in-intellij-idea</guid><dc:creator><![CDATA[Andrey Fadeev]]></dc:creator><pubDate>Sun, 07 Jan 2024 11:49:06 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!0QHb!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87baf319-2de4-484f-a636-fc45c8fe572b_3840x2160.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!0QHb!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87baf319-2de4-484f-a636-fc45c8fe572b_3840x2160.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!0QHb!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87baf319-2de4-484f-a636-fc45c8fe572b_3840x2160.png 424w, https://substackcdn.com/image/fetch/$s_!0QHb!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87baf319-2de4-484f-a636-fc45c8fe572b_3840x2160.png 848w, https://substackcdn.com/image/fetch/$s_!0QHb!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87baf319-2de4-484f-a636-fc45c8fe572b_3840x2160.png 1272w, https://substackcdn.com/image/fetch/$s_!0QHb!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87baf319-2de4-484f-a636-fc45c8fe572b_3840x2160.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!0QHb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87baf319-2de4-484f-a636-fc45c8fe572b_3840x2160.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/87baf319-2de4-484f-a636-fc45c8fe572b_3840x2160.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:3172327,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!0QHb!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87baf319-2de4-484f-a636-fc45c8fe572b_3840x2160.png 424w, https://substackcdn.com/image/fetch/$s_!0QHb!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87baf319-2de4-484f-a636-fc45c8fe572b_3840x2160.png 848w, https://substackcdn.com/image/fetch/$s_!0QHb!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87baf319-2de4-484f-a636-fc45c8fe572b_3840x2160.png 1272w, https://substackcdn.com/image/fetch/$s_!0QHb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87baf319-2de4-484f-a636-fc45c8fe572b_3840x2160.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Hi, a quick one! I&#8217;ve just realized that the New UI in IntelliJ supports a background image - so I decided to give it a try. </p><p>I&#8217;m pretty sure it wasn&#8217;t possible in IDEA for a while (probably before the New UI was released) - but I&#8217;m not sure now. </p><p>Anyway, it&#8217;s possible now - so you can upgrade the look of your editor!</p><p>I&#8217;ve recently been to the Seven Sisters and Eastbourne and took a picture there and it works really well as the background. </p><p>It works in all sidebars (project tree, REPL, terminal) and looks fantastic to my taste.</p><p>Here is what you need to do:</p><ul><li><p>Enable the New UI in IDEA: Settings &#8594; Appearance &amp; Behaviour &#8594; New UI </p></li><li><p>Set background image: Settings &#8594; Appearance &amp; Behaviour &#8594; Appearance &#8594; UI Options</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.andreyfadeev.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.andreyfadeev.com/subscribe?"><span>Subscribe now</span></a></p></li></ul><p>Have a nice day!</p><p></p>]]></content:encoded></item><item><title><![CDATA[Navigating the AWS Cloud: A Beginner's Guide to Getting Started]]></title><description><![CDATA[AWS for the first time could be really overwhelming. In this blog post, I&#8217;ll point your attention to the most commonly used AWS services and describe their place in the bigger picture.]]></description><link>https://blog.andreyfadeev.com/p/navigating-the-aws-cloud-a-beginners</link><guid isPermaLink="false">https://blog.andreyfadeev.com/p/navigating-the-aws-cloud-a-beginners</guid><dc:creator><![CDATA[Andrey Fadeev]]></dc:creator><pubDate>Sat, 06 Jan 2024 15:07:41 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!VKRA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F73512449-10a5-45f9-9cc1-543eea7a53a9_1920x1080.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!VKRA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F73512449-10a5-45f9-9cc1-543eea7a53a9_1920x1080.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!VKRA!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F73512449-10a5-45f9-9cc1-543eea7a53a9_1920x1080.png 424w, https://substackcdn.com/image/fetch/$s_!VKRA!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F73512449-10a5-45f9-9cc1-543eea7a53a9_1920x1080.png 848w, https://substackcdn.com/image/fetch/$s_!VKRA!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F73512449-10a5-45f9-9cc1-543eea7a53a9_1920x1080.png 1272w, https://substackcdn.com/image/fetch/$s_!VKRA!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F73512449-10a5-45f9-9cc1-543eea7a53a9_1920x1080.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!VKRA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F73512449-10a5-45f9-9cc1-543eea7a53a9_1920x1080.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/73512449-10a5-45f9-9cc1-543eea7a53a9_1920x1080.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1222004,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!VKRA!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F73512449-10a5-45f9-9cc1-543eea7a53a9_1920x1080.png 424w, https://substackcdn.com/image/fetch/$s_!VKRA!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F73512449-10a5-45f9-9cc1-543eea7a53a9_1920x1080.png 848w, https://substackcdn.com/image/fetch/$s_!VKRA!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F73512449-10a5-45f9-9cc1-543eea7a53a9_1920x1080.png 1272w, https://substackcdn.com/image/fetch/$s_!VKRA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F73512449-10a5-45f9-9cc1-543eea7a53a9_1920x1080.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Entering the AWS Console for the first time could be really overwhelming. Once you click on the search tab you will see a dropdown containing a huge list.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.andreyfadeev.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.andreyfadeev.com/subscribe?"><span>Subscribe now</span></a></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!RdIK!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7ca9873-7cd4-4960-b136-ddbdcb34aee7_1914x4526.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!RdIK!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7ca9873-7cd4-4960-b136-ddbdcb34aee7_1914x4526.png 424w, https://substackcdn.com/image/fetch/$s_!RdIK!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7ca9873-7cd4-4960-b136-ddbdcb34aee7_1914x4526.png 848w, https://substackcdn.com/image/fetch/$s_!RdIK!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7ca9873-7cd4-4960-b136-ddbdcb34aee7_1914x4526.png 1272w, https://substackcdn.com/image/fetch/$s_!RdIK!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7ca9873-7cd4-4960-b136-ddbdcb34aee7_1914x4526.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!RdIK!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7ca9873-7cd4-4960-b136-ddbdcb34aee7_1914x4526.png" width="1456" height="3443" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a7ca9873-7cd4-4960-b136-ddbdcb34aee7_1914x4526.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:3443,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1400828,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!RdIK!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7ca9873-7cd4-4960-b136-ddbdcb34aee7_1914x4526.png 424w, https://substackcdn.com/image/fetch/$s_!RdIK!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7ca9873-7cd4-4960-b136-ddbdcb34aee7_1914x4526.png 848w, https://substackcdn.com/image/fetch/$s_!RdIK!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7ca9873-7cd4-4960-b136-ddbdcb34aee7_1914x4526.png 1272w, https://substackcdn.com/image/fetch/$s_!RdIK!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa7ca9873-7cd4-4960-b136-ddbdcb34aee7_1914x4526.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>And how do you suppose to know that EC2 (standing for Amazon Elastic Compute Cloud) is your virtual machine? </p><p>So the purpose of this guide is to highlight the most commonly used AWS Cloud services so you are not lost when you start your AWS journey. </p><h1>VPC and networking </h1><blockquote><p>Amazon Virtual Private Cloud (Amazon VPC) is a service provided by Amazon Web Services (AWS) that allows users to create isolated virtual networks within the AWS cloud, enabling them to launch and manage resources with control over network configuration and security settings.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!iwM-!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30663110-b265-4065-8364-4dd08ef84c7e_521x311.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!iwM-!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30663110-b265-4065-8364-4dd08ef84c7e_521x311.png 424w, https://substackcdn.com/image/fetch/$s_!iwM-!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30663110-b265-4065-8364-4dd08ef84c7e_521x311.png 848w, https://substackcdn.com/image/fetch/$s_!iwM-!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30663110-b265-4065-8364-4dd08ef84c7e_521x311.png 1272w, https://substackcdn.com/image/fetch/$s_!iwM-!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30663110-b265-4065-8364-4dd08ef84c7e_521x311.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!iwM-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30663110-b265-4065-8364-4dd08ef84c7e_521x311.png" width="521" height="311" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/30663110-b265-4065-8364-4dd08ef84c7e_521x311.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;normal&quot;,&quot;height&quot;:311,&quot;width&quot;:521,&quot;resizeWidth&quot;:521,&quot;bytes&quot;:14043,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!iwM-!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30663110-b265-4065-8364-4dd08ef84c7e_521x311.png 424w, https://substackcdn.com/image/fetch/$s_!iwM-!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30663110-b265-4065-8364-4dd08ef84c7e_521x311.png 848w, https://substackcdn.com/image/fetch/$s_!iwM-!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30663110-b265-4065-8364-4dd08ef84c7e_521x311.png 1272w, https://substackcdn.com/image/fetch/$s_!iwM-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30663110-b265-4065-8364-4dd08ef84c7e_521x311.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>VPC is definitely worth mentioning, you&#8217;ll need to create one so you have a place where you can put other AWS resources (like EC2, databases, etc.) - but most likely if you are joining a company that already using AWS - it should have VPC and networking setup so you won&#8217;t need to configure it yourself and probably won&#8217;t need to modify it too much. </p><p>But once you are more familiar with AWS in general I highly recommend setting up your personal AWS account and going through the VPC setup yourself. It will be definitely useful long-term and will learn a lot of new things (for most people), like CIDR blocks, private and public subnets, how to use VPC flow logs, etc. </p><p>If you want to learn more - start here: <a href="https://docs.aws.amazon.com/vpc/latest/userguide/what-is-amazon-vpc.html">https://docs.aws.amazon.com/vpc/latest/userguide/what-is-amazon-vpc.html</a></p><h1>EC2 (Amazon Elastic Compute Cloud)</h1><p>Now let&#8217;s move to something more used in day-to-day work: EC2. So it&#8217;s just a fancy name for you well-known virtual machines, you can request an instance (in most cases some sort of Linux) with some amount of CPU and RAM then log into it (via SSH from the local machine or from AWS Console) and do whatever you need. Install tools, run your applications, etc. </p><h1><strong>AWS Containers services</strong></h1><p>In many recent years, I don&#8217;t remember seeing anything that wasn&#8217;t eventually deployed as a container, so this is probably the most important section that you need to learn about AWS. </p><p>Of course, before you deploy something as a container you need to build it and publish it. So once you&#8217;ve built your Docker image (how and where you are doing it is up to you, that could be locally in the simplest case or as part of your CI pipeline) - it&#8217;s time to publish your image. </p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!N1fW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b1bc6ee-79a1-45da-a0b2-edf08113efd7_2360x738.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!N1fW!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b1bc6ee-79a1-45da-a0b2-edf08113efd7_2360x738.png 424w, https://substackcdn.com/image/fetch/$s_!N1fW!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b1bc6ee-79a1-45da-a0b2-edf08113efd7_2360x738.png 848w, https://substackcdn.com/image/fetch/$s_!N1fW!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b1bc6ee-79a1-45da-a0b2-edf08113efd7_2360x738.png 1272w, https://substackcdn.com/image/fetch/$s_!N1fW!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b1bc6ee-79a1-45da-a0b2-edf08113efd7_2360x738.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!N1fW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b1bc6ee-79a1-45da-a0b2-edf08113efd7_2360x738.png" width="1456" height="455" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9b1bc6ee-79a1-45da-a0b2-edf08113efd7_2360x738.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:455,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:75123,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!N1fW!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b1bc6ee-79a1-45da-a0b2-edf08113efd7_2360x738.png 424w, https://substackcdn.com/image/fetch/$s_!N1fW!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b1bc6ee-79a1-45da-a0b2-edf08113efd7_2360x738.png 848w, https://substackcdn.com/image/fetch/$s_!N1fW!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b1bc6ee-79a1-45da-a0b2-edf08113efd7_2360x738.png 1272w, https://substackcdn.com/image/fetch/$s_!N1fW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b1bc6ee-79a1-45da-a0b2-edf08113efd7_2360x738.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><blockquote><p><strong>Amazon Elastic Container Registry (Amazon ECR)</strong> is a fully managed container registry offering high-performance hosting, so you can reliably deploy application images and artifacts anywhere.</p></blockquote><p>ECR is the service to store your Docker images (privately), so you can use them in other AWS services, like ECS or EKS. A simple way of thinking about ECR is as a Docker Hub that is private to your AWS account. </p><p>The next bit is more interesting - how and where do you actually run your containers? And there are a couple of options. </p><h2>Amazon Elastic Kubernetes Service (Amazon EKS)</h2><blockquote><p>Amazon Elastic Kubernetes Service (Amazon EKS) is a managed Kubernetes service to run Kubernetes in the AWS cloud and on-premises data centers. In the cloud, Amazon EKS automatically manages the availability and scalability of the Kubernetes control plane nodes responsible for scheduling containers, managing application availability, storing cluster data, and other key tasks.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!T3DI!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb60a3c8c-6b89-4bde-8f1d-2483ae0a2142_1678x724.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!T3DI!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb60a3c8c-6b89-4bde-8f1d-2483ae0a2142_1678x724.png 424w, https://substackcdn.com/image/fetch/$s_!T3DI!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb60a3c8c-6b89-4bde-8f1d-2483ae0a2142_1678x724.png 848w, https://substackcdn.com/image/fetch/$s_!T3DI!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb60a3c8c-6b89-4bde-8f1d-2483ae0a2142_1678x724.png 1272w, https://substackcdn.com/image/fetch/$s_!T3DI!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb60a3c8c-6b89-4bde-8f1d-2483ae0a2142_1678x724.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!T3DI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb60a3c8c-6b89-4bde-8f1d-2483ae0a2142_1678x724.png" width="1456" height="628" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b60a3c8c-6b89-4bde-8f1d-2483ae0a2142_1678x724.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:628,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:65261,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!T3DI!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb60a3c8c-6b89-4bde-8f1d-2483ae0a2142_1678x724.png 424w, https://substackcdn.com/image/fetch/$s_!T3DI!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb60a3c8c-6b89-4bde-8f1d-2483ae0a2142_1678x724.png 848w, https://substackcdn.com/image/fetch/$s_!T3DI!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb60a3c8c-6b89-4bde-8f1d-2483ae0a2142_1678x724.png 1272w, https://substackcdn.com/image/fetch/$s_!T3DI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb60a3c8c-6b89-4bde-8f1d-2483ae0a2142_1678x724.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>So one option is to use an AWS-managed Kubernetes cluster (EKS) - it is a really powerful option as you basically have all Kubernetes features available, but it&#8217;s still quite a lot to manage. Yes the cluster itself is managed by AWS, but everything on top (e.g. a tool to deploy your Helm charts, etc) is up to the operating team in your company. </p><p>Honestly, I prefer a simpler approach to running containers in AWS - and it is ECS. </p><h2>Amazon Elastic Container Service (ECS)</h2><blockquote><p>Amazon Elastic Container Service (ECS) is a fully managed container orchestration service that helps you to more efficiently deploy, manage, and scale containerized applications</p></blockquote><p>So a couple of things to understand about ECS, first of all, new a need to create an ECS cluster - this will be the place where you can put your services. </p><p>The next thing is to define the underlying infrastructure, and again same as with EKS you have two options: EC2 and Fargate. </p><h3>On EC2</h3><p>In the case of EC2, you are responsible for creating instances first (that could be just single EC2 instances or they could live inside an auto-scaling group) - then you can add those instances to the ECS cluster, so they will be available for ECS to place your ECS services and tasks. </p><h3>On Fargate</h3><blockquote><p>AWS Fargate is a serverless, pay-as-you-go compute engine that lets you focus on building applications without managing servers. Moving tasks such as server management, resource allocation, and scaling to AWS does not only improve your operational posture, but also accelerates the process of going from idea to production on the cloud, and lowers the total cost of ownership.&nbsp;</p></blockquote><p>In the case of Fargate, it&#8217;s even simpler - you don&#8217;t have to manage underlying EC2 at all.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!AqtD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f6fa3b2-400d-42bf-ba06-451729af9071_2334x948.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!AqtD!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f6fa3b2-400d-42bf-ba06-451729af9071_2334x948.png 424w, https://substackcdn.com/image/fetch/$s_!AqtD!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f6fa3b2-400d-42bf-ba06-451729af9071_2334x948.png 848w, https://substackcdn.com/image/fetch/$s_!AqtD!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f6fa3b2-400d-42bf-ba06-451729af9071_2334x948.png 1272w, https://substackcdn.com/image/fetch/$s_!AqtD!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f6fa3b2-400d-42bf-ba06-451729af9071_2334x948.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!AqtD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f6fa3b2-400d-42bf-ba06-451729af9071_2334x948.png" width="1456" height="591" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8f6fa3b2-400d-42bf-ba06-451729af9071_2334x948.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:591,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:105752,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!AqtD!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f6fa3b2-400d-42bf-ba06-451729af9071_2334x948.png 424w, https://substackcdn.com/image/fetch/$s_!AqtD!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f6fa3b2-400d-42bf-ba06-451729af9071_2334x948.png 848w, https://substackcdn.com/image/fetch/$s_!AqtD!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f6fa3b2-400d-42bf-ba06-451729af9071_2334x948.png 1272w, https://substackcdn.com/image/fetch/$s_!AqtD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f6fa3b2-400d-42bf-ba06-451729af9071_2334x948.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Regarding costs, on paper Fargate is a bit more expensive, in reality, to make EC2-based deployment cost-efficient you need to spend a lot of time optimizing and balancing your tasks and underlying EC2 infrastructure and it&#8217;s really hard to fully automate. So you will end up with not fully utilized EC2 instances at some point. If your company don&#8217;t have a dedicated infrastructure team - I would suggest just using Fargate and focusing on the applications, not the infrastructure. </p><h3>ECS services and tasks </h3><p>In ECS you define your deployments and ECS services and tasks. The service will contain one or multiple tasks and you can define things like deployment strategy, instance count, etc. ECS tasks on the other hand describe which containers to run as part of the task, the core here is container definitions - JSON data that defines your deployment. I really like the simplicity of the format and that you can just use Terraform without any additional tools to provision your ECS services. </p><p>A simple task definition will look something like this: </p><pre><code>{
  "family": "my-service",
  "containerDefinitions": [
    {
      "name": "web",
      "image": "my/image:latest",
      "cpu": 128,
      "memory": 512,
      "portMappings": [
        {
          "containerPort": 8080,
          "protocol": "tcp"
        }
      ]
      "essential": true
    }
  ]
}</code></pre><h1>Load balancers (ALB, NLB)</h1><p>If you decide to use ECS, most likely you will need a load balancer in front of your ECS service, so you will have a single DNS name for your service and the load will be distributed across your ECS service instances. The integration between ECS and ALB (Application load balancer) is really common and easy to configure. There are other use cases for load balancers as well (not related to ECS).</p><h1>S3 (Amazon Simple Storage Service)</h1><p>The next crucial and really popular AWS service is S3 (it&#8217;s a triple S in the service name, that&#8217;s why it&#8217;s called S3). But it&#8217;s also really easy to describe - you can store any files or objects you want. The granularity is the S3 bucket where you can put your objects in a nested way (different S3 paths).</p><h1>Databases</h1><p>It&#8217;s almost a 100% chance that you&#8217;ll use some kind of database in your system. Depending on your architecture it will be most likely relational (SQL) or NoSQL database.</p><h2><strong>Amazon Relational Database Service (</strong>RDS) </h2><p>If you want a PostgreSQL or MySQL you&#8217;ll use an RDS with the required engine type. The other thing to consider is <strong>Aurora</strong> which is the same RDS in a nutshell but with additional options like high availability and easier to scale. </p><h2>NoSQL</h2><p>A popular NoSQL database solution is <strong>DynamoDB</strong>. There is also a managed Cassandra (called <strong>Amazon Keyspaces</strong>).</p><h1>CloudWatch</h1><p><strong>CloudWatch</strong> is a service where you will be able to find your logs and metrics. Most AWS services have a way to configure CloudWatch integration to save logs and additional metrics. There are some metrics available by default - so you can just explore what&#8217;s available. There is also an option to configure custom metrics and set up alerts and dashboards so you have better visibility of your system. </p><h1>Messaging </h1><p>There are different options regarding messaging and queues so you can pick something depending on your needs. </p><h2><strong>Amazon Simple Queue Service (</strong>SQS)</h2><blockquote><p>Amazon SQS provides queues for high-throughput, system-to-system messaging. You can use queues to decouple heavyweight processes and to buffer and batch work. Amazon SQS stores messages until microservices and serverless applications process them.</p></blockquote><h2>Kinesis </h2><blockquote><p>Amazon Kinesis cost-effectively processes and analyzes streaming data at any scale as a fully managed service. With Kinesis, you can ingest real-time data, such as video, audio, application logs, website clickstreams, and IoT telemetry data, for machine learning (ML), analytics, and other applications.</p></blockquote><h2>Managed Kafka (MSK) </h2><blockquote><p>Securely stream data with a fully managed, highly available Apache Kafka service</p></blockquote><p>It&#8217;s out of the scope of the article to discuss differences and where to use what - but hopefully you won&#8217;t be surprised now when you hear SQS, Kinesis or MSK.</p><h1>Serverless (AWS Lambda)</h1><blockquote><p>AWS Lambda is a compute service that runs your code in response to events and automatically manages the compute resources, making it the fastest way to turn an idea into a modern, production, serverless applications.</p></blockquote><p>Some people use AWS Lambda quite heavily, and some do not. AWS Lambda is a service that will allow you to run your serverless functions (in supported languages) without provisioning the infrastructure (like EC2s).</p><h1>Analytics</h1><p>If you work with data and analytics, most likely you will end up working with <strong>Athena</strong> (a query engine, for example on top of data stored in S3), <strong>AWS Glue</strong> (a tool to manage schemas for your Athena tables)</p><h1>AWS Costs</h1><p>It&#8217;s also worth mentioning the AWS Costs Explorer - a place where you can check your costs (hopefully they are not skyrocketing). A useful feature there is that you can split it by service, resource or tag (it&#8217;s really important to tag our resources when you create them). </p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.andreyfadeev.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.andreyfadeev.com/subscribe?"><span>Subscribe now</span></a></p><p>I hope that was a useful overview of AWS Cloud. There are a lot of nuances in all of the services and it just takes time to become familiar - so don&#8217;t worry too much! </p>]]></content:encoded></item><item><title><![CDATA[Event sourcing with PostgreSQL and Clojure]]></title><description><![CDATA[In this post let&#8217;s take a look at how to implement event-sourcing in Clojure on top of PostgreSQL (links to the source code of a fully working example and the YouTube video are included).]]></description><link>https://blog.andreyfadeev.com/p/event-sourcing-with-postgresql-and</link><guid isPermaLink="false">https://blog.andreyfadeev.com/p/event-sourcing-with-postgresql-and</guid><dc:creator><![CDATA[Andrey Fadeev]]></dc:creator><pubDate>Sat, 23 Dec 2023 21:14:53 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!y46v!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5180ced-a984-424e-96d5-ee26b6569738_1280x720.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!y46v!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5180ced-a984-424e-96d5-ee26b6569738_1280x720.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!y46v!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5180ced-a984-424e-96d5-ee26b6569738_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!y46v!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5180ced-a984-424e-96d5-ee26b6569738_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!y46v!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5180ced-a984-424e-96d5-ee26b6569738_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!y46v!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5180ced-a984-424e-96d5-ee26b6569738_1280x720.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!y46v!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5180ced-a984-424e-96d5-ee26b6569738_1280x720.png" width="1280" height="720" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f5180ced-a984-424e-96d5-ee26b6569738_1280x720.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:720,&quot;width&quot;:1280,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:846948,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!y46v!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5180ced-a984-424e-96d5-ee26b6569738_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!y46v!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5180ced-a984-424e-96d5-ee26b6569738_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!y46v!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5180ced-a984-424e-96d5-ee26b6569738_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!y46v!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5180ced-a984-424e-96d5-ee26b6569738_1280x720.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Although things like <a href="https://www.datomic.com/">Datomic</a> and <a href="https://xtdb.com">XTDB</a> exist - sometimes you are just not ready to commit to a completely different storage type. So what if you want to keep using a well-known relational database (PostgreSQL in our case) but still try to implement an event-sourcing pattern? </p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.andreyfadeev.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.andreyfadeev.com/subscribe?"><span>Subscribe now</span></a></p><h2>What is the event-sourcing pattern?</h2><p>ChatGPT is quite good at giving short summaries and definitions, so here it is:</p><blockquote><p>Event sourcing is a software design pattern where the state of a system is determined by a sequence of immutable events, allowing for a complete audit trail and the ability to reconstruct the system's state at any point in time. It involves storing and replaying events to represent changes in the application state rather than maintaining the current state directly.</p></blockquote><p>Check out this article as well: <a href="https://learn.microsoft.com/en-us/azure/architecture/patterns/event-sourcing">https://learn.microsoft.com/en-us/azure/architecture/patterns/event-sourcing</a></p><h2>PostgreSQL and Clojure implementation</h2><p>The database table structure will be really simple, to store all the data we will need just a single table:</p><pre><code>create table events
(
    id             uuid primary key   default gen_random_uuid(),
    type           text      not null,
    aggregate_id   uuid      not null,
    aggregate_type text      not null,
    payload        jsonb     not null,
    created_at     timestamp not null default current_timestamp
);</code></pre><p>So `aggregate_type` is the type of the entity (or resource): `Card`, `Order`, `Customer` or whatever your application needs, and `aggregate_id` is the ID of the resource. </p><p>The `type` field is the type of individual event inside the resource, the naming could be different, but I prefer to include the resource name so it&#8217;s more readable: OrderCreated, OrderPaid, CardDispatched, CustomerBlocked - again, it could be any event that happens in your system. </p><p>The most interesting part is of course the `payload` field, it is the JSON payload and here we have the flexibility of NoSQL inside the relational database - so you just put any data related to the event in the payload. </p><h3>SQL queries</h3><p>Let&#8217;s take a look now at a couple of SQL queries that will be most commonly used:</p><p>Starting with the simplest one, getting a single event by ID:</p><pre><code>select * from events where id = :id</code></pre><p>Getting all event for the resource ID:</p><pre><code>select * from events 
where aggregate_id = :aggregate_id 
order by created_at</code></pre><p>In this case, it is usually useful to apply ordering by `created_at` so we can combine events in order later in the code (you can of course do the sorting in code as well) - we will see that in a bit!</p><p>Things get a bit tricky if you want to search for multiple resources where a condition based on the data of the payload is true, in this case, we need to find all aggregate ids first and then we need to find all other events for those aggregate ids. The most natural way of doing it is using an `IN` statement, like this:</p><pre><code>SELECT * FROM events
WHERE aggregate_id IN (
    SELECT aggregate_id from events
    WHERE payload -&gt;&gt; 'customer_id' = :customer_id
      AND aggregate_type = 'customer'
)</code></pre><p>However, we found out that using a `JOIN` works better in PostgreSQL for most cases, so the query becomes:</p><pre><code>SELECT DISTINCT e1.* FROM events e1
  INNER JOIN events e2 using (aggregate_id)
  WHERE e2.payload -&gt;&gt; 'customer_id' = :customer_id 
    AND aggregate_type = 'customer'</code></pre><p>That&#8217;s it for the DB side, let&#8217;s take a look at how to combine those events in code in Clojure! </p><h2>Show me some Clojure code</h2><p>The core of the Clojure logic will be the `apply-event` function, we are going to use Clojure multi-methods here:</p><pre><code>(defmulti apply-event
          (fn [_state event]
            (keyword
              (str (:aggregate-type event)
                   "/"
                   (:type event)))))</code></pre><p>After that we need to register this function for all our event types, for example:</p><pre><code>
(defmethod apply-event :order/order-created
  [_state event]
  (merge
    {:resource-type (:aggregate-type event)
     :order-id (:aggregate-id event)
     :created-at (:created-at event)}
    (:payload event)))

(defmethod apply-event :order/order-paid
  [state event]
  (merge state
         (:payload event)
         {:updated-at (:created-at event)}))

(defmethod apply-event :order/order-dispatched
  [state event]
  (merge state
         (:payload event)
         {:updated-at (:created-at event)}))</code></pre><p>Note that sometimes we just ignore the state, `_state`, as we know this event should always be the first one for a particular resource type, so here we just building the initial state. In all other event types, we are using the passed state and updating it. </p><p>Finally, we need to apply this `apply-event` function to the list of events, in this case, let&#8217;s introduce a new helper function:</p><pre><code>(defn project
  ([events]
   (project {} events))
  ([state events]
   (reduce apply-event state events)))</code></pre><p>And the final code after we get events from DB will look like this:</p><pre><code>(defn get-by-aggregate-id
  [{:keys [datasource]} aggregate-id]
  (let [select-query (-&gt; {:select :*
                          :from :events
                          :where [:= :aggregate-id aggregate-id]
                          :order-by [:created-at]}
                         (sql/format))
        events (jdbc/execute!
                 (datasource)
                 select-query
                 {:builder-fn rs/as-unqualified-kebab-maps})]
    (-&gt;&gt; events
         (map (fn [event]
                (update event :payload &lt;-jsonb)))
         (project))))</code></pre><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.andreyfadeev.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.andreyfadeev.com/subscribe?"><span>Subscribe now</span></a></p><h2>Final thoughts</h2><p>Of course, this pattern is not coming for free (compared to mutable state in the database), some queries are much more complex or even not possible, so it always depends on the use case.</p><p>Also, a common problem is that now we are calculating the state of the resource from events on the fly each time - which can lead to issues with the performance. A common solution to that is to build a separate view which is updated based on the sequence of events - so now in addition to the event log we will have our normal tables with the current state of the resource, but it should be used only for the read operations.</p><p><a href="https://github.com/andfadeev/real-world-clojure-api/commit/6ca1c361742567dd985bad24f3265d93969d01e4">The link to the source code for this example</a></p><p>For those who prefer the video format here is the link to my YouTube: </p><div id="youtube2-BR0EqKqDmuA" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;BR0EqKqDmuA&quot;,&quot;startTime&quot;:null,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/BR0EqKqDmuA?rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.andreyfadeev.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.andreyfadeev.com/subscribe?"><span>Subscribe now</span></a></p>]]></content:encoded></item><item><title><![CDATA[Production-Ready Clojure: show me the libraries!]]></title><description><![CDATA[In this blog post, I&#8217;ll try to make an overview of the most used libraries in real production-grade Clojure applications. I hope this guide helps you navigate the rich Clojure ecosystem.]]></description><link>https://blog.andreyfadeev.com/p/production-ready-clojure-show-me</link><guid isPermaLink="false">https://blog.andreyfadeev.com/p/production-ready-clojure-show-me</guid><dc:creator><![CDATA[Andrey Fadeev]]></dc:creator><pubDate>Wed, 20 Dec 2023 20:47:43 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!qVEW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb963aa62-ef50-4f7b-958d-8786f3b0be9a_1920x1080.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!qVEW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb963aa62-ef50-4f7b-958d-8786f3b0be9a_1920x1080.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!qVEW!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb963aa62-ef50-4f7b-958d-8786f3b0be9a_1920x1080.jpeg 424w, https://substackcdn.com/image/fetch/$s_!qVEW!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb963aa62-ef50-4f7b-958d-8786f3b0be9a_1920x1080.jpeg 848w, https://substackcdn.com/image/fetch/$s_!qVEW!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb963aa62-ef50-4f7b-958d-8786f3b0be9a_1920x1080.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!qVEW!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb963aa62-ef50-4f7b-958d-8786f3b0be9a_1920x1080.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!qVEW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb963aa62-ef50-4f7b-958d-8786f3b0be9a_1920x1080.jpeg" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b963aa62-ef50-4f7b-958d-8786f3b0be9a_1920x1080.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:862112,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!qVEW!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb963aa62-ef50-4f7b-958d-8786f3b0be9a_1920x1080.jpeg 424w, https://substackcdn.com/image/fetch/$s_!qVEW!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb963aa62-ef50-4f7b-958d-8786f3b0be9a_1920x1080.jpeg 848w, https://substackcdn.com/image/fetch/$s_!qVEW!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb963aa62-ef50-4f7b-958d-8786f3b0be9a_1920x1080.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!qVEW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb963aa62-ef50-4f7b-958d-8786f3b0be9a_1920x1080.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Hello, Clojure enthusiasts! </p><p>I'm Andrey, and in today's blog post, I want to share my thoughts on the libraries and tools we use in production when working with Clojure.</p><p>Whether you're a seasoned developer or just starting your journey with Clojure, I hope this information proves valuable.</p><h2>Libraries vs Frameworks</h2><p>Before delving into specifics, let's address a crucial point. While there are web frameworks in the Clojure ecosystem, I recommend, especially for beginners, to start by combining libraries instead. This approach provides a deeper understanding of how different pieces work together. Later, if you find frameworks appealing, feel free to explore them. However, in my experience with real production code, I've rarely (never) seen frameworks used.</p><p>As a framework - `biff` (<a href="https://biffweb.com">https://biffweb.com</a>) looks interesting, maybe for beginners, it will be a quick way to get up and running. </p><p>Anyway in this blog post, we will focus on libraries!</p><h2>Managing Dependencies</h2><p>Now, let's talk about managing dependencies. Traditionally, leiningen has been the go-to tool, but `deps.edn` is gaining traction. While Leningen still works fine, `deps.edn` is becoming a standard in new projects.</p><ul><li><p><a href="https://leiningen.org">https://leiningen.org</a></p></li><li><p><a href="https://clojure.org/guides/deps_and_cli">https://clojure.org/guides/deps_and_cli</a></p></li></ul><h2>Managing Components</h2><p>Handling components in a Clojure system is crucial. While you can roll your solution, using a library can add structure and make it easier to manage start and stop logic. Two solid options that I can recommend here are: </p><ul><li><p>`component` - <a href="https://github.com/stuartsierra/component">https://github.com/stuartsierra/component</a></p></li><li><p>`integrant` - <a href="https://github.com/weavejester/integrant">https://github.com/weavejester/integrant</a></p></li></ul><p>System creation with `component` will look like this:</p><pre><code>(defn example-system [config-options]
  (let [{:keys [host port]} config-options]
    (component/system-map
      :db (new-database host port)
      :scheduler (new-scheduler)
      :app (component/using
             (example-component config-options)
             {:database  :db
              :scheduler :scheduler}))))</code></pre><p>With `integrant` you&#8217;ll define your system as a plain Clojure map (or an EDN file) with custom tags:</p><pre><code>(def config
  {:adapter/jetty {:port 8080, :handler (ig/ref :handler/greet)}
   :handler/greet {:name "Alice"}})

(defmethod ig/init-key :adapter/jetty [_ {:keys [handler] :as opts}]
  (jetty/run-jetty handler (-&gt; opts (dissoc :handler) (assoc :join? false))))

(defmethod ig/init-key :handler/greet [_ {:keys [name]}]
  (fn [_] (resp/response (str "Hello " name))))</code></pre><p>Integrant could work quite well with the `aero` library, but again there are two possible approaches, so I recommend reading both links to learn more: </p><ul><li><p><a href="https://www.pixelated-noise.com/blog/2022/04/28/integrant-and-aero/index.html">https://www.pixelated-noise.com/blog/2022/04/28/integrant-and-aero/index.html</a></p></li><li><p><a href="https://robjohnson.dev/posts/aero-and-integrant/">https://robjohnson.dev/posts/aero-and-integrant/</a></p></li></ul><p>I'd advise against using `mount` (https://github.com/tolitius/mount) due to its potential to encourage bad coding practices (IMO) - sharing global state and making functions impure but using global components directly (not passed through input arguments). That could be overcome with some best practices (not allowing this pattern) - the issue that it is almost impossible to enforce.</p><h2>Managing configuration</h2><p>This is pretty simple for me, a good option is `aero` (<a href="https://github.com/juxt/aero">https://github.com/juxt/aero</a>), the configuration file is defined in EDN and it&#8217;s really easy to read:</p><pre><code>{:input-topic #env KAFKA_INPUT_TOPIC
 :output-topic #env KAFKA_OUTPUT_TOPIC
 :kafka {:bootstrap.servers #env KAFKA_BOOTSTRAP_SERVERS
                 :application.id #env KAFKA_APPLICATION_ID
                 :security.protocol "SSL"
                 :ssl.keystore.type "PKCS12"
                 :ssl.truststore.type "JKS"
                 :auto.offset.reset #env KAFKA_AUTO_OFFSET_RESET
                 :producer.compression.type #or [#env KAFKA_PRODUCER_COMPRESSION_TYPE "zstd"]
                 :processing.guarantee #or [#env KAFKA_PROCESSING_GUARANTEE "exactly_once_v2"]
                 :producer.acks "all"}
 :server {:host "0.0.0.0"
          :port 10124}
 :service-version #env SERVICE_VERSION}</code></pre><p>So on startup of your application, you just render it (so it will fill the placeholders with values from environment variables) and pass it to your application system. A smart idea would be to use one of the schema validation libraries and create a schema in your application configuration - so the application won&#8217;t start if it&#8217;s not valid (that will usually mean that you forgot to pass the required environment variable).</p><p>If you want to keep it simple use `(System/getenv "ENV_VAR")`.</p><h2>Schemas</h2><p>For schemas and validation, there are three solid options: </p><ul><li><p>`schema` - <a href="https://github.com/plumatic/schema">https://github.com/plumatic/schema</a></p></li><li><p>`clojure.spec` - <a href="https://github.com/clojure/spec.alpha">https://github.com/clojure/spec.alpha</a></p></li><li><p>`malli` - <a href="https://github.com/metosin/malli">https://github.com/metosin/malli</a></p></li></ul><p>I prefer to use `malli` most of the time, an example of schema will look like this:</p><pre><code>(def kafka-config-base-schema
  (m/schema
    [:map
     [:bootstrap.servers :string]
     [:application.id :string]
     [:auto.offset.reset [:enum "earliest" "latest" "none"]]
     [:producer.compression.type [:enum "none" "zstd"]]
     [:processing.guarantee [:enum "at_least_once" "exactly_once_v2"]]
     [:producer.acks [:enum "0" "1" "all"]]]))</code></pre><h2>HTTP Server</h2><p>For spinning up HTTP servers, we've primarily used Jetty as the base. On top of Jetty, you can use Ring or Pedestal - both are fine!</p><ul><li><p><a href="https://github.com/ring-clojure/ring">https://github.com/ring-clojure/ring</a></p></li><li><p><a href="http://pedestal.io/pedestal/0.7-pre/index.html">http://pedestal.io/pedestal/0.7-pre/index.html</a></p></li></ul><p>There was a period when `http-kit` was popular, I&#8217;m not a huge fan TBH, mainly because there is a lack of support if you will finally want some advanced monitoring for your application: APM and distributed tracing from the OpenTelementry and auto-instrumentation (Datadog, NewRelic, etc). </p><h2>HTTP Router</h2><p>In addition to the base HTTP server and HTTP abstraction, you&#8217;ll need a way to route your request to correct handlers, one of the oldest options is Compojure, it&#8217;s simple to learn and use but has its limitation being macros-based. People tend to use data-based approaches, so there are libraries on the plate like:</p><ul><li><p><a href="https://github.com/juxt/bidi">https://github.com/juxt/bidi</a></p></li><li><p><a href="https://github.com/metosin/reitit">https://github.com/metosin/reitit</a></p></li></ul><p>If you&#8217;ve decided to use Pedestal not Ring it comes with its own data-based way to define routes - and it&#8217;s good enough for most use cases (also Reitit can work on top of Pedestal as well).</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.andreyfadeev.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>HTTP Requests</h2><p>When it comes to handling HTTP requests, `clj-http` is a reliable choice, utilizing the Apache HTTP library. </p><p>If you're on Java 11 or higher, the built-in Java HTTP client is also worth exploring, for example, the <a href="https://github.com/babashka/http-client">https://github.com/babashka/http-client</a></p><h2>Working with JSON</h2><p>Using JSON is popular nowadays (I don&#8217;t even remember working with anything else), a couple of libraries to work with JSON in Clojure:</p><ul><li><p><a href="https://github.com/dakrone/cheshire">https://github.com/dakrone/cheshire</a></p></li><li><p><a href="https://github.com/metosin/jsonista">https://github.com/metosin/jsonista</a></p></li><li><p><a href="https://github.com/clojure/data.json">https://github.com/clojure/data.json</a></p></li><li><p><a href="https://github.com/cnuernber/charred">https://github.com/cnuernber/charred</a></p></li></ul><p>Check this link - it&#8217;s a good overview of the JSON libraries available: <a href="https://www.juxt.pro/blog/json-in-clojure/">https://www.juxt.pro/blog/json-in-clojure/</a></p><h2>Working with Relational Databases</h2><p>For working with relational databases, `next.jdbc` (<a href="https://github.com/seancorfield/next-jdbc">https://github.com/seancorfield/next-jdbc</a>) is the current standard, and HikariCP works well for connection pools. </p><p>Regarding SQL query libraries, consider either HoneySQL or HugSQL, both of which we've used in production.</p><h2>Other Libraries</h2><ul><li><p>`core.cache` or `core.memoize` if you&#8217;d like a bit more advanced caching library</p></li><li><p>`core.async` if you are big on the idea of building non-blocking code (be careful it&#8217;s really easy to overengineer here)</p></li><li><p>`claypoole` - a great library to work with Java thread pools in Clojure: <a href="https://github.com/clj-commons/claypoole">https://github.com/clj-commons/claypoole</a></p></li></ul><h2>Other tools</h2><ul><li><p>`babashka` (https://babashka.org) - great tool to write scripts in Clojure, could sound crazy at first, but give it a try if you haven&#8217;t before! We have all our scripting for CI written in babashka now!</p></li><li><p>`cljfmt` - my go-to code formatter </p></li><li><p>`clj-kondo` - great static linter for Clojure code</p></li></ul><p>We use both `cljfmt` and `clj-kondo` as pipeline steps in the CI to keep the Clojure code tidy!</p><h2>Tests </h2><p>Although Clojure developers rely on REPL a lot to debug and write code, it&#8217;s still crucial to eventually produce tests for your code. There are different levels of testing available, the most common groups I&#8217;ve seen so far are: </p><ul><li><p>`unit` - simple tests that do not require any complex setup or starting your system (or parts of the system), call your functions or chains of functions and assert the result</p></li><li><p>`persistence` - it is slightly more high-level, in those tests we are talking to the relational database (or other type of persistent storage) and checking for side-effects (e.g. a database row was created as the result of the test), but we still don&#8217;t usually start our application system</p></li><li><p>`component` - it is testing the full flow that requires spinning your application system, here are a couple of examples: call the HTTP endpoint of your test system and expect correct side-effects (change in the database, event published to the queue, a call to the third party system was made, etc)</p></li></ul><p>Regarding writing tests, I just suggest you stick with the simplest option: `clojure.test` that comes out-of-the-box with Clojure. </p><p>The slightly more interesting topic is which test runner to use. Here we have a bunch of options: </p><ul><li><p>`lein test` - in case you are using Leininen, that comes out-of-the-box</p></li><li><p>`eftest` - has more configuration options and better reporting, and could be used for both `leiningen` and `deps.edn`: <a href="https://github.com/weavejester/eftest">https://github.com/weavejester/eftest</a></p></li><li><p>`cognitect-labs/test-runner` - a simple (but good enough) option if you are using `deps.edn` and missing a built-in option to run tests: <a href="https://github.com/cognitect-labs/test-runner">https://github.com/cognitect-labs/test-runner</a></p></li><li><p>`kaocha` - a test runner with a lot of features and great docs, for both Leiningen and `deps.edn`: <a href="https://github.com/lambdaisland/kaocha">https://github.com/lambdaisland/kaocha</a></p></li></ul><p>Although there are a bunch of options, I don&#8217;t suggest you spend too much time picking one, test runner is doing a simple job: search and run your tests - so you will be fine in any case. Better focus on writing more robust and reliable tests so you worry less after refactoring your code!</p><h2>Conclusion</h2><p>And there you have it &#8211; a comprehensive overview of the libraries and tools we use in production Clojure code (or at least I&#8217;ve tried). I hope this guide helps you navigate the rich Clojure ecosystem. </p><p>If you prefer the video content, you can find almost the same as the video on my channel: </p><div id="youtube2-bME124Ky8M0" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;bME124Ky8M0&quot;,&quot;startTime&quot;:null,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/bME124Ky8M0?rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.andreyfadeev.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p><p></p>]]></content:encoded></item></channel></rss>