<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Posts on blog.iankulin.com</title><link>https://blog.iankulin.com/tags/posts/</link><description>Recent content in Posts on blog.iankulin.com</description><generator>Hugo</generator><language>en-AU</language><lastBuildDate>Sat, 10 Jan 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.iankulin.com/tags/posts/index.xml" rel="self" type="application/rss+xml"/><item><title>VS Code Dev Containers</title><link>https://blog.iankulin.com/vs-code-dev-containers/</link><pubDate>Sat, 10 Jan 2026 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/vs-code-dev-containers/</guid><description>&lt;h3 id="remote-ssh"&gt;Remote-SSH&lt;/h3&gt;
&lt;p&gt;One of the things I&amp;rsquo;ve done a bit in Visual Studio Code is using it&amp;rsquo;s ability to work on a different machine over SSH. I have a couple of LXCs on a server set up for different languages - one for C++ and another for Rust. They are things I don&amp;rsquo;t work in often, and I didn&amp;rsquo;t want to set them up on my laptop, but thought I might want them again sometime in the future.&lt;/p&gt;
&lt;p&gt;This is straightforward in VS Code - You install the &lt;a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-ssh"&gt;remote ssh extension&lt;/a&gt; locally, then choose &lt;code&gt;Remote-SSH: Connect to Host&lt;/code&gt; in the command palette. A few seconds later, it appears that you&amp;rsquo;re working locally, but actually you are working in a remote session on the server your VS Code instance is SSH&amp;rsquo;d into (a small indicator in the bottom left is the tell). You need to clone your project from git since you&amp;rsquo;re working in the server&amp;rsquo;s file system, and there&amp;rsquo;s some complexity with extensions since you&amp;rsquo;re sort of working in a new VS Code instance, but apart from that it feels the same as working locally.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/architecture-ssh.png" width="968" alt="Remote SSH VS Code architecture from https://code.visualstudio.com/docs/remote/ssh"&gt;
&lt;p&gt;The official docs for &lt;a href="https://code.visualstudio.com/docs/remote/ssh"&gt;Remote Development using SSH are here.&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="why-now"&gt;Why now?&lt;/h3&gt;
&lt;p&gt;The reason I&amp;rsquo;ve been interested in this again lately is to provide a more secure environment for using AI agentic coding tools like Claude Code. If it&amp;rsquo;s running in it&amp;rsquo;s own server (that I can easily recreate from a snapshot) I don&amp;rsquo;t have to worry about dramas like having &lt;a href="https://www.theregister.com/2025/12/01/google_antigravity_wipes_d_drive/"&gt;my hard drive deleted&lt;/a&gt; by Gemini.&lt;/p&gt;
&lt;p&gt;But what if you don&amp;rsquo;t have the ability to spin up an environment on your homelab? There&amp;rsquo;s a few options, but probably the easiest for VS Code users is to work in a &amp;lsquo;Dev Container&amp;rsquo;.&lt;/p&gt;
&lt;h3 id="dev-containers"&gt;Dev Containers&lt;/h3&gt;
&lt;p&gt;Working in a Dev Container is basically the same as remoting into another server with VS Code, but in this case the other server is a container spun up in Docker.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/architecture-containers.png" width="968" alt="VS Code Dev Container architecture. From https://code.visualstudio.com/docs/devcontainers/containers"&gt;
&lt;p&gt;There&amp;rsquo;s a couple of important differences from working on a remote server:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;we&amp;rsquo;re working on our local files;&lt;/li&gt;
&lt;li&gt;Dev containers are an important system for sharing repeatable development environments, so it&amp;rsquo;s well integrated into VS Code - the tooling is nice.&lt;/li&gt;
&lt;li&gt;You need Docker/Docker desktop running locally.&lt;/li&gt;
&lt;li&gt;You need a container image to work with, and a &lt;code&gt;devcontainer.json&lt;/code&gt; file to tell VS Code how to manage all this.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The rest of this post will focus on the basics of working in a Dev Container. There is also a good explanation of all this in the &lt;a href="https://code.visualstudio.com/docs/devcontainers/tutorial"&gt;VS Code docs&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="the-container"&gt;The Container&lt;/h2&gt;
&lt;p&gt;After you&amp;rsquo;ve installed the VS Code &lt;a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers"&gt;extension&lt;/a&gt;, and set up your local Docker environment, you&amp;rsquo;re going to need a Docker container to work in. There&amp;rsquo;s a &lt;a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers"&gt;big list of existing containers&lt;/a&gt; that cover most scenarios, but I prefer to roll my own. This is achieved by writing a Dockerfile.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;FROM node:24-bookworm
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;# Use non-root user for development
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;USER node
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;# Development environment
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ENV NODE_ENV=development
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;# Default command
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;CMD [&amp;#34;npm&amp;#34;, &amp;#34;start&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I&amp;rsquo;m going to name this &lt;code&gt;Dockerfile.dev&lt;/code&gt; and put it in the &lt;code&gt;.devcontainer&lt;/code&gt; directory - later we&amp;rsquo;ll point to it from our &lt;code&gt;devcontainer.json&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2026-01-09-at-16.57.06.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The Dockerfile describes the system that we&amp;rsquo;ll be working in when we&amp;rsquo;re working on our project - it&amp;rsquo;s like the specification of our remote &amp;lsquo;server&amp;rsquo;. Sometimes you might want to install other stuff here - for example if you are a vim enthusiast, you might &lt;code&gt;apt install vim&lt;/code&gt; in the Dockerfile, but generally you should keep it reasonably generic.&lt;/p&gt;
&lt;h3 id="devcontainerjson"&gt;devcontainer.json&lt;/h3&gt;
&lt;p&gt;Next we need to tell VS Code how to work in the container we&amp;rsquo;ve specified.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;name&amp;#34;: &amp;#34;Node.js Dev Container&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;build&amp;#34;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;dockerfile&amp;#34;: &amp;#34;Dockerfile.dev&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;forwardPorts&amp;#34;: [
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 3000
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;postCreateCommand&amp;#34;: &amp;#34;npm install&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This goes in the .devcontainer directory. It&amp;rsquo;s almost all self explanatory - we tell the extension what container we&amp;rsquo;re using - in this case building it from &lt;code&gt;Dockerfile.dev&lt;/code&gt;, export the port we&amp;rsquo;re running on, and run a command in the terminal of our new system once it exists.&lt;/p&gt;
&lt;h3 id="whats-missing"&gt;What&amp;rsquo;s missing?&lt;/h3&gt;
&lt;p&gt;Alert readers will have noticed there&amp;rsquo;s a couple of things that we need which are missing.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;VS Code Server - the way that VS Code is working in this configuration is that it&amp;rsquo;s running on our local machine, but connecting to a &amp;ldquo;VS Code Server&amp;rdquo; inside the container.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Bind mounts to our local directory - Once we load up the dev container, all our local files will need to be available in VS Code somehow.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So how to these get in the container? We don&amp;rsquo;t have to do anything to deal with these two issues. This is part of the magic that the VS Code Dev Container extension is doing for us. After the container is created, the extension:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;installs the server binary, and starts it&lt;/li&gt;
&lt;li&gt;bind mounts the local workspace to &lt;code&gt;/workspaces/&amp;lt;your-folder-name&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;And sets that as the WORKDIR in the container&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="lets-go"&gt;Let&amp;rsquo;s go&lt;/h3&gt;
&lt;p&gt;Okay, with the &lt;code&gt;Dockerfile.dev&lt;/code&gt; and &lt;code&gt;devcontainer.json&lt;/code&gt; in place, we&amp;rsquo;re now ready to launch our devcontainer.&lt;/p&gt;
&lt;p&gt;In the VSCode command pallet, run &lt;code&gt;Dev Containers: Reopen in container&lt;/code&gt;. The first run will take a few moments since the container has to be built first, but then it should look something like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2026-01-09-at-17.49.40.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The big clue that you&amp;rsquo;re in the dev container now is the blue message in the bottom left corner. Most everything that we could do in the local environment we can do here in the container - for example editing code and running it.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2026-01-09-at-17.54.43.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2026-01-09-at-17.54.53.png" alt=""&gt;&lt;/p&gt;
&lt;h3 id="extensions"&gt;Extensions&lt;/h3&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2026-01-09-at-18.05.45.png" width="427" alt=""&gt;
&lt;p&gt;If you have a look at your VS Code extensions while working in this dev container, you&amp;rsquo;ll see that some are still installed, but others are now greyed out. Some extensions only effect the UI (&amp;ldquo;UI Extensions&amp;rdquo; - listed under &amp;ldquo;Local - Installed&amp;rdquo;) so they live in the local VS Code, others have functionality that needs to run in the workspace environment so they (&amp;ldquo;Workspace extensions&amp;rdquo;) need to live in the VS Code server part.&lt;/p&gt;
&lt;p&gt;Any UI Extensions you had installed before will still be there since they live in the local VS Code, but they others will be greyed out and unavailable unless you install them into the dev container.&lt;/p&gt;
&lt;p&gt;In the image here you can see my &amp;ldquo;vscode-icons&amp;rdquo; and &amp;ldquo;vscode-pdf&amp;rdquo; which only effect how things are displayed are both still available, but &amp;ldquo;Beancount&amp;rdquo; &amp;amp; &amp;ldquo;Cline&amp;rdquo; that need access to files need to be installed in the container if I want to use them on this project.&lt;/p&gt;
&lt;p&gt;We can click on the &amp;ldquo;Install in Dev Container&amp;rdquo; button for any of these, and it will be installed into the dev container. I need ESLint for this project so I&amp;rsquo;ll could that - then it will appear in the &amp;lsquo;Dev Container&amp;rsquo; tab. This is a good way of mimicking the normal workflow for users, but this extension is only persisted in the container while it exists - If we make any changes to the &lt;code&gt;dockerfile&lt;/code&gt; or &lt;code&gt;devcontainer.json&lt;/code&gt; VS Code will rebuild the container and the extension will be gone again.&lt;/p&gt;
&lt;p&gt;If we want a workspace extension, a more persistent way to install it is to add it to our &lt;code&gt;devcontainer.json&lt;/code&gt;&lt;/p&gt;
&lt;h3 id="adding-extensions-to-devcontainerjson"&gt;Adding extensions to devcontainer.json&lt;/h3&gt;
&lt;p&gt;Extensions added this way will be the same for anyone who uses our dev container definition (which is what I&amp;rsquo;m calling the combined &lt;code&gt;dockerfile&lt;/code&gt; and &lt;code&gt;devcontainer.json&lt;/code&gt;), including if they just clone the project from git. In fact, this was the original intention of dev containers - to have a fully reproducible development environment that is stored with its project.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s add a couple of extensions.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;name&amp;#34;: &amp;#34;Node.js Dev Container&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;build&amp;#34;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;dockerfile&amp;#34;: &amp;#34;Dockerfile.dev&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;customizations&amp;#34;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;vscode&amp;#34;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;extensions&amp;#34;: [
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;dbaeumer.vscode-eslint&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;esbenp.prettier-vscode&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;forwardPorts&amp;#34;: [
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 3000
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;postCreateCommand&amp;#34;: &amp;#34;npm install&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you make these changes in VS Code, it will probably pop up and ask you if it can rebuild the container, otherwise open the command palette and select &amp;ldquo;Dev Containers: Rebuild Container&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;You might be wondering where we get the extension id&amp;rsquo;s from (like &lt;code&gt;dbaeumer.vscode-eslint&lt;/code&gt;) The easiest way to to right click on one in the extension list and select &amp;ldquo;Copy Extension ID&amp;rdquo;. It&amp;rsquo;s also listed on the extension web page.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2026-01-10-at-15.23.47.jpg" alt=""&gt;&lt;/p&gt;
&lt;h3 id="finally"&gt;Finally&lt;/h3&gt;
&lt;p&gt;So that&amp;rsquo;s the basics of getting a simple dev container set up. The code for this is on &lt;a href="https://github.com/IanKulin/devcont-demo"&gt;GitHub here&lt;/a&gt;. There&amp;rsquo;s a crucial issue we haven&amp;rsquo;t solved though - how to pull in your ssh keys for pushing the code to external repositories. We&amp;rsquo;ll deal with that in a future post.&lt;/p&gt;</description></item><item><title>Getting Ghostty to Work on Synology</title><link>https://blog.iankulin.com/getting-ghostty-to-work-on-synology/</link><pubDate>Mon, 28 Jul 2025 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/getting-ghostty-to-work-on-synology/</guid><description>&lt;p&gt;Ghostty is a terminal application that I don&amp;rsquo;t really &lt;em&gt;need&lt;/em&gt; (it&amp;rsquo;s &lt;a href="https://ghostty.org/docs/about"&gt;listed features&lt;/a&gt; either already exist in the MacOS terminal, or seem so esoteric or marginal that I can&amp;rsquo;t imagine any real benefit from them in my normal use), but I &lt;em&gt;wanted&lt;/em&gt; to be one of the cool kids, so I thought I&amp;rsquo;d give it a try.&lt;/p&gt;
&lt;p&gt;After fiddling around with the themes for a bit I renamed it to &amp;rsquo;term-ghosty.app&amp;rsquo; so I&amp;rsquo;d remember to use it (ie when I pop up spotlight and type &amp;rsquo;term&amp;rsquo; it will come up) and got on with my day. Ten minutes later I&amp;rsquo;d run into a problem.&lt;/p&gt;
&lt;h3 id="the-problem"&gt;The Problem&lt;/h3&gt;
&lt;p&gt;I was ssh&amp;rsquo;d into a Synology NAS, and needed to use a command from two commands ago, in my long experience, pressing the up arrow twice works universally well, but not in Ghostty on this host:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2025-07-26-at-11.31.38.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;This is a purely visual glitch - if I press return at this stage, it will run &lt;code&gt;command one&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;My first thought was to CTRL-U to clear the line:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2025-07-26-at-11.32.00.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I guess not. Oh well, lets &lt;code&gt;clear&lt;/code&gt; and try all this again.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2025-07-26-at-11.32.07.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;So - a clue. This is a Ghostty problem, not a weird shell issue. Also, this wasn&amp;rsquo;t just a visual thing - the command was also not working.&lt;/p&gt;
&lt;p&gt;I logged in with regular terminal to confirm that everything was still working with that.&lt;/p&gt;
&lt;h3 id="xterm-ghostty"&gt;xterm-ghostty&lt;/h3&gt;
&lt;p&gt;If you google &amp;lsquo;ghostty arrow history problem&amp;rsquo; you&amp;rsquo;ll likely find a number of github issues that are all closed after the developer has posted a link to &lt;a href="https://ghostty.org/docs/help/terminfo#ssh"&gt;this part of the docs&lt;/a&gt; explaining that you need to compile the Ghostty&amp;rsquo;s terminfo into the config on this host.&lt;/p&gt;
&lt;p&gt;At first I was a bit aghast at this complicated solution - but we need to keep in mind this is a terminal program that I&amp;rsquo;m sure is only used by tech orientated people who love fiddling with things. This is reflected in other design choices in Ghostty (going into &amp;lsquo;Settings&amp;rsquo; from the menu just opens the config file in a text editor).&lt;/p&gt;
&lt;p&gt;I downloaded &lt;a href="https://iterm2.com/index.html"&gt;iTerm2&lt;/a&gt; as a likely competitor to Ghostty and tried it on the same host - everything worked perfectly. I tried Ghostty on several of my VM&amp;rsquo;s, VPS&amp;rsquo;s and LXC&amp;rsquo;s. All no problem. So what&amp;rsquo;s going on?&lt;/p&gt;
&lt;h3 id="whats-going-on"&gt;What&amp;rsquo;s Going On?&lt;/h3&gt;
&lt;p&gt;If you open your terminal, and type &lt;code&gt;echo $TERM&lt;/code&gt; it will tell you the &lt;code&gt;TERM&lt;/code&gt; value. This is used by the shell to know how to interpret the inputs it&amp;rsquo;s receiving (for example, what to do when the user wants to &lt;code&gt;clear&lt;/code&gt; the screen). Unless you are using Ghostty, it will almost certainly be set to &lt;code&gt;xterm-256color&lt;/code&gt;&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2025-07-26-at-12.03.36.png" width="808" alt=""&gt;
&lt;p&gt;In Ghostty, it will say &lt;code&gt;xterm-ghostty&lt;/code&gt;&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2025-07-26-at-12.04.55.png" width="772" alt=""&gt;
&lt;p&gt;The reason I don&amp;rsquo;t have this same problem with Ghostty on my MacBook or the Debian base hosts is that those operating systems have the Ghostty &amp;rsquo;terminfo&amp;rsquo; entries in them, whereas apparently Synology does not (or not yet anyway).&lt;/p&gt;
&lt;p&gt;But that does that explain why iTerm2 works on Synology. Let&amp;rsquo;s look at it&amp;rsquo;s TERM value.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2025-07-26-at-12.12.45.png" width="882" alt=""&gt;
&lt;p&gt;Ah, so it&amp;rsquo;s just claiming to be xterm-256color the same as the mac terminal emulator - which all hosts will know since it&amp;rsquo;s an ancient thing. (xterm is the terminal from the X-Windows systems of the 1980&amp;rsquo;s).&lt;/p&gt;
&lt;p&gt;This is a developer choice, Ghostty could also claim it was an xterm-256color and this problem would not have popped up. I&amp;rsquo;m assuming they have decided this short term pain is worth it for some long term gain (of being able to do things an xter-256color terminal emulator can not).&lt;/p&gt;
&lt;h3 id="choices"&gt;Choices&lt;/h3&gt;
&lt;p&gt;Now we have two choices to fix this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Override the TERM choice the Ghostty developer has made for this host&lt;/li&gt;
&lt;li&gt;Compile the correct &lt;code&gt;terminfo&lt;/code&gt; into the config on this host&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Option one means we&amp;rsquo;d lose any special sauce Ghostty does (which I already said I don&amp;rsquo;t need, but could conceivably regret in the future if they do something cool).&lt;/p&gt;
&lt;p&gt;Option two feels like the proper (and is the one that Ghostty recommends).&lt;/p&gt;
&lt;p&gt;I suppose there is a third choice - wait until Synology includes the Ghostty terminfo in their distro. I get the vibe from the slightly scary MOTD when I ssh in that they would really prefer you did not, so I can&amp;rsquo;t seem them going out of their way to include it. Also they are not based on another distro as far as I can see, so they are not going to accidentally include it from an upstream. I feel this choice will never bear fruit.&lt;/p&gt;
&lt;h3 id="overriding-the-term"&gt;Overriding the TERM&lt;/h3&gt;
&lt;p&gt;ssh can have a config set for a host so we can override the TERM value. If you have something like this in &lt;code&gt;~/.ssh/config&lt;/code&gt; we can fix the issue.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Host NAS-DS2
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; SetEnv TERM=xterm-256color
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2025-07-26-at-12.46.46.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;And everything works again - I can up arrow to access the history without problems and the &lt;code&gt;clear&lt;/code&gt; command works as advertised. Note that I didn&amp;rsquo;t need to reload the ssh config - it gets read every time you run the ssh command.&lt;/p&gt;
&lt;p&gt;An extra reason for using this approach is that if you have a good naming convention for your hosts (I worked in the &amp;lsquo;data processing&amp;rsquo; department of a bank in the 1990&amp;rsquo;s so I learned good naming conventions for hosts) then you can wild-card this entry to for all of them. You might have guessed that all the Synology NAS&amp;rsquo;s I manage are named &lt;code&gt;NAS-DS&amp;lt;some positive integer&amp;gt;&lt;/code&gt;. Let&amp;rsquo;s change the config to say:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Host NAS-DS*
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; SetEnv TERM=xterm-256color
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2025-07-26-at-12.51.17.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Nice. If I had hundreds of these to deal with, that&amp;rsquo;s definitely the solution I&amp;rsquo;d be going for. Also, if you&amp;rsquo;ve come here because you had this exact problem stop here. This is the best you are going to do.&lt;/p&gt;
&lt;h3 id="installing-the-terminfo"&gt;Installing the terminfo&lt;/h3&gt;
&lt;p&gt;The alternate approach is to extract the xterm-ghostty &lt;code&gt;terminfo&lt;/code&gt; off the current machine (in my case a MacBook) to the other host, and compile it into the available &lt;code&gt;terminfo&lt;/code&gt;&amp;rsquo;s there. This seems more invasive, but it&amp;rsquo;s a per user thing and can be reversed.&lt;/p&gt;
&lt;p&gt;The command given in the &lt;a href="https://ghostty.org/docs/help/terminfo#ssh"&gt;Ghostty docs&lt;/a&gt; is:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;infocmp -x xterm-ghostty | ssh YOUR-SERVER -- tic -x -
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Let&amp;rsquo;s try it:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2025-07-26-at-13.02.16.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;This is not that surprising - Synology DSM is minimal (I think it uses busybox) so it&amp;rsquo;s missing lots of these commands. You might think that&amp;rsquo;s okay, I&amp;rsquo;ll compile it on this machine then &lt;code&gt;scp&lt;/code&gt; it across. That would be something like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;infocmp -x xterm-ghostty &amp;gt; xterm-ghostty.src
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;tic -x -o ./terminfo xterm-ghostty.src
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;scp -r ./terminfo/78 ds2_admin@NAS-DS2:~/.terminfo/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;But that won&amp;rsquo;t work because you don&amp;rsquo;t have &lt;code&gt;scp&lt;/code&gt; on the Synology either. So perhaps you think you&amp;rsquo;ll enable rysnc in the NAS GUI and rsync the file in:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rsync -av ./terminfo/78 ds2_admin@NAS-DS2:~/.terminfo/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Which will copy the file over, but it still won&amp;rsquo;t work. It&amp;rsquo;s just too minimal.&lt;/p&gt;
&lt;p&gt;I imagine this process works for other distros or it wouldn&amp;rsquo;t be in the Ghostty docs. But it does not work for Synology NASs in 2025.&lt;/p&gt;</description></item><item><title>State of AI tooling (for me)</title><link>https://blog.iankulin.com/state-of-ai-tooling-for-me/</link><pubDate>Mon, 07 Jul 2025 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/state-of-ai-tooling-for-me/</guid><description>&lt;p&gt;I&amp;rsquo;ve been meaning to write this for a couple of weeks, so let&amp;rsquo;s get to it - things are moving to fast to reflect too long; which is it&amp;rsquo;s own risk.&lt;/p&gt;
&lt;p&gt;In March, I wrote about &lt;a href="https://blog.iankulin.com/where-im-up-to-with-ai-for-coding/"&gt;how I was using AI in coding&lt;/a&gt;, which was Codeium (now Windsurf) in VS Code for completions, and ChatGPT and Claude online for architecture questions and code gen that was more than half a function.&lt;/p&gt;
&lt;h3 id="media"&gt;Media&lt;/h3&gt;
&lt;p&gt;In my usual keeping-current media consumption I hit a couple of surprises:&lt;/p&gt;
&lt;p&gt;Steve Yegge on Changelog &amp;ldquo;&lt;a href="https://changelog.com/friends/96"&gt;Adventures in babysitting coding agents&lt;/a&gt;&amp;rdquo; - Steve is the author of a book on Vibe Coding which is not due out till later in the year, by which time it will surely be out of date, but also works for &lt;a href="https://sourcegraph.com/amp"&gt;Sourcegraph on Amp&lt;/a&gt; which is an agentic tool aimed at enterprise. His pitch was that agentic coding (where the AI can do things - read and edit files, run command line tools etc) is ready now for most tasks, and that the returns on the minimal effort required to code something with prompts are so good that it opens up a lot of projects you wouldn&amp;rsquo;t have bothered with. So you should pick a utility and write it with one of the agentic coding tools. He mentioned a heap - Amp (obviously) but also Cursor, Cline, Claude code and so on.&lt;/p&gt;
&lt;p&gt;I think this probably blew up the ChangeLog peeps discord, and it certainly took me aback a bit - like who&amp;rsquo;d be letting a hallucinating bot loose in their terminal??&lt;/p&gt;
&lt;p&gt;I wanted a bit more science input, and got that from another podcast - &lt;a href="https://ocdevel.com/mlg"&gt;Machine Learning Guide&lt;/a&gt; from &lt;a href="https://www.youtube.com/@ocdevel"&gt;Tyler Renelle&lt;/a&gt;, specifically &lt;a href="https://ocdevel.com/mlg/mla-22"&gt;episodes 22-24&lt;/a&gt;. I can&amp;rsquo;t recommend Tyler highly enough - a very clear thoughtful communicator. I&amp;rsquo;ll be going back to listen to his whole course in machine learning.&lt;/p&gt;
&lt;p&gt;So all that was enough for me to think there&amp;rsquo;s definitely something here, so I&amp;rsquo;d better look at it and see if I need to change.&lt;/p&gt;
&lt;h3 id="tokens"&gt;Tokens&lt;/h3&gt;
&lt;p&gt;Unfortunately this decision was a few days after I&amp;rsquo;d canceled my monthly Claude plan on the basis that I could just buy $35 of tokens and use them in my selfhosted &lt;a href="https://www.librechat.ai/"&gt;LibreChat. (LibreChat&lt;/a&gt; is basically the Claude/ChatGPT interface that you can connect to any model and pay with tokens).&lt;/p&gt;
&lt;p&gt;My thought was that that way, I could swap between different AI companies models as I fancied, and it would probably end up being cheaper. Which, maybe it would have if I&amp;rsquo;d kept using LLMs the way I had been&amp;hellip;&lt;/p&gt;
&lt;h3 id="cline"&gt;Cline&lt;/h3&gt;
&lt;p&gt;Instead what I did was install the &lt;a href="https://cline.bot/"&gt;Cline&lt;/a&gt; add-on in VS Code and gave it my API keys so it could gobble up tokens. The interface is like a chat - you can say things like &amp;ldquo;Turn this node/express app this into a TypeScript project&amp;rdquo; and if you&amp;rsquo;re using Claude as the backend, it will go ahead and make a plan to do that. But then, it will ask you to switch from &amp;ldquo;Plan&amp;rdquo; to &amp;ldquo;Act&amp;rdquo; and jump in and edit the files, run the tests, run the linter etc then loop on that until that is finished. It asks permission for things as it needs, and at first I&amp;rsquo;d carefully inspect what it was doing and why before granting any of them, but very quickly just trusted it with everything. (No doubt there will be a big attack based on this in 2025 - why not take screenshots of my open BitWarden and send them to Russia?).&lt;/p&gt;
&lt;p&gt;If you haven&amp;rsquo;t seen an agentic tool powered by Claude Sonnet doing this stuff, prepare to be amazed. Tool use by AI&amp;rsquo;s is definitely the future, and probably not just for code. It still does sometimes get stuck in a rabbit hole - I find if it hasn&amp;rsquo;t solved it&amp;rsquo;s own problems after a couple of helpful interventions from me, it&amp;rsquo;s probably not going to (I guess it&amp;rsquo;s poisoned it&amp;rsquo;s own context too much) and it&amp;rsquo;s easier to kill it and give it a different (often more focused) prompt on a fresh start. I just used git for my rollback on those occasions though I understand others (perhaps Cursor?) have &amp;lsquo;checkpoints&amp;rsquo; built into the tool.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/do-all-the-things-meme-template-full-e9a85cb2.webp" width="800" alt=""&gt;
&lt;p&gt;A feature of Cline is that it shows you the token use and dollar amount as it&amp;rsquo;s working. I burned through USD20 of Anthropic tokens in about 4 days of coding all the things. I would just open up a project I use in VS Code, and have the Forgejo issues for it on the other screen, and copy them across to Cline one at a time.&lt;/p&gt;
&lt;p&gt;Since these were serious projects I was making branches and code-checking them manually at the pull request stage, but for utilities that can fit on a single web page (something like &amp;ldquo;I want to drop a word doc of school report comments on here, and have you switch out the names to fakes ones that you can replace later with the real ones, and there&amp;rsquo;s a button for me to download the fake name version&amp;rdquo;) I wouldn&amp;rsquo;t look at the code, just the results.&lt;/p&gt;
&lt;p&gt;I topped up the Anthropic money, but also gave Cline my OpenAI, Deep Seek and Gemini API keys and quickly came to these (probably not reliable) conclusions.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Claude Sonnet 4 - the GOAT.&lt;/li&gt;
&lt;li&gt;OpenAI ChatGPT 4 - okay, but not as good as Sonnet. The cost is 2/3 of Sonnet but it&amp;rsquo;s probably 85% as good. So mathematically good value, but in practice that last little bit makes Sonnet way more useful.&lt;/li&gt;
&lt;li&gt;Deepseek - 1/10th of the price of Sonnet. Less good than either of the other two, but I still found myself using it for low intelligence tasks. For example I might get Sonnet to make a detailed plan for renaming a concept in a code base eg &lt;em&gt;&amp;ldquo;I&amp;rsquo;ve been referring to these little files as URLs but now I want them called download jobs everywhere&amp;rdquo;&lt;/em&gt;. Then with that written by Sonnet into a markdown file with things like &amp;ldquo;&lt;code&gt;[ ] in utilities.js on line 321 rename function validateURL() to validateJob()&lt;/code&gt;&amp;rdquo; I&amp;rsquo;d let Deepseek do the grunt work of all the file edits before swapping back to Claude to do the linting and test error fixing.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can see all these cost details right in Cline as you are switching between the models.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2025-07-07-at-15.43.54.png" width="841" alt=""&gt;
&lt;h3 id="claude-code"&gt;Claude Code&lt;/h3&gt;
&lt;p&gt;It is also possible to add your Anthropic API key into Claude code and let it eat your tokens in exchange for a in-browser Space Invaders clone or whatever, so I tried this weird idea of just vibing from the CLI instead of my editor. It worked really, really well. I&amp;rsquo;d still have the code open in VS Code, and review it at the commit stage (I still do this now), but it was very impressive. Sadly, the model or system prompt is so tuned for action that I very quickly ran out of tokens again.&lt;/p&gt;
&lt;h3 id="dollar-dollar-bill-yall"&gt;Dollar, dollar bill y&amp;rsquo;all&lt;/h3&gt;
&lt;p&gt;About this time, I became aware that some level of Claude Code use is included in the &lt;a href="https://www.anthropic.com/pricing"&gt;Pro Plan&lt;/a&gt; - ie the USD20/month plan I&amp;rsquo;d been on before. A trick for new players is that if you&amp;rsquo;re changing from tokens to a plan you need to &lt;code&gt;/logout&lt;/code&gt; and &lt;code&gt;/login&lt;/code&gt; in Claude Code to switch it over to your plan (yes I had to top up my Anthropic credits again).&lt;/p&gt;
&lt;p&gt;With that hurdle passed I was now 100% in on Claude Code. On this plan I can generally code for a couple of hours a night without ever seeing the warning appear. On the weekend, I might get to about lunchtime, then have to wait a couple of hours to start again.&lt;/p&gt;
&lt;h3 id="gemini-drops"&gt;Gemini drops&lt;/h3&gt;
&lt;p&gt;At the end of June, Google launched &lt;a href="https://blog.google/technology/developers/introducing-gemini-cli-open-source-ai-agent/"&gt;Gemini CLI&lt;/a&gt;. I&amp;rsquo;d somehow ended up with $50 of free tokens without entering any billing details and had been using them in Cline, so I had some feeling about Gemini 2.5 and what it is capable of. I think the Gemini CLI is free (ie no token use) for personal use at the moment. It is not as good as Claude Code + Sonnet yet. I understand it has a very large context window, so perhaps if you&amp;rsquo;re working on a very big project it will be comparatively stronger - &lt;a href="https://www.youtube.com/watch?v=nfOVgz_omlU"&gt;Armin Ronacher says he uses it from inside Claude Code to summarise large code bases&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Still - &amp;ldquo;free&amp;rdquo; is a compelling value argument. I&amp;rsquo;ll often break out Gemini CLI if I&amp;rsquo;ve run out of Claude Code time, but not on serious projects.&lt;/p&gt;
&lt;h3 id="tips-and-tricks-for-agentic-coding-with-claude-code"&gt;Tips and Tricks for Agentic Coding with Claude Code&lt;/h3&gt;
&lt;p&gt;So, from the sources above, my own experience and the vibes from the zeitgeist, here is some things that work, right now in July 2025.&lt;/p&gt;
&lt;p&gt;Plan - &amp;gt; Act&lt;/p&gt;
&lt;p&gt;I guess I learned this from Cline. Ask for a detailed plan for the change - if it&amp;rsquo;s heading on a wrong track you&amp;rsquo;ll usually pick it up here. I&amp;rsquo;ll often ask for this as a markdown file when it completely understands the job and has explained it back to me.&lt;/p&gt;
&lt;p&gt;Mistakes are Cheaper Early&lt;/p&gt;
&lt;p&gt;Once it gets going on a big task, it will often run for for ten minutes or so. I don&amp;rsquo;t want to do ten minutes worth of AI datacentre environmental damage for code I&amp;rsquo;m going to throw away. So I will read through the proposal before I let it get to work. In Claude Code, [SHIFT][TAB] switches between plan and act. I don&amp;rsquo;t let it out of plan until there is a good plan that I&amp;rsquo;m happy with.&lt;/p&gt;
&lt;p&gt;Guardrails&lt;/p&gt;
&lt;p&gt;I think this was in my previous article. Have it set up linting, tests &amp;amp; formatting, and make a rule that it runs them. It needs a feedback loop, this is one of them. I&amp;rsquo;ve also jumped fully into TypeScript for Javascript with AI coding. It&amp;rsquo;s like a slightly over-enthusiastic junior developer who has read books on every subject and API in the world. The guardrails force it to do things better.&lt;/p&gt;
&lt;p&gt;Good Coding Practices&lt;/p&gt;
&lt;p&gt;Small files, good architecture, clear names, good project organisation. Small amount of up to date documentation that describes the shape and why of things. I like to keep the &amp;lsquo;sprints&amp;rsquo; small. So I&amp;rsquo;m going from one working app state to another. I also frequently refactor - probably more than in my own handcoding.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2025-07-07-at-14.17.04.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;CLAUDE.md&lt;/p&gt;
&lt;p&gt;These agent instructions get sent up with every chat. Keep them succinct. I take my cue about what it needs by watching it work. If it&amp;rsquo;s grepping all the time to find the main functions, I write a section about where they are and what they do - it&amp;rsquo;s a token saver mechanism. If you get Claude Code to update it, I think it puts too much in - since it&amp;rsquo;s going with every request it will chew up tokens reading it so this is a balancing act.&lt;/p&gt;
&lt;p&gt;Tell it what tools, and where things are. For example if I don&amp;rsquo;t tell it that it can use the Playwright MCP to check any UI changes it makes it usually won&amp;rsquo;t bother. I give it a &lt;code&gt;temp/&lt;/code&gt; directory to write disposable scripts in.&lt;/p&gt;
&lt;p&gt;MCP&lt;/p&gt;
&lt;p&gt;MCP was everywhere a couple of weeks ago, but it&amp;rsquo;s possible that it will be a fad - you don&amp;rsquo;t need a git or a github MCP server, just tell Claude to use it on the command line. The only MCP server I install is Playwright.&lt;/p&gt;
&lt;p&gt;Start Over&lt;/p&gt;
&lt;p&gt;Since the whole chat history is going up with every request, you want the least amount of baggage. As soon as we don&amp;rsquo;t need what&amp;rsquo;s in the context for the next job, I clear it. If I do need it, but it&amp;rsquo;s gotten long or contains direction changes, I ask for a markdown file of the plan, then restart with that.&lt;/p&gt;
&lt;p&gt;Chat is still helpful&lt;/p&gt;
&lt;p&gt;I have browser tabs open with ChatGPT and Claude and use them for all the self-contained queries, for instance &lt;em&gt;&amp;ldquo;Should I hand code the interfaces to different LLMs or is there a good library that does this?&amp;rdquo;&lt;/em&gt; isn&amp;rsquo;t something to do in Claude Code - it doesn&amp;rsquo;t need access to your project. Do that somewhere else with VC money.&lt;/p&gt;
&lt;h3 id="keep-learning"&gt;Keep Learning&lt;/h3&gt;
&lt;p&gt;This is fast moving. I am getting great value out of these tools right now with these techniques, but we are in a new, changing, exciting, world with this stuff.&lt;/p&gt;</description></item><item><title>Writing a Browser Extension</title><link>https://blog.iankulin.com/writing-a-browser-extension/</link><pubDate>Sun, 22 Jun 2025 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/writing-a-browser-extension/</guid><description>&lt;p&gt;Web pages are mostly just a collection of HTML, CSS, and JavaScript, so if we had some way of adding some of these into a web page, perhaps from our browser we could add new behaviour to a web page, right?&lt;/p&gt;
&lt;p&gt;Yes; users have long used tools like Greasemonkey (or similar userscript managers) to inject scripts into pages. Better still, modern browsers expose JavaScript APIs that let us interact directly with the browser itself. Enter: browser extensions.&lt;/p&gt;
&lt;p&gt;It turns out this is quite simple to do. And, it&amp;rsquo;s well documented (here&amp;rsquo;s the &lt;a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Your_first_WebExtension"&gt;step-by-step for Firefox&lt;/a&gt;), but if you want, follow along with me while I solve a gripe with a browser extension for Firefox.&lt;/p&gt;
&lt;h3 id="extension-or-add-on"&gt;Extension or Add-On?&lt;/h3&gt;
&lt;p&gt;Before we get started, let&amp;rsquo;s clear up some naming ambiguity. Firefox has &lt;a href="https://addons.mozilla.org/en-US/firefox/"&gt;add-on&amp;rsquo;s&lt;/a&gt; - these are extensions, plus some goodies like themes. I&amp;rsquo;m going to continue to call them &lt;em&gt;extensions&lt;/em&gt; - it&amp;rsquo;s browser non-specific and you&amp;rsquo;re using the &lt;a href="https://extensionworkshop.com/documentation/develop/about-the-webextensions-api/"&gt;Web-Extension API&lt;/a&gt; which is mostly-browser agnostic way of doing these things.&lt;/p&gt;
&lt;h3 id="the-gripe"&gt;The Gripe&lt;/h3&gt;
&lt;p&gt;I often want to steal an image from the web, so I right click to open it in a new tab, and save it, only to find it&amp;rsquo;s a .&lt;code&gt;webp&lt;/code&gt; that can&amp;rsquo;t be used for whatever I wanted to steal it for. I often notice is that the original image is a .jpg but it&amp;rsquo;s been converted (often by a SASS image conversion product such as &lt;a href="https://docs.imgix.com/en-US/apis/rendering/overview"&gt;imgx&lt;/a&gt;, &lt;a href="https://imagekit.io/guides/image-optimization/#chapter-7---resizing-images-to-fit-the-layout"&gt;imagekit&lt;/a&gt;, or &lt;a href="https://sirv.com/help/articles/dynamic-imaging/"&gt;sirv&lt;/a&gt; which apply transformations via query parameters like ?w=600). They&amp;rsquo;ll have links like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;https://demo.sirv.com/look.jpg?w=600
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;a href="https://demo.sirv.com/look.jpg?w=600"&gt;View embed&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;or&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;https://demo.sirv.com/look.jpg?w=200
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/look.jpg" alt=""&gt;&lt;/p&gt;
&lt;h3 id="the-solution"&gt;The solution&lt;/h3&gt;
&lt;p&gt;These are &amp;ldquo;query parameters&amp;rdquo;. It&amp;rsquo;s a smart, simple way of applying transformations to images. On the server end, the parameters are interpreted as what to do to each image. Of course, if you just remove the parameters, you get the original image.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;https://demo.sirv.com/look.jpg
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/look-c0a2d705-f421-4d99-9ba9-1b8694354e78.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;And if I&amp;rsquo;m stealing it, that&amp;rsquo;s what I want. So my plan is to write a browser extension that allows me right click on an image, and open it up in a new tab with all the query parameters removed.&lt;/p&gt;
&lt;h3 id="manifest"&gt;Manifest&lt;/h3&gt;
&lt;p&gt;You might have heard about the new &amp;ldquo;Manifest 3&amp;rdquo; that limits what extensions can do (and breaks ad blockers) in Chrome? Manifest v2 remains fully supported in Firefox, and offers simpler APIs for things like background scripts, which are perfect for small utility extensions like this. Here&amp;rsquo;s our manifest.js:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;manifest_version&amp;#34;: 2,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;name&amp;#34;: &amp;#34;Clean Image Opener&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;version&amp;#34;: &amp;#34;1.1&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;description&amp;#34;: &amp;#34;Open images in new tabs with query parameters stripped&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;permissions&amp;#34;: [&amp;#34;contextMenus&amp;#34;, &amp;#34;tabs&amp;#34;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;incognito&amp;#34;: &amp;#34;spanning&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;background&amp;#34;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;scripts&amp;#34;: [&amp;#34;background.js&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The manifest is the meta-data portion of the extension. Most of these fields are pretty obvious, but let&amp;rsquo;s talk about a couple:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;**&amp;quot;permissions&amp;quot;: [&amp;quot;contextMenus&amp;quot;, &amp;quot;tabs&amp;quot;]**&lt;/code&gt; - here we are specifying what permissions our code is going to need. The browser uses this to block API calls that would need any other permissions. It&amp;rsquo;s part of &lt;em&gt;principle of least privilege&lt;/em&gt; system that makes clear to users what can be done, then builds those restrictions into the execution.&lt;/p&gt;
&lt;p&gt;In this case were asking for &lt;code&gt;&amp;quot;contextMenus&amp;quot;&lt;/code&gt; because we want to add something to the right click menu, and &lt;code&gt;&amp;quot;tabs&amp;quot;&lt;/code&gt; because we want to open one with a URL we pass it.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s also worth noting that these are the browser functions that are being restricted - our code still has access to the web page data (ie the URL of the image we&amp;rsquo;re right clicking on) since the DOM is all in the user scope anyway - for example you can open the developer tools to access that information. This is still a potential risk though - for example if a malicious extension wanted to collect that viewed image url and export it as telemetry. Browser extensions need defenses other than the manifest for those types of attacks.&lt;/p&gt;
&lt;p&gt;There are many different permissions - &amp;ldquo;history&amp;rdquo;, &amp;ldquo;clipboard&amp;rdquo;, &amp;ldquo;webrequest&amp;rdquo;. The important intent is that the user can reasonably be aware of what&amp;rsquo;s being asked and weigh it up against what the extension is doing for them. The first layer of security for add-ons lies with us - the developer. Browser extensions have wide access to user activity and should be kept as minimal as possible to avoid abuse or privacy leakage.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;**&amp;quot;incognito&amp;quot;: &amp;quot;spanning&amp;quot;**&lt;/code&gt; - Here we&amp;rsquo;re saying how we want the extension to work in incognito mode. In the Manifest 2 specification there are three options for incognito - &amp;ldquo;not_allowed&amp;rdquo;, &amp;ldquo;split&amp;rdquo; and &amp;ldquo;spanning&amp;rdquo;. &amp;ldquo;split&amp;rdquo; was intended to allow the extension in both modes, but not allow data to be shared between them - essentially to run separate copies of the extension in each mode. It&amp;rsquo;s not implemented in modern Firefox - I suspect because of earlier security problems, so we&amp;rsquo;re using &amp;ldquo;spanning&amp;rdquo; which should indicate to the user of the extension that data may be passed between the two modes.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;**&amp;quot;background&amp;quot;: { &amp;quot;scripts&amp;quot;: [&amp;quot;background.js&amp;quot;] }**&lt;/code&gt; - we want our script to run in the background (to listen for right-clicks and act on them) so it goes in here.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s have a look at the first part of background.js:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;// Create the context menu item when the extension starts
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;browser.contextMenus.create({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; id: &amp;#34;open-clean-image&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; title: &amp;#34;Open image in new tab (no parameters)&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; contexts: [&amp;#34;image&amp;#34;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; documentUrlPatterns: [&amp;#34;&amp;lt;all_urls&amp;gt;&amp;#34;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;title:&lt;/code&gt; is just the text that appears in the context menu when the user write clicks on an image (this is the &lt;code&gt;contexts: [&amp;quot;image&amp;quot;]&lt;/code&gt; part). This can happen on &lt;code&gt;[&amp;quot;&amp;lt;all_urls&amp;gt;&amp;quot;]&lt;/code&gt; (we could have restricted it to a particular domain or sub-domain). The &lt;code&gt;id:&lt;/code&gt; is just how we are going to reference it in the click handler. Speaking of:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; Handle context menu clicks
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;browser&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;contextMenus&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;onClicked&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;addListener&lt;span style="color:#eceff4"&gt;((&lt;/span&gt;info&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; tab&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;info&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;menuItemId &lt;span style="color:#81a1c1"&gt;===&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;open-clean-image&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; Get the image URL &lt;span style="color:#81a1c1;font-weight:bold"&gt;and&lt;/span&gt; strip query parameters
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; originalUrl &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; info&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;srcUrl&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; cleanUrl &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; stripQueryParameters&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;originalUrl&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; Open the clean URL &lt;span style="color:#81a1c1;font-weight:bold"&gt;in&lt;/span&gt; a new tab
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; browser&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;tabs
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;create&lt;span style="color:#eceff4"&gt;({&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; url&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; cleanUrl&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; active&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;true&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;catch&lt;span style="color:#eceff4"&gt;((&lt;/span&gt;error&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; console&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;error&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Failed to create tab:&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; error&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Not much explanation needed here due to good naming and generous comments ;-)&lt;/p&gt;
&lt;p&gt;And that&amp;rsquo;s pretty much the whole extension. Of course there&amp;rsquo;s a &lt;code&gt;stripQueryParameters()&lt;/code&gt; somewhere. For now, just imagine it says:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;function stripQueryParameters(url) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; return url.split(&amp;#39;?&amp;#39;)[0];
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="testing"&gt;Testing&lt;/h3&gt;
&lt;p&gt;To load our new Firefox extension to try it out, go to the url &lt;a href="debugging#/runtime/this-firefox"&gt;about:debugging#/runtime/this-firefox&lt;/a&gt; where we want to &amp;ldquo;Load a Temporary Add-on&amp;rdquo; - in the open dialog choose your manifest file.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2025-06-21-at-20.23.08.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Now the extension should be working for normal browser mode, to enable it in incognito, you&amp;rsquo;ll need to head into the Firefox menu &amp;ldquo;Add-ons and themes&amp;rdquo; (&lt;a href="addons"&gt;about:addons&lt;/a&gt;) then open the &amp;ldquo;Manage&amp;rdquo; menu for the extension and turn on &amp;ldquo;Run in Private Windows&amp;rdquo;.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2025-06-22-at-08.27.38.png" width="913" alt=""&gt;
&lt;h3 id="publishing"&gt;Publishing&lt;/h3&gt;
&lt;p&gt;Publishing an extension to the Firefox Add-ons store could be a whole post, but it&amp;rsquo;s the sort of thing you can follow your nose through, by starting at a&lt;a href="https://addons.mozilla.org/en-US/developers/"&gt;ddons.mozilla.org/en-US/developers&lt;/a&gt;. There is a semi-manual review process, so don&amp;rsquo;t expect it to be instant.&lt;/p&gt;
&lt;p&gt;The source for this project is &lt;a href="https://github.com/IanKulin/clean-image/tree/v1.1"&gt;available here&lt;/a&gt;, or install it into Firefox from &lt;a href="https://addons.mozilla.org/en-US/firefox/addon/clean-image-opener/"&gt;here&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>End to end testing - Cypress basics</title><link>https://blog.iankulin.com/end-to-end-testing-cypress-basics/</link><pubDate>Mon, 12 May 2025 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/end-to-end-testing-cypress-basics/</guid><description>&lt;p&gt;When you&amp;rsquo;ve made a change to your web-app, do you run it then click around the new bits to check it works? Good start, but instead of doing that yourself, do it in a faster, more comprehensive and automated way with an end-to-end (E2E) testing setup using &lt;a href="https://www.cypress.io/"&gt;Cypress&lt;/a&gt;. Here&amp;rsquo;s how.&lt;/p&gt;
&lt;h3 id="e2e"&gt;E2E&lt;/h3&gt;
&lt;p&gt;End to End testing is testing your app as a user might - by clicking links, entering data, looking at the screen and checking everything is okay, but it&amp;rsquo;s scripted like a unit test and the results are checked with assertions. Like unit testing this allows you to build up a collection of comprehensive tests that easily detect for unexpected behaviours - not just in the results of functions in your app, but in the user experience of the app.&lt;/p&gt;
&lt;p&gt;In the case of Cypress, this works by running your app in an instrumented browser. The tests are written in JavaScript and might ask things like &amp;ldquo;Click the &amp;lsquo;Home&amp;rsquo; link&amp;rdquo; and have an assertion similar to &amp;ldquo;check the home page loaded&amp;rdquo;. Let&amp;rsquo;s see how that will look.&lt;/p&gt;
&lt;h3 id="how-it-looks"&gt;How it looks&lt;/h3&gt;
&lt;p&gt;In the app I&amp;rsquo;m working on, if you view an individual customer (say at &amp;ldquo;&lt;a href="http://127.0.0.1:3002/customers/1"&gt;http://127.0.0.1:3002/customers/1&lt;/a&gt;&amp;rdquo;) there&amp;rsquo;s a &amp;ldquo;Home&amp;rdquo; link at the top which takes you to the list of customers (at &amp;ldquo;&lt;a href="http://127.0.0.1:3002/customers"&gt;http://127.0.0.1:3002/customers&lt;/a&gt;&amp;rdquo;).&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2025-04-15-at-10.22.34.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the test code:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;describe(&amp;#39;Page Navigation&amp;#39;, () =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; it(&amp;#39;should navigate to the customers list when clicking the Home link&amp;#39;, () =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; // visit the customer details page
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.visit(&amp;#39;http://127.0.0.1:3002/customers/1&amp;#39;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; // find and click the Home link
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.get(&amp;#39;a&amp;#39;).contains(&amp;#39;Home&amp;#39;).click();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; // verify we navigated to the customers list page
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.url().should(&amp;#39;eq&amp;#39;, &amp;#39;http://127.0.0.1:3002/customers&amp;#39;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you&amp;rsquo;ve been writing unit tests before, this format will be familiar, but let&amp;rsquo;s look at the steps:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cy.visit(&amp;#39;http://127.0.0.1:3002/customers/1&amp;#39;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You guessed it - we&amp;rsquo;re telling Cypress to visit that page.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cy.get(&amp;#39;a&amp;#39;).contains(&amp;#39;Home&amp;#39;).click();
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I&amp;rsquo;m not sure if Cypress uses &lt;a href="https://jquery.com/"&gt;JQuery&lt;/a&gt;, or just a JQuery like syntax, either way, what we&amp;rsquo;re doing here is selecting the &amp;lsquo;&amp;lt;a &amp;hellip;&amp;gt;&amp;rsquo; tag. Of course our page probably contains several anchor tags, so we&amp;rsquo;re refining this search to the anchor tag that contains &amp;lsquo;Home&amp;rsquo;. Note that there&amp;rsquo;s an implied assertion here. If there is no &lt;a&gt; link on the page containing &amp;lsquo;Home&amp;rsquo;, this test will fail with an error saying something like &amp;ldquo;Expected to find content: &amp;lsquo;Home&amp;rsquo; within the element: &lt;a&gt; but never did.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Finally the &lt;code&gt;click()&lt;/code&gt; at the end of the statement tells Cypress to click this link.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cy.url().should(&amp;#39;eq&amp;#39;, &amp;#39;http://127.0.0.1:3002/customers&amp;#39;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Before we look at this statement, consider that we haven&amp;rsquo;t told Cypress to wait for a bit for the results of our click() to process - one of the benefits of Cypress is it just figures that out magically.&lt;/p&gt;
&lt;p&gt;This statement is an assertion - the URL &lt;code&gt;should&lt;/code&gt; equal (&lt;code&gt;eq&lt;/code&gt;) the URL we&amp;rsquo;ve provided.&lt;/p&gt;
&lt;p&gt;So that gives us a quick overview of a simple test. Naturally Cypress has a heap more operators and assertion types to help us test our application - basically everything you could think of as user-facing testing. Let&amp;rsquo;s look at a simple demo app then work through the tests we might try for this.&lt;/p&gt;
&lt;h3 id="the-app"&gt;The App&lt;/h3&gt;
&lt;p&gt;This app is a simple demo I wrote for an earlier blog post about using the Express router. We have &lt;em&gt;Customers&lt;/em&gt; and &lt;em&gt;Orders&lt;/em&gt;, a single &lt;em&gt;customer&lt;/em&gt; can have zero-many &lt;em&gt;orders&lt;/em&gt;. The opening page is a list of all customers. Clicking on a customer shows the details for that customer, including a list of their orders. Clicking on an order shows the detail for that order, including a link the customer it belongs to.&lt;/p&gt;
&lt;p&gt;The Customer and Order detail views have delete links, and a deletion of a customer should cascade to delete that customer&amp;rsquo;s orders.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2025-04-15-at-11.50.32-1.png" width="946" alt=""&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2025-04-15-at-11.50.22-1.png" width="999" alt=""&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2025-04-15-at-10.22.34-2.png" alt=""&gt;&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2025-04-12-at-13.12.28-1.png" width="861" alt=""&gt;
&lt;h3 id="installing-cypress"&gt;Installing Cypress&lt;/h3&gt;
&lt;p&gt;Installing Cypress is straightforward. The install steps from the docs are &lt;a href="https://docs.cypress.io/app/get-started/install-cypress"&gt;here&lt;/a&gt;, but really it&amp;rsquo;s just starting your Node project (so you&amp;rsquo;ve got a package.json) then &lt;code&gt;npm install cypress --save-dev&lt;/code&gt; to add it as a dev dependency. It&amp;rsquo;s a big download so expect it to take a bit. It includes lodash, some AWS stuff, tldts, day.js, a heap of vue stuff - just, it&amp;rsquo;s a lot of big dependencies. Also since Cypress itself does some cool stuff linking into the browser - that functionality requires some code.&lt;/p&gt;
&lt;h3 id="the-tests"&gt;The Tests&lt;/h3&gt;
&lt;p&gt;Actually - the code in our very simple demo above covers about 70% of the testing I do, and the pattern of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Select a user element&lt;/li&gt;
&lt;li&gt;Click it or add some text&lt;/li&gt;
&lt;li&gt;Select another user element to check that worked&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;comes up again and again. So I&amp;rsquo;m going to try not to repeat myself too much. Most of what&amp;rsquo;s new in the following tests will be extra selectors, and assertions. We won&amp;rsquo;t cover all of them, but rather a smattering to get started with.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; // test for customers list page
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; describe(&amp;#34;Customers Page&amp;#34;, () =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; it(&amp;#34;should have the home page redirect to customers page&amp;#34;, () =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.visit(&amp;#34;http://localhost:3002&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.url().should(&amp;#34;include&amp;#34;, &amp;#34;/customers&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.get(&amp;#34;h1&amp;#34;).contains(&amp;#34;Customers&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; it(&amp;#34;should display a list of customers&amp;#34;, () =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.visit(&amp;#34;http://localhost:3002/customers&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.get(&amp;#34;li&amp;#34;).should(&amp;#34;have.length.at.least&amp;#34;, 5);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.get(&amp;#34;li&amp;#34;).eq(0).contains(&amp;#34;Alice&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; it(&amp;#34;should have working links to customer details&amp;#34;, () =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.visit(&amp;#34;http://localhost:3002/customers&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; // click the first customer (Alice)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.get(&amp;#34;a&amp;#34;).contains(&amp;#34;Alice Johnson&amp;#34;).click();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.url().should(&amp;#34;include&amp;#34;, &amp;#34;/customers/&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.get(&amp;#34;h2&amp;#34;).contains(&amp;#34;Alice Johnson&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="should"&gt;.should()&lt;/h4&gt;
&lt;p&gt;There&amp;rsquo;s a &lt;a href="https://docs.cypress.io/api/commands/should"&gt;massive list of should() assertions&lt;/a&gt;, and they depend a bit on what you&amp;rsquo;ve chained on to. In the first example we looked at we used &lt;code&gt;&amp;quot;eq&amp;quot;&lt;/code&gt; for equals, in the example directly above we&amp;rsquo;ve used &lt;code&gt;&amp;quot;include&amp;quot;&lt;/code&gt; for a partial match, and &lt;code&gt;&amp;quot;have.length.at.least&amp;quot;&lt;/code&gt; for what it says on the box.&lt;/p&gt;
&lt;p&gt;Another handy thing might be testing for &lt;code&gt;&amp;quot;not.exist&amp;quot;.&lt;/code&gt; In my example app if I want to test deleting a &lt;em&gt;customer&lt;/em&gt;, I can check they exist in the customers list, click delete, then check that they no longer exist in the list:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; it(&amp;#34;should delete a customer when delete link is clicked&amp;#34;, () =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; // first check the customer exists
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.visit(&amp;#34;http://localhost:3002/customers&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.get(&amp;#34;a&amp;#34;).contains(&amp;#34;Hannah Abbott&amp;#34;).should(&amp;#34;exist&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; // visit the customer page and delete
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.visit(&amp;#34;http://localhost:3002/customers/8&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.get(&amp;#34;a&amp;#34;).contains(&amp;#34;Delete customer&amp;#34;).click();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; // verify the customer is deleted
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.url().should(&amp;#34;include&amp;#34;, &amp;#34;/customers&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.get(&amp;#34;a&amp;#34;).contains(&amp;#34;Hannah Abbott&amp;#34;).should(&amp;#34;not.exist&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="get"&gt;.get()&lt;/h4&gt;
&lt;p&gt;We&amp;rsquo;ve already seen selecting an anchor tag with get(&amp;ldquo;a&amp;rdquo;) - this will work for any HTML tag, but of course you&amp;rsquo;ll frequently need more specificity than that. As &lt;a href="https://docs.cypress.io/api/commands/get#__docusaurus_skipToContent_fallback"&gt;described in the docs&lt;/a&gt;, most of the JQuery selectors will also work with get.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;// Select by element type
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cy.get(&amp;#39;button&amp;#39;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;// Select by class
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cy.get(&amp;#39;.my-class&amp;#39;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;// Select by ID
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cy.get(&amp;#39;#my-id&amp;#39;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;// Combining selectors
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cy.get(&amp;#39;button.primary#submit&amp;#39;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;// Select by attribute
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cy.get(&amp;#39;[data-test=&amp;#34;submit-button&amp;#34;]&amp;#39;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Those first four are straightforward, but you might not know about attributes.&lt;/p&gt;
&lt;h3 id="attributes"&gt;Attributes&lt;/h3&gt;
&lt;p&gt;As part of the HTML specification, tags can have attributes. You&amp;rsquo;ve been using them all along. For example. this button:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;button id=&amp;#34;submit&amp;#34; class=&amp;#34;btn primary&amp;#34; type=&amp;#34;submit&amp;#34;&amp;gt;Submit&amp;lt;/button&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;has attributes for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;id&lt;/li&gt;
&lt;li&gt;class&lt;/li&gt;
&lt;li&gt;type&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These all have particular meanings for HTML, CSS and JavaScript, but actually we can make up our own. For example we could say:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;button type=&amp;#34;submit&amp;#34; data-test=&amp;#34;submit-button&amp;#34;&amp;gt;Submit&amp;lt;/button&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;There&amp;rsquo;s no specification for &amp;lsquo;data-test&amp;rsquo;, it&amp;rsquo;s just a convention, we could just have easily said:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;button type=&amp;#34;submit&amp;#34; data-green-zebra=&amp;#34;submit-button&amp;#34;&amp;gt;Submit&amp;lt;/button&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Note that I&amp;rsquo;ve kept the &lt;code&gt;data-&lt;/code&gt; prefix - that is part of the &lt;a href="https://www.w3schools.com/tags/att_global_data.asp"&gt;HTML5 specification&lt;/a&gt;. We could probably make up anything and it would work, but maybe it would conflict with something in a future HTML version, so best stick to &amp;ldquo;data-&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Using attributes for specifying the element we want is highly recommended. Although the element you want to click might currently be the third &lt;a&gt; in a &lt;ul&gt; inside the &lt;nav&gt; - it&amp;rsquo;s easy to imagine it being moved in a future update to your app. Once we&amp;rsquo;ve written an E2E test, we want it to continue to work across future app development, and using a &amp;lsquo;data-test&amp;rsquo; attribute supports this.&lt;/p&gt;
&lt;h3 id="invoke-and-then"&gt;.invoke() and then()&lt;/h3&gt;
&lt;p&gt;Sometimes the exact test you need might not be available, or you need to do some operation as part of your testing that requires a bit more processing. In that case, you can chain the &lt;code&gt;invoke()&lt;/code&gt; method. This allows you to call any jQuery method on an element you&amp;rsquo;ve selected with Cypress, letting you extract specific properties or manipulate the element in ways that aren&amp;rsquo;t covered by Cypress&amp;rsquo;s built-in assertions.&lt;/p&gt;
&lt;p&gt;Once you&amp;rsquo;ve got it, you can use &lt;code&gt;then()&lt;/code&gt; to run an arrow function against it to do something. The pseudo codes looks a bit like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cy.get(selector)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .invoke(jQueryMethod) // Extract what you need
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .then((result) =&amp;gt; { // Process it with your own logic
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; // Custom processing
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Let&amp;rsquo;s look at an example. Imagine the HTML of our page looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;span class=&amp;#34;price&amp;#34;&amp;gt;$24.99&amp;lt;/span&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And we want to check that the price was greater than $20 - perhaps we are supposed to have added tax or something. Our test could look like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cy&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;.price&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;invoke&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;text&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;then&lt;span style="color:#eceff4"&gt;((&lt;/span&gt;priceText&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; priceValue &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; parseFloat&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;priceText&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;replace&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;$&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; expect&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;priceValue&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;to&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;be&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;greaterThan&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;20.0&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This selects this span based on it&amp;rsquo;s class, then saves the text &amp;lsquo;$24.99&amp;rsquo; to &lt;code&gt;priceText&lt;/code&gt;, extracts the value to a JavaScript number, and asserts it to be greater than 20.&lt;/p&gt;
&lt;p&gt;In my demo app, I use this to check the cascading delete - when we delete the customer, the orders for that customer should also be deleted. Rather than hard code the order number we can use invoke/then to extract it from the text which looks like this:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;8 - 2025-03-08 - $200&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Then, after deleting the customer (and orders) we navigate to the orders page to make sure that order number does not exist there any more.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; describe&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Cascading Deletions&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; it&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;should delete owned orders when a customer is deleted&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; first make a note of an order &lt;span style="color:#81a1c1;font-weight:bold"&gt;for&lt;/span&gt; a specific customer
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;visit&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;http://localhost:3002/customers/4&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;h2&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;contains&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Diana Prince&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; note an order ID that belongs to this customer
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;ul li a&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;first&lt;span style="color:#eceff4"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;invoke&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;text&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;then&lt;span style="color:#eceff4"&gt;((&lt;/span&gt;orderText&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; extract the full order text to use &lt;span style="color:#81a1c1;font-weight:bold"&gt;for&lt;/span&gt; matching later
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; orderTextFull &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; orderText&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;trim&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; extract just the order ID number
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; orderId &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; orderText&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;split&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34; &amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)[&lt;/span&gt;&lt;span style="color:#b48ead"&gt;0&lt;/span&gt;&lt;span style="color:#eceff4"&gt;]&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;trim&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; delete the customer
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;a&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;contains&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Delete customer&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;click&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; verify customer is deleted
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;url&lt;span style="color:#eceff4"&gt;()&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;should&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;include&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;/customers&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;a&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;contains&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Diana Prince&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;should&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;not.exist&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; check that the order is also deleted 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;visit&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;http://localhost:3002/orders&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; make sure we&lt;span style="color:#a3be8c"&gt;&amp;#39;re matching the exact order (not just a substring)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;a&lt;span style="color:#eceff4"&gt;[&lt;/span&gt;href&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/orders/${orderId}&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;]&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;should&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;not.exist&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;There is a lot, lot more to Cypress to this, but with what we&amp;rsquo;ve covered here it&amp;rsquo;s possible to write a comprehensive suite of tests that will test all of the functionality in this demo app in &lt;a href="https://github.com/IanKulin/route-demo/blob/main/cypress/e2e/home.cy.js"&gt;about 200 lines&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="using-cypress"&gt;Using Cypress&lt;/h3&gt;
&lt;p&gt;So that&amp;rsquo;s all the code, but how does it look to test like this? For me this is one of the things that makes end-to-end testing cool. I love seeing it&amp;rsquo;s click away in the browser at super speed as my tests turn green.&lt;/p&gt;
&lt;p&gt;First of all I start my app - so however you normally do this. For me, it&amp;rsquo;s dropping to the terminal in VSCode and running it in Node. Something like &lt;code&gt;node index.js&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Once that&amp;rsquo;s going we can start Cypress. Since the first terminal is running my node app, we need to spawn another terminal to run Cypress in. This is a simple matter in VSCode - just hit that + button I&amp;rsquo;ve circled in the screen shot below. You can swap between the different terminals you have open by clicking on them in the list underneath that + button.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2025-04-15-at-16.02.23.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;We start Cypress in the new terminal with &lt;code&gt;npx cypress open&lt;/code&gt; but the magic does not happen in the terminal, this thing pops up:&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2025-04-15-at-16.09.06.png" width="900" alt=""&gt;
&lt;p&gt;We&amp;rsquo;re doing E2E testing, so select that.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2025-04-15-at-16.09.16.png" width="900" alt=""&gt;
&lt;p&gt;I&amp;rsquo;m in a Chrome mood today, so next we see this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2025-04-15-at-16.09.29.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve only got one test file - &lt;code&gt;home.cy.js&lt;/code&gt;, so I click that. The tests are listed down the left side of the browser, and my app in an iFrame to the right. As the tests are running, I can see the app flicking through each step. In a couple of seconds the seventeen tests that comprise many page manipulations and assertions are finished and I can see the results.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2025-04-15-at-17.14.27.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;If we click on a test, the details for it open up, and a screenshot of the application state at the time of that test is displayed.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2025-04-15-at-17.16.52.png" width="900" alt=""&gt;
&lt;p&gt;The most common debugging problem I&amp;rsquo;ve run into is when I didn&amp;rsquo;t write the selection correctly (and didn&amp;rsquo;t use a data- attribute). These are easily checked in this view by hovering over the one we&amp;rsquo;re interested in - the element that Cypress used in this step will be highlighted.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2025-04-15-at-17.20.34.png" width="900" alt=""&gt;
&lt;p&gt;So, how does a failed test look. I can create that in this test suit just by running the tests again. It won&amp;rsquo;t be able to delete orders or customers it deleted in the earlier run.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2025-04-15-at-17.24.56.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;So, at the start of this &amp;lsquo;delete order&amp;rsquo; test, I&amp;rsquo;m checking if the order exists, and it doesn&amp;rsquo;t (because we didn&amp;rsquo;t reset after deleting it last time). We can see from the error message that Cypress waited 4 seconds in case it was a timing issue. It&amp;rsquo;s displayed the test case where the failure has occurred. This along with the before and after snapshots of the app around each test make locating problems a breeze.&lt;/p&gt;
&lt;h3 id="resets"&gt;Resets&lt;/h3&gt;
&lt;p&gt;The pattern above (where you run a test twice and it fails the second time because the first execution changed the state) is common. To avoid this, we need some system of resetting the state. Cypress has a mocha like &amp;lsquo;beforeEach&amp;rsquo; ability. You most always need this for logging things in:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;describe(&amp;#39;My app tests&amp;#39;, () =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; beforeEach(() =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; // This code runs before each test in this block
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.visit(&amp;#39;/login&amp;#39;); // for example
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.get(&amp;#39;input[name=username]&amp;#39;).type(&amp;#39;user&amp;#39;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.get(&amp;#39;input[name=password]&amp;#39;).type(&amp;#39;password&amp;#39;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.get(&amp;#39;button[type=submit]&amp;#39;).click();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; it(&amp;#39;should show the dashboard after login&amp;#39;, () =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.url().should(&amp;#39;include&amp;#39;, &amp;#39;/dashboard&amp;#39;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.contains(&amp;#39;Welcome&amp;#39;).should(&amp;#39;be.visible&amp;#39;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; it(&amp;#39;should navigate to settings&amp;#39;, () =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.get(&amp;#39;nav&amp;#39;).contains(&amp;#39;Settings&amp;#39;).click();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.url().should(&amp;#39;include&amp;#39;, &amp;#39;/settings&amp;#39;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;But for apps that need things like a fresh database before testing, it&amp;rsquo;s a bit trickier. In the past I&amp;rsquo;ve sometimes created some sort of /test-reset endpoint which feels like an unreasonable security risk. The proper answer is to shell out with a Cypress task. That way we can do things like copy in test data, or spin up a whole test environment in a container. These are meaty topics for another post - but really, our tests should be re-run-able, and run-able in any order so we might come back to that.&lt;/p&gt;
&lt;h3 id="test"&gt;Test&lt;/h3&gt;
&lt;p&gt;Any testing is better than none, and if you use these sorts of tools that make it easier you&amp;rsquo;ll find you&amp;rsquo;ll add to them, especially when errors crop up. If I sit down to add end-to-end tests to an existing app, I nearly always find things I want to change to make it better. Use end to end testing.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/IanKulin/route-demo"&gt;code&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Express router for better code organisation</title><link>https://blog.iankulin.com/express-router-for-better-code-organisation/</link><pubDate>Mon, 28 Apr 2025 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/express-router-for-better-code-organisation/</guid><description>&lt;p&gt;A Node/Express app I&amp;rsquo;m working on has been sprouting routes so much that the &lt;code&gt;server.js&lt;/code&gt; file has swollen to 800 lines - way past my 200-250 comfort zone, so it&amp;rsquo;s time to organise the routes into their own files. That seems like a good topic for a beginner blog post, so let&amp;rsquo;s dive in.&lt;/p&gt;
&lt;p&gt;Imagine we&amp;rsquo;ve written this little Node/Express app.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;import express from &lt;span style="color:#a3be8c"&gt;&amp;#34;express&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;import &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; dbCustomersGet&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; dbCustomersGetById&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; dbCustomersDelete&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; dbOrdersGet&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; dbOrdersGetById&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; dbOrdersGetByCustomerId&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; dbOrdersDelete&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt; from &lt;span style="color:#a3be8c"&gt;&amp;#34;./db.js&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; app &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; express&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;set&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;view engine&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;ejs&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; port &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#b48ead"&gt;3002&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;use&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;express&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;urlencoded&lt;span style="color:#eceff4"&gt;({&lt;/span&gt; extended&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;true&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;redirect&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/customers&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/customers&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; customers &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; dbCustomersGet&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;render&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;customers&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; customers &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/customers/:id&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; customer &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; dbCustomersGetById&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;params&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;id&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; orders &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; dbOrdersGetByCustomerId&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;params&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;id&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;render&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;customer&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; customer&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; orders &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/customers/:id/delete&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; dbCustomersDelete&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;params&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;id&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;redirect&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/customers&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/orders&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; orders &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; dbOrdersGet&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;render&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;orders&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; orders &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/orders/:id&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; order &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; dbOrdersGetById&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;params&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;id&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; customer &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; dbCustomersGetById&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;order&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;customerId&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;render&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;order&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; order&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; customer &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/orders/:id/delete&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; dbOrdersDelete&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;params&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;id&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;redirect&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/orders&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;listen&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;port&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; console&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;log&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;Listening on http&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt;&lt;span style="color:#b48ead"&gt;127.0&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;&lt;span style="color:#b48ead"&gt;0.1&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;port&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Although concocted, this would seem familiar to anyone who&amp;rsquo;s built a CRUD business app.&lt;/p&gt;
&lt;p&gt;One thing I&amp;rsquo;ve done better here than in the real app I&amp;rsquo;m fixing is that the routes are carefully named - all the &amp;lsquo;orders&amp;rsquo; routes begin with &lt;code&gt;/orders&lt;/code&gt;, all the &amp;lsquo;customers&amp;rsquo; routes with &lt;code&gt;/customers&lt;/code&gt;. As we&amp;rsquo;ll see, this is going to make separating them out much easier.&lt;/p&gt;
&lt;h3 id="express-router"&gt;Express Router&lt;/h3&gt;
&lt;p&gt;Like almost everything in Express, the router is middleware. Let&amp;rsquo;s look at how our index.js has changed once we&amp;rsquo;ve moved the routes out into a &lt;code&gt;customers.js&lt;/code&gt; and an &lt;code&gt;orders.js&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;import express from &lt;span style="color:#a3be8c"&gt;&amp;#34;express&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;import customersRouter from &lt;span style="color:#a3be8c"&gt;&amp;#34;./routes/customers.js&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;import ordersRouter from &lt;span style="color:#a3be8c"&gt;&amp;#34;./routes/orders.js&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; app &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; express&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;set&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;view engine&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;ejs&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; port &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#b48ead"&gt;3002&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;use&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;express&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;urlencoded&lt;span style="color:#eceff4"&gt;({&lt;/span&gt; extended&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;true&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; routers
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;use&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/customers&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; customersRouter&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;use&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/orders&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; ordersRouter&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; root route redirect to customers
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;redirect&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/customers&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;listen&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;port&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; console&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;log&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;Listening on http&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt;&lt;span style="color:#b48ead"&gt;127.0&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;&lt;span style="color:#b48ead"&gt;0.1&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;port&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So much neater!&lt;/p&gt;
&lt;p&gt;First of all, the imports for all my database functions are gone - they&amp;rsquo;ll be in the files for our two routes.&lt;/p&gt;
&lt;p&gt;There are a couple of new imports though - our two &amp;lsquo;routers&amp;rsquo;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;import customersRouter from &amp;#34;./routes/customers.js&amp;#34;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;import ordersRouter from &amp;#34;./routes/orders.js&amp;#34;;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then further down, they are installed as middleware:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;// routers
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app.use(&amp;#34;/customers&amp;#34;, customersRouter);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app.use(&amp;#34;/orders&amp;#34;, ordersRouter);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You can pretty much see from this code how this works. Any routes that begin with &amp;ldquo;/customers&amp;rdquo; are sent off to the &lt;code&gt;customersRouter&lt;/code&gt; which we&amp;rsquo;ve imported from &lt;code&gt;&amp;quot;./routes/customers.js&amp;quot;&lt;/code&gt;, and the routes for &amp;ldquo;/orders&amp;rdquo; go to the &lt;code&gt;ordersRouter&lt;/code&gt;. Any route requests that don&amp;rsquo;t match those will be sought in the main file where the app is declared.&lt;/p&gt;
&lt;p&gt;You might have noticed how we&amp;rsquo;re organising the routes - there&amp;rsquo;s a &amp;ldquo;routes&amp;rdquo; folder and they&amp;rsquo;re dropped in there. That&amp;rsquo;s not a requirement, but it&amp;rsquo;s a common convention.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2025-04-04-at-20.42.50.png" width="800" alt=""&gt;
&lt;p&gt;Let&amp;rsquo;s have a look at one of the route files:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;import express from &lt;span style="color:#a3be8c"&gt;&amp;#34;express&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;import &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; dbCustomersGet&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; dbCustomersGetById&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; dbCustomersDelete&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; dbOrdersGetByCustomerId&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt; from &lt;span style="color:#a3be8c"&gt;&amp;#34;../db.js&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; router &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; express&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;Router&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; GET &lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;customers
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;router&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; customers &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; dbCustomersGet&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;render&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;customers&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; customers &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; GET &lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;customers&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;id
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;router&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/:id&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; customer &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; dbCustomersGetById&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;params&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;id&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; orders &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; dbOrdersGetByCustomerId&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;params&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;id&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;render&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;customer&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; customer&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; orders &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; GET &lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;customers&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;id&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;delete
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;router&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/:id/delete&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; dbCustomersDelete&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;params&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;id&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;redirect&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/customers&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;export&lt;/span&gt; default router&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is nice. We&amp;rsquo;re only importing the customer database functions, and we&amp;rsquo;ve got all the customer routes in one place in an easily comprehensible file.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s really only one gotcha here which we alluded to earlier. You&amp;rsquo;ll notice how I&amp;rsquo;ve added a comment over each route?&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; GET &lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;customers&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;id
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;router&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/:id&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; customer &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; dbCustomersGetById&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;params&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;id&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; orders &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; dbOrdersGetByCustomerId&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;params&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;id&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;render&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;customer&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; customer&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; orders &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is because in the process of specifying that this file deals with all the &amp;ldquo;/customers&amp;rdquo; routes, that part of the request URL has been stripped off - so a call to &lt;code&gt;http://127.0.0.1:3002/customers/5&lt;/code&gt; arrives here as &lt;code&gt;/5&lt;/code&gt;. It&amp;rsquo;s another common practice to put the route path in a comment as I&amp;rsquo;ve done here as a reminder to myself. I wish the Express team had just left the requests unaltered.&lt;/p&gt;
&lt;h3 id="conclusion"&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Really, that&amp;rsquo;s about all there is to using the Express Router to split your routes out into files; it&amp;rsquo;s quite straightforward. A good naming convention for your routes so that logical groups of routes all start with the same specifier will be a great help.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/IanKulin/route-demo"&gt;Code on GitHub&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Functional Javascript array methods</title><link>https://blog.iankulin.com/functional-javascript-array-methods/</link><pubDate>Mon, 14 Apr 2025 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/functional-javascript-array-methods/</guid><description>&lt;p&gt;I&amp;rsquo;ve been whipping up a little mock-database unit that has a few access functions but actually stores the data as arrays for a demo project for a post I&amp;rsquo;m writing. In the process I wrote this gem:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;export&lt;/span&gt; function dbOrdersAdd&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;order&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; orderCopy &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;...&lt;/span&gt;order &lt;span style="color:#eceff4"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; since id is a stringified number&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; finding the &lt;span style="color:#81a1c1"&gt;max&lt;/span&gt; is a bit of a mess
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; maxId &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; orders&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;reduce&lt;span style="color:#eceff4"&gt;((&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;max&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; o&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; Math&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;max&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;max&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; parseInt&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;o&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;id&lt;span style="color:#eceff4"&gt;)),&lt;/span&gt; &lt;span style="color:#b48ead"&gt;0&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; orderCopy&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;id &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#bf616a"&gt;String&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;maxId &lt;span style="color:#81a1c1"&gt;+&lt;/span&gt; &lt;span style="color:#b48ead"&gt;1&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; orders&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;push&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;orderCopy&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;...&lt;/span&gt;orderCopy &lt;span style="color:#eceff4"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In the comment I&amp;rsquo;m claiming the code is a bit of a mess (and from a readability point that&amp;rsquo;s true) but actually I love the elegance of using the &lt;code&gt;reduce()&lt;/code&gt; method here.&lt;/p&gt;
&lt;p&gt;It also occurred to me, that a year or so ago, these functional array methods were completely novel to me. So I thought it my be interesting to talk about &lt;code&gt;reduce()&lt;/code&gt;, and why I&amp;rsquo;m calling it a &amp;ldquo;functional&amp;rdquo; method.&lt;/p&gt;
&lt;h2 id="reduce"&gt;Reduce&lt;/h2&gt;
&lt;p&gt;If you think of all the things you&amp;rsquo;re likely to do with a collection like an array, the most common thing is to iterate over it. Javascript has you covered - with the &lt;code&gt;forEach()&lt;/code&gt; method. This just executes the callback function you pass:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[1, 2, 3].forEach(x =&amp;gt; console.log(x));
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s super handy, and gets used a lot. But what if I wanted to do something like summing all the values in an array. In the olden days we might write something like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; numbers &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#eceff4"&gt;[&lt;/span&gt;&lt;span style="color:#b48ead"&gt;1&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#b48ead"&gt;2&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#b48ead"&gt;3&lt;/span&gt;&lt;span style="color:#eceff4"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;let sum &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#b48ead"&gt;0&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;for&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;let i &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#b48ead"&gt;0&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; i &lt;span style="color:#81a1c1"&gt;&amp;lt;&lt;/span&gt; numbers&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;length&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; i&lt;span style="color:#81a1c1"&gt;++&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sum &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; sum &lt;span style="color:#81a1c1"&gt;+&lt;/span&gt; numbers&lt;span style="color:#eceff4"&gt;[&lt;/span&gt;i&lt;span style="color:#eceff4"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;console&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;log&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;sum&lt;span style="color:#eceff4"&gt;);&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; &lt;span style="color:#b48ead"&gt;6&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;But all the cool young things be like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; numbers &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#eceff4"&gt;[&lt;/span&gt;&lt;span style="color:#b48ead"&gt;1&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#b48ead"&gt;2&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#b48ead"&gt;3&lt;/span&gt;&lt;span style="color:#eceff4"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; sum &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; numbers&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;reduce&lt;span style="color:#eceff4"&gt;((&lt;/span&gt;acc&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; num&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; acc &lt;span style="color:#81a1c1"&gt;+&lt;/span&gt; num&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#b48ead"&gt;0&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;console&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;log&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;sum&lt;span style="color:#eceff4"&gt;);&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; &lt;span style="color:#b48ead"&gt;6&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;reduce() is doing a little bit more work for us than forEach(). It is a machine for &amp;ldquo;reducing&amp;rdquo; the values in an array to a single number. It takes two parameters. The first one is a function, and the second is the starting value for the &amp;lsquo;accumulator&amp;rsquo;.&lt;/p&gt;
&lt;p&gt;In our function above &lt;code&gt;(acc, num) =&amp;gt; acc + num&lt;/code&gt;, &amp;lsquo;acc&amp;rsquo; is used by reduce() as the accumulator, and &amp;rsquo;num&amp;rsquo; is the value from the array. So the steps being carried out in the code above are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;set acc = 0&lt;/li&gt;
&lt;li&gt;apply &lt;code&gt;(acc, num) =&amp;gt; acc + num&lt;/code&gt; where num is 1 - so 0+1=1, acc is now equal to 1&lt;/li&gt;
&lt;li&gt;apply &lt;code&gt;(acc, num) =&amp;gt; acc + num&lt;/code&gt; where num is 2 - so 1+2=3, acc is now 3&lt;/li&gt;
&lt;li&gt;apply &lt;code&gt;(acc, num) =&amp;gt; acc + num&lt;/code&gt; where num is 3 - so 2+3=6, acc is now 6&lt;/li&gt;
&lt;li&gt;save that into &lt;code&gt;sum&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If we wanted to find the max in an array of numbers we could:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; numbers &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#eceff4"&gt;[&lt;/span&gt;&lt;span style="color:#b48ead"&gt;1&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#b48ead"&gt;2&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#b48ead"&gt;3&lt;/span&gt;&lt;span style="color:#eceff4"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; maxnum &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; numbers&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;reduce&lt;span style="color:#eceff4"&gt;((&lt;/span&gt;acc&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; num&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; Math&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;max&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;acc&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; num&lt;span style="color:#eceff4"&gt;),&lt;/span&gt; &lt;span style="color:#b48ead"&gt;0&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;console&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;log&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;maxnum&lt;span style="color:#eceff4"&gt;);&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; &lt;span style="color:#b48ead"&gt;3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Very nice!&lt;/p&gt;
&lt;h2 id="functional-programming"&gt;Functional Programming&lt;/h2&gt;
&lt;p&gt;There are a million explanations about what functional programming is and what it&amp;rsquo;s strengths are, so for here, let&amp;rsquo;s just summarise it as:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;functions are &amp;lsquo;pure&amp;rsquo; - ie they have no side effects, and access and set no values from outside the function. Because of this, the same inputs always result in a particular output. They are fully encapsulated.&lt;/li&gt;
&lt;li&gt;The data passed in, and returned is immutable (data in general is immutable)&lt;/li&gt;
&lt;li&gt;Lot&amp;rsquo;s of functions - functions as &amp;lsquo;first-class-citizens&amp;rsquo;, functions passed into functions. It&amp;rsquo;s function focused - hence the name.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The strengths I like about a functional approach is it&amp;rsquo;s high unit-test-ability, and the elegance of passing a function as a parameter to broaden the scope of a function.&lt;/p&gt;</description></item><item><title>Moving a domain from Wordpress</title><link>https://blog.iankulin.com/moving-a-domain-from-wordpress/</link><pubDate>Mon, 30 Dec 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/moving-a-domain-from-wordpress/</guid><description>&lt;p&gt;I love the convenience of a hosted blog on wordpress.com, but one of the justifications for my &amp;lsquo;investment&amp;rsquo; in homelab hardware and learning time was that I&amp;rsquo;d reduce my spend on hosted platforms by self-hosting them. I&amp;rsquo;ve already quit Evernote and dropped back to the free plan on Dropbox by building systems to replace them for less money and more data sovereignty. And now, the recent &lt;a href="https://techcrunch.com/2024/09/25/wordpress-org-bans-wp-engine-blocks-it-from-accessing-its-resources/"&gt;Wordpress drama&lt;/a&gt; has made me uneasy about Matt having control of domains I&amp;rsquo;ve got registered with wordpress.&lt;/p&gt;
&lt;p&gt;For the moment, I&amp;rsquo;m leaving content there, but I&amp;rsquo;d like to keep my options open for the future, so that means moving any domains to an independent registrar, in my case, &lt;a href="https://porkbun.com/"&gt;porkbun&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Wordpress have a &lt;a href="https://wordpress.com/support/domains/transfer-domain-registration/"&gt;good article&lt;/a&gt; explaining their part of the process (kudos to them for not trying to make it difficult) but I ran into a bump not mentioned there, so it&amp;rsquo;s worth writing out the steps for future travelers.&lt;/p&gt;
&lt;h4 id="make-sure-your-email-is-correct"&gt;Make sure your email is correct&lt;/h4&gt;
&lt;p&gt;It probably is fine, but this process is going to rely on you having control of the email address attached to your wordpress account. If you don&amp;rsquo;t currently receive the emails for renewals etc, then you need to fix that first. Registrars like to be careful that they are not giving away people&amp;rsquo;s domains to bad actors, so there will be a bit of a &amp;ldquo;verify you own this email that is the contact for the domain&amp;rdquo; dance as part of this process.&lt;/p&gt;
&lt;h4 id="be-settled"&gt;Be settled&lt;/h4&gt;
&lt;p&gt;For reasons outside WordPress&amp;rsquo;s control, you can&amp;rsquo;t be moving domains around all the time. It needs to have been with the current registrar for 60 days. If not, you&amp;rsquo;ll just need to wait that out.&lt;/p&gt;
&lt;p&gt;Even if that&amp;rsquo;s not your situation, still keep in mind this transfer will take about a week. There are ways of pointing a domain elsewhere a bit quicker, but actually moving it takes five days or more.&lt;/p&gt;
&lt;h4 id="turn-the-transfer-lock-off"&gt;Turn the transfer lock off&lt;/h4&gt;
&lt;p&gt;Most domain registrars allow you to (probably be default) &lt;a href="https://www.icann.org/resources/pages/locked-2013-05-03-en"&gt;&amp;rsquo;lock&amp;rsquo; a domain&lt;/a&gt; to prevent changes. To get to this on Wordpress, go into your site, and look in &amp;ldquo;Upgrades&amp;rdquo; | &amp;ldquo;Domains&amp;rdquo; for &amp;ldquo;Transfer&amp;rdquo;. There&amp;rsquo;s a toggle to turn that off.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-11-09-at-5.19.45-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-11-09-at-5.19.45-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This is also where you can request the &amp;ldquo;Authorization Code&amp;rdquo;. This is the key that you&amp;rsquo;ll take over to your new domain registrar. But don&amp;rsquo;t do that yet - that&amp;rsquo;s what I did and got this:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-11-09-at-5.29.57-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-11-09-at-5.29.57-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Lol. What?! Someone objected by fax to me moving my domain? I feel like the only people who could have done that to this transfer I initiated two seconds ago could be Wordpress.&lt;/p&gt;
&lt;h4 id="turn-private-registration-off"&gt;Turn Private Registration off&lt;/h4&gt;
&lt;p&gt;To their credit (again) this was explained in another email shortly after:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-11-16-at-7.02.20-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-11-16-at-7.02.20-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Ah, so I need to turn &amp;lsquo;private registration&amp;rsquo; off. This is the mechanism that hides your personal details as a domain owner from scammers and grifters. Apparently it has to be &amp;lsquo;off&amp;rsquo; to transfer the site. This is not a source of stress to me, as soon as the domain is transferred to PorkBun, the apparent owner of the domain when someone does a &lt;code&gt;whois&lt;/code&gt; on it, will be &amp;ldquo;Private By Design LLC&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Once again, this setting is in the Wordpress site settings under &amp;ldquo;Domain&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-11-09-at-5.32.29-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-11-09-at-5.32.29-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4 id="get-your-authorization-code"&gt;Get your Authorization Code&lt;/h4&gt;
&lt;p&gt;Now is the time to hit the button in Wordpress to request the &amp;ldquo;Authorization Code&amp;rdquo;. It will be sent to the email attached to the domain. This is the hex string you&amp;rsquo;ll need to take to your domain registrar to request the transfer.&lt;/p&gt;
&lt;h4 id="start-the-transfer"&gt;Start the transfer&lt;/h4&gt;
&lt;p&gt;I guess every domain registrar will have a slightly different set up. With porkbun, I just went to &lt;a href="https://porkbun.com/transfer"&gt;https://porkbun.com/transfer&lt;/a&gt; and entered the domain name and the authorisation code. They did charge me $11 for this, then advised that the transfer would take about five days. Maybe that&amp;rsquo;s built into the domain transfer system to allow more people to object by fax.&lt;/p&gt;
&lt;p&gt;On the porkbun status page for the transfer, I was able to set up an A record to the wordpress IP where by blog still lives, so that the second the transfer went through, it would be set up to direct traffic there with a minimal downtime. I guess in this case that would have no effect since the wordpress name servers would still be in place (see further down), but it&amp;rsquo;s a good idea since often when you&amp;rsquo;re moving a domain, the losing registrar would be deleting your name-server entry.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/5later.jpg" width="300" alt=""&gt;
&lt;p&gt;Once the email came through on the sixth day, I checked the domain was still pointing to the blog, and it was all good. But we&amp;rsquo;re not done yet.&lt;/p&gt;
&lt;h4 id="change-the-nameservers"&gt;Change the nameservers&lt;/h4&gt;
&lt;p&gt;Although I&amp;rsquo;ve now got control of the domain, we&amp;rsquo;re still using WordPress&amp;rsquo;s nameservers. That&amp;rsquo;s not a big deal for me, but I do want to bring them over to porkbun so it&amp;rsquo;s the same setup as all my other domains. Before I nuke the wordpress nameservers, we need to check what records are in it.&lt;/p&gt;
&lt;p&gt;First step is to see who are the nameservers for a domain. We do this with the &lt;code&gt;dig NS &amp;lt;domain-name&amp;gt;&lt;/code&gt; command:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-11-16-at-8.27.37-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-11-16-at-8.27.37-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In this example the name servers are &lt;code&gt;b.iana-servers.net&lt;/code&gt; and &lt;code&gt;a.iana-servers.net&lt;/code&gt; In the case of your wordpress blog they are probably &lt;code&gt;ns1.wordpress.com&lt;/code&gt; etc.&lt;/p&gt;
&lt;p&gt;Once you know the name of the nameservers you can query them with the domain name to see what the records are. The most important will be the A records, but you probably want to go ahead and check the MX (mail) and TXT records as well so you can reproduce them on the new registrar.&lt;/p&gt;
&lt;p&gt;This is done with &lt;code&gt;dig @&amp;lt;name-server&amp;gt; &amp;lt;domain-name&amp;gt; &amp;lt;record-type&amp;gt;&lt;/code&gt; for example &lt;code&gt;dig @b.iana-servers.net example.com A&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-11-16-at-8.33.21-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;In this case there is a single A record pointing the domain to 93.184.215.14 We need to note all of these to reproduce them in the domain settings in your new registrar. Again this is going to be different for each one, but if you&amp;rsquo;ve ever pointed a domain anywhere, you&amp;rsquo;ll know how to do it on yours.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-11-16-at-8.40.11-am-1.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-11-16-at-8.40.11-am-1.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4 id="change-the-nameservers-1"&gt;Change the nameservers&lt;/h4&gt;
&lt;p&gt;Now those records are all in, it&amp;rsquo;s time to change the nameservers. There will be an option somewhere in your domain management tools at the registrar to allow for this. In my case, I&amp;rsquo;ll be switching to porkbun&amp;rsquo;s default ones.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-11-16-at-8.45.30-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-11-16-at-8.45.30-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4 id="profit"&gt;Profit&lt;/h4&gt;
&lt;p&gt;That all was a bit of a dance, but it feels good to be in control of the domain so I can redirect it in the future if needed.&lt;/p&gt;
&lt;p&gt;Edit from the future: This (pointing the domain I now controlled at my wordpress.com blog) died after a couple of weeks. I&amp;rsquo;m not sure if they changed something, but when I went into the wordpress settings to check it was still set up to use an external domain name, it greeted me with an &amp;lsquo;upgrade&amp;rsquo; offer to turn that on at an annual cost greater than my old plan. So, I had to hurriedly set up a wordpress instance on a VPS - which turned out to be not much drama and will probably be the subject of a future post.&lt;/p&gt;</description></item><item><title>Clear explanation of how transformer AI works</title><link>https://blog.iankulin.com/clear-explanation-of-how-transformer-ai-works/</link><pubDate>Mon, 28 Oct 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/clear-explanation-of-how-transformer-ai-works/</guid><description>&lt;p&gt;If you&amp;rsquo;re interested in how generative AI works, check out &lt;a href="https://ishananand.com/"&gt;Ishan Anand&lt;/a&gt;&amp;rsquo;s Youtube series &amp;ldquo;&lt;a href="https://www.youtube.com/@Spreadsheetsareallyouneed"&gt;Spreadsheets are all you need&lt;/a&gt;&amp;rdquo;. He steps through the basics using an Excel spreadsheet that encompasses most of GPT-2. Just doing that is an impressive (and hilarious) feat, but he also has a knack for teaching, so you&amp;rsquo;ll come away with a good understanding of AI and how some of it&amp;rsquo;s limitations manifest.&lt;/p&gt;
&lt;p&gt;Ishan is selling a course, which I guess these are the first three lessons of, and I got a lot out of them. It&amp;rsquo;s also possible to &lt;a href="https://github.com/ianand/spreadsheets-are-all-you-need/releases/tag/v0.6.1"&gt;download the spreadsheet&lt;/a&gt; he uses in the course to play with.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/FyeN5tXMnJ8?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;
</description></item><item><title>Authentication basics for Node apps</title><link>https://blog.iankulin.com/authentication-basics-for-node-apps/</link><pubDate>Mon, 19 Aug 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/authentication-basics-for-node-apps/</guid><description>&lt;p&gt;&lt;a href="https://unsplash.com/photos/calahorra-tower-torre-de-la-calahorra-in-cordoba-spain-a-fortified-gate-built-during-the-late-12th-century-by-the-almohads-to-protect-the-nearby-roman-bridge-in-the-historic-center-of-cordoba-andalusia-spain-ECsukeqrDoo"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-08-10-at-8.59.01-pm.jpg" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Pretty much every serious web app needs to include a way for users to log in securely and to be served their content. Since there&amp;rsquo;s a lot of complexity in this, it&amp;rsquo;s highly advisable to use good libraries to support this. In a future post we&amp;rsquo;re going to use those libraries, but first I want to explain what&amp;rsquo;s happening at the lower level and tease out some of the concepts as we build a secure system from the ground up.&lt;/p&gt;
&lt;h3 id="http"&gt;HTTP&lt;/h3&gt;
&lt;p&gt;Before we dive into our authentication story, it&amp;rsquo;s worth thinking about how HTTP works and putting some names to things. We often don&amp;rsquo;t think too much about this level because the mechanics are most abstracted away for us by libraries such as express.js.&lt;/p&gt;
&lt;p&gt;A HTTP &lt;em&gt;request&lt;/em&gt; is just a bunch of lines of text arriving at TCP port 80. It&amp;rsquo;s an agreed on &lt;a href="https://www.rfc-editor.org/rfc/rfc9110.html#name-example-message-exchange"&gt;Internet standard&lt;/a&gt; originally written by &lt;a href="https://en.wikipedia.org/wiki/Tim_Berners-Lee"&gt;Tim Berners-Lee&lt;/a&gt;. The request will include the type of request it is (GET, POST etc), the resource being requested (usually a web-page) - these make up the &lt;em&gt;request line&lt;/em&gt;. Then there will be some lines of data called the &lt;em&gt;header&lt;/em&gt; that might include things like the type of browser making the request, and optionally a &lt;em&gt;body&lt;/em&gt; of the request. The body might contain form data being submitted or a JSON description of an object. If there is a body, there will be a blank line separating it from the header.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;GET /hello.txt HTTP/1.1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;User-Agent: curl/7.64.1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Host: www.example.com
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Accept-Language: en, mi
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Similarly, the HTTP &lt;em&gt;response&lt;/em&gt; is just some lines of text. A &lt;em&gt;status line&lt;/em&gt; (which includes the famous &lt;em&gt;status code&lt;/em&gt; such as 404), some &lt;em&gt;headers&lt;/em&gt; and the &lt;em&gt;body&lt;/em&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;HTTP/1.1 200 OK
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Date: Mon, 27 Jul 2009 12:28:53 GMT
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Server: Apache
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Last-Modified: Wed, 22 Jul 2009 19:15:56 GMT
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ETag: &amp;#34;34aa387-d-1568eb00&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Accept-Ranges: bytes
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Content-Length: 51
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Vary: Accept-Encoding
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Content-Type: text/plain
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Hello World! My content includes a trailing CRLF.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="sessions"&gt;Sessions&lt;/h3&gt;
&lt;p&gt;A web app might be serving thousands of users, so we need some way for the server to know which user it is talking to. If our app is a todo list, we don&amp;rsquo;t want to be showing Jane&amp;rsquo;s todo items to Fred - each user only wants to see their own items. A common way of doing this is that the browser making requests to the server could send a bit of text along with each request. These little bits of text are called &amp;lsquo;cookies&amp;rsquo;.&lt;/p&gt;
&lt;p&gt;In a very simple example, the cookie could contain the name of our user - for example &amp;lsquo;Fred&amp;rsquo; or &amp;lsquo;Jane&amp;rsquo;. Then when the server received each request, it could read the cookie to know which user was making the request. Here&amp;rsquo;s our code:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; express &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;express&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; app &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; express&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; Route to handle requests
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;/&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;headers&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;cookie &lt;span style="color:#81a1c1"&gt;&amp;amp;&amp;amp;&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;headers&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;cookie&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;includes&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;name=Fred&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;))&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;Hello Fred!&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;headers&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;cookie &lt;span style="color:#81a1c1"&gt;&amp;amp;&amp;amp;&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;headers&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;cookie&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;includes&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;name=Jane&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;))&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;Hello Jane!&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;Hello stranger!&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; Start the server
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; PORT &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#b48ead"&gt;3000&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;listen&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;PORT&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; console&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;log&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;Server is running on port &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;PORT&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The cookie is just a line of text included in the header of the request. Perhaps the request looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;GET / HTTP/1.1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Accept: application/json, text/plain, */*
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Cookie: name=Fred
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;User-Agent: axios/1.5.1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Accept-Encoding: gzip, compress, deflate, br
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Host: 127.0.0.1:3000
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;At the user&amp;rsquo;s end the cookie is probably stored in an sqlite database - this implementation detail is left up to the browser. When the users browser sends the request, it checks to see if it&amp;rsquo;s got a cookie for this host and encodes it into the header of the request.&lt;/p&gt;
&lt;h4 id="testing-this-code"&gt;Testing this code&lt;/h4&gt;
&lt;p&gt;There&amp;rsquo;s no simple way to test the server code above since regular browsers don&amp;rsquo;t allow us to set the cookie values. There are however a number of tools that can send customised requests. Some examples of these API testing tools are &lt;a href="https://www.postman.com/"&gt;Postman&lt;/a&gt; and Insomnia. Since the &lt;a href="https://news.ycombinator.com/item?id=37680126"&gt;Insomnia rug-pull&lt;/a&gt;, I&amp;rsquo;ve been a big fan of &lt;a href="https://www.usebruno.com/"&gt;Bruno&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;All of these tools allow you to specify the URL, the type of request, and any header or body to go with it. They can make the call to the server and show the results.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/brunoexample.png" width="1000" alt=""&gt;
&lt;h3 id="setting-a-cookie"&gt;Setting a Cookie&lt;/h3&gt;
&lt;p&gt;Our server as it stands at the moment is not very secure. Any hacker can just change the value of the cookie to see the content intended for Fred or Jane. We&amp;rsquo;ll get to authentication eventually, and when we do, we&amp;rsquo;ll need to be able to &lt;em&gt;set&lt;/em&gt; a cookie in the client. How does that work?&lt;/p&gt;
&lt;p&gt;Again, we&amp;rsquo;ll npm install a little library to assist us. &lt;a href="https://github.com/expressjs/cookie-parser#readme"&gt;cookie-parser&lt;/a&gt; is some middleware that lets us easily work with cookies. For the demonstration we&amp;rsquo;ll just add some routes to set the name to &amp;lsquo;Jane&amp;rsquo; or to clear it. Setting it to &amp;lsquo;Jane&amp;rsquo; will look like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;// Route to set a cookie for &amp;#39;Jane&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app.get(&amp;#34;/setuserjane&amp;#34;, (req, res) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res.cookie(&amp;#34;name&amp;#34;, &amp;#34;Jane&amp;#34;); // Set a cookie named &amp;#39;name&amp;#39; with value &amp;#39;Jane&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res.send(&amp;#34;Cookie set for Jane&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And clearing it, like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;// Route to clear the &amp;#39;name&amp;#39; cookie
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app.get(&amp;#34;/clearuser&amp;#34;, (req, res) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res.clearCookie(&amp;#34;name&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res.send(&amp;#34;Cookie cleared&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And since we&amp;rsquo;re using cookie-parser, we may as well use it for reading the cookie to tidy things up a bit as well&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; express &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;express&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; cookieParser &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;cookie-parser&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; app &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; express&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; cookie middleware
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;use&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;cookieParser&lt;span style="color:#eceff4"&gt;());&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;cookies&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name &lt;span style="color:#81a1c1"&gt;===&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Fred&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Hello Fred!&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;cookies&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name &lt;span style="color:#81a1c1"&gt;===&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Jane&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Hello Jane!&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Hello stranger!&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; Route to set a cookie &lt;span style="color:#81a1c1;font-weight:bold"&gt;for&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#39;Jane&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/setuserjane&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;cookie&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Jane&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Cookie set for Jane&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; Route to clear the &lt;span style="color:#a3be8c"&gt;&amp;#39;name&amp;#39;&lt;/span&gt; cookie
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/clearuser&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;clearCookie&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Cookie cleared&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; Start the server
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; PORT &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#b48ead"&gt;3000&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;listen&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;PORT&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; console&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;log&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;Server is running on port &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;PORT&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With this code, we can just use a regular browser for testing. Visiting &lt;code&gt;127.0.0.1:3000/clearuser&lt;/code&gt; will delete the &lt;code&gt;name&lt;/code&gt; cookie, which we could test by visiting &lt;code&gt;127.0.0.1:3000&lt;/code&gt; and getting the &amp;ldquo;Hello stranger!&amp;rdquo; message. If we then go to &lt;code&gt;127.0.0.1:3000/setuserjane&lt;/code&gt; and back to &lt;code&gt;127.0.0.1:3000&lt;/code&gt; we&amp;rsquo;ll see &amp;ldquo;Hello Jane!&amp;rdquo;.&lt;/p&gt;
&lt;h3 id="session-id"&gt;Session ID&lt;/h3&gt;
&lt;p&gt;Clearly this setup is still insecure since a hacker can easily just include a name cookie to pretend to be any particular user. A better system would be to store a unique ID in the cookie, then match that internally to a particular user. This means we&amp;rsquo;d have to maintain the links between each GUID and user on the server, but it would massively reduce the chance of a hacker being able to pretend to be a particular user since the chance of correctly guessing a GUID would be very low.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s think about what we&amp;rsquo;d need to do to make this work for /setuserjane.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;generate a unique ID&lt;/li&gt;
&lt;li&gt;save that ID along with &amp;lsquo;Jane&amp;rsquo; in the local store&lt;/li&gt;
&lt;li&gt;save the UID to the cookie to go back to the browser&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/setuserjane&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; sessionId &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; uuidv4&lt;span style="color:#eceff4"&gt;();&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; Generate a new GUID
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sessions&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;push&lt;span style="color:#eceff4"&gt;({&lt;/span&gt; sessionId&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; name&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Jane&amp;#34;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;cookie&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;sessionId&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; sessionId&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Session set for Jane&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then when we needed to check who the user was at a route, we&amp;rsquo;d need to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;extract the session ID from the cookie if there is one&lt;/li&gt;
&lt;li&gt;look it up in the server&amp;rsquo;s session store&lt;/li&gt;
&lt;li&gt;use that to identify the name&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; sessionId &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;cookies&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;sessionId&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; session &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; sessions&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;find&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;s &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; s&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;sessionId &lt;span style="color:#81a1c1"&gt;===&lt;/span&gt; sessionId&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;session&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;Hello &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;session&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;!&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Hello stranger!&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here&amp;rsquo;s the whole thing. The store of session id:name keypairs is just an array of objects (so it will be wiped on every server restart), and we&amp;rsquo;re using the uuid library to generate globally unique ids.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; express &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;express&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; cookieParser &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;cookie-parser&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; v4&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; uuidv4 &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;uuid&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; app &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; express&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; cookie middleware
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;use&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;cookieParser&lt;span style="color:#eceff4"&gt;());&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; &lt;span style="color:#bf616a"&gt;Array&lt;/span&gt; to store session objects
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; sessions &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#eceff4"&gt;[];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; sessionId &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;cookies&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;sessionId&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; session &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; sessions&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;find&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;s &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; s&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;sessionId &lt;span style="color:#81a1c1"&gt;===&lt;/span&gt; sessionId&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;session&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;Hello &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;session&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;!&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Hello stranger!&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; Route to set a session &lt;span style="color:#81a1c1;font-weight:bold"&gt;for&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#39;Jane&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/setuserjane&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; sessionId &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; uuidv4&lt;span style="color:#eceff4"&gt;();&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; Generate a new GUID
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sessions&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;push&lt;span style="color:#eceff4"&gt;({&lt;/span&gt; sessionId&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; name&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Jane&amp;#34;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;cookie&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;sessionId&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; sessionId&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Session set for Jane&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; Route to clear the session
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/clearuser&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; sessionId &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;cookies&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;sessionId&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; index &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; sessions&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;findIndex&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;s &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; s&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;sessionId &lt;span style="color:#81a1c1"&gt;===&lt;/span&gt; sessionId&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;index &lt;span style="color:#81a1c1"&gt;!==&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;&lt;span style="color:#b48ead"&gt;1&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sessions&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;splice&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;index&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#b48ead"&gt;1&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;clearCookie&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;sessionId&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Session cleared&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; Start the server
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; PORT &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#b48ead"&gt;3000&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;listen&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;PORT&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; console&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;log&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;Server is running on port &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;PORT&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="express-session"&gt;express-session&lt;/h2&gt;
&lt;p&gt;The code above is a great improvement, however in practice, instead of managing session ids ourselves, we&amp;rsquo;d make use of express-session. Although general good practice is to avoid dependencies, when we&amp;rsquo;re working with security related code, it&amp;rsquo;s often advisable to use a trusted library since they will have already dealt with a lot of the edge cases and potential weaknesses.&lt;/p&gt;
&lt;p&gt;This is the case with &lt;code&gt;express-session&lt;/code&gt; which does basically what we have above, but also deals with potential cross-site scripting, regenerates session id&amp;rsquo;s to avoid fixation attacks, and signs the cookies to reduce the chance of session data being tampered with. express-session will also handle the storage for the key value pairs for us.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; express &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;express&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; cookieParser &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;cookie-parser&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; session &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;express-session&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; app &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; express&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; cookie middleware
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;use&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;cookieParser&lt;span style="color:#eceff4"&gt;());&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; session middleware
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;use&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; session&lt;span style="color:#eceff4"&gt;({&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; secret&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;REtKU9xyvahuHGd3&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; Replace with a strong secret key
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; resave&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;false&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; saveUninitialized&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;true&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cookie&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; secure&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;false&lt;/span&gt; &lt;span style="color:#eceff4"&gt;},&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; Set to &lt;span style="color:#81a1c1"&gt;true&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; using HTTPS
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;session&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;Hello &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;session&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;!&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Hello stranger!&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; Route to set a session &lt;span style="color:#81a1c1;font-weight:bold"&gt;for&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#39;Jane&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/setuserjane&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;session&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Jane&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Session set for Jane&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; Route to clear the session
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/clearuser&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;session&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;destroy&lt;span style="color:#eceff4"&gt;((&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Error clearing session&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Session cleared&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; Start the server
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; PORT &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#b48ead"&gt;3000&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;listen&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;PORT&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; console&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;log&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;Server is running on port &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;PORT&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="authentication-flow"&gt;Authentication flow&lt;/h3&gt;
&lt;p&gt;Everyone in the world by now is familiar with having to use a username and password to sign into a web app and use it. If we think about how that is going to work with the session ID, it would be something like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When a user tries to access a route that needs authorisation, we check the session object to see if there&amp;rsquo;s a logged in user attached to it.&lt;/li&gt;
&lt;li&gt;If there is, then the route is served, if not they are redirected to a log in page&lt;/li&gt;
&lt;li&gt;At the log in page, we take a username and password, and check it against an internal store. If they match, we update the session to identify the user&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app.get(&amp;#34;/&amp;#34;, (req, res) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; if (req.session.name) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res.send(`Hello ${req.session.name}!`);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } else {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res.render(&amp;#34;login.ejs&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I&amp;rsquo;m using the EJS templating system for this app because it will be handy for later. I&amp;rsquo;m not going to explain it more here other than to say you can just imagine the above is loading the login form HTML. In fact, it just looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;!DOCTYPE html&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;html lang=&amp;#34;en&amp;#34;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;head&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;meta charset=&amp;#34;UTF-8&amp;#34; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;meta name=&amp;#34;viewport&amp;#34; content=&amp;#34;width=device-width, initial-scale=1.0&amp;#34; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;title&amp;gt;Login&amp;lt;/title&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/head&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;body&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;h1&amp;gt;Login&amp;lt;/h1&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;form action=&amp;#34;/login&amp;#34; method=&amp;#34;post&amp;#34;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;div&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;label for=&amp;#34;username&amp;#34;&amp;gt;Username:&amp;lt;/label&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;input type=&amp;#34;text&amp;#34; id=&amp;#34;username&amp;#34; name=&amp;#34;username&amp;#34; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/div&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;div&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;label for=&amp;#34;password&amp;#34;&amp;gt;Password:&amp;lt;/label&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;input type=&amp;#34;password&amp;#34; id=&amp;#34;password&amp;#34; name=&amp;#34;password&amp;#34; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/div&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;button type=&amp;#34;submit&amp;#34;&amp;gt;Login&amp;lt;/button&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/form&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/body&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/html&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This form posts to the /login route, which looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;post&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/login&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; username&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; password &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;body&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;username &lt;span style="color:#81a1c1"&gt;===&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;demo&amp;#34;&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;&amp;amp;&amp;amp;&lt;/span&gt; password &lt;span style="color:#81a1c1"&gt;===&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;password&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;session&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; username&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Logged in&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Invalid username or password&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It extracts the user name and password from the body of the request (ie from the form). If they are a match, then it sets &amp;ldquo;name&amp;rdquo; in the session which signifies to the rest of the app that we are validly logged in.&lt;/p&gt;
&lt;p&gt;To log out, we just tell express-session to destroy the session:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app.get(&amp;#34;/logout&amp;#34;, (req, res) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; req.session.destroy((err) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; if (err) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res.send(&amp;#34;Error clearing session&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } else {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res.send(&amp;#34;Session cleared&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="tidy-up"&gt;Tidy up&lt;/h3&gt;
&lt;p&gt;We just need a bit of refactoring before we move on. Currently our &lt;code&gt;/login&lt;/code&gt; route only allows a single user, and is not great to read, let&amp;rsquo;s change it to:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;post&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/login&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; username&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; password &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;body&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;isValidCredentials&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;username&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; password&lt;span style="color:#eceff4"&gt;))&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;session&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; username&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Logged in&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Invalid username or password&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s better, and for the isValidCredentials() we&amp;rsquo;ll check against an array of objects like so:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; validCredentials &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#eceff4"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; username&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;demo&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; password&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;password&amp;#34;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; username&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Jane&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; password&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;password&amp;#34;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; username&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Fred&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; password&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;password&amp;#34;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;function isValidCredentials&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;username&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; password&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; validCredentials&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;some&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;cred&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; cred&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;username &lt;span style="color:#81a1c1"&gt;===&lt;/span&gt; username &lt;span style="color:#81a1c1"&gt;&amp;amp;&amp;amp;&lt;/span&gt; cred&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;password &lt;span style="color:#81a1c1"&gt;===&lt;/span&gt; password
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you haven&amp;rsquo;t met the JavaScript &lt;code&gt;.some()&lt;/code&gt; method, it&amp;rsquo;s used to run a callback function against the elements in an array until it returns true or comes to the end of an array.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;ve made a few changes, lets revisit the complete server.js code:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; npm install cookie&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;parser express express&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;session
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; express &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;express&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; cookieParser &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;cookie-parser&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; session &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;express-session&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; bodyParser &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;body-parser&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; app &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; express&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; Set up view engine
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;set&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;views&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;views&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;set&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;view engine&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;ejs&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;use&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;cookieParser&lt;span style="color:#eceff4"&gt;());&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;use&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;bodyParser&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;urlencoded&lt;span style="color:#eceff4"&gt;({&lt;/span&gt; extended&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;false&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; session middleware
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;use&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; session&lt;span style="color:#eceff4"&gt;({&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; secret&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;REtKU9xyvahuHGd3&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; Replace with a strong secret key
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; resave&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;false&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; saveUninitialized&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;true&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cookie&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; secure&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;false&lt;/span&gt; &lt;span style="color:#eceff4"&gt;},&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; Set to &lt;span style="color:#81a1c1"&gt;true&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; using HTTPS
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;session&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;Hello &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;session&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;!&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;render&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;login.ejs&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; Route to clear the session
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/logout&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;session&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;destroy&lt;span style="color:#eceff4"&gt;((&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Error clearing session&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Session cleared&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; validCredentials &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#eceff4"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; username&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;demo&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; password&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;password&amp;#34;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; username&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Jane&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; password&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;password&amp;#34;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; username&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Fred&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; password&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;password&amp;#34;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;function isValidCredentials&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;username&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; password&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; validCredentials&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;some&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;cred&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; cred&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;username &lt;span style="color:#81a1c1"&gt;===&lt;/span&gt; username &lt;span style="color:#81a1c1"&gt;&amp;amp;&amp;amp;&lt;/span&gt; cred&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;password &lt;span style="color:#81a1c1"&gt;===&lt;/span&gt; password
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;post&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/login&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; username&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; password &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;body&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;isValidCredentials&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;username&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; password&lt;span style="color:#eceff4"&gt;))&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;session&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; username&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Logged in&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Invalid username or password&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; Start the server
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; PORT &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#b48ead"&gt;3000&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;listen&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;PORT&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; console&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;log&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;Server is running on port &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;PORT&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="plaintext-passwords"&gt;Plaintext passwords&lt;/h4&gt;
&lt;p&gt;It&amp;rsquo;s a bad idea to ever store passwords in plaintext anywhere. A solution for this is to hash the password before storing it, then when we need to test a password a user has entered, we test the hash of the password the user has entered against the hashes we have stored. I&amp;rsquo;m being very casual in my language here - I should probably be saying &lt;em&gt;salting&lt;/em&gt; and &lt;em&gt;hashing&lt;/em&gt;. For the purposes of this discussion the idea is to turn each password into gobbledygook in such a way it&amp;rsquo;s not possible to turn it back into the password.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;re going to use the &lt;a href="https://www.npmjs.com/package/bcrypt"&gt;bcrypt&lt;/a&gt; to do the heavy lifting for us since it&amp;rsquo;s going to be more cryptographically sound than anything we could write.&lt;/p&gt;
&lt;p&gt;The encryption process is resource intensive, so these are going to be async operations.It&amp;rsquo;s a small trade-off for the security we&amp;rsquo;re adding.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; bcrypt &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;bcrypt&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; validCredentials &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#eceff4"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; username&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;demo&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; hashedPassword&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;$2b$10$MYd23sm2O1AuAU1l0sPV7enE.XkJpTYC4fga1Dm8Wx33u/8T.L9HC&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; username&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Jane&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; hashedPassword&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;$2b$10$MYd23sm2O1AuAU1l0sPV7enE.XkJpTYC4fga1Dm8Wx33u/8T.L9HC&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; username&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Fred&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; hashedPassword&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;$2b$10$MYd23sm2O1AuAU1l0sPV7enE.XkJpTYC4fga1Dm8Wx33u/8T.L9HC&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;async function isValidCredentials&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;username&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; password&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; user &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; validCredentials&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;find&lt;span style="color:#eceff4"&gt;((&lt;/span&gt;cred&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; cred&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;username &lt;span style="color:#81a1c1"&gt;===&lt;/span&gt; username&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;!&lt;/span&gt;user&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;false&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; await bcrypt&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;compare&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;password&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; user&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;hashedPassword&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;post&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/login&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; async &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; username&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; password &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;body&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;await isValidCredentials&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;username&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; password&lt;span style="color:#eceff4"&gt;))&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;session&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; username&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Logged in&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Invalid username or password&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Okay, now we have a login system, with safeish password storage and session management so the user doesn&amp;rsquo;t have to log in on every page.&lt;/p&gt;
&lt;h3 id="persisting-sessions"&gt;Persisting sessions&lt;/h3&gt;
&lt;p&gt;One last thing before we wrap up this overly long post. Currently, if Jane is logged in, and the server is rebooted, when she returns, her session will have been eliminated. That&amp;rsquo;s to say, her browser will pass the session id in it&amp;rsquo;s cookie, but the server won&amp;rsquo;t recognise it and will force her to log in again. That&amp;rsquo;s not the end of the world (in fact a future improvement should probably be to expire sessions every now and then) but it would be nicer if the session information survived server reboots.&lt;/p&gt;
&lt;p&gt;By default, &lt;code&gt;express-session&lt;/code&gt; uses a memory store, but this can be swapped out for other types of stores. Frequently, production apps will use a database of some kind to keep the session data, but for a single instance app with a hundred or so users a simpler system is just to use the host file system. Such a thing is built into express-session in the form of &lt;code&gt;session-file-store&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Implementing this is simple, we just need to declare a variable for the class, then include it in our initialisation of the session middleware.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; FileStore &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;session-file-store&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)(&lt;/span&gt;session&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; app &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; express&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; Set up view engine
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;set&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;views&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;views&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;set&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;view engine&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;ejs&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;use&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;cookieParser&lt;span style="color:#eceff4"&gt;());&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;use&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;bodyParser&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;urlencoded&lt;span style="color:#eceff4"&gt;({&lt;/span&gt; extended&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;false&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; session middleware
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;use&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; session&lt;span style="color:#eceff4"&gt;({&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; secret&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;REtKU9xyvahuHGd3&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; Replace with a strong secret key
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; resave&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;false&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; saveUninitialized&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;true&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cookie&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; secure&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;false&lt;/span&gt; &lt;span style="color:#eceff4"&gt;},&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; store&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; new FileStore&lt;span style="color:#eceff4"&gt;({&lt;/span&gt;logFn&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; function&lt;span style="color:#eceff4"&gt;(){}})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You don&amp;rsquo;t need the business with &lt;code&gt;logFn&lt;/code&gt;, that&amp;rsquo;s just a hack to subdue the logs. Without it, express-session logs an error each time a session id arrives in a cookie and there&amp;rsquo;s no corresponding file for it. That happens all the time when I&amp;rsquo;m developing so I foolishly turn it off.&lt;/p&gt;
&lt;p&gt;Now every time a session is created, it will be stored as a text file of JSON in the sessions directory. When a browser makes a request, the express-session will check for a file matching the session id from the cookie, and load the session data from it if needed.&lt;/p&gt;
&lt;p&gt;Since express-session is now dealing with our cookies, we can eliminate cookie-parser.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s where we&amp;rsquo;re up to:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; express &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;express&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; session &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;express-session&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; bodyParser &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;body-parser&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; bcrypt &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;bcrypt&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; FileStore &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;session-file-store&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)(&lt;/span&gt;session&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; app &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; express&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; Set up view engine
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;set&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;views&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;views&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;set&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;view engine&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;ejs&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;use&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;bodyParser&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;urlencoded&lt;span style="color:#eceff4"&gt;({&lt;/span&gt; extended&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;false&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; session middleware
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;use&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; session&lt;span style="color:#eceff4"&gt;({&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; secret&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;REtKU9xyvahuHGd3&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; Replace with a strong secret key
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; resave&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;false&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; saveUninitialized&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;true&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cookie&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; secure&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;false&lt;/span&gt; &lt;span style="color:#eceff4"&gt;},&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; store&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; new FileStore&lt;span style="color:#eceff4"&gt;({&lt;/span&gt;logFn&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; function&lt;span style="color:#eceff4"&gt;(){}})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;session&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;Hello &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;session&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;!&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;render&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;login.ejs&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; Route to clear the session
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/logout&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;session&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;destroy&lt;span style="color:#eceff4"&gt;((&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Error clearing session&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Session cleared&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; validCredentials &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#eceff4"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; username&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;demo&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; hashedPassword&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;$2b$10$MYd23sm2O1AuAU1l0sPV7enE.XkJpTYC4fga1Dm8Wx33u/8T.L9HC&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; username&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Jane&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; hashedPassword&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;$2b$10$MYd23sm2O1AuAU1l0sPV7enE.XkJpTYC4fga1Dm8Wx33u/8T.L9HC&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; username&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Fred&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; hashedPassword&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;$2b$10$MYd23sm2O1AuAU1l0sPV7enE.XkJpTYC4fga1Dm8Wx33u/8T.L9HC&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;async function isValidCredentials&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;username&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; password&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; user &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; validCredentials&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;find&lt;span style="color:#eceff4"&gt;((&lt;/span&gt;cred&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; cred&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;username &lt;span style="color:#81a1c1"&gt;===&lt;/span&gt; username&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;!&lt;/span&gt;user&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;false&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; await bcrypt&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;compare&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;password&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; user&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;hashedPassword&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;post&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/login&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; async &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; username&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; password &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;body&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;await isValidCredentials&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;username&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; password&lt;span style="color:#eceff4"&gt;))&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;session&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; username&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Logged in&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Invalid username or password&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; Start the server
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; PORT &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#b48ead"&gt;3000&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;listen&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;PORT&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; console&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;log&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;Server is running on port &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;PORT&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="where-next"&gt;Where next?&lt;/h3&gt;
&lt;p&gt;It&amp;rsquo;s been a bit of a trek to get to this point, so I&amp;rsquo;m winding this up here, and we&amp;rsquo;ll take it to the next level in a future post. Some of the next steps to explore are to move our secrets out of the source file, and to use &lt;a href="https://www.npmjs.com/package/passport"&gt;Passport.js&lt;/a&gt; like the two million other projects who downloaded it this week.&lt;/p&gt;</description></item><item><title>LLM coding question comparison using Ollama</title><link>https://blog.iankulin.com/llm-coding-question-comparison-using-ollama/</link><pubDate>Mon, 29 Jul 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/llm-coding-question-comparison-using-ollama/</guid><description>&lt;p&gt;Now Ollama has made it simple enough for anyone who can use a terminal to run large language models locally, naturally I&amp;rsquo;ve gone overboard downloading too many to play with. I&amp;rsquo;m increasingly feeling they definitely have a place in the devops/coding arsenal of tools, but which model is best?&lt;/p&gt;
&lt;p&gt;If you go on HuggingFace to look at a new model you&amp;rsquo;re interested, they often have great comparisons like this.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://huggingface.co/deepseek-ai/DeepSeek-Coder-V2-Lite-Instruct"&gt;&lt;img src="https://blog.iankulin.com/images/performance.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;There has been a lot of work in crafting these and other benchmarks which are often comprehensive and well thought out. I&amp;rsquo;ve also seen people doing fun things, like &lt;a href="https://youtu.be/B0uMFWAGUzI?t=145"&gt;this guy&lt;/a&gt;, who is just pasting coding challenges off a web page into an LLM and seeing if it can solve them (spoiler - mostly it can solve &lt;a href="https://www.w3resource.com/python-exercises/basic/python-basic-1-exercise-141.php"&gt;coding problems that are probably part of it&amp;rsquo;s training set&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;A factor to keep in mind when looking at these charts is that they are probably running unquantised (uncompressed is a close enough analogy) models on fleets of &lt;a href="https://www.nvidia.com/en-au/data-center/h100/"&gt;$60K graphics cards&lt;/a&gt;. I can use that if I pay them $20 a month and have an internet connection, but I want to pay $0 and run it on my M1 MacBook - that&amp;rsquo;s why I downloaded &lt;a href="https://ollama.com/"&gt;Ollama&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;So what follows is my completely unscientific testing of the models I&amp;rsquo;ve downloaded. Basically, I&amp;rsquo;ll ask them the same question (that I think I know the answer to) and time their response, and subjectively judge their output. For the question I&amp;rsquo;ve chosen:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Thinking about Docker, what&amp;rsquo;s the difference between [CMD] and [EntryPoint]?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This seems like a fairly specific bit of knowledge someone might want to know about, I know the answer, and the first page of google results are mostly good so there should be sufficient training data. I&amp;rsquo;ve put both terms in square brackets as a red herring, and same with the camelcase for ENTRYPOINT. I also didn&amp;rsquo;t specify that these are both usually defined in the dockerfile. I&amp;rsquo;ve had a go at the same question, and &lt;a href="https://blog.iankulin.com/dockerfile-cmd-vs-entrypoint/"&gt;published it here last week&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="the-results"&gt;The results&lt;/h3&gt;
&lt;p&gt;According to me, I am the winner.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/2309065-t2sarahconnor2.jpg" width="640" alt=""&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-07-03-at-2.37.43-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The word count for my answer would be a bit higher if we counted the text in my images, which we probably should. I made up my times by guessing what they&amp;rsquo;d be if you asked me this question.&lt;/p&gt;
&lt;h3 id="the-contestants"&gt;The contestants&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;codeqwen&lt;/em&gt; and &lt;a href="https://chat.deepseek.com/coder"&gt;&lt;em&gt;deepseek-coder&lt;/em&gt;&lt;/a&gt; are both optimised for chatting about code, which I&amp;rsquo;m claiming Docker skills are a legitimate part of. They both also do autocomplete, and I&amp;rsquo;m using &lt;em&gt;codeqwen&lt;/em&gt; for that in VSCode. &lt;em&gt;deepseek-coder&lt;/em&gt; is about twice as big, and you&amp;rsquo;d think better, which it was, but in my opinion, only a little. codeqwen had a clear error and &lt;em&gt;deepseek-coder&lt;/em&gt; was a bit muddled in some parts but did a great job of wrapping it up with an explanation of where you&amp;rsquo;d use both.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;phi3&lt;/em&gt;&amp;rsquo;s is small (half the size of most of the others here) and great for chatting. For general questions it&amp;rsquo;s very impressive for it&amp;rsquo;s size, but was useless for this task. It&amp;rsquo;s interesting to me that the smartest and the stupidest AI&amp;rsquo;s had the most to say, and that my explanation was almost the exact size of all the others.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/d11c1d71-92aa-43dd-9b44-39e7ac1b2727_1600x900.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;dolphin-mistral&lt;/em&gt;&amp;rsquo;s claim to fame is that it&amp;rsquo;s uncensored. So if you ask it how to build an improvised explosive, overturn an election, or trick a co-worker into falling in love with you, it will happily tell you - something the other models here cannot. Basically, it laughs at the first law of robotics. Even though launching a Docker container is not illegal or unethical, it had a reasonable, usable answer for our question.&lt;/p&gt;
&lt;p&gt;I tried two versions of &lt;em&gt;llama3&lt;/em&gt;. To explain the difference, we need to go into an explanation of how large language models work, which I don&amp;rsquo;t know, so I&amp;rsquo;m just going to hallucinate it for you:&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/input.jpg" width="800" alt=""&gt;
&lt;p&gt;If you vacuum up heaps of input (the training data), then filter out all the cruft (&amp;rsquo;the&amp;rsquo;, &amp;lsquo;a&amp;rsquo;, &amp;rsquo;not)&amp;rsquo;, then put it into a special multidimensional database so that similar things are near each other (eg &amp;lsquo;rose&amp;rsquo; is near &amp;lsquo;flower&amp;rsquo;, &amp;lsquo;red&amp;rsquo; and &amp;rsquo;titanic&amp;rsquo; and a long way from &amp;lsquo;bulldozer&amp;rsquo; and &amp;lsquo;antidisestablishmentarianism&amp;rsquo;) and the database also includes how far away from each other those things are, then it is very, very, big. Too big to put on my MacBook.&lt;/p&gt;
&lt;p&gt;We can reduce the size of it by &amp;lsquo;quantising&amp;rsquo; it which is a word I&amp;rsquo;ve heard on a podcast and might mean reducing the resolution of the numbers representing the distances between concepts in the database. This is the &amp;lsquo;q8&amp;rsquo; and &amp;lsquo;q4&amp;rsquo; you can see in the tags in the table. &amp;lsquo;q4&amp;rsquo; is going to be smaller, but less accurate than a &amp;lsquo;q8&amp;rsquo; of the same data.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the situation with these two versions of &lt;em&gt;llama3&lt;/em&gt; - one is more &amp;lsquo;compressed&amp;rsquo;. The relationship between the quantitation and the usefulness of the model is not linear for many applications, and that seems to be the case here. The bigger model did produce some more detail, but I actually preferred the output of the smaller one.&lt;/p&gt;
&lt;h3 id="conclusion"&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;It would be foolish to put much weight on a conclusion from a single run of a dubious test analyzed by a subjective carbon based lifeform, but anyway&amp;hellip;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;All of these models produced a useful starting point except &lt;em&gt;phi3&lt;/em&gt;. You probably could have just used what the others produced and gone on working with your dockerfile and things would have worked out fine.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;llama3&lt;/em&gt;&amp;rsquo;s performance matches my experience of other times I&amp;rsquo;ve been using it. It&amp;rsquo;s just pretty great for what it is.&lt;/li&gt;
&lt;li&gt;Most of the explanations over-complicated things - this probably could have been fixed with a better prompt.&lt;/li&gt;
&lt;li&gt;It&amp;rsquo;s sort of magic when you think this is most of the world&amp;rsquo;s knowledge squashed into 4GB on my laptop in a form I can just ask questions of&lt;/li&gt;
&lt;li&gt;There is room for improvement&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It&amp;rsquo;s easy to imagine that if these models were able to reach out to the internet and check what they&amp;rsquo;d come up with then generate a response by combining their first guess and their new knowledge, they&amp;rsquo;d be a lot better. That&amp;rsquo;s basically what &lt;a href="https://www.perplexity.ai/search/thinking-about-docker-what-s-t-9wJXPl_iTv2BLE60.QXgGA"&gt;Perplexity&lt;/a&gt; does, and it&amp;rsquo;s output is better than any of my local models, and it includes some of the links it used which would probably clear up any further questions. That sort of functionality is not far away for local models, and something like it is running in &lt;a href="https://useanything.com/"&gt;AnythingLLM&lt;/a&gt;, so I expect these will be indispensable tools in a year.&lt;/p&gt;</description></item><item><title>dockerfile - CMD vs ENTRYPOINT</title><link>https://blog.iankulin.com/dockerfile-cmd-vs-entrypoint/</link><pubDate>Mon, 22 Jul 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/dockerfile-cmd-vs-entrypoint/</guid><description>&lt;p&gt;There are two entries we often have at the end of a &lt;code&gt;dockerfile&lt;/code&gt; (which is the file that tells Docker how an image is to be built).&lt;/p&gt;
&lt;p&gt;They are similar in that when the container is launched from an image, these commands will be executed. For example, both of the dockerfiles below will print &amp;ldquo;Hello World&amp;rdquo; when run.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;doc-&lt;/code&gt;entry:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;FROM debian:stable-slim
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ENTRYPOINT [&amp;#34;echo&amp;#34;, &amp;#34;Hello World from ENTRYPOINT&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;doc-cmd&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;FROM debian:stable-slim
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;CMD [&amp;#34;echo&amp;#34;, &amp;#34;Hello World&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-07-03-at-1.45.26-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The key difference between them is that CMD can be overridden:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-07-03-at-1.47.44-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;You can see from this that the ENTRYPOINT command is just added on to by the extra command line argument, but the CMD one is replaced entirely.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s possible to have an ENTRYPOINT and a CMD in your dockerfile:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;doc-both&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;FROM debian:stable-slim
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ENTRYPOINT [&amp;#34;echo&amp;#34;, &amp;#34;Hello World from ENTRYPOINT&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;CMD [&amp;#34;&amp;amp; Hello World from CMD&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-07-03-at-1.55.45-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Naturally, only the CMD is overridden if we pass in extra values.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-07-03-at-1.58.40-pm.png" alt=""&gt;&lt;/p&gt;
&lt;h4 id="other-things-of-note"&gt;Other things of note&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Although these demos are at the command line, we&amp;rsquo;d see the same behaviour if we&amp;rsquo;d added a CMD to a docker compose file and started the container that way.&lt;/li&gt;
&lt;li&gt;You can have multiple ENTRYPOINTs or CMDs in a file, they are all ignored except the last one.&lt;/li&gt;
&lt;li&gt;The best place to learn more about &lt;a href="https://docs.docker.com/reference/dockerfile/#entrypoint"&gt;ENTRYPOINT&lt;/a&gt; and &lt;a href="https://docs.docker.com/reference/dockerfile/#cmd"&gt;CMD&lt;/a&gt; is in the official &lt;a href="https://docs.docker.com/reference/dockerfile/"&gt;Docker docs for dockerfile&lt;/a&gt;, not from an AI.&lt;/li&gt;
&lt;li&gt;Most times, what you&amp;rsquo;re looking at the end of your dockerfile is ENTRYPOINT. Just use CMD to add a default behaviour that you&amp;rsquo;re happy for your image users to overide.&lt;/li&gt;
&lt;li&gt;Don&amp;rsquo;t confuse either of these with RUN - that happens during the image build, ENTRYPOINT and CMD are used when the container is launched/run.&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Using LLMs for coding</title><link>https://blog.iankulin.com/using-llms-for-coding/</link><pubDate>Mon, 01 Jul 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/using-llms-for-coding/</guid><description>&lt;p&gt;&lt;a href="https://madmuseum.org/events/ghost-shell"&gt;&lt;img src="https://blog.iankulin.com/images/ghost-in-the-shell_07.jpg" alt="Ghost in the Shell
© Manga Entertainment 1996
"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This post looks at the context for some of my thinking about AI for supporting software development, and where I&amp;rsquo;ve landed on it for the time being.&lt;/p&gt;
&lt;h3 id="the-landscape"&gt;The landscape&lt;/h3&gt;
&lt;p&gt;I &lt;a href="https://blog.iankulin.com/chatgpts-code-writing/"&gt;briefly wrote about ChatGPT&amp;rsquo;s&lt;/a&gt; coding ability at the end of 2022. The wide availability of this tool marked the beginning of what I think can fairly be described as a revolution. The controversies that have crystalised since have not dampened my amazement of this step forward in what compute can do, especially around natural language processing.&lt;/p&gt;
&lt;p&gt;The next big news in this story was Microsoft&amp;rsquo;s launch of Github Copilot. In business terms this was a brilliant move - owning the most popular code editor, and leveraging the world&amp;rsquo;s biggest collection of public code to create a product that &lt;a href="https://visualstudiomagazine.com/Articles/2024/02/05/copilot-numbers.aspx"&gt;millions of people&lt;/a&gt; are prepared to pay $10 a month for can only be regarded as a success.&lt;/p&gt;
&lt;p&gt;At the same time as Microsoft established a new revenue stream, LLMs have been an exciting area of open source growth, especially the excellent Python libraries and the tools in the LangChain ecosystem.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s not all rainbows and unicorns though - there&amp;rsquo;s a few valid points that AI skeptics have coalesced around.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Training data - although this is a bigger issue for general models (where masses of web content has been vacuumed up) than it is for code, it is still an issue. If a model is trained on some non-permissively licensed code, and the generative AI I&amp;rsquo;m using includes that code in a commit, then a license, or at least some ethics have been breached.&lt;/li&gt;
&lt;li&gt;Quality (1) - You can see from the feature images in many of the posts in this blog during my MidJourney enthusiasm that generative AI is not perfect. Before I abandoned them I started to prefer the mangled writing and fingers of the engines, but no one wants the software equivalent of mangled fingers in their codebases. I suspect this particular aspect of the quality of the code will probably have a technological solution - we&amp;rsquo;re in the very early days after all.&lt;/li&gt;
&lt;li&gt;Quality (2) - A trickier quality problem is people writing code using AI where they do not fully understand the code they are committing. I imagine this is going to be a growing issue for projects, especially anything with a profit motive such as bug bonuses. Projects have mechanisms like code reviews and pull requests, but if submissions can be low-effort and checking them is high-effort, that asymmetry is going to be painful.&lt;/li&gt;
&lt;li&gt;Poisoned well - As the amount of AI code in codebases increases, then AI is trained on those codebases this will quickly become a snake eating it&amp;rsquo;s tail as AI is training itself on it&amp;rsquo;s own code. If allowed, this would tend to slowly evolve future codebases to use techniques favoured by early coding LLMs. The current amount of machine influenced code on &lt;a href="https://decrypt.co/147191/no-human-programmers-five-years-ai-stability-ceo"&gt;GitHub is definitely not 41%&lt;/a&gt; but it must be some, and is likely to increase, so this is a factor that will need some thought.&lt;/li&gt;
&lt;li&gt;Exfiltrating code - if you use an external LLM, such as GitHub Copilot to write commercial code, who can see your code? Since it&amp;rsquo;s being transmitted to the AI in order to make autocomplete suggestions, the answer is Microsoft, or some other company. How does that intersect with your company&amp;rsquo;s policies? I assume, based on the questions I&amp;rsquo;ve asked Copilot over the last year, that I&amp;rsquo;d never be considered for a coding job at Microsoft :-)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="i-for-one-welcome-our-new-robot-overlords"&gt;I, for one, welcome our new robot overlords&lt;/h3&gt;
&lt;img src="https://blog.iankulin.com/images/hailants-1.jpg" width="512" alt=""&gt;
&lt;p&gt;In an industry particularly known for excessive hype-cycles, it&amp;rsquo;s important to critically examine what we&amp;rsquo;re doing, but for the moment, I&amp;rsquo;ve landed on the position that these are good tools for me to use. Here&amp;rsquo;s my thinking.&lt;/p&gt;
&lt;p&gt;My situation is that I&amp;rsquo;m a very experienced developer, with solid expertise in several languages and programing paradigms, and with a degree that was strong in looking at the meta level of languages and software development processes, but, I&amp;rsquo;ve got no professional experience in modern languages. Because of this, a lot of my process has been knowing what I wanted to do, using google or stack overflow to figure out the mechanics of that in whatever language I&amp;rsquo;m using, then translating that into the context of the code I&amp;rsquo;m working on. Generative AI fits extremely well into that need - instead of jumping into a browser window to look something up, I&amp;rsquo;m just writing a descriptive comment of my intentions, then tabbing through the suggestions to chose an approach.&lt;/p&gt;
&lt;p&gt;My particular style is also well suited to these tools - I like clear, simple to reason about code. If I can write a pure function for something, I do. I like to break my code up into separated concerns with clear interfaces, I don&amp;rsquo;t prematurely optimise. I use descriptive variable, function and object names. I like to work with established, well documented languages and popular libraries, and I prefer to reduce external dependencies. All of these habits make it easier for an AI assistant to access the context of what I&amp;rsquo;m doing, and therefore to make better quality suggestions.&lt;/p&gt;
&lt;h3 id="my-journey"&gt;My journey&lt;/h3&gt;
&lt;p&gt;I started out using ChatGPT 3 then 3.5 as a sort of super-google/stack-overflow eliminator.&lt;/p&gt;
&lt;p&gt;Then with the public launch of &lt;a href="https://github.com/features/copilot"&gt;GitHub Copilot&lt;/a&gt;, I trialed that in VSCode and it was a great experience. I guess they didn&amp;rsquo;t invent the idea for the greyed out auto-complete suggestion you can tab to accept, but it feels like a natural way to work with this stuff.&lt;/p&gt;
&lt;p&gt;I paid for Copilot for a couple of months. But then heard about &lt;a href="https://codeium.com/"&gt;Codium&lt;/a&gt;, probably on &lt;a href="https://syntax.fm/show/728/ai-superpowers-with-kevin-hou-and-codeium"&gt;Syntax&lt;/a&gt;, which is free for individual developers (for now - thank you VC funding). I haven&amp;rsquo;t done any careful comparisons, but its definitely of the same order. I suspect Copilot is doing something better with the local context. For example I use a plain text accounting system called &lt;a href="https://beancount.github.io/docs/beancount_language_syntax.html#transactions"&gt;Bean Count&lt;/a&gt; in VSCode. Copilot is able to understand these transactions and make much useful suggestions than Codium. I assume this is just inferred from my local files since there would not be much training data for them, and it suggests the correct accounts based on the payees which must be from local context.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve probably done more work with Codium, 80% of it on Javascript, than with Copilot. It&amp;rsquo;s definitely a workable solution and a great choice if you want a Copilot type experience without paying for it, or have questions about Microsoft&amp;rsquo;s training data.&lt;/p&gt;
&lt;p&gt;More recently I&amp;rsquo;ve started playing with local models to avoid the problem of exfiltrating my code - I strongly feel I can&amp;rsquo;t use AI assisted coding with client code if I don&amp;rsquo;t know what&amp;rsquo;s happening it. If I can run a local model, that problem is avoided.&lt;/p&gt;
&lt;p&gt;I code on an early M1 MacBook, so &lt;a href="https://ollama.com/"&gt;Ollama&lt;/a&gt; is an easy to use choice. I&amp;rsquo;ve tried &lt;a href="https://ai.meta.com/blog/meta-llama-3/"&gt;llama3&lt;/a&gt; and &lt;a href="https://qwenlm.github.io/blog/codeqwen1.5/"&gt;codeqwen1.5&lt;/a&gt; in the terminal for a bit, but missed the ChatGPT web experience. To get that back, I&amp;rsquo;ve been running &lt;a href="https://openwebui.com/"&gt;Open WebUI&lt;/a&gt; in a docker container.&lt;/p&gt;
&lt;p&gt;More recently, I&amp;rsquo;ve installed the &lt;a href="https://docs.continue.dev/intro"&gt;Continue&lt;/a&gt; VSCode extension that allows those Ollama managed models to work in VSCode, including the auto-suggestions (following &lt;a href="https://www.davegray.codes/posts/bye-copilot-how-to-create-a-local-ai-coding-assistant-for-free"&gt;Dave Gray&amp;rsquo;s blog post&lt;/a&gt;). I&amp;rsquo;ve got a few long flights coming up over the next week, so it will be good to be able to work offline with that help.&lt;/p&gt;
&lt;p&gt;I haven&amp;rsquo;t really done more than play with CodeQwen in VSCode via Continue so far, but my initial impression is that it&amp;rsquo;s comparable to Copilot, although the extra second of waiting for auto-suggestions did make me look up M3max MacBook pricing. Logic tells you that a 4GB model on a MacBook is going to be less capable than the giant GPT4 powered Copilot, but &lt;a href="https://qwenlm.github.io/blog/codeqwen1.5/"&gt;this comparison&lt;/a&gt; suggests the difference is not an order of magnitude (although the model size is). From limited playing around in small JavaScript codebases, they seem similar, with the local model just being a bit slower.&lt;/p&gt;
&lt;p&gt;If this is a revolution, it&amp;rsquo;s one we&amp;rsquo;re at the start of, and I certainly reserve the right to change my mind about AI assistance in coding, but I suspect it&amp;rsquo;s our future and I&amp;rsquo;m excited at the productivity boost it currently gives me working in languages I&amp;rsquo;m new to.&lt;/p&gt;</description></item><item><title>Virtual Hosts on "Static Web Server"</title><link>https://blog.iankulin.com/virtual-hosts-on-static-web-server/</link><pubDate>Mon, 22 Apr 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/virtual-hosts-on-static-web-server/</guid><description>&lt;p&gt;I&amp;rsquo;ve been running &lt;a href="https://blog.iankulin.com/nginx-proxy-manager/"&gt;NGINX Proxy Manager&lt;/a&gt; (NPM) in my homelab for a bit, and I&amp;rsquo;ve been meaning to clean up the VPS that runs most of my websites and public facing servers, so I&amp;rsquo;m considering running NGINX Proxy Manager on that VPS. While NGINX Proxy Manager wraps up the configs in a beautiful GUI, in the process you lose some of NGINXs capabilities. In particular there&amp;rsquo;s no GUI way to serve static virtual hosts from NGINX Proxy Manager.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s a pity, since it seems like it wouldn&amp;rsquo;t be a lot of work, but in any case we can easily just run another web server and proxy to it. I guess I could run another instance of NGINX, but on $5 VPSs memory is a bit scarce, and since my sites are extremely low traffic, perhaps something a bit lighter is in order.&lt;/p&gt;
&lt;p&gt;An option mentioned in several posts for this exact situation is the very well named &lt;a href="https://static-web-server.net/"&gt;Static Web Server&lt;/a&gt;. It&amp;rsquo;s written in Rust, the docker image is less that 10MB, and it claims to be able to serve for virtual hosts. Let&amp;rsquo;s give it a try.&lt;/p&gt;
&lt;h3 id="directory-structure"&gt;Directory Structure&lt;/h3&gt;
&lt;p&gt;My routine setup now is that everything runs in docker. There&amp;rsquo;s a directory under the home directory of my user for named for the container which holds the &lt;code&gt;docker-compose.yml&lt;/code&gt;. Underneath that there&amp;rsquo;s two sub-directories: &lt;code&gt;data&lt;/code&gt; - which holds all the app data, and &lt;code&gt;config&lt;/code&gt; - which holds the app settings files.&lt;/p&gt;
&lt;p&gt;In the data directory, I want to have sub-directories for each virtual host, then inside them a &lt;code&gt;public&lt;/code&gt; directory which will hold the files to be served. This will allow me to keep config or other files for each virtual host in the directory above &lt;code&gt;public&lt;/code&gt; which should not be accessible. That seems like a good arrangement so I can manage each virtual host in git and pull the sites down from a repository into their directory without things like the &lt;code&gt;.gitignore&lt;/code&gt; being exposed to the world.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-04-20-at-8.15.14-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-04-20-at-8.15.14-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m running Tailscale on this VM, so it can be referred to by any of the addresses &lt;code&gt;100.124.218.26&lt;/code&gt;, &lt;code&gt;192.168.100.35&lt;/code&gt; or &lt;code&gt;ct357-sws&lt;/code&gt;. These are going to be stand- ins for the different virtual host names. The index.html files you can see are all different; I&amp;rsquo;ve edited them so each one just outputs the name of the directory it is being served from. eg:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-04-20-at-9.20.26-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-04-20-at-9.20.26-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="docker"&gt;Docker&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;version&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;3.3&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;services&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; website&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; image&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; joseluisq&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;static&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;web&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;server&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#b48ead"&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; container_name&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;sws&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ports&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt; &lt;span style="color:#b48ead"&gt;80&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#b48ead"&gt;80&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; restart&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; unless&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;stopped
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; environment&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt; SERVER_CONFIG_FILE&lt;span style="color:#81a1c1"&gt;=/&lt;/span&gt;etc&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;config&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;toml
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; volumes&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;./&lt;/span&gt;data&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;./&lt;/span&gt;config&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;config&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;toml&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;etc&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;config&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;toml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You&amp;rsquo;re probably familiar with docker-compose by now, but if not, the volumes lines probably need explaining. They map &amp;lsquo;real-world&amp;rsquo; directories on our server, to the internal directories &lt;em&gt;inside&lt;/em&gt; the container. This is a useful thing - we could copy our files into the container but those changes would be ephemeral, they&amp;rsquo;d be gone next time we restart the container. By creating these links, we can store the web server configs and data on our server, but from the point of view of the app, they appear inside.&lt;/p&gt;
&lt;p&gt;An example will make this more concrete, lets look at the first line:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;./&lt;/span&gt;data&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is saying that the directory inside the container named &lt;code&gt;/var&lt;/code&gt; is mapped onto the external directory &lt;code&gt;./data&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Consider our file in the tree listing above &lt;code&gt;~/sws/data/100.124.218.26/public/index.html&lt;/code&gt; the web server running inside the container will see that file at &lt;code&gt;/var/100.124.218.26/public/index.html&lt;/code&gt; It seems weird at first, but you soon get used to this sort of container directory mapping maths.&lt;/p&gt;
&lt;h3 id="config"&gt;Config&lt;/h3&gt;
&lt;p&gt;There&amp;rsquo;s excellent documentation about Static Web Server on their &lt;a href="https://static-web-server.net/"&gt;web site&lt;/a&gt;, so I&amp;rsquo;m not going to go through the whole &lt;code&gt;config.toml&lt;/code&gt; file, in any case, I&amp;rsquo;ve left nearly everything on the defaults. Instead, lets scroll down to the bottom and look at the virtual hosts settings.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;[[&lt;/span&gt;advanced&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;virtual&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;hosts&lt;span style="color:#eceff4"&gt;]]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;host &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;100.124.218.26&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;/var/100.124.218.26/public&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;[[&lt;/span&gt;advanced&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;virtual&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;hosts&lt;span style="color:#eceff4"&gt;]]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;host &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;ct357-sws&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;/var/ct357-sws/public&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;[[&lt;/span&gt;advanced&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;virtual&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;hosts&lt;span style="color:#eceff4"&gt;]]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;host &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;192.168.100.35&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;/var/192.168.100.35/public&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="it-works"&gt;It works&lt;/h3&gt;
&lt;p&gt;A quick &lt;code&gt;docker compose up -d&lt;/code&gt;, and we&amp;rsquo;re in business.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-04-21-at-8.52.14-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-04-21-at-8.57.27-pm.png" alt=""&gt;&lt;/p&gt;</description></item><item><title>Deploying a Node app in Docker</title><link>https://blog.iankulin.com/deploying-a-node-app-in-docker/</link><pubDate>Sun, 31 Mar 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/deploying-a-node-app-in-docker/</guid><description>&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Cargo_ship#/media/File:Cargo_Ship_Puerto_Cortes.jpg"&gt;&lt;img src="https://blog.iankulin.com/images/cargo_ship_puerto_cortes.jpg" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;When I wrote the install instructions for mdserver (little Markdown server Node app) on it&amp;rsquo;s &lt;a href="https://github.com/IanKulin/mdserver"&gt;github page&lt;/a&gt; it was something like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Have node.js installed and working&lt;/li&gt;
&lt;li&gt;Clone the repo&lt;/li&gt;
&lt;li&gt;Start with &lt;code&gt;npm start&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Which is great if you know &lt;a href="https://blog.iankulin.com/installing-a-node-app-on-a-server/"&gt;how to do those things&lt;/a&gt; (they are bread and butter to a web dev) but not if you&amp;rsquo;re a self-hoster who just wants a web server that converts markdown to HTML on the fly. For any situation where you just want to use the app, what you probably want is a Docker image of the app.&lt;/p&gt;
&lt;h3 id="docker"&gt;Docker&lt;/h3&gt;
&lt;p&gt;Docker &lt;em&gt;containers&lt;/em&gt; are similar to a virtual machine in the sense that they need to be hosted, and are relatively isolated from other processes except is some explicitly defined ways. Docker images are stored in repositories (the default one is &lt;a href="https://hub.docker.com/"&gt;DockerHub&lt;/a&gt;). It probably sounds like a wasteful process to ship an entire operating system with every little app - this is somewhat overcome by the images being built up in layers, and duplicated layers don&amp;rsquo;t need to be shlipped around since they are cached.&lt;/p&gt;
&lt;p&gt;So to deploy our Node app as a Docker container, we need to build an image, and store it on Docker Hub. From there, users can deploy it from their command lines by calling it directly or declaratively with a &lt;code&gt;docker-compose.yml&lt;/code&gt; file.&lt;/p&gt;
&lt;h3 id="dockerfile"&gt;Dockerfile&lt;/h3&gt;
&lt;p&gt;To create a Docker image of our app that can be distributed, we run the &lt;code&gt;docker build&lt;/code&gt; command which reads a file named &lt;code&gt;Dockerfile&lt;/code&gt; to create the image. Here&amp;rsquo;s the &lt;code&gt;Dockerfile&lt;/code&gt; for the mdserver app.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;# Use an official Node.js runtime as the base image
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;FROM node:20-alpine
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;# Set the working directory in the container
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;WORKDIR /usr/src/app
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;# Copy package.json and package-lock.json to the working directory
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;COPY package*.json .
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;RUN npm install
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;# Copy the rest of the application source code to the container
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;COPY ./server.js .
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;COPY ./LICENSE .
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;COPY ./readme.md .
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;# Expose the port that the Node.js app will listen on
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;EXPOSE 3000
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;# Define the command to start your Node.js app
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;CMD [ &amp;#34;node&amp;#34;, &amp;#34;server.js&amp;#34; ]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;FROM node:20-alpine&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;FROM&lt;/code&gt; keyword specifies the base image we&amp;rsquo;re starting with. It could just be something like a Debian base, then in the following commands in the Dockerfile we&amp;rsquo;d install Node, but Node (and lots of other web dev tool builders) have provided &lt;a href="https://hub.docker.com/_/node/"&gt;official Docker images&lt;/a&gt; that they have crafted to make it easier for us. In this case I&amp;rsquo;m specifying that I want the image based on the lightweight Alpine Linux distro, with version 20 of Node installed on it.&lt;/p&gt;
&lt;p&gt;Note that when Node created the &lt;code&gt;node:20-alpine&lt;/code&gt; image, their Dockerfile probably started with &lt;code&gt;FROM [alpine:3.18](https://hub.docker.com/_/alpine)&lt;/code&gt; - you see? Layers.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;WORKDIR /usr/src/app&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;So now we&amp;rsquo;ve got a container with a fully working install of Linux (or close enough to that so we can think of it like that - I&amp;rsquo;m pretty sure there&amp;rsquo;s no kernel). This command is saying that all the next commands are going to refer to the working directory &lt;em&gt;inside&lt;/em&gt; the container as &lt;code&gt;/usr/src/app&lt;/code&gt;. In effect its as if you&amp;rsquo;d ssh&amp;rsquo;d in and run &lt;code&gt;mkdir /usr/scr/app &amp;amp;&amp;amp; cd mkdir /usr/scr/app&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;COPY package*.json .&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve written before about the &lt;a href="https://blog.iankulin.com/sorting-out-node-package-dependencies-when-cloning-old-repos/"&gt;intricacies of the package files in Node&lt;/a&gt;. Basically these files (&lt;code&gt;package.json&lt;/code&gt; and &lt;code&gt;package-lock.json&lt;/code&gt;) specify the dependencies for out project. The dependencies are all sitting in the node_modules folder, but having a listing of them in the package files means we can just check them into source control and not worry about that bloated folder.&lt;/p&gt;
&lt;p&gt;This &lt;code&gt;COPY&lt;/code&gt; command, just copies them both into out container image - the &lt;code&gt;.&lt;/code&gt; at the end just means the current working directory inside the container - ie &lt;code&gt;/usr/src/app&lt;/code&gt; in our case.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;RUN npm install&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Now the package files are inside the container, we just run &lt;code&gt;npm install&lt;/code&gt;, exactly the same as we would on a server, in order to download all of the dependencies for our app into the container. If that looks like you could just say &lt;code&gt;RUN&lt;/code&gt; then run any old Linux command then you&amp;rsquo;re getting the hang of it. You can &lt;code&gt;apt install&lt;/code&gt; stuff, &lt;code&gt;echo&lt;/code&gt; a line into a config file - whatever you need.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;COPY ./server.js .&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;COPY ./LICENSE .&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;COPY ./readme.md .&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;For this trivial app, we only need the one source file, but I like to copy the license and readme in as well. It&amp;rsquo;s possible for future users of the container to run commands in their copy of the container, so it&amp;rsquo;s conceivable someone might look in here to read them. Once again, the second parameter specifies where in the container the files are copied to, and once again we&amp;rsquo;ve said the current work dir.&lt;/p&gt;
&lt;p&gt;You&amp;rsquo;ll very commonly see &lt;code&gt;COPY . .&lt;/code&gt; in Dockerfiles. This is saying copy all the files in the current directory to the working directory inside the container image. I guess that way you don&amp;rsquo;t miss anything, but do I really need a copy of my &lt;code&gt;Dockerfile&lt;/code&gt;, my vscode settings, my &lt;code&gt;node_modules&lt;/code&gt; folder in the image? No. There is a way to avoid copying that stuff in - add a &lt;code&gt;.dockerignore&lt;/code&gt; file to your project. This works exactly like a &lt;code&gt;.gitignore&lt;/code&gt; - you just list one file or directory per line, and then the &lt;code&gt;COPY&lt;/code&gt; command will know not to bother with it,&lt;/p&gt;
&lt;p&gt;&lt;code&gt;EXPOSE 3000&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;My node app is set to use port 3000, so we need to tell Docker to open that port for us since by default everything&amp;rsquo;s locked down. Note that the user of this container won&amp;rsquo;t be stuck with this decision, when they start the container, they can specify where in the outside world this internal container is going to be mapped to. That could be port &lt;code&gt;8080&lt;/code&gt;, &lt;code&gt;80&lt;/code&gt; or whatever.&lt;/p&gt;
&lt;p&gt;CMD [ &amp;ldquo;node&amp;rdquo;, &amp;ldquo;server.js&amp;rdquo; ]&lt;/p&gt;
&lt;p&gt;Finally, Docker needs to know how to start our app. This command is not being run now (when we&amp;rsquo;re building the image) it&amp;rsquo;s used by Docker when it launches the containerised app. I&amp;rsquo;m not sure why it is an array of strings instead of just a string, but it is. Just break it at each space in your command to run the app.&lt;/p&gt;
&lt;p&gt;If you look back at the list of manual steps I started this post with, you&amp;rsquo;ll see that we&amp;rsquo;ve pretty much just re-implemented them in the Dockerfile:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;set up a node environment&lt;/li&gt;
&lt;li&gt;copy the files in&lt;/li&gt;
&lt;li&gt;run server.js&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Obviously there&amp;rsquo;s lots more you can do with Dockerfiles, but the underlying concept is pretty straightforward - you&amp;rsquo;re setting up the whole environment for your app to run in so it can be mostly independent from its host OS.&lt;/p&gt;
&lt;h3 id="build-step"&gt;Build Step&lt;/h3&gt;
&lt;p&gt;To create the image from the Dockerfile, you are going to need Docker. I&amp;rsquo;m working on a Mac so I&amp;rsquo;ve got &lt;a href="https://www.docker.com/products/docker-desktop/"&gt;Docker Desktop&lt;/a&gt; installed. When it&amp;rsquo;s running there&amp;rsquo;s the little whale up in the toolbar.&lt;/p&gt;
&lt;p&gt;You don&amp;rsquo;t need a &lt;a href="https://hub.docker.com/"&gt;DockerHub&lt;/a&gt; account to build the image, but you&amp;rsquo;ll need one to upload it, and for naming your build, so head there now and create one. It is possible to use other registries for storing your images, but by default docker looks at it&amp;rsquo;s own registry, so that&amp;rsquo;s the best place to start when you&amp;rsquo;re figuring things out.&lt;/p&gt;
&lt;p&gt;When you&amp;rsquo;re working with Docker images and registries, to uniquely identify an image, it usually has a name format like this:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;username&amp;gt;/&amp;lt;imagename&amp;gt;:&amp;lt;tag&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Usually the tag will be a version number, or perhaps &lt;code&gt;:latest&lt;/code&gt;. The build command for our image could be this:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;docker build -t iankulin/mdserver:latest&lt;/code&gt; .&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-12.27.51-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;This will load the .dockerignore then step through the Dockerfile to build our image. The image is stored away by Docker - we don&amp;rsquo;t need to worry about where. You can get the list at the command line with &lt;code&gt;docker images&lt;/code&gt;, or if you&amp;rsquo;re running Docker Desktop, on the &amp;lsquo;images&amp;rsquo; tab.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-12.36.22-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-12.36.22-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I have skipped quite a bit of detail about the build step and options. For example I sometimes use the &lt;code&gt;--platform&lt;/code&gt; flag to specify &lt;code&gt;linux/amd64&lt;/code&gt; if I&amp;rsquo;m testing on one of my homelab VMs rather than &lt;code&gt;linux/arm64&lt;/code&gt; if I&amp;rsquo;m running the container on the mac. Also, we don&amp;rsquo;t have to just build from the local machine, it&amp;rsquo;s just as straightforward to build from your GitHub repo as part of a CI/CD system. I&amp;rsquo;m not planning to go into any of that today, except I will force it to build for x86 since it is my plan to test on the homelab VM&amp;rsquo;s.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;docker build --platform linux/amd64 -t iankulin/mdserver:latest .&lt;/code&gt;&lt;/p&gt;
&lt;h3 id="the-registry"&gt;The Registry&lt;/h3&gt;
&lt;p&gt;So the images can be available to anyone, we need to make it available in a Docker Registry. The most famous one of these, and the one set up as the default for all the docker commands, is &lt;a href="https://hub.docker.com/"&gt;Docker Hub&lt;/a&gt;. Despite some &lt;a href="https://www.docker.com/blog/no-longer-sunsetting-the-free-team-plan/"&gt;missteps&lt;/a&gt;, it&amp;rsquo;s still the main place people and organisations store docker images.&lt;/p&gt;
&lt;p&gt;In order to push an image to a registry, we need to be signed in to it. As I&amp;rsquo;m using Docker Desktop, and I&amp;rsquo;m signed in to Docker Hub on that. I&amp;rsquo;ve skipped that step, but if you needed to, you&amp;rsquo;d use the &lt;a href="https://docs.docker.com/engine/reference/commandline/login/"&gt;docker login&lt;/a&gt; command. Once that&amp;rsquo;s sorted, the push is easy:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;docker push iankulin/mdserver:latest&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-2.12.15-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-2.12.15-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In this output, you can see some of the efficiencies of the layers - docker recognises (from the UUIDs) that the Alpine and Node layers are ones that I pulled down from it when I was creating the image locally, so it doesn&amp;rsquo;t send them back to Docker Hub.&lt;/p&gt;
&lt;p&gt;If we go to Docker Hub and search for mdserver, we should be able to find it now available to the public.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-2.10.44-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-2.10.44-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="using-the-image"&gt;Using the image&lt;/h3&gt;
&lt;p&gt;Now it&amp;rsquo;s in the registry, anyone can use it as easily as any of the Docker images - NGINX, Jellyfin - whatever. I provide a docker-compose file in the repo, it looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;version: &amp;#39;3&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;services:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; mdserver:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; image: iankulin/mdserver:latest
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ports:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &amp;#34;3000:3000&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; volumes:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - ./public:/usr/src/app/public 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So any user can just drop that into a directory, and enter &lt;code&gt;docker compose up -d&lt;/code&gt; then the image will be pulled down and run, and they&amp;rsquo;ll have their server live.&lt;/p&gt;</description></item><item><title>Hosting Your Own Docker Registry</title><link>https://blog.iankulin.com/hosting-your-own-docker-registry/</link><pubDate>Mon, 25 Mar 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/hosting-your-own-docker-registry/</guid><description>&lt;p&gt;&lt;a href="https://unsplash.com/photos/architectural-photography-of-cargo-containers-stack-hP4ZiN1_kdk?utm_content=creditShareLink&amp;utm_medium=referral&amp;utm_source=unsplash"&gt;&lt;img src="https://blog.iankulin.com/images/tri-eptaroka-mardiana-hp4zin1_kdk-unsplash.jpg" width="640" alt="Photo by Tri Eptaroka Mardianam on Unsplash
"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The Docker &lt;a href="https://docs.docker.com/subscription/core-subscription/details/"&gt;Personal (ie free tier) plan&lt;/a&gt; currently allows one private repository, but even if you want to pay for the next level where you can have unlimited repositories, you may still want to host your own private registry - it&amp;rsquo;s going to be quicker inside your network, and you won&amp;rsquo;t run up against Docker&amp;rsquo;s pull/push limits if you are hammering it with your CI/CD system.&lt;/p&gt;
&lt;p&gt;There are fancier tools, but in this post we&amp;rsquo;ll look at the basics of how to use the official registry app from Docker.&lt;/p&gt;
&lt;h3 id="initial-setup"&gt;Initial Setup&lt;/h3&gt;
&lt;p&gt;The &lt;a href="https://hub.docker.com/_/registry"&gt;registry app&lt;/a&gt; is (unsurprisingly) dockerised. So I&amp;rsquo;ve created a directory for the &lt;code&gt;docker-compose.yml&lt;/code&gt; file, and a &lt;code&gt;data&lt;/code&gt; sub directory.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-23-at-7.50.43-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;And the yaml.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;services: registry: image: registry:2 container_name: registry restart: unless-stopped ports: - &amp;#34;5000:5000&amp;#34; environment: REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /data volumes: - ./data:/data
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;docker compose up&lt;/code&gt;, and bingo. Our registry is live.&lt;/p&gt;
&lt;h3 id="creating-an-image"&gt;Creating an image&lt;/h3&gt;
&lt;p&gt;Now our registry is up, let&amp;rsquo;s jump over to another machine, and create an image to store in it. I&amp;rsquo;m only going to minimally explain this, since if you&amp;rsquo;re interested in your own registry, you&amp;rsquo;ve probably been down this path.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-23-at-1.24.50-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;dockerfile&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;FROM busyboxRUN mkdir /appCOPY script.sh /app/script.shWORKDIR /appRUN chmod +x script.shCMD [&amp;#34;./script.sh&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;script.sh&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#5e81ac;font-style:italic"&gt;#!/bin/shecho &amp;#34;Hello from Docker!&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So basically, this image contains a small Linux distro, and all it does is run a script that outputs &amp;ldquo;Hello from Docker!&amp;rdquo; to the console. We can build our image by switching into the directory with the &lt;code&gt;dockerfile&lt;/code&gt; and running:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo docker build -t hello-docker .
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-23-at-1.37.15-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;If you want to run it to check my docker skills, use&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo docker run hello-docker
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="pushing--insecure"&gt;Pushing &amp;amp; Insecure&lt;/h3&gt;
&lt;p&gt;Now I want to push the image we&amp;rsquo;ve created to the new registry we set up earlier, but we&amp;rsquo;re going to run into a problem.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m using two Debian virtual machines (LXCs actually) both on my homelab network. They&amp;rsquo;ve been named with Tailscale to make things clearer in the screenshots. (If you&amp;rsquo;re following along you&amp;rsquo;ll probably be using IP addresses). Importantly, there are no TLS certificates, self-signed or otherwise.&lt;/p&gt;
&lt;p&gt;First we need to tag our image to include the registry name:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo docker tag hello-docker:latest ct390-docker-reg:5000/hello-docker
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-23-at-1.53.18-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;And we&amp;rsquo;ll try to push it up to our registry with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker push ct390-docker-reg:5000/hello-docker
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-23-at-2.35.40-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;What&amp;rsquo;s happening is that Docker would (quite reasonably) prefer to only work over secure connections. We can override this on this machine for today&amp;rsquo;s demo purposes by adding an exception for our self-hosted registry. You&amp;rsquo;ll need to create the file &lt;code&gt;/etc/docker/daemon.json&lt;/code&gt; and add the registry that&amp;rsquo;s going to be allowed like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{ &amp;#34;insecure-registries&amp;#34; : [ &amp;#34;ct390-docker-reg:5000&amp;#34; ]}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If we restart docker and retry the push now, it should work:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-23-at-2.43.02-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;That looks like it worked. If we wanted to check, we can just hit an endpoint on the registry:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl http://ct390-docker-reg:5000/v2/_catalog
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-23-at-2.49.36-pm.png" alt=""&gt;&lt;/p&gt;
&lt;h3 id="pulling--insecure"&gt;Pulling &amp;amp; Insecure&lt;/h3&gt;
&lt;p&gt;Of course the ultimate test is going to be to use this image from a third machine, so let&amp;rsquo;s spin one up with a clean docker install with no images and try to run the image we&amp;rsquo;ve just added to our registry.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;re going to have the same challenge pulling from a non-TLS registry as we had pushing to it, and the workaround is going to be exactly the same - add the registry to the insecure list in the &lt;code&gt;/etc/docker/daemon.json&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;echo &lt;span style="color:#a3be8c"&gt;&amp;#39;{ &amp;#34;insecure-registries&amp;#34; : [ &amp;#34;ct390-docker-reg:5000&amp;#34; ]}&amp;#39;&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;|&lt;/span&gt; sudo tee &lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;etc&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;docker&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;daemon&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;jsonsudo systemctl daemon&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;reloadsudo systemctl restart docker
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now we can run it. Since we don&amp;rsquo;t have the image locally yet, docker will pull it down for us from the registry before running it:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-03-23-at-3.19.03-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;And that&amp;rsquo;s it. Our own private Docker registry to store our images.&lt;/p&gt;
&lt;h4 id="references"&gt;References&lt;/h4&gt;
&lt;p&gt;In writing this post, I relied on some these resources:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Digital Ocean - &lt;a href="https://www.digitalocean.com/community/tutorials/how-to-set-up-a-private-docker-registry-on-ubuntu-20-04"&gt;How To Set Up a Private Docker Registry on Ubuntu 20.04&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Baeldung - &lt;a href="https://www.baeldung.com/ops/docker-private-registry"&gt;Configure a Private Docker Registry&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;O&amp;rsquo;Reilly - &lt;a href="https://www.oreilly.com/library/view/kubernetes-in-the/9781492043270/app03.html"&gt;Configuring Docker to Push or Pull from an Insecure Registry&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Certbot - removing a domain</title><link>https://blog.iankulin.com/certbot-removing-a-domain/</link><pubDate>Mon, 18 Mar 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/certbot-removing-a-domain/</guid><description>&lt;p&gt;I had a number of domains all running on one host when I first set them up with certbot. One started to be serious, so I moved it to another host and ran certbot there. That all worked perfectly, but of course, the old domain is still part of the original certificate, so when I went to renew it, it came up with some errors.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s a few commands that are going to help navigate this situation if you&amp;rsquo;ve found yourself in the same spot:&lt;/p&gt;
&lt;h4 id="show-all-certificates-and-which-domains"&gt;Show all certificates and which domains&lt;/h4&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo certbot certificates
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="renew-just-some-domains"&gt;Renew just some domains&lt;/h4&gt;
&lt;p&gt;There&amp;rsquo;s no way to delete a domain from a certificate, the process is to renew it, but just for the domains you want to keep. Certbot will notice you&amp;rsquo;ve missed some and warn you that you&amp;rsquo;re effectively deleting them.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo certbot --cert-name &amp;lt;certifcate-name&amp;gt; -d &amp;lt;domain1&amp;gt; -d &amp;lt;domain-2&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>Beginning Node App Security</title><link>https://blog.iankulin.com/beginning-node-app-security/</link><pubDate>Fri, 16 Feb 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/beginning-node-app-security/</guid><description>&lt;p&gt;Since I&amp;rsquo;m using Tailscale to painlessly manage all my networking on the homeserver here and my remotes, I&amp;rsquo;ve had the luxury of being a bit casual about the security of my internal apps and self hosted dev tools. I&amp;rsquo;m currently iterating on a web app that requires public access, and is therefore up on a VPS and exposed to all the evils of the open internet.&lt;/p&gt;
&lt;p&gt;I am in no way a security expert, but here&amp;rsquo;s a few of the (reasonably simple) steps I&amp;rsquo;ve taken to secure my node app.&lt;/p&gt;
&lt;h3 id="put-it-behind-nginx"&gt;Put it behind Nginx&lt;/h3&gt;
&lt;p&gt;I could just change the port number of the Node app to listen to port 80 and connect it directly to the world, but Nginx is battle hardened for outward facing tasks so that seems safer. Additionally, it opens up a lot of functionality such as putting my app on a subdomain and some other security options we&amp;rsquo;ll come to. Putting Nginx in front of your app like this is called &amp;lsquo;reverse proxying&amp;rsquo;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	server_name sub.example.com;	location / { 		proxy_pass http://localhost:3000;			proxy_http_version 1.1;		proxy_set_header Upgrade $http_upgrade;		proxy_set_header Connection &amp;#39;upgrade&amp;#39;;		proxy_set_header Host $host;		proxy_cache_bypass $http_upgrade;	}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="basic-auth"&gt;Basic Auth&lt;/h3&gt;
&lt;p&gt;One of the Nginx options is the ability to turn on &amp;lsquo;&lt;a href="https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-http-basic-authentication/"&gt;basic auth&lt;/a&gt;&amp;rsquo;. This can be enabled for a route, subdomain, or a whole domain. It forces a user to authenticate before being able to access resources from that area. It&amp;rsquo;s basic in the sense that the password is manually set on the server in a file that the nginx conf points to. Ideally your app will have comprehensive auth built in, but (especially when you are still developing it) basic auth is a quick and easy way to prevent all of the internet from accessing your app.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	server_name sub.example.com;	location / {		auth_basic &amp;#34;Secure app&amp;#34;;	 auth_basic_user_file /etc/nginx/.htpasswd;	}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="https"&gt;HTTPS&lt;/h3&gt;
&lt;p&gt;Enforcing SSL connections is much easier than it used to be (thank you L&lt;a href="https://blog.iankulin.com/certbot-lets-encrypt-are-great/"&gt;et&amp;rsquo;s Encrypt and Certbot&lt;/a&gt;) and it keeps all the data being sent between your app and the user&amp;rsquo;s browser encrypted - including the username and password you are using for your auth.&lt;/p&gt;
&lt;h3 id="logging"&gt;Logging&lt;/h3&gt;
&lt;p&gt;Ensuring that logs are turned on means that you&amp;rsquo;ve got some chance of detecting problems and possible attacks. In fact, you&amp;rsquo;ll probably be aghast at the number of bots that start accessing your server as soon as it&amp;rsquo;s live. For the most part, they are probing for the existence of known vulnerabilities in well known packages - may of the php. When I look up the IP addresses for these, they almost always come from China or Eastern Europe.&lt;/p&gt;
&lt;p&gt;Note that logging does involve some future maintenance. Logs need rotated and deleted or we&amp;rsquo;ll soon be running out of disk space.&lt;/p&gt;
&lt;h3 id="fail2ban"&gt;Fail2ban&lt;/h3&gt;
&lt;p&gt;Manually checking the logs is not effective, we need to automate this a bit. The things I&amp;rsquo;m looking for in my system is brute force attempts at breaking the basic auth, and the same with SSH.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/fail2ban/fail2ban"&gt;Fail2Ban&lt;/a&gt; can automate this (and many other things, but I&amp;rsquo;m just using these two) by scanning the logs for failed attempts. There are various settings to determine the thresholds - say check for 5 failed attempts in 10 minutes then ban the IP address for 30 minutes. Once the threshold is met, Fail2Ban updates iptables (the internal firewall) to block them.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ian&lt;span style="color:#bf616a"&gt;@&lt;/span&gt;novel&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;ironic&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;~$&lt;/span&gt; sudo fail2ban&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;client status sshdStatus &lt;span style="color:#81a1c1;font-weight:bold"&gt;for&lt;/span&gt; the jail&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; sshd&lt;span style="color:#81a1c1"&gt;|-&lt;/span&gt; Filter&lt;span style="color:#81a1c1"&gt;|&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;|-&lt;/span&gt; Currently failed&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;	&lt;span style="color:#b48ead"&gt;1&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;|&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;|-&lt;/span&gt; Total failed&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;	&lt;span style="color:#b48ead"&gt;2960&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;|&lt;/span&gt; &lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt; &lt;span style="color:#bf616a"&gt;File&lt;/span&gt; list&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;	&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;log&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;auth&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;log&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt; Actions &lt;span style="color:#81a1c1"&gt;|-&lt;/span&gt; Currently banned&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;	&lt;span style="color:#b48ead"&gt;6&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;|-&lt;/span&gt; Total banned&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;	&lt;span style="color:#b48ead"&gt;483&lt;/span&gt; &lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt; Banned &lt;span style="color:#bf616a"&gt;IP&lt;/span&gt; list&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;	&lt;span style="color:#b48ead"&gt;218.92&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;&lt;span style="color:#b48ead"&gt;0.27&lt;/span&gt; &lt;span style="color:#b48ead"&gt;61.177&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;&lt;span style="color:#b48ead"&gt;172.136&lt;/span&gt; &lt;span style="color:#b48ead"&gt;61.177&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;&lt;span style="color:#b48ead"&gt;172.140&lt;/span&gt; &lt;span style="color:#b48ead"&gt;218.92&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;&lt;span style="color:#b48ead"&gt;0.113&lt;/span&gt; &lt;span style="color:#b48ead"&gt;218.92&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;&lt;span style="color:#b48ead"&gt;0.31&lt;/span&gt; &lt;span style="color:#b48ead"&gt;218.92&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;&lt;span style="color:#b48ead"&gt;0.76&lt;/span&gt;ian&lt;span style="color:#bf616a"&gt;@&lt;/span&gt;novel&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;ironic&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;~$&lt;/span&gt; sudo fail2ban&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;client status nginx&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;http&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;authStatus &lt;span style="color:#81a1c1;font-weight:bold"&gt;for&lt;/span&gt; the jail&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; nginx&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;http&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;auth&lt;span style="color:#81a1c1"&gt;|-&lt;/span&gt; Filter&lt;span style="color:#81a1c1"&gt;|&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;|-&lt;/span&gt; Currently failed&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;	&lt;span style="color:#b48ead"&gt;1&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;|&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;|-&lt;/span&gt; Total failed&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;	&lt;span style="color:#b48ead"&gt;6&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;|&lt;/span&gt; &lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt; &lt;span style="color:#bf616a"&gt;File&lt;/span&gt; list&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;	&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;log&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;nginx&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;error&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;log&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt; Actions &lt;span style="color:#81a1c1"&gt;|-&lt;/span&gt; Currently banned&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;	&lt;span style="color:#b48ead"&gt;0&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;|-&lt;/span&gt; Total banned&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;	&lt;span style="color:#b48ead"&gt;1&lt;/span&gt; &lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt; Banned &lt;span style="color:#bf616a"&gt;IP&lt;/span&gt; list&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;	ian&lt;span style="color:#bf616a"&gt;@&lt;/span&gt;novel&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;ironic&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;~$&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In the output above, you can see there are 6 ip addresses currently blocked for trying to crack SSH, and there&amp;rsquo;s been 483 banned in the couple of days since I turned it on - so this is a very common attack vector. The basic auth one just has a single ban (from when I tested it). I&amp;rsquo;m not sure why I like looking at the list of bans so much, but I do!&lt;/p&gt;
&lt;h3 id="cloud-firewall"&gt;Cloud Firewall&lt;/h3&gt;
&lt;p&gt;Many VPS providers will have a cloud firewall (although they may call it something else). We can use this to lock down all the ports we are not using to massively reduce the attack surface for this machine. One of the very appealing things about this firewall which is external to the VPS is that it&amp;rsquo;s accessed via the VPS provider web interface - so it&amp;rsquo;s not possible to lock yourself out if you make a mistake - as opposed to when you&amp;rsquo;re SSHd in and fiddling with iptables.&lt;/p&gt;
&lt;p&gt;Since this VPS is just running web apps, I just have ports 80, 443 and 22 open.&lt;/p&gt;
&lt;h3 id="no-root-login"&gt;No root login&lt;/h3&gt;
&lt;p&gt;By default, Ubuntu does not allow root login by password, but once I&amp;rsquo;ve added a new user and added them to the sudo group, I turn it off entirely. Most of those SSH attempts that failed would have been trying common user names including root, so may as well take it out of the possibilities.&lt;/p&gt;
&lt;h3 id="ssh-keys"&gt;SSH keys&lt;/h3&gt;
&lt;p&gt;Apart from being more convenient, well managed SSH keys are much safer than using passwords. So my new user copies up their keys and I set that user to no login with password as well.&lt;/p&gt;
&lt;h3 id="updates"&gt;Updates&lt;/h3&gt;
&lt;p&gt;One of the wonderful things about open source and the modern web, is that as new vulnerabilities are being discovered, they are being patched. We only get them if we run updates though. It&amp;rsquo;s possible (and recommended) to use &lt;a href="https://www.digitalocean.com/community/tutorials/how-to-keep-ubuntu-20-04-servers-updated"&gt;automatic updates&lt;/a&gt;, but I have a weekend ansible routine to do them that I like to look at the output of to check everything&amp;rsquo;s healthy.&lt;/p&gt;
&lt;h3 id="monitoring"&gt;Monitoring&lt;/h3&gt;
&lt;p&gt;I use a &lt;a href="https://blog.iankulin.com/simple-api-endpoint-in-go/"&gt;very simple monitoring system&lt;/a&gt; for all the VM&amp;rsquo;s and containers in my Tailnet - just checking the root disk space and available memory. This is exposed as an http endpoint, then checked by &lt;a href="https://blog.iankulin.com/uptime-kuma-nfty/"&gt;Uptime Kuma&lt;/a&gt;. That&amp;rsquo;s better than nothing, but for a production system not really enough. This is one of the areas I&amp;rsquo;ll revisit in the future.&lt;/p&gt;</description></item><item><title>User Sessions &amp; Cookies in Node</title><link>https://blog.iankulin.com/user-sessions-cookies-in-node/</link><pubDate>Fri, 09 Feb 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/user-sessions-cookies-in-node/</guid><description>&lt;p&gt;When you are learning app development, you can create all sorts of apps that work for you, but for any serious app, it&amp;rsquo;s going to need to authenticate users and persist sessions across visits. So much so, that as a professional developer, you&amp;rsquo;ll probably build that out first - it becomes a sort of boiler plate you always drop in.&lt;/p&gt;
&lt;p&gt;In this post, focusing on the server side, using node, express, and particularly express-session, I&amp;rsquo;ll try and build up from nothing to a reasonable usable user login system explaining the increasing complexity and reasons for it. To follow along you&amp;rsquo;ll need basic familiarity with node and express.&lt;/p&gt;
&lt;h3 id="the-problem-were-addressing"&gt;The problem we&amp;rsquo;re addressing&lt;/h3&gt;
&lt;p&gt;For most web applications, we need to persist state &lt;em&gt;per user&lt;/em&gt;. For example, if you go to a drawing app and start a drawing, you want it to be there when you come back to the app. Additionally, you don&amp;rsquo;t want to come back to someone else&amp;rsquo;s half-drawn app, or, have them drawing over your picture. What we really want is something like this:&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/20240126-sessions-1.drawio-1.png" width="283" alt=""&gt;
&lt;p&gt;User 1 sees their picture of the star, and User 2 sees their picture of a heart.&lt;/p&gt;
&lt;p&gt;Since HTTP is &lt;a href="https://en.wikipedia.org/wiki/Stateless_protocol"&gt;stateless&lt;/a&gt; - a request to &lt;code&gt;/picture&lt;/code&gt; from one user is indistinguishable from another users request to &lt;code&gt;/picture&lt;/code&gt; - so we need to add something to allow the server to distinguish between the two. The something would be a bit of &lt;em&gt;state&lt;/em&gt; that the server passes back to the user, then the user sends it in with their actions so the server can identify them.&lt;/p&gt;
&lt;p&gt;There are a few of ways to do this. The first (which is not the subject of this post) is to store that in the URL. For example, when a user in the above app requests to create a picture, we could generate a &lt;a href="https://www.techtarget.com/searchwindowsserver/definition/GUID-global-unique-identifier"&gt;GUID&lt;/a&gt; for them, then redirect them to a URL based on that - perhaps /picture/cbe34f. Thereafter, all their requests could include that GUID. This can be a useful way of managing session state and has some affordances that the other method does not, but it&amp;rsquo;s not the most common.&lt;/p&gt;
&lt;p&gt;Another system that was extensively used in the early days of the web was to embed a hidden input with the GUID in the HTML returned to the user. When the user submitted the form later, the GUID was available&lt;/p&gt;
&lt;p&gt;The most common method is for the server to create a bit of state (our GUID), send it to the user and have the user&amp;rsquo;s browser store it, and return it with every request. You will know this bit of state as a &lt;em&gt;cookie&lt;/em&gt;.&lt;/p&gt;
&lt;h3 id="code-example"&gt;Code example&lt;/h3&gt;
&lt;p&gt;Enough theory, lets look at some code. If you google &amp;lsquo;simple node express session&amp;rsquo; you&amp;rsquo;ll find this, or something almost identical. Instead of the state we&amp;rsquo;re trying to persist being a picture of a heart or a star, we&amp;rsquo;re trying to remember how many times each user has visited our web site. Note these are separate counts for each user - a new visitor will start at zero, then count up each time they come back or refresh their page.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;re using a couple of packages, so to run this example, you&amp;rsquo;ll first need to install them with &lt;code&gt;npm i express express-session&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; express &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;express&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; session &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;express-session&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; app &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; express&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;use&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;session&lt;span style="color:#eceff4"&gt;({&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; secret&lt;span style="color:#81a1c1"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#39;your-secret-key&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; resave&lt;span style="color:#81a1c1"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;false&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; saveUninitialized&lt;span style="color:#81a1c1"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;/&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#616e87;font-style:italic"&gt;// Access session data
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;session&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;views&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; req&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;session&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;views&lt;span style="color:#81a1c1"&gt;++&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; req&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;session&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;views &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#b48ead"&gt;1&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;`Views: &lt;/span&gt;&lt;span style="color:#a3be8c"&gt;${&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;session&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;views&lt;span style="color:#a3be8c"&gt;}&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;listen&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;3000&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; console&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;log&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;Server is running on port 3000&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If we load this page from &lt;code&gt;http://localhost:3000&lt;/code&gt; it says &amp;ldquo;Views: 1&amp;rdquo;. Reloading the page it will say &amp;ldquo;Views: 2&amp;rdquo;. If I open a different browser and visit the server, we&amp;rsquo;ll be back to &amp;ldquo;Views: 1&amp;rdquo;. So what&amp;rsquo;s the magic that&amp;rsquo;s happening here?&lt;/p&gt;
&lt;p&gt;On the first request from a browser it hasn&amp;rsquo;t seen before, express-session is generating a session ID, and sending that back to the browser (along with &amp;lsquo;Views: 1&amp;rsquo;) and asking the browser to store it as a cookie. Thereafter, every request to this website &lt;code&gt;http://localhost:3000&lt;/code&gt; will include the cookie containing the session ID. The number of views is being stored on the server in memory. Express-session does the work of looking up the number of views for a particular session ID returned in a cookie so we have the luxury of just grabbing that from &lt;code&gt;rec.session.views&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Thanks to the magic of browser development tools, we can have a look in the request header from the browser and see the cookie with it&amp;rsquo;s session ID contents:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-01-26-at-9.42.43-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s also possible to go and find the cookie your browser has stored. Again, this is easiest in the developer tools - look under &amp;lsquo;Storage&amp;rsquo;:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-01-26-at-10.19.16-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-01-26-at-10.19.16-am.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Currently, we&amp;rsquo;re only storing the links between session_ids and the number of views in memory in the server, so if the server restarts, we&amp;rsquo;ll lose them, and everyone will go back to &amp;lsquo;Views: 1&amp;rsquo;.&lt;/p&gt;
&lt;h3 id="persisting-across-server-restarts"&gt;Persisting across server restarts&lt;/h3&gt;
&lt;p&gt;If we want our view counts to not be reset to zero each time the server restarts, we&amp;rsquo;ll need to save them somehow. express-session doesn&amp;rsquo;t let us access its internal array of sessions, but it does provide a mechanism for that access with the concept of &lt;em&gt;stores&lt;/em&gt;. Usually we&amp;rsquo;d keep the sessions in a database, but as files is a simpler solution for a blog post. There is a package called &lt;code&gt;session-file-store&lt;/code&gt; that will slot into &lt;code&gt;expression-session&lt;/code&gt; that will just use the file system to persist the session-view count key pairs for us. After installing it with &lt;code&gt;npm i session-file-store&lt;/code&gt;, we just declare a const for it, and pass it in when initialising the session middleware.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; express &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;express&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; session &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;express-session&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; FileStore &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;session-file-store&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)(&lt;/span&gt;session&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; app &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; express&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;use&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; session&lt;span style="color:#eceff4"&gt;({&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; secret&lt;span style="color:#81a1c1"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;your-secret-key&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#616e87;font-style:italic"&gt;// This should be a secret, used to sign the session ID cookie
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; resave&lt;span style="color:#81a1c1"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;false&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; saveUninitialized&lt;span style="color:#81a1c1"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;true&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; store&lt;span style="color:#81a1c1"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;new&lt;/span&gt; FileStore&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; name&lt;span style="color:#81a1c1"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;session_cookie&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#616e87;font-style:italic"&gt;// Access session data
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;session&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;views&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; req&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;session&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;views&lt;span style="color:#81a1c1"&gt;++&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; req&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;session&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;views &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#b48ead"&gt;1&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;`Views: &lt;/span&gt;&lt;span style="color:#a3be8c"&gt;${&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;session&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;views&lt;span style="color:#a3be8c"&gt;}&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;listen&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;3000&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; console&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;log&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Server is running on port 3000&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now view counts for each browser are persisted even when the server is re-started. If we look inside one of the files we should see a view_count field:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-01-26-at-12.22.45-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-01-26-at-12.22.45-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The name of each file is the session id, but these don&amp;rsquo;t match the session id&amp;rsquo;s you see in the browser cookies - presumably because they&amp;rsquo;ve been encrypted by the secret we used when establishing the express-session.&lt;/p&gt;
&lt;p&gt;So this is a better experience - our users see their own view counts increasing on each refresh, and the view counts don&amp;rsquo;t get lost when you take the server down for maintenance. But what happens if the user wants to pick up their phone and check their view count. It&amp;rsquo;s a different browser without the cookie containing the session id, so they&amp;rsquo;ll get &amp;lsquo;View: 1&amp;rsquo; again.&lt;/p&gt;
&lt;p&gt;Or, what if your partner sits down at your laptop to check &lt;em&gt;their&lt;/em&gt; view count, expecting it to be &amp;lsquo;1&amp;rsquo; because they&amp;rsquo;ve never visited this page, and they see your view count instead?&lt;/p&gt;
&lt;h3 id="users"&gt;Users&lt;/h3&gt;
&lt;p&gt;Our current system is really based around browser instances, but we want it to be about people. So a better system, and one that you&amp;rsquo;ll be used to is the concept of &lt;em&gt;logging in&lt;/em&gt; as a &lt;em&gt;user&lt;/em&gt;. This can solve both the problems we described above - users will be able to log in from any browser and see only their own data.&lt;/p&gt;
&lt;p&gt;This is going to take things up a substantial step in complexity because now instead of two bits of data (the cookie with the session_id, and the session store linking the session_id and the view count) there is going to be:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The cookie with the &lt;em&gt;session_id&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;The session store linking the &lt;em&gt;session_id&lt;/em&gt; and the &lt;em&gt;user&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;A new store we will have to manage that links the &lt;em&gt;user&lt;/em&gt; and the &lt;em&gt;view_count&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Also, we&amp;rsquo;ll need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Some mechanism to create a user&lt;/li&gt;
&lt;li&gt;Some mechanism to log in&lt;/li&gt;
&lt;li&gt;And log out&lt;/li&gt;
&lt;li&gt;If there&amp;rsquo;s no user logged in, redirect to the log in page&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="log-out"&gt;Log out&lt;/h4&gt;
&lt;p&gt;Logging the user out is reasonably simple - we just delete the session information. In a real app we&amp;rsquo;d have a &amp;rsquo;log out&amp;rsquo; button of some sort, but for simplicity here, I&amp;rsquo;m just going to add a &amp;rsquo;log out&amp;rsquo; route.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app.get(&amp;#34;/logout&amp;#34;, (req, res) =&amp;gt; { req.session.destroy((err) =&amp;gt; { if (err) { return console.log(err); } res.clearCookie(&amp;#34;session_cookie&amp;#34;); res.redirect(&amp;#34;/&amp;#34;); });});
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;clearCookie()&lt;/code&gt; tells the browser to delete the cookie - which just saves us from console messages later when the browser sends it, but express-session can&amp;rsquo;t find the matching session.&lt;/p&gt;
&lt;p&gt;If you add this to the code from earlier and run it, view will count up as before, but when you visit the &lt;code&gt;/logout&lt;/code&gt; endpoint views will be set back to 1.&lt;/p&gt;
&lt;h4 id="users--view-counts"&gt;Users &amp;amp; view counts&lt;/h4&gt;
&lt;p&gt;In a real application we&amp;rsquo;d be keeping this in a database, but again for simplicity of this demo, I&amp;rsquo;m just going to keep an array of objects and persist them as a text file. The array will be user_views, and somewhere at the top of our code we&amp;rsquo;ll add:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;let user_views = [];// if the user_views.json file exists, read it and parse it to user_views arrayif (fs.existsSync(&amp;#34;./user_views.json&amp;#34;)) { user_views = JSON.parse(fs.readFileSync(&amp;#34;./user_views.json&amp;#34;));}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="creating-a-user"&gt;Creating a user&lt;/h4&gt;
&lt;p&gt;We&amp;rsquo;ll use a route parameter to create a new user (like &lt;code&gt;/create/jane&lt;/code&gt; or /&lt;code&gt;create/robert&lt;/code&gt; where the second part is accessible in &lt;code&gt;req.params&lt;/code&gt;). That user will be set as the session user, then we&amp;rsquo;ll add it to our array and save the array to disk.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/create/:user&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; user &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;params&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;user&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; views &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#b48ead"&gt;0&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;session&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;user &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; user&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; user_views&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;push&lt;span style="color:#eceff4"&gt;({&lt;/span&gt; user&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; views &lt;span style="color:#eceff4"&gt;});&lt;/span&gt; fs&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;writeFile&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;./user_views.json&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; JSON&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;stringify&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;user_views&lt;span style="color:#eceff4"&gt;),&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;);&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;User &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;user&lt;span style="color:#eceff4"&gt;}&lt;/span&gt; created &lt;span style="color:#81a1c1"&gt;&amp;amp;&lt;/span&gt; logged &lt;span style="color:#81a1c1;font-weight:bold"&gt;in&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="logging-a-user-in"&gt;Logging a user in&lt;/h4&gt;
&lt;p&gt;If the user is in our array, we&amp;rsquo;ll set the session user to it, otherwise redirect to the create endpoint:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app.get(&amp;#34;/login/:user&amp;#34;, (req, res) =&amp;gt; { // see if this user is in the user_views array if (user_views.find((u) =&amp;gt; u.user === req.params.user)) { req.session.user = req.params.user; res.send(`User ${req.params.user} logged in`); return; } else { res.redirect(`/create/${req.params.user}`); }});
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="showing-the-view-count"&gt;Showing the view count&lt;/h4&gt;
&lt;p&gt;The default route that shows our view count has a few jobs to do:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Check we&amp;rsquo;re logged in, if not, tell the user to log in&lt;/li&gt;
&lt;li&gt;If we are logged in, fetch the view count for that user&lt;/li&gt;
&lt;li&gt;Increment the view count&lt;/li&gt;
&lt;li&gt;Show it to the user&lt;/li&gt;
&lt;li&gt;Re-save the user_views data since we&amp;rsquo;ve mutated it&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;session&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;user&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; user_view &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; user_views&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;find&lt;span style="color:#eceff4"&gt;((&lt;/span&gt;u&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; u&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;user &lt;span style="color:#81a1c1"&gt;===&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;session&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;user&lt;span style="color:#eceff4"&gt;);&lt;/span&gt; user_view&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;views&lt;span style="color:#81a1c1"&gt;++&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;User &lt;span style="color:#a3be8c"&gt;&amp;#34;${req.session.user}&amp;#34;&lt;/span&gt; has &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;user_view&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;views&lt;span style="color:#eceff4"&gt;}&lt;/span&gt; views&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt; writeUserViewFile&lt;span style="color:#eceff4"&gt;();&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Please log in&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So that&amp;rsquo;s our system for associating the view counts with a user done. Here&amp;rsquo;s the whole thing where we&amp;rsquo;re up to (I&amp;rsquo;ve done a little refactoring).&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; express &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;express&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; session &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;express-session&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; FileStore &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;session-file-store&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)(&lt;/span&gt;session&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; fs &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;fs&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; app &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; express&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; user_view_file &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;./user_views.json&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; cookie_name &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;session_cookie&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;let&lt;/span&gt; user_views &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#eceff4"&gt;[];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;// if the user_views.json file exists, read it and parse it to user_views array
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;fs&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;existsSync&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;user_view_file&lt;span style="color:#eceff4"&gt;))&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; user_views &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; JSON&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;parse&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;fs&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;readFileSync&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;user_view_file&lt;span style="color:#eceff4"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;function&lt;/span&gt; writeUserViewFile&lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; fs&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;writeFile&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;user_view_file&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; JSON&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;stringify&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;user_views&lt;span style="color:#eceff4"&gt;),&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; console&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;log&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;function&lt;/span&gt; findUser&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;user&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; user_views&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;find&lt;span style="color:#eceff4"&gt;((&lt;/span&gt;u&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; u&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;user &lt;span style="color:#81a1c1"&gt;===&lt;/span&gt; user&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;use&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; session&lt;span style="color:#eceff4"&gt;({&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; secret&lt;span style="color:#81a1c1"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;your-secret-keyz&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#616e87;font-style:italic"&gt;// This should be a secret, used to sign the session ID cookie
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; resave&lt;span style="color:#81a1c1"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;false&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; saveUninitialized&lt;span style="color:#81a1c1"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;true&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; store&lt;span style="color:#81a1c1"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;new&lt;/span&gt; FileStore&lt;span style="color:#eceff4"&gt;(),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; name&lt;span style="color:#81a1c1"&gt;:&lt;/span&gt; cookie_name&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;session&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;user&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; user_view &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; findUser&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;session&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;user&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; user_view&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;views&lt;span style="color:#81a1c1"&gt;++&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;`User &amp;#34;&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;${&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;session&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;user&lt;span style="color:#a3be8c"&gt;}&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34; has &lt;/span&gt;&lt;span style="color:#a3be8c"&gt;${&lt;/span&gt;user_view&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;views&lt;span style="color:#a3be8c"&gt;}&lt;/span&gt;&lt;span style="color:#a3be8c"&gt; views`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; writeUserViewFile&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Please log in&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/logout&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; req&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;session&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;destroy&lt;span style="color:#eceff4"&gt;((&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; console&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;log&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;clearCookie&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;cookie_name&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;redirect&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/create/:user&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; user &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; req&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;params&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;user&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; views &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#b48ead"&gt;0&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; req&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;session&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;user &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; user&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; user_views&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;push&lt;span style="color:#eceff4"&gt;({&lt;/span&gt; user&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; views &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; writeUserViewFile&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;`User &lt;/span&gt;&lt;span style="color:#a3be8c"&gt;${&lt;/span&gt;user&lt;span style="color:#a3be8c"&gt;}&lt;/span&gt;&lt;span style="color:#a3be8c"&gt; created &amp;amp; logged in`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/login/:user&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#616e87;font-style:italic"&gt;// see if this user is in the user_views array
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;findUser&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;params&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;user&lt;span style="color:#eceff4"&gt;))&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; req&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;session&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;user &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; req&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;params&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;user&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;`User &lt;/span&gt;&lt;span style="color:#a3be8c"&gt;${&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;params&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;user&lt;span style="color:#a3be8c"&gt;}&lt;/span&gt;&lt;span style="color:#a3be8c"&gt; logged in`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;redirect&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;`/create/&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;${&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;params&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;user&lt;span style="color:#a3be8c"&gt;}&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;listen&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;3000&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; console&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;log&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Server is running on port 3000&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="authentication"&gt;Authentication&lt;/h3&gt;
&lt;p&gt;It won&amp;rsquo;t have escaped your attention that our view counting app isn&amp;rsquo;t at all secure. At the moment, any one can log in as &amp;lsquo;jane&amp;rsquo; and see her view count. The common way to address this is to also require a password.&lt;/p&gt;
&lt;p&gt;I will eventually have to start serving some actual HTML, but for this next step I&amp;rsquo;ll stick to just using the routes. So our user interface will be this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Logging in &lt;code&gt;/login/&amp;lt;user&amp;gt;/&amp;lt;password&amp;gt;&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;Check the user exists, and that this is the correct password&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Creating a new user &lt;code&gt;/create/&amp;lt;user&amp;gt;/&amp;lt;password&amp;gt;&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;Save the new user and password to the file.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We&amp;rsquo;ll do create first:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/create/:user/:password&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; user &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;params&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;user&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; password &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;params&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;password&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; views &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#b48ead"&gt;0&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;session&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;user &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; user&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; user_views&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;push&lt;span style="color:#eceff4"&gt;({&lt;/span&gt; user&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; password&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; views &lt;span style="color:#eceff4"&gt;});&lt;/span&gt; writeUserViewFile&lt;span style="color:#eceff4"&gt;();&lt;/span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;User &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;user&lt;span style="color:#eceff4"&gt;}&lt;/span&gt; created &lt;span style="color:#81a1c1"&gt;&amp;amp;&lt;/span&gt; logged &lt;span style="color:#81a1c1;font-weight:bold"&gt;in&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We should probably first check there&amp;rsquo;s not an existing user with the same name to avoid having two users and the second user never being able to log in.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/create/:user/:password&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;findUser&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;params&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;user&lt;span style="color:#eceff4"&gt;))&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;User &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;params&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;user&lt;span style="color:#eceff4"&gt;}&lt;/span&gt; already exists&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; user &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;params&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;user&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; password &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;params&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;password&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; views &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#b48ead"&gt;0&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;session&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;user &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; user&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; user_views&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;push&lt;span style="color:#eceff4"&gt;({&lt;/span&gt; user&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; password&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; views &lt;span style="color:#eceff4"&gt;});&lt;/span&gt; writeUserViewFile&lt;span style="color:#eceff4"&gt;();&lt;/span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;User &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;user&lt;span style="color:#eceff4"&gt;}&lt;/span&gt; created &lt;span style="color:#81a1c1"&gt;&amp;amp;&lt;/span&gt; logged &lt;span style="color:#81a1c1;font-weight:bold"&gt;in&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;and logging in:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/login/:user/:password&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; user &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; findUser&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;params&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;user&lt;span style="color:#eceff4"&gt;);&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;user &lt;span style="color:#81a1c1"&gt;&amp;amp;&amp;amp;&lt;/span&gt; user&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;password &lt;span style="color:#81a1c1"&gt;===&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;params&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;password&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;session&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;user &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;params&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;user&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;User &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;params&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;user&lt;span style="color:#eceff4"&gt;}&lt;/span&gt; logged &lt;span style="color:#81a1c1;font-weight:bold"&gt;in&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;Incorrect username &lt;span style="color:#81a1c1;font-weight:bold"&gt;or&lt;/span&gt; password&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Although this is an improvement, clearly, it would a stretch to call this improved &lt;em&gt;security&lt;/em&gt;. Here&amp;rsquo;s a few things that are jumping out at me, and I&amp;rsquo;m sure there&amp;rsquo;s some being missed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We&amp;rsquo;re transmitting plaintext passwords over http&lt;/li&gt;
&lt;li&gt;We have plaintext passwords in a URL - any intermediate networking that&amp;rsquo;s recording access logs will the logging our passwords&lt;/li&gt;
&lt;li&gt;We&amp;rsquo;re storing plaintext passwords on our server in the &lt;code&gt;user_views.json&lt;/code&gt; file. So when our server is breached, our users&amp;rsquo; passwords will get sold on the dark web and they&amp;rsquo;ll be hacked if they&amp;rsquo;ve reused name/password combos.&lt;/li&gt;
&lt;li&gt;Passwords are limited to characters that reliably work in URLs&lt;/li&gt;
&lt;li&gt;Our passwords and user names may be open to injection attacks&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So it seems like there is four jobs to do:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Encrypt the passwords&lt;/li&gt;
&lt;li&gt;Don&amp;rsquo;t use URLs for passing this data&lt;/li&gt;
&lt;li&gt;Sanitise our inputs&lt;/li&gt;
&lt;li&gt;Enforce HTTPS&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="passwords"&gt;Passwords&lt;/h4&gt;
&lt;p&gt;Although I described this as &amp;rsquo;encrypting&amp;rsquo; that&amp;rsquo;s not exactly what we&amp;rsquo;re going to do. The current state of the art for this is to &lt;em&gt;hash&lt;/em&gt; and &lt;em&gt;salt&lt;/em&gt; passwords before storing them.&lt;/p&gt;
&lt;p&gt;hash - turn the password into some gooblygook such that that the hashed version always comes out the same if you put the same password into it. Preferably the hash is always the same length regardless of the length of the input password.&lt;/p&gt;
&lt;p&gt;salt - mix some random characters in to the the hashed password so that the combination of hashing and salting the password comes out different every time, even though you are putting in the same password as input to the process.&lt;/p&gt;
&lt;p&gt;How this is going to work is that once we get the users password, we&amp;rsquo;ll hash and salt it, then save the result of that in our array (and eventually file). If someone has access to that file, it&amp;rsquo;s practically impossible for them to reverse engineer the password - even if they have a known password somewhere else in the file to work from. Then when a user attempts to log in, we&amp;rsquo;ll take the password they gave us and hash it, and we&amp;rsquo;ll &amp;lsquo;un-salt&amp;rsquo; the password from the file and compare those two hashed versions of the passwords. If they are the same, then we know the passwords were also the same, and we can log this user in.&lt;/p&gt;
&lt;p&gt;This is a fun area of computational science, so it&amp;rsquo;s somewhat tempting to write our own hash and salting functions. This is widely regarded as a Bad Idea. As long as you don&amp;rsquo;t run into supply chain problems, a proper library, written by cryptographic experts, subject to public scrutiny, that gets updates when vulnerabilities are discovered is always going to be more secure. Almost universally, the answer for JS developers is &lt;a href="https://www.npmjs.com/package/bcrypt"&gt;bcrypt&lt;/a&gt;. We&amp;rsquo;ll install that with &lt;code&gt;npm i bcrypt&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s our create and login endpoints using bcrypt with the changes highlighted.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/create/:user/:password&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;findUser&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;params&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;user&lt;span style="color:#eceff4"&gt;))&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;User &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;params&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;user&lt;span style="color:#eceff4"&gt;}&lt;/span&gt; already exists&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; user &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;params&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;user&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;hash&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; bcrypt&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;hashSync&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;params&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;password&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; saltRounds&lt;span style="color:#eceff4"&gt;);&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; views &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#b48ead"&gt;0&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;session&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;user &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; user&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;  user_views&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;push&lt;span style="color:#eceff4"&gt;({&lt;/span&gt; user&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;hash&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; views &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;  writeUserViewFile&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;  res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;User &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;user&lt;span style="color:#eceff4"&gt;}&lt;/span&gt; created &lt;span style="color:#81a1c1"&gt;&amp;amp;&lt;/span&gt; logged &lt;span style="color:#81a1c1;font-weight:bold"&gt;in&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt; &lt;span style="color:#eceff4"&gt;});});&lt;/span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/login/:user/:password&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; user &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; findUser&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;params&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;user&lt;span style="color:#eceff4"&gt;);&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;user &lt;span style="color:#81a1c1"&gt;&amp;amp;&amp;amp;&lt;/span&gt; bcrypt&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;compareSync&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;params&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;password&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; user&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;hash&lt;span style="color:#eceff4"&gt;))&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;session&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;user &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;params&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;user&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;User &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;params&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;user&lt;span style="color:#eceff4"&gt;}&lt;/span&gt; logged &lt;span style="color:#81a1c1;font-weight:bold"&gt;in&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;Incorrect username &lt;span style="color:#81a1c1;font-weight:bold"&gt;or&lt;/span&gt; password&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="forms"&gt;Forms&lt;/h4&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-01-27-at-1.44.22-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-01-27-at-1.44.22-pm.png" width="800" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;To move the passwords out of the URLs, we&amp;rsquo;ll present a simple forms to the user for creating and logging in. The log in page is the basic user name / password form you&amp;rsquo;ve built before. This is served from the app.get(&amp;quot;/login&amp;quot;) route.&lt;/p&gt;
&lt;p&gt;And here&amp;rsquo;s the endpoint it posts to:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;post&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/login&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; user &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; findUser&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;body&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;username&lt;span style="color:#eceff4"&gt;);&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;user &lt;span style="color:#81a1c1"&gt;&amp;amp;&amp;amp;&lt;/span&gt; bcrypt&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;compareSync&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;body&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;password&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; user&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;hash&lt;span style="color:#eceff4"&gt;))&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;session&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;user &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;body&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;username&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;session&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;save&lt;span style="color:#eceff4"&gt;((&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;Cookie saving error, &amp;lt;a href=&amp;#34;/login&amp;#34;&amp;gt;try again&amp;lt;/a&amp;gt;`&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;redirect&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;Incorrect username &lt;span style="color:#81a1c1;font-weight:bold"&gt;or&lt;/span&gt; password&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;&amp;lt;&lt;/span&gt;a href&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/login&amp;#34;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;&amp;gt;&lt;/span&gt;try again&lt;span style="color:#81a1c1"&gt;&amp;lt;/&lt;/span&gt;a&lt;span style="color:#81a1c1"&gt;&amp;gt;&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Normally express-session will deal with saving the session without us having to worry about, but I decided that a successful login should be followed to a re-direct to the view count page.&lt;/p&gt;
&lt;p&gt;Since express-session normally does its saving at the end of a http response, and if we&amp;rsquo;re redirecting, that response hasn&amp;rsquo;t happened. There&amp;rsquo;s a bit more about that &lt;a href="https://expressjs.com/en/resources/middleware/session.html"&gt;here&lt;/a&gt; (search for session save).&lt;/p&gt;
&lt;p&gt;Apart from that, the changes are really just grabbing the user name and passwords from the form post body instead of the URL.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;/register&lt;/code&gt; form is the same as the log in, but with a second password field and some client side scripting to check the two passwords are the same. Processing the new user is very similar to the previous &lt;code&gt;create&lt;/code&gt; route.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;post&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/register&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;findUser&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;body&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;username&lt;span style="color:#eceff4"&gt;))&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;User &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;body&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;username&lt;span style="color:#eceff4"&gt;}&lt;/span&gt; already exists&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; user &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;body&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;username&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;hash&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; bcrypt&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;hashSync&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;body&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;password&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; saltRounds&lt;span style="color:#eceff4"&gt;);&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; views &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#b48ead"&gt;0&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;session&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;user &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; user&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; user_views&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;push&lt;span style="color:#eceff4"&gt;({&lt;/span&gt; user&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;hash&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; views &lt;span style="color:#eceff4"&gt;});&lt;/span&gt; writeUserViewFile&lt;span style="color:#eceff4"&gt;();&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;session&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;save&lt;span style="color:#eceff4"&gt;((&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;Cookie saving error, &amp;lt;a href=&amp;#34;/login&amp;#34;&amp;gt;try again&amp;lt;/a&amp;gt;`&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;redirect&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#eceff4"&gt;});});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="sanitising-inputs"&gt;Sanitising inputs&lt;/h4&gt;
&lt;p&gt;Cautious developers do not trust any input from users. There are numerous libraries to deal with cleaning it up which I&amp;rsquo;d recommend you consider, but for our case here let&amp;rsquo;s think through what the possibilities are:&lt;/p&gt;
&lt;p&gt;password - the password is going to be hashed and salted before it is stored, and never used to build HTML. The only risk I can think of would be if a very long password might cause some sort of trouble - so we can just truncate it. Note we don&amp;rsquo;t even need to tell the user - if we truncate it when they register, and when they log in, they&amp;rsquo;ll still match.&lt;/p&gt;
&lt;p&gt;user name - is stored in a json file, and is output to the user. In the future that output is likely to be HTML. &lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s our &lt;code&gt;users_view.json&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[ { &amp;#34;user&amp;#34;: &amp;#34;user1&amp;#34;, &amp;#34;hash&amp;#34;: &amp;#34;$2b$10$8Zf9LZWH78mWnSKjxKQxXe9TlPoqe7L3SOABcPHIUQ5Pq3jIbVQVm&amp;#34;, &amp;#34;views&amp;#34;: 16 }, { &amp;#34;user&amp;#34;: &amp;#34;user2&amp;#34;, &amp;#34;hash&amp;#34;: &amp;#34;$2b$10$f/QAQ7we6Hh/hTx35LjfGeYtCY8aRG3ZqbJZqhEZRDXUqxkKCgPhq&amp;#34;, &amp;#34;views&amp;#34;: 14 }, { &amp;#34;user&amp;#34;: &amp;#34;user3&amp;#34;, &amp;#34;hash&amp;#34;: &amp;#34;$2b$10$KBZe6orYpjG3JeIGhnLDCuyYQXxfVZYBjovGf3XIdU8rr6kXlNrLC&amp;#34;, &amp;#34;views&amp;#34;: 1 }]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The obvious attack here would be injection. For example a hacker might register with the user name:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;user4&amp;quot;, &amp;quot;role&amp;quot;: &amp;quot;admin&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s a smart try at privileged escalation. Even if we had an &amp;lsquo;admin&amp;rsquo; role, it wouldn&amp;rsquo;t actually work though since any double quotes will be escaped by JSON.stringify(), but to be cautious, we can eliminate the possibility by just deleting any double quotes out.&lt;/p&gt;
&lt;p&gt;Another injection possibility would be with HTML. Perhaps we will display the logged in user later inside a &lt;div&gt;, something like:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;div&amp;gt;User: user1&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Our hacker might try registering this as their user name as:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;user1&amp;lt;/div&amp;gt;&amp;lt;script&amp;gt;alert('you've been hacked')&amp;lt;/script&amp;gt;&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s not great to allow anyone on the internet to run code in our visitors&amp;rsquo; browsers.&lt;/p&gt;
&lt;p&gt;You should really use a library designed by someone who knows what they are doing, but I just wanted to do enough here to prompt you to think about. For that demo purpose, I&amp;rsquo;m going to replace all &amp;quot; &amp;lt; and &amp;gt; with _&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;// sanitise a string by replacing all &amp;#34; &amp;lt; and &amp;gt; with _ (underscore) // and truncating it at 20 charactersfunction sanitise(str) { return str.replace(/[&amp;#34;&amp;lt;&amp;gt;]/g, &amp;#34;_&amp;#34;).slice(0, 20);}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is all a bit doge. If we are going to have rules like this (and the other rules we should have about min lengths) we should be implementing them in the browser and on the backend, and there should be helpful prompting to the users to enable them to understand and correct their inputs. As I mentioned earlier, this is just to get you to think about it.&lt;/p&gt;
&lt;h4 id="https"&gt;HTTPS&lt;/h4&gt;
&lt;p&gt;In the list of four security improvements we wanted to make, the last one was to enforce HTTPS. There&amp;rsquo;s two places to do this. One is that when we initialise our session at the top of the app, we can tell it we want &amp;lsquo;secure&amp;rsquo; cookies. This setting means that cookies will not be sent over plain HTTP, but only the end-to-end encrypted HTTPS. Currently our cookies contain a user name:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{ &amp;#34;cookie&amp;#34;: { &amp;#34;originalMaxAge&amp;#34;: null, &amp;#34;expires&amp;#34;: null, &amp;#34;httpOnly&amp;#34;: true, &amp;#34;path&amp;#34;: &amp;#34;/&amp;#34; }, &amp;#34;__lastAccess&amp;#34;: 1706315491081, &amp;#34;user&amp;#34;: &amp;#34;user1&amp;#34;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Even though a user name isn&amp;rsquo;t a lot of information, it could still be critical. If there was rumors about your company being acquired and a hacker leaked that j_bezos had been looking at your view counts, it could have implications. To turn on secure cookies:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app.use( session({ secret: sessionSecret, resave: false, saveUninitialized: true, store: new FileStore(), name: cookie_name, cookie: { secure: true, httpOnly: true, }, }));
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Note that your infrastructure needs to support HTTPS for this to be useful - if the cookies are not sent, this particular app is rendered useless.&lt;/p&gt;
&lt;p&gt;But we want to enforce HTTPS anyway, because a bigger problem is that if we don&amp;rsquo;t someone could intercept the login form data and collect plaintext usernames and passwords. &lt;/p&gt;
&lt;p&gt;I generally do this by running all web services behind NGINX as a proxy. Then the NGINX configs can be set to redirect all HTTP requests to HTTPS, and valid requests can be passed off to your app. Here&amp;rsquo;s the sort of thing you might have in a config.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;server { listen 80; server_name viewcount.example.com; return 301 https://$host$request_uri;}server { listen 443 ssl; server_name viewcount.example.com; # SSL certificate configuration ssl_certificate /path/to/your/certificate.crt; ssl_certificate_key /path/to/your/private-key.key; # Additional SSL configuration, such as preferred protocols and ciphers, can be added here location / { # Your application configuration goes here # Proxy pass to your Node.js or other application server # Example for proxying to a Node.js server running on localhost:3000 proxy_pass http://localhost:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection &amp;#39;upgrade&amp;#39;; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; }}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;There are other security actions that can be taken at the NGINX level - things like rate limiting, blocking particular IP ranges, and access logging - all of which can be handy for protecting your endpoint from bad actors.&lt;/p&gt;
&lt;h4 id="other-security"&gt;Other security&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;I&amp;rsquo;d normally have the cookie secret in a .env file that was being .gitignored. A good hacker hobby is to scan public code repos for API keys and other secrets.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/helmetjs/helmet"&gt;Helmet.js&lt;/a&gt; is a sort of magic bullet for enforcing some security around request headers.&lt;/li&gt;
&lt;li&gt;Probably a stack of other things - ask your senior dev.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here&amp;rsquo;s our app with the changes we&amp;rsquo;ve discussed:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; express &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;express&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; session &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;express-session&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; FileStore &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;session-file-store&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)(&lt;/span&gt;session&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; fs &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;fs&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; app &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; express&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; bcrypt &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;bcrypt&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; saltRounds &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#b48ead"&gt;10&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; user_view_file &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;./user_views.json&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; cookie_name &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;session_cookie&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;use&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;express&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;urlencoded&lt;span style="color:#eceff4"&gt;({&lt;/span&gt; extended&lt;span style="color:#81a1c1"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;false&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;let&lt;/span&gt; user_views &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#eceff4"&gt;[];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;// if the user_views.json file exists, read it and parse it to user_views array
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;fs&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;existsSync&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;user_view_file&lt;span style="color:#eceff4"&gt;))&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; user_views &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; JSON&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;parse&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;fs&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;readFileSync&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;user_view_file&lt;span style="color:#eceff4"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;function&lt;/span&gt; writeUserViewFile&lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; fs&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;writeFile&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;user_view_file&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; JSON&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;stringify&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;user_views&lt;span style="color:#eceff4"&gt;),&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; console&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;log&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;function&lt;/span&gt; findUser&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;user&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; user_views&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;find&lt;span style="color:#eceff4"&gt;((&lt;/span&gt;u&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; u&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;user &lt;span style="color:#81a1c1"&gt;===&lt;/span&gt; user&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;// sanitise a string by replacing all &amp;#34; &amp;lt; and &amp;gt; with _ (underscore)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;// and truncating it at 20 characters
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;function&lt;/span&gt; sanitise&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;str&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; str&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;replace&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#ebcb8b"&gt;/[&amp;#34;&amp;lt;&amp;gt;]/g&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;_&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;).&lt;/span&gt;slice&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;0&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#b48ead"&gt;20&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;use&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; session&lt;span style="color:#eceff4"&gt;({&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; secret&lt;span style="color:#81a1c1"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;your-secret-keyz&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#616e87;font-style:italic"&gt;// This should be a secret, used to sign the session ID cookie
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; resave&lt;span style="color:#81a1c1"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;false&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; saveUninitialized&lt;span style="color:#81a1c1"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;true&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; store&lt;span style="color:#81a1c1"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;new&lt;/span&gt; FileStore&lt;span style="color:#eceff4"&gt;(),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; name&lt;span style="color:#81a1c1"&gt;:&lt;/span&gt; cookie_name&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;session&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;user&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; user_view &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; findUser&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;session&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;user&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;!&lt;/span&gt;user_view&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;`Error finding user, &amp;lt;a href=&amp;#34;/login&amp;#34;&amp;gt;try again&amp;lt;/a&amp;gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; user_view&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;views&lt;span style="color:#81a1c1"&gt;++&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;`User &amp;#34;&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;${&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;session&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;user&lt;span style="color:#a3be8c"&gt;}&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34; has &lt;/span&gt;&lt;span style="color:#a3be8c"&gt;${&lt;/span&gt;user_view&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;views&lt;span style="color:#a3be8c"&gt;}&lt;/span&gt;&lt;span style="color:#a3be8c"&gt; views, &amp;lt;a href=&amp;#34;/&amp;#34;&amp;gt;reload&amp;lt;/a&amp;gt; or &amp;lt;a href=&amp;#34;/logout&amp;#34;&amp;gt;logout&amp;lt;/a&amp;gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; writeUserViewFile&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;redirect&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/login&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/logout&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; req&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;session&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;destroy&lt;span style="color:#eceff4"&gt;((&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; console&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;log&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;clearCookie&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;cookie_name&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;redirect&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;post&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/register&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; user &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; sanitise&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;body&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;username&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;findUser&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;user&lt;span style="color:#eceff4"&gt;))&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;`User &lt;/span&gt;&lt;span style="color:#a3be8c"&gt;${&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;body&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;username&lt;span style="color:#a3be8c"&gt;}&lt;/span&gt;&lt;span style="color:#a3be8c"&gt; already exists`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; hash &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; bcrypt&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;hashSync&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;body&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;password&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; saltRounds&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; views &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#b48ead"&gt;0&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; req&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;session&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;user &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; user&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; user_views&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;push&lt;span style="color:#eceff4"&gt;({&lt;/span&gt; user&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; hash&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; views &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; writeUserViewFile&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; req&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;session&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;save&lt;span style="color:#eceff4"&gt;((&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;Cookie saving error, &amp;lt;a href=&amp;#34;/login&amp;#34;&amp;gt;try again&amp;lt;/a&amp;gt;`&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;redirect&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;post&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/login&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; username &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; sanitise&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;body&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;username&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; user &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; findUser&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;username&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;user &lt;span style="color:#81a1c1"&gt;&amp;amp;&amp;amp;&lt;/span&gt; bcrypt&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;compareSync&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;body&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;password&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; user&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;hash&lt;span style="color:#eceff4"&gt;))&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; req&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;session&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;user &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; username&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; req&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;session&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;save&lt;span style="color:#eceff4"&gt;((&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;Cookie saving error, &amp;lt;a href=&amp;#34;/login&amp;#34;&amp;gt;try again&amp;lt;/a&amp;gt;`&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;redirect&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;`Incorrect username or password, &amp;lt;a href=&amp;#34;/login&amp;#34;&amp;gt;try again&amp;lt;/a&amp;gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/login&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;status&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;200&lt;/span&gt;&lt;span style="color:#eceff4"&gt;).&lt;/span&gt;sendFile&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;__dirname &lt;span style="color:#81a1c1"&gt;+&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;/login.html&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/register&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;status&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;200&lt;/span&gt;&lt;span style="color:#eceff4"&gt;).&lt;/span&gt;sendFile&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;__dirname &lt;span style="color:#81a1c1"&gt;+&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;/register.html&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;listen&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;3000&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; console&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;log&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Server is running on port 3000&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="conclusion"&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Nearly every web app we write is going to need a user auth and session management solution. In this very long post we&amp;rsquo;ve looked at a way to develop that from scratch in Express/Node. In the process our code base went from about 10 lines to 130. Now that it&amp;rsquo;s done however, the only extra code to ensure users are only accessing the routes they should will be a line at the entry point of each route.&lt;/p&gt;
&lt;p&gt;Since building out session management is such a common and onerous task, and one that can have serious consequences if not done correctly, you might be wondering if there&amp;rsquo;s libraries to do some of this, and other, lifting for us. There is, and I plan to look at some in the future.&lt;/p&gt;</description></item><item><title>Fly.io, Uptime Kuma &amp; scraping a status page</title><link>https://blog.iankulin.com/fly-io-uptime-kuma-scraping-a-status-page/</link><pubDate>Fri, 02 Feb 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/fly-io-uptime-kuma-scraping-a-status-page/</guid><description>&lt;p&gt;&lt;a href="https://dribbble.com/shots/5657880-Fly-io-Logo"&gt;&lt;img src="https://blog.iankulin.com/images/c1fef772e2dca5e1ab8c812f465c95a8.png" width="800" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been aware since I set up &lt;a href="https://blog.iankulin.com/uptime-kuma-nfty/"&gt;Uptime Kuma&lt;/a&gt; for my monitoring, that having an instance on my local network monitoring my VPS websites wasn&amp;rsquo;t ideal. The main reason being that the flakiest part of my infrastructure is my 4G home internet, so if that goes down I have no website monitoring, and even if I did, the notifications couldn&amp;rsquo;t get out.&lt;/p&gt;
&lt;p&gt;Of course, it would also be a simple matter to run an instance on the VPS that I host the sites on, but that has a similar problem in that if the VPS goes down, so does my monitoring of the VPS. What I really need is a third, independent space to run an instance.&lt;/p&gt;
&lt;h3 id="uptime-robot"&gt;Uptime Robot&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://uptimerobot.com/"&gt;Uptime Robot&lt;/a&gt; is a monitoring service that seems somehow related to Uptime Kuma? They have some of the same terminology and colour schemes - so I&amp;rsquo;m not really sure. Perhaps it&amp;rsquo;s a fork, or perhaps Uptime Kuma was inspired by Robot. Robot does have an API which is a nice addition, since ideally if my monitoring is spread around, I&amp;rsquo;d like to pull it all back into one &amp;lsquo;pane of glass&amp;rsquo; by having my system monitor the remote for how many &amp;lsquo;down&amp;rsquo; sites it&amp;rsquo;s tracking. It also has a number of other extra features such as heartbeat monitoring.&lt;/p&gt;
&lt;p&gt;Uptime Robot is a paid service, but like nearly all VC funded things growing a user base it has a free tier with some restrictions. I like NTFY for my notifications, but on Robot I could only access email notifications. There are iOS and Android apps, but I didn&amp;rsquo;t try them.&lt;/p&gt;
&lt;h3 id="third-space"&gt;Third Space&lt;/h3&gt;
&lt;p&gt;Ideally, I like to run another Uptime Kuma in a VPS on a different provider. I&amp;rsquo;ve heard that &lt;a href="https://www.oracle.com/au/cloud/free/"&gt;Oracle have a free tier&lt;/a&gt; which seems like it would be fine for this application, but a more interesting idea that I&amp;rsquo;ve been thinking of using for other projects is Fly.io.&lt;/p&gt;
&lt;h3 id="flyio"&gt;Fly.io&lt;/h3&gt;
&lt;p&gt;Fly.io own physical servers in colo datacentres around the world on which they offer compute based on &lt;a href="https://www.amazon.science/blog/how-awss-firecracker-virtual-machines-work"&gt;Firecracker VM&amp;rsquo;s&lt;/a&gt;. The cute bit is that you give them a Docker container, and they unpack it into one of these fast baby VM&amp;rsquo;s.&lt;/p&gt;
&lt;p&gt;The exact nature of their &amp;lsquo;free tier&amp;rsquo; is hard to figure out from their &lt;a href="https://fly.io/docs/about/pricing/"&gt;pricing page&lt;/a&gt;, but based on &lt;a href="https://community.fly.io/t/fly-io-free-tier-billing/11432"&gt;some answers to questions in their forum&lt;/a&gt;, and &lt;a href="https://jfmadrid.notion.site/Uptime-Kuma-for-Free-on-Fly-io-e5eeead6dfb4425b8403c100ec986191"&gt;blog posts from others who have set up Uptime Kuma&lt;/a&gt; there, it sounds like the deal is that if you use one shared CPU &lt;em&gt;and&lt;/em&gt; keep your storage under 3GB &lt;em&gt;and&lt;/em&gt; the charges for your use add up to less than $5/month - then it&amp;rsquo;s free. I did have to provide credit card details, so if &lt;a href="https://www.youtube.com/watch?v=N6lYcXjd4pg"&gt;I get a $71,393 bill,&lt;/a&gt; I&amp;rsquo;ll come back here and edit this. (&lt;em&gt;edit from the future: eight months later I haven&amp;rsquo;t paid a cent&lt;/em&gt;).&lt;/p&gt;
&lt;p&gt;To get Uptime Kuma running on Fly.io, I followed &lt;a href="https://jfmadrid.notion.site/Uptime-Kuma-for-Free-on-Fly-io-e5eeead6dfb4425b8403c100ec986191"&gt;this guide&lt;/a&gt;, but the steps where basically:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create an account on Fly.io&lt;/li&gt;
&lt;li&gt;Install the Fly.io command line tools and run a command to &amp;lsquo;create&amp;rsquo; your app&lt;/li&gt;
&lt;li&gt;Create a &amp;lsquo;&lt;a href="https://github.com/lubien/fly-uptime-kuma/blob/main/fly.toml"&gt;fly.toml&lt;/a&gt;&amp;rsquo; file which is a text config file pointing to the docker image and supplying some details such as ports and location&lt;/li&gt;
&lt;li&gt;Use the CLI to set the disk space needed, and &amp;lsquo;deploy&amp;rsquo; the app&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It was impressive how simple all this was. If the intention of the free tier is to get you to try it, and show you how painless it is to deploy any dockerised app to the edge, then mission accomplished.&lt;/p&gt;
&lt;p&gt;You can check on the status of your app at &lt;a href="https://fly.io/dashboard"&gt;https://fly.io/dashboard&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-01-16-at-6.31.22-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-01-16-at-6.31.22-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;And go to &lt;appname&gt;.fly.dev to see your app. On the free tier, you&amp;rsquo;re on a shared IPV4 address but it is possible to use your own domain if desired - that&amp;rsquo;s one of the things to set up in the .toml file.&lt;/p&gt;
&lt;p&gt;It is remarkable what you can deploy for free in the golden age of venture capital.&lt;/p&gt;
&lt;h3 id="extracting-status"&gt;Extracting Status&lt;/h3&gt;
&lt;p&gt;One of Uptime Kuma&amp;rsquo;s functions is to provide public (ie viewable without being logged in) &amp;lsquo;status&amp;rsquo; pages, and if all the services you&amp;rsquo;ve added to that status group are up, it has. great big heading saying &amp;ldquo;All Systems Operational&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-01-16-at-7.38.45-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-01-16-at-7.38.45-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;So my plan to pull this status into my homelab instance of Uptime Kuma was just to add this remote status page as a monitor, and search for the keyword &amp;lsquo;All Systems Operational&amp;rsquo;. If that was found, I&amp;rsquo;d know everything was good. But of course, this is a modern web-app (I think using &lt;a href="https://vuejs.org/"&gt;Vue&lt;/a&gt;), so that text does not exist in the page, it&amp;rsquo;s added to the DOM by some JavaScript after the page is loaded based on some client side processing of (probably) some JSON data it pulls in.&lt;/p&gt;
&lt;p&gt;One option would be to use a web scraping library to write something to access this piece of information. On a page like this, that would involve a headless browser rendering the DOM then exposing it.&lt;/p&gt;
&lt;p&gt;But of course, the Javascript that is building the page we&amp;rsquo;re looking at is getting its data from somewhere, so it&amp;rsquo;s probably easier for us to grab that data directly and process it ourselves. How do we see where the data is from? We use the browser tools to look at the network requests when the page is loaded.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-01-16-at-7.20.50-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-01-16-at-7.20.50-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;So if you view the status page at &lt;code&gt;&amp;lt;whatever.com&amp;gt;/status/&amp;lt;page_name&amp;gt;&lt;/code&gt;, it loads some data from &lt;code&gt;&amp;lt;whatever.com&amp;gt;/api/status-page/heartbeat/&amp;lt;page_name&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The JSON that&amp;rsquo;s returned from this request contains two objects: &lt;code&gt;heartbeatlist&lt;/code&gt;, and &lt;code&gt;uptimelist&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-01-16-at-8.06.05-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-01-16-at-8.06.05-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;heartbeatlist&lt;/code&gt; contains the last 50 retrievals for each of the URL&amp;rsquo;s being monitored. Each of these retrievals has a status (1 for up, 0 for down) and the response time. &lt;code&gt;uptimelist&lt;/code&gt; is the fraction of uptime. You can see in the data above that the first URL has a lower percentage of up-time (because I failed it to check my understanding of the status data).&lt;/p&gt;
&lt;p&gt;So I need to write an endpoint that requests this data, then checks the last array element of each of the URLs in the heartbeat list, then spit out some text saying if all the URL&amp;rsquo;s in this status group are available. That&amp;rsquo;s quite doable, I have the skills, but it&amp;rsquo;s probably a two hour job to do properly.&lt;/p&gt;
&lt;p&gt;Since this is an open source project, a better use of that time would be to add this functionality to Uptime Kuma so it would be available to anyone with the same problem. It might be a niche case, but the code to provide this output would be simpler inside the project and much more durable than reverse engineering it.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s have a look at the source and see what it&amp;rsquo;s like.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-01-16-at-8.34.24-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-01-16-at-8.34.24-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Well, well well. What do we have here? There&amp;rsquo;s an api route that outputs an SVG badge for a status page. The badge says &amp;lsquo;Degraded&amp;rsquo; in amber if some of the URL&amp;rsquo;s are down, and &amp;lsquo;Up&amp;rsquo; in green if they are all up. Those words are present in an aria label and the svg &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; tag, so they&amp;rsquo;ll be detectable by the Uptime Kuma &amp;lsquo;keyword&amp;rsquo; search.&lt;/p&gt;
&lt;p&gt;Five minutes later, we&amp;rsquo;re in business. Thank you open source!&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-01-16-at-8.41.52-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-01-16-at-8.41.52-pm.png" width="772" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>htmx - A To Do Example</title><link>https://blog.iankulin.com/htmx-a-to-do-example/</link><pubDate>Fri, 05 Jan 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/htmx-a-to-do-example/</guid><description>&lt;img src="https://blog.iankulin.com/images/0-eawgkaegdkhvqwcg.png" width="1000" alt=""&gt;
&lt;p&gt;HTMX is an interesting project to me, and I&amp;rsquo;ve used it a bit in my large collection of 70% completed side projects, but haven&amp;rsquo;t really discussed it here. The plan for this post is to talk briefly about what it is exactly, then convert a simple &amp;lsquo;conventional&amp;rsquo; (HTML/CSS/Javascript) app to htmx and think about some the differences.&lt;/p&gt;
&lt;h3 id="htmx"&gt;htmx&lt;/h3&gt;
&lt;p&gt;You could (I recommend you do) read the &lt;a href="https://hypermedia.systems/book/contents/"&gt;book&lt;/a&gt; about the concepts behind &lt;a href="https://htmx.org/"&gt;htmx&lt;/a&gt;. Carson Gross (the man behind htmx) calls it a book, but its quite the treatise, it could fairly be called a manifesto.&lt;/p&gt;
&lt;p&gt;The book points out that the &amp;lsquo;hyper&amp;rsquo; bit of hypertext markup language, is currently limited to a couple of tags - &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt;, and &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The anchor tag sends a request to the server that says something like &amp;lsquo;fetch the HTML from this URL and completely replace the current view with it&amp;rsquo;&lt;/li&gt;
&lt;li&gt;The form tag with a &lt;code&gt;post&lt;/code&gt; method says to the server &amp;lsquo;do something with this data&amp;rsquo;, or with the &lt;code&gt;get&lt;/code&gt; method, &amp;lsquo;get me the thing I&amp;rsquo;m describing&amp;rsquo;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So the first paradigm shift in htmx is &lt;em&gt;&amp;lsquo;why don&amp;rsquo;t we give all html tags the superpower to make server calls&lt;/em&gt;?&lt;em&gt;&amp;rsquo;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Of course, we can do this in JavaScript - call any endpoint with &lt;code&gt;put&lt;/code&gt;, &lt;code&gt;patch&lt;/code&gt;, &lt;code&gt;get&lt;/code&gt; etc, then add listeners for the code to many parts of the DOM, but Carson has imagined (and then implemented) an alternative timeline where HTML kept being developed to have this capability without the developer having to leave their beloved HTML.&lt;/p&gt;
&lt;p&gt;The other &amp;lsquo;big thing&amp;rsquo; is what the server returns, and what we do with it. We&amp;rsquo;re used to just getting data (JSON these days, but XML in the days when senior devs had big beards) then the front-end JavaScript needing to know about the data and it&amp;rsquo;s format and the intent so it can present it, and the affordances (options for the user to do things with it) available. These things (the presented data and the affordances) combine to represent the application state.&lt;/p&gt;
&lt;p&gt;This is an old concept from the birth of &lt;a href="https://en.wikipedia.org/wiki/HATEOAS"&gt;REST&lt;/a&gt; with a terrible acronym HATEOAS - &amp;ldquo;Hypermedia as the engine of application state&amp;rdquo;. Just passing some JSON (which is usually regarded as a REST practice) breaks this constraint. Instead, we&amp;rsquo;d pass back (in practical terms) HTML that contains the data and it&amp;rsquo;s affordances (the books sometimes calls this the hypermedia &lt;em&gt;representation&lt;/em&gt; of the data). In my Todo app, this might be the to do items, in a list, with the button to mark them as done.&lt;/p&gt;
&lt;p&gt;A key benefit of HATEOAS at it&amp;rsquo;s inception was that the server in this relationship could change business logic without any change to the client end. That argument can still be made to some extent, but a more important benefit of HATEOAS for htmx is that it means so little processing needs done in the client, we don&amp;rsquo;t need a programming language beyond what we&amp;rsquo;ve got in html/x. This is the second big thing in htmx:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Returning application state (data plus affordances) in HTML from the server means we don&amp;rsquo;t need an extra programming language to process it.&lt;/em&gt;&lt;/p&gt;
&lt;h3 id="in-practice"&gt;In practice&lt;/h3&gt;
&lt;p&gt;So what does this all mean in practice?&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;In htmx, HTML tags get &lt;em&gt;attributes&lt;/em&gt; if they need to talk to the server. The attributes say what endpoint they are hitting, with what method, and where to put the returned HTML.&lt;/li&gt;
&lt;li&gt;The server responds with chunks of HTML.&lt;/li&gt;
&lt;li&gt;The client slots that in where it&amp;rsquo;s supposed to go.&lt;/li&gt;
&lt;li&gt;You can write SPA type applications where a part of the page can be updated without a full refresh, without any Javascript*&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This last point explains some of the keen interest of htmx from the non-JavaScript language people. If you&amp;rsquo;re a Python, Go or Ruby developer with low love for JS, this is an easy sell.&lt;/p&gt;
&lt;h3 id="traditional-version"&gt;Traditional Version&lt;/h3&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-12-22-at-7.47.24-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I want to show you a demo htmx app, but first let&amp;rsquo;s look at the Javascript version. It is the Todo app from day one of that coding Udemy you never finished. There&amp;rsquo;s a list of items to do, shown sequentially on the screen. Each one has a button to mark it as done, and at the bottom, a spot to enter a new one.&lt;/p&gt;
&lt;p&gt;My &amp;lsquo;basic&amp;rsquo; Javascript version is based around a Node/Express server. Express serves the .html, .css &amp;amp; .js statically, then runs an API for creating, reading and deleting the Todo items as JSON.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-html" data-lang="html"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;&amp;lt;!-- index.html for simple todo app that uses node endpoints to process json--&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#5e81ac;font-style:italic"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;html&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;lang&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;en&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;head&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;meta&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;charset&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;UTF-8&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;meta&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;name&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;viewport&amp;#34;&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;content&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;width=device-width, initial-scale=1.0&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;title&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;Todos&lt;span style="color:#eceff4"&gt;&amp;lt;/&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;title&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;link&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;rel&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;stylesheet&amp;#34;&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;href&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;./styles.css&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;&amp;lt;/&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;head&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;body&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;&amp;lt;!-- Main section--&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;main&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;h1&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;To do&lt;span style="color:#eceff4"&gt;&amp;lt;/&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;h1&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;ul&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;id&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;todos_list&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;ul&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#616e87;font-style:italic"&gt;&amp;lt;!-- the list of todo items from the database gets inserted here--&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#616e87;font-style:italic"&gt;&amp;lt;!-- form to add a todo --&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;form&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;action&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/todos&amp;#34;&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;method&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;POST&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;input&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;type&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;text&amp;#34;&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;name&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;todo&amp;#34;&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;id&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;todo&amp;#34;&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;required&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;button&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;type&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;submit&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;Add&lt;span style="color:#eceff4"&gt;&amp;lt;/&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;button&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;/&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;form&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;/&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;main&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;script&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;src&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;index.js&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;script&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;&amp;lt;/&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;body&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;&amp;lt;/&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;html&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Nothing fancy there. The Todo items will go in the &lt;ul&gt; once we&amp;rsquo;ve got them. The endpoints won&amp;rsquo;t really surprise you either. We&amp;rsquo;re using SQLite for the persistence. There&amp;rsquo;s a &lt;code&gt;get /todos&lt;/code&gt; to get the whole list, a &lt;code&gt;post&lt;/code&gt; to add one, and a &lt;code&gt;delete&lt;/code&gt; to remove one.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;// Simple Express ToDo app using SQLite3
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; express &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;express&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; app &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; express&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; port &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#b48ead"&gt;3000&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; sqlite3 &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;sqlite3&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;).&lt;/span&gt;verbose&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; db &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;new&lt;/span&gt; sqlite3&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;Database&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;db/todos.sqlite&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;use&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;express&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;json&lt;span style="color:#eceff4"&gt;());&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;use&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;express&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;static&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;public&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;/todos&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; db&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;all&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;SELECT * FROM todos&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; rows&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;status&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;500&lt;/span&gt;&lt;span style="color:#eceff4"&gt;).&lt;/span&gt;json&lt;span style="color:#eceff4"&gt;({&lt;/span&gt; error&lt;span style="color:#81a1c1"&gt;:&lt;/span&gt; err&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;message &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;status&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;200&lt;/span&gt;&lt;span style="color:#eceff4"&gt;).&lt;/span&gt;json&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;rows&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;post&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;/todos&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;!&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;body&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;todo_item&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;status&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;400&lt;/span&gt;&lt;span style="color:#eceff4"&gt;).&lt;/span&gt;json&lt;span style="color:#eceff4"&gt;({&lt;/span&gt; error&lt;span style="color:#81a1c1"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#39;Missing todo_item field in request body&amp;#39;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; db&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;run&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;INSERT INTO todos (todo_item) VALUES (?)&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; req&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;body&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;todo_item&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;function&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;status&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;500&lt;/span&gt;&lt;span style="color:#eceff4"&gt;).&lt;/span&gt;json&lt;span style="color:#eceff4"&gt;({&lt;/span&gt; error&lt;span style="color:#81a1c1"&gt;:&lt;/span&gt; err&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;message &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#616e87;font-style:italic"&gt;// well behaved APIs return the newly created resource
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; db&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;SELECT * FROM todos WHERE id = ?&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;this&lt;/span&gt;&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;lastID&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; row&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;status&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;500&lt;/span&gt;&lt;span style="color:#eceff4"&gt;).&lt;/span&gt;json&lt;span style="color:#eceff4"&gt;({&lt;/span&gt; error&lt;span style="color:#81a1c1"&gt;:&lt;/span&gt; err&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;message &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;status&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;201&lt;/span&gt;&lt;span style="color:#eceff4"&gt;).&lt;/span&gt;json&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;row&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;delete&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;/todos/:id&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; db&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;run&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;DELETE FROM todos WHERE id = ?&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; req&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;params&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;id&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;status&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;500&lt;/span&gt;&lt;span style="color:#eceff4"&gt;).&lt;/span&gt;json&lt;span style="color:#eceff4"&gt;({&lt;/span&gt; error&lt;span style="color:#81a1c1"&gt;:&lt;/span&gt; err&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;message &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;status&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;204&lt;/span&gt;&lt;span style="color:#eceff4"&gt;).&lt;/span&gt;end&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;// if it doesn&amp;#39;t exist, create the table
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;db&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;run&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;CREATE TABLE IF NOT EXISTS todos (id INTEGER PRIMARY KEY AUTOINCREMENT, todo_item TEXT)&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;// close the database gracefully on exit
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;process&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;on&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;SIGINT&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; db&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;close&lt;span style="color:#eceff4"&gt;((&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; console&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;error&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;message&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; console&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;log&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;Database closed.&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; process&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;exit&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;0&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;// start the server on port 3000 
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;listen&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;port&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; console&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;log&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;`Todo app listening on http://localhost:&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;${&lt;/span&gt;port&lt;span style="color:#a3be8c"&gt;}&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;All the work of translating the data into a representation is being done in the Javascript.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;// index.js - code for simple todo app
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;// json data served from node endpoint
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;function&lt;/span&gt; createTodoItem&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;todoItemText&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; id&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; li &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;document&lt;/span&gt;&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;createElement&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;li&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; button &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;document&lt;/span&gt;&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;createElement&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;button&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; button&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;innerHTML &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#39;Done&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; button&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;addEventListener&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;click&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; handleDelete&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;id&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; li&lt;span style="color:#eceff4"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#616e87;font-style:italic"&gt;// Create a text node with the todo text and append it to the li
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; todoTextNode &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;document&lt;/span&gt;&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;createTextNode&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;todoItemText&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; li&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;appendChild&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;todoTextNode&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#616e87;font-style:italic"&gt;// Then append the delete button
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; li&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;appendChild&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;button&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; li&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;function&lt;/span&gt; handleDelete&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;id&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; li&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; fetch&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;/todos/&amp;#39;&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;+&lt;/span&gt; id&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; method&lt;span style="color:#81a1c1"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#39;DELETE&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;.&lt;/span&gt;then&lt;span style="color:#eceff4"&gt;(()&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; li&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;remove&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;// Fetch the todo items to build the list
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;fetch&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;/todos&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;.&lt;/span&gt;then&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;res &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;json&lt;span style="color:#eceff4"&gt;())&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;.&lt;/span&gt;then&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;todos &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#616e87;font-style:italic"&gt;// Loop through todos and add to list
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; todos&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;forEach&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;todo &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; li &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; createTodoItem&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;todo&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;todo_item&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; todo&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;id&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;document&lt;/span&gt;&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;querySelector&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;#todos_list&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;).&lt;/span&gt;appendChild&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;li&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;// handler for adding a todo item
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;document&lt;/span&gt;&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;querySelector&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;form&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;).&lt;/span&gt;addEventListener&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;submit&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;e&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; e&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;preventDefault&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; todo_item &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;document&lt;/span&gt;&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;querySelector&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;#todo&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;).&lt;/span&gt;value&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; fetch&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;/todos&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; method&lt;span style="color:#81a1c1"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#39;POST&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; headers&lt;span style="color:#81a1c1"&gt;:&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#39;Content-Type&amp;#39;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#39;application/json&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; body&lt;span style="color:#81a1c1"&gt;:&lt;/span&gt; JSON&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;stringify&lt;span style="color:#eceff4"&gt;({&lt;/span&gt; todo_item &lt;span style="color:#eceff4"&gt;})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;.&lt;/span&gt;then&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;res &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;json&lt;span style="color:#eceff4"&gt;())&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;.&lt;/span&gt;then&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;data &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; li &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; createTodoItem&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;todo_item&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; data&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;id&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;document&lt;/span&gt;&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;querySelector&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;#todos_list&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;).&lt;/span&gt;appendChild&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;li&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;document&lt;/span&gt;&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;querySelector&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;#todo&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;).&lt;/span&gt;value &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Again, there&amp;rsquo;s no startling innovation. When it loads, the API is called to get the list of todo items which are turned into list items with &amp;lsquo;Done&amp;rsquo; buttons and appended to the &lt;ul&gt;. Then there&amp;rsquo;s some code for adding a new item. We&amp;rsquo;ll come back to some of this in more detail later when we look at the htmx.&lt;/p&gt;
&lt;h3 id="htmx-version"&gt;htmx Version&lt;/h3&gt;
&lt;h4 id="indexhtml"&gt;index.html&lt;/h4&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;&amp;lt;!&lt;/span&gt;DOCTYPE html&lt;span style="color:#81a1c1"&gt;&amp;gt;&amp;lt;&lt;/span&gt;html lang&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;en&amp;#34;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;&amp;gt;&amp;lt;&lt;/span&gt;head&lt;span style="color:#81a1c1"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;&amp;lt;&lt;/span&gt;meta charset&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;UTF-8&amp;#34;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;&amp;lt;&lt;/span&gt;meta name&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;viewport&amp;#34;&lt;/span&gt; content&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;width=device-width, initial-scale=1.0&amp;#34;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;&amp;gt;&lt;/span&gt; https&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt;unpkg&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;com&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;htmx&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;org&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;dist&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;htmx&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;min&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;js &lt;span style="color:#81a1c1"&gt;&amp;lt;&lt;/span&gt;title&lt;span style="color:#81a1c1"&gt;&amp;gt;&lt;/span&gt;Todos&lt;span style="color:#81a1c1"&gt;&amp;lt;/&lt;/span&gt;title&lt;span style="color:#81a1c1"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;&amp;lt;&lt;/span&gt;link rel&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;stylesheet&amp;#34;&lt;/span&gt; href&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;./styles.css&amp;#34;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;head&lt;span style="color:#81a1c1"&gt;&amp;gt;&amp;lt;&lt;/span&gt;body&lt;span style="color:#81a1c1"&gt;&amp;gt;&amp;lt;!--&lt;/span&gt; Main section&lt;span style="color:#81a1c1"&gt;--&amp;gt;&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;&amp;lt;&lt;/span&gt;main&lt;span style="color:#81a1c1"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;&amp;lt;&lt;/span&gt;h1&lt;span style="color:#81a1c1"&gt;&amp;gt;&lt;/span&gt;To &lt;span style="color:#81a1c1;font-weight:bold"&gt;do&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;&amp;lt;/&lt;/span&gt;h1&lt;span style="color:#81a1c1"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;&amp;lt;&lt;/span&gt;ul id&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;todos_list&amp;#34;&lt;/span&gt; hx&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;get&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/todos&amp;#34;&lt;/span&gt; hx&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;trigger&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;load&amp;#34;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;ul&lt;span style="color:#81a1c1"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;&amp;lt;!--&lt;/span&gt; the list of todo items from the database gets inserted here&lt;span style="color:#81a1c1"&gt;--&amp;gt;&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;&amp;lt;!--&lt;/span&gt; form to add a todo &lt;span style="color:#81a1c1"&gt;--&amp;gt;&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;&amp;lt;&lt;/span&gt;form hx&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;post&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/todos&amp;#34;&lt;/span&gt; hx&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;target&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;#todos_list&amp;#34;&lt;/span&gt; hx&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;swap&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;beforeend&amp;#34;&lt;/span&gt; hx&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;on&lt;span style="color:#eceff4"&gt;::&lt;/span&gt;after&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;request&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;this.reset()&amp;#34;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;&amp;lt;&lt;/span&gt;input type&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;text&amp;#34;&lt;/span&gt; name&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;todo_item&amp;#34;&lt;/span&gt; id&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;todo&amp;#34;&lt;/span&gt; required&lt;span style="color:#81a1c1"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;&amp;lt;&lt;/span&gt;button type&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;submit&amp;#34;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;&amp;gt;&lt;/span&gt;Add&lt;span style="color:#81a1c1"&gt;&amp;lt;/&lt;/span&gt;button&lt;span style="color:#81a1c1"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;&amp;lt;/&lt;/span&gt;form&lt;span style="color:#81a1c1"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;&amp;lt;/&lt;/span&gt;main&lt;span style="color:#81a1c1"&gt;&amp;gt;&lt;/span&gt;                  &lt;span style="color:#81a1c1"&gt;&amp;lt;/&lt;/span&gt;body&lt;span style="color:#81a1c1"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;html&lt;span style="color:#81a1c1"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The script tag at the top pulls in html (which is actually just 14K of gzipped Javascript) from a CDN. You can alternatively download it and serve it statically with your other assets. It&amp;rsquo;s worth noting, that&amp;rsquo;s all the tooling involved - no build tools etc.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;&amp;lt;&lt;/span&gt;ul id&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;todos_list&amp;#34;&lt;/span&gt; hx&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;get&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/todos&amp;#34;&lt;/span&gt; hx&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;trigger&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;load&amp;#34;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;ul&lt;span style="color:#81a1c1"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is the unordered list I want the todo items to go into. When the page is loaded (hx-trigger) we&amp;rsquo;ll hit the &lt;code&gt;app.get(&amp;quot;/todos&amp;quot;)&lt;/code&gt; endpoint (hx-get). I don&amp;rsquo;t need to specify where the returned html goes to - by default it&amp;rsquo;s the innerHTML of the calling tag.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;form hx-post=&amp;#34;/todos&amp;#34; hx-target=&amp;#34;#todos_list&amp;#34; hx-swap=&amp;#34;beforeend&amp;#34; hx-on::after-request=&amp;#34;this.reset()&amp;#34;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is the form for adding a new todo item. It&amp;rsquo;s going to hit the &lt;code&gt;app.post(&amp;quot;/todos&amp;quot;)&lt;/code&gt; endpoint (hx-post), and the returned HTML (which will be the the new list item to add to the list) needs to go onto the unordered list we talked about earlier (hx-target). The hx-swap=&amp;ldquo;beforeend&amp;rdquo; part means the returned list item will be inserted just before the end of the &lt;ul&gt; - ie as the last item in the list.&lt;/p&gt;
&lt;p&gt;After the user has hit return or the &amp;lsquo;Add&amp;rsquo; button to save their todo item, I don&amp;rsquo;t want the text they just entered to be sitting there, so a tiny Javascript snippet needs to be run. There are a heap of html hooks for these sorts of jobs (hx-on::afterrequest).&lt;/p&gt;
&lt;p&gt;The final change is that we&amp;rsquo;ve removed the script tag at the bottom that referred to our own Javascript code. None of that is needed now - the application state is delivered as complete HTML by the server.&lt;/p&gt;
&lt;h4 id="serverjs"&gt;server.js&lt;/h4&gt;
&lt;p&gt;Now our node/express server. I&amp;rsquo;ll dump the whole file here, then talk about each part.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;// Simple Express ToDo app using SQLite3 &amp;amp; HTMX
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; express &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;express&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; app &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; express&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; port &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#b48ead"&gt;3000&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; sqlite3 &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;sqlite3&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;).&lt;/span&gt;verbose&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; db &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;new&lt;/span&gt; sqlite3&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;Database&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;db/todos.sqlite&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;use&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;express&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;static&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;public&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;use&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;express&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;urlencoded&lt;span style="color:#eceff4"&gt;({&lt;/span&gt; extended&lt;span style="color:#81a1c1"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;true&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;function&lt;/span&gt; htmlForTodoItem&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;uid&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; item_text&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;let&lt;/span&gt; html &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;`&amp;lt;li&amp;gt;&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;${&lt;/span&gt;item_text&lt;span style="color:#a3be8c"&gt;}&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; html &lt;span style="color:#81a1c1"&gt;+=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;`&amp;lt;button hx-delete=&amp;#34;todos/&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;${&lt;/span&gt;uid&lt;span style="color:#a3be8c"&gt;}&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34; hx-target=&amp;#34;closest li&amp;#34; `&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; html &lt;span style="color:#81a1c1"&gt;+=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#39;hx-swap=&amp;#34;outerHTML&amp;#34;&amp;gt;Done&amp;lt;/button&amp;gt;&amp;lt;/li&amp;gt;&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; html&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;/todos&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; db&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;all&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;SELECT * FROM todos&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; rows&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; console&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;log&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;message&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;status&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;500&lt;/span&gt;&lt;span style="color:#eceff4"&gt;).&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;&amp;lt;li&amp;gt;database error&amp;lt;/li&amp;gt;&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#616e87;font-style:italic"&gt;// loop through the rows and create a list item for each
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;let&lt;/span&gt; list &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; rows&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;forEach&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;row &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; list &lt;span style="color:#81a1c1"&gt;+=&lt;/span&gt; htmlForTodoItem&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;row&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;id&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; row&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;todo_item&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;status&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;200&lt;/span&gt;&lt;span style="color:#eceff4"&gt;).&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;list&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;post&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;/todos&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;!&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;body&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;todo_item&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;status&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;400&lt;/span&gt;&lt;span style="color:#eceff4"&gt;).&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;Missing todo_item field in request body&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; db&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;run&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;INSERT INTO todos (todo_item) VALUES (?)&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; req&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;body&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;todo_item&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;function&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; console&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;log&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;message&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;status&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;500&lt;/span&gt;&lt;span style="color:#eceff4"&gt;).&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;&amp;lt;li&amp;gt;database error&amp;lt;/li&amp;gt;&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#616e87;font-style:italic"&gt;// return just this item for HTMX to insert at the list end
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;status&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;200&lt;/span&gt;&lt;span style="color:#eceff4"&gt;).&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;htmlForTodoItem&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;this&lt;/span&gt;&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;lastID&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; req&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;body&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;todo_item&lt;span style="color:#eceff4"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;delete&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;/todos/:id&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; db&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;run&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;DELETE FROM todos WHERE id = ?&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; req&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;params&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;id&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; console&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;log&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;message&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;status&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;500&lt;/span&gt;&lt;span style="color:#eceff4"&gt;).&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;&amp;lt;li&amp;gt;database error&amp;lt;/li&amp;gt;&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;status&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;200&lt;/span&gt;&lt;span style="color:#eceff4"&gt;).&lt;/span&gt;end&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;// if it doesn&amp;#39;t exist, create the table
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;db&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;run&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;CREATE TABLE IF NOT EXISTS todos (id INTEGER PRIMARY KEY AUTOINCREMENT, todo_item TEXT)&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;// close the database gracefully on exit
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;process&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;on&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;SIGINT&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; db&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;close&lt;span style="color:#eceff4"&gt;((&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; console&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;error&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;message&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; console&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;log&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;Database closed.&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; process&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;exit&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;0&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;// start the server on port 3000 
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;listen&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;port&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; console&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;log&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;`Todo app listening on http://localhost:&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;${&lt;/span&gt;port&lt;span style="color:#a3be8c"&gt;}&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The first change in the top of the file is that we&amp;rsquo;ve removed the middleware for JSON request bodies and switched to URLEncoded which is what htmx will be sending us by default. Then we dive into this function which builds the HTML for each of the Todo items encapsulated in an &lt;li&gt; with it&amp;rsquo;s &amp;lsquo;Done&amp;rsquo; button to delete it.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;function htmlForTodoItem(uid, item_text) { let html = `&amp;lt;li&amp;gt;${item_text}`; html += `&amp;lt;button hx-delete=&amp;#34;todos/${uid}&amp;#34; hx-target=&amp;#34;closest li&amp;#34; `; html += &amp;#39;hx-swap=&amp;#34;outerHTML&amp;#34;&amp;gt;Done&amp;lt;/button&amp;gt;&amp;lt;/li&amp;gt;&amp;#39;; return html;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It&amp;rsquo;s my habit to name these little fragments like this - htmlForXXXX - and group them at the top of the file. I used to use EJS templates, and that&amp;rsquo;s a valid approach, but the functions seem less complicated somehow.&lt;/p&gt;
&lt;p&gt;The remaining code is our endpoints:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;app.get('/todos'&lt;/code&gt; - called when the page loads. Calls the htmlForTodoItem() for each todo item in the database, the returns all of that to be inserted into the &lt;UL&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;app.post('/todos'&lt;/code&gt; - for adding a single new todo item. It saves it in the database, then returns the new item as an &lt;li&gt; to be inserted at the end of the list.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;app.delete('/todos/:id&lt;/code&gt;&amp;rsquo; - this is the route called by the &amp;lsquo;Done&amp;rsquo; button on each todo item. It deletes the item from the database and pointedly returns nothing with that &lt;code&gt;res.status(200).end();&lt;/code&gt; - this is important, because the way the &lt;li&gt; is being deleted from the page is that it&amp;rsquo;s being replaced with what is returned - ie nothing.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These match up to the original versions, with the only significant difference being that instead of returning raw JSON, the HTML is being returned.&lt;/p&gt;
&lt;h3 id="reflections"&gt;Reflections&lt;/h3&gt;
&lt;p&gt;As far as I can see, these two apps are identical to the user. The htmx version is going to be a bit larger over the wire with that initial pull down of 14K, but we&amp;rsquo;re only saving 1.6K of Javascript by eliminating our index.js. That 14K is a big deal in a tiny app like this, but probably not for any serious app.&lt;/p&gt;
&lt;p&gt;In regard to the developer experience; Is the htmx version easier to live with and maintain? For me, I think the answer is yes - I&amp;rsquo;d rather think at the level of &amp;lsquo;add this html to the end of the list&amp;rsquo; rather than &amp;lsquo;document query selector appendtochild&amp;rsquo; then programmatically build a list item from the JSON i&amp;rsquo;m interpreting , so it&amp;rsquo;s a useful abstraction. I acknowledge this is going to be a highly subjective thing.&lt;/p&gt;
&lt;p&gt;The killer usecase for htmx is going to be for people who want a bunch of cool modern stuff in their web apps, but who don&amp;rsquo;t want to write frontend Javascript. So for Go, Rust, PHP, Ruby etc people it&amp;rsquo;s probably a no-brainer. This is probably also my situation, I&amp;rsquo;m a strong server-side Javascript programmer, but have little interest in learning all the cool stuff around the DOM.&lt;/p&gt;
&lt;p&gt;htmx might appeal to developers with a &lt;a href="https://benhoyt.com/writings/the-small-web-is-beautiful/"&gt;small-web&lt;/a&gt; flavour to their dev-politics. If you like semantic html, accessibility, reducing bandwidth and power consumption of your apps, and being a good guardian of your users&amp;rsquo; data on the servers you control, you&amp;rsquo;ll probably like htmx. If you like AWS lambda functions, Angular, Next, Vercel and outsourcing your auth to Octa, htmx may not be your thing.&lt;/p&gt;
&lt;p&gt;The type of app is going to be a major consideration when deciding if htmx is an appropriate choice. If you are writing the next Google Sheets, htmx is not going to be able to do that, you need the raw power of JS. If your bread and butter is commercial CRUD apps and you want to make them quicker, avoid page flashes, and have modern UI magic such as search results that update as you type, then htmx is going to be your jam.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s not an unrealistic dream that the functionality in htmx becomes part of the HTML specification. The &lt;a href="http://hypermedia.systems/book/contents/"&gt;book&lt;/a&gt; sets out some good arguments for it, and the htmx implementation of that shows how possible and appealing it is. Whether it does or doesn&amp;rsquo;t, I expect to be using a lot in the future.&lt;/p&gt;</description></item><item><title>Simple SQLite in Express</title><link>https://blog.iankulin.com/simple-sqlite-in-express/</link><pubDate>Thu, 28 Dec 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/simple-sqlite-in-express/</guid><description>&lt;p&gt;I don&amp;rsquo;t have experience with &lt;a href="https://www.sqlite.org/index.html"&gt;SQLite&lt;/a&gt; and want to shift one of my apps over from Mongoose since apparently SQLite is &lt;a href="https://www.sqlite.org/whentouse.html"&gt;much more capable&lt;/a&gt; than I imagined. My usual tactic when trying something new is to try and get a minimal project working on it, so what follows is the simplest possible node/express REST API to demo SQLite.&lt;/p&gt;
&lt;p&gt;The simplest possible Express app is going to look something like this. Of course we would have gone to the terminal with &lt;code&gt;npm i express&lt;/code&gt; first so this could run.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; express &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;express&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; app &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; express&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; port &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#b48ead"&gt;3000&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;/&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;Hello, World!&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);});&lt;/span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;listen&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;port&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; console&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;log&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;Server is running at http&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt;localhost&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;port&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The only thing to add to this for the moment is some middleware to allow Express to parse JSON body payloads.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app.use(express.json());
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="body-v-query"&gt;Body v Query&lt;/h4&gt;
&lt;p&gt;I&amp;rsquo;ll just take you on a short detour here if you&amp;rsquo;re not familiar with HTTP requests. There&amp;rsquo;s a couple of common ways to send some data along with a request. The oldest one is to shove it all in the URL. You will have seen these sorts of things:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;http://localhost:3000/adduser?name=Fred&amp;amp;email=fred@example.com&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;If you want all of your data to fit in a link, these are great. They can be bookmarked and so on. You see them all the time - especially in links with heaps of tracking data. The &amp;lsquo;?&amp;rsquo; denotes it as a query.&lt;/p&gt;
&lt;p&gt;The code to process the GET request above would look like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;/adduser&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; name &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;query&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; email &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;query&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;email&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Note that I&amp;rsquo;ve used a GET request here, when semantically a POST would make more sense. That&amp;rsquo;s because you can only use query strings for GETs.&lt;/p&gt;
&lt;p&gt;Often the data you want to pass is going to be more complex, or you don&amp;rsquo;t want it in the URL for other reasons (for example, you wouldn&amp;rsquo;t want a user to be able to bookmark a record delete request). In that case you use the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Request/body"&gt;body&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;You can stuff all sorts of text data in the body. Most times you are going to want JSON. I do for this demo, so that&amp;rsquo;s why I&amp;rsquo;ve added the &lt;code&gt;expresss.json()&lt;/code&gt; middleware - all the hard work will be done for me and I can just do this to access the information that&amp;rsquo;s passed as part of the request:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;post&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;/users&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; name &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;body&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; email &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;body&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;email&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="adding-sqlite"&gt;Adding SQLite&lt;/h3&gt;
&lt;p&gt;Once you&amp;rsquo;ve run &lt;code&gt;npm i sqlite3&lt;/code&gt; at the terminal to install the package, at the top of our app somewhere - probably where we&amp;rsquo;re requiring the other packages - we&amp;rsquo;ll need this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; sqlite3 &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;sqlite3&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;verbose&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; db &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; new sqlite3&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;Database&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;db/test.sqlite&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is just requiring the package, and giving us &lt;code&gt;db&lt;/code&gt; as the variable for the SQLite database connection. The database is actually a file in the &lt;code&gt;db/&lt;/code&gt; directory.&lt;/p&gt;
&lt;p&gt;On the first run, obviously this will be empty, so somewhere before the listen command, we need to create a table.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;// if the &amp;#39;users&amp;#39; table doesn&amp;#39;t exist, // create it with &amp;#39;name&amp;#39; and &amp;#39;email&amp;#39; columnsdb.run(&amp;#39;CREATE TABLE IF NOT EXISTS users (name TEXT, email TEXT)&amp;#39;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you&amp;rsquo;ve never encountered SQL before, and was expecting a bunch of methods being passing in structs to do this, this is going to be alarming. But no - in SQL we do things by sending the engine a string. This has created a massive &lt;a href="https://en.wikipedia.org/wiki/SQL_injection"&gt;attack surface&lt;/a&gt;, but it&amp;rsquo;s also a convenient and very readable convention.&lt;/p&gt;
&lt;p&gt;For our simple purposes today, that could be enough infrastructure for SQLite, but because we are good programmers, we&amp;rsquo;ll correctly close the database when the app undergoes an orderly shutdown by adding this before the &lt;code&gt;app.listen&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;// close the database connection when the app is shutting downprocess.on(&amp;#39;SIGINT&amp;#39;, () =&amp;gt; { db.close((err) =&amp;gt; { if (err) { console.error(&amp;#39;Error closing SQLite database:&amp;#39;, err.message); } else { process.exit(0); } });});
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="working-with-sqlite"&gt;Working with SQLite&lt;/h3&gt;
&lt;p&gt;That&amp;rsquo;s the infrastructure out of the way. Now, onto our CRUD (create, read, update, delete) operations to manipulate our stored data. Since this is just a demo, the simplest way to show these is just to have endpoints for each one. To exercise these endpoints you could use the development tools in your browser, but most people will use an API testing tool like Postman, or Insomnia. I much prefer &lt;a href="https://blog.iankulin.com/we-need-to-talk-about-bruno/"&gt;Bruno&lt;/a&gt; for this job, so I&amp;rsquo;ll use that (and suggest you do too, get it &lt;a href="https://www.usebruno.com/"&gt;here&lt;/a&gt;).&lt;/p&gt;
&lt;h4 id="create-add"&gt;Create (add)&lt;/h4&gt;
&lt;p&gt;We already started on that earlier, and explained the concept of passing in data via the &amp;lsquo;body&amp;rsquo; here&amp;rsquo;s the complete thing:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; endpoint to add a user record to the users table&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; expects a name &lt;span style="color:#81a1c1;font-weight:bold"&gt;and&lt;/span&gt; email &lt;span style="color:#81a1c1;font-weight:bold"&gt;in&lt;/span&gt; the JSON body of the requestapp&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;post&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;/users&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; name &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;body&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; email &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;body&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;email&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; sql &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#bf616a"&gt;`&lt;/span&gt;INSERT INTO users &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; email&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; VALUES &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;${name}&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;${email}&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; db&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;run&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;sql&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; function&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;status&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;500&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;message&lt;span style="color:#eceff4"&gt;);&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;status&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;201&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;json&lt;span style="color:#eceff4"&gt;({&lt;/span&gt; rowid&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; this&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;lastID &lt;span style="color:#eceff4"&gt;});&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#eceff4"&gt;});});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So this code processes a request to our server - something like &lt;code&gt;http://localhost:3000/users&lt;/code&gt; and expects the body payload to contain some JSON with a name and email. It could look like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{ &amp;#34;name&amp;#34;: &amp;#34;John Doe&amp;#34;, &amp;#34;email&amp;#34;: &amp;#34;john.doe@example.com&amp;#34;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And when run in Bruno:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-12-16-at-10.24.54-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-12-16-at-10.24.54-am.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4 id="read"&gt;Read&lt;/h4&gt;
&lt;p&gt;There&amp;rsquo;s a couple of reads we can do, one where all the data is returned, and one where only a specific record is. Let&amp;rsquo;s do the big one first, since we&amp;rsquo;ll use it a lot while we&amp;rsquo;re writing the rest!&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; endpoint to get all users from the users table &lt;span style="color:#81a1c1;font-weight:bold"&gt;in&lt;/span&gt; the databaseapp&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;/users&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; sql &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#39;SELECT rowid, * FROM users&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; db&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;all&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;sql&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; rows&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;status&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;500&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;message&lt;span style="color:#eceff4"&gt;);&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;status&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;200&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;rows&lt;span style="color:#eceff4"&gt;);&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#eceff4"&gt;});});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Which looks like this in Bruno:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-12-16-at-10.35.56-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-12-16-at-10.35.56-am.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Or if we just want one in particular, we&amp;rsquo;ll pass the id in the URL.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; endpoint to get a single user from the users table &lt;span style="color:#81a1c1;font-weight:bold"&gt;in&lt;/span&gt; the database&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; expects an id &lt;span style="color:#81a1c1;font-weight:bold"&gt;in&lt;/span&gt; the URLapp&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;/user/:id&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; id &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;params&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;id&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; sql &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#bf616a"&gt;`&lt;/span&gt;SELECT rowid&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;*&lt;/span&gt; FROM users WHERE rowid &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;id&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; db&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;sql&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; row&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;status&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;500&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;message&lt;span style="color:#eceff4"&gt;);&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;status&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;200&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;row&lt;span style="color:#eceff4"&gt;);&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#eceff4"&gt;});});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-12-16-at-10.55.25-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-12-16-at-10.55.25-am.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4 id="update"&gt;Update&lt;/h4&gt;
&lt;p&gt;You&amp;rsquo;re probably getting the hang of this now.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; endpoint to update a user record &lt;span style="color:#81a1c1;font-weight:bold"&gt;in&lt;/span&gt; the users table &lt;span style="color:#81a1c1;font-weight:bold"&gt;in&lt;/span&gt; the database&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; expects a name&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;and&lt;/span&gt; email &lt;span style="color:#81a1c1;font-weight:bold"&gt;in&lt;/span&gt; the JSON body of the request&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; expects an id &lt;span style="color:#81a1c1;font-weight:bold"&gt;in&lt;/span&gt; the URLapp&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;put&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;/user/:id&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; id &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;params&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;id&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; name &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;body&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; email &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;body&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;email&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; sql &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#bf616a"&gt;`&lt;/span&gt;UPDATE users SET name &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;${name}&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; email &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;${email}&amp;#34;&lt;/span&gt; WHERE rowid &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;id&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; db&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;run&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;sql&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;status&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;500&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;message&lt;span style="color:#eceff4"&gt;);&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;status&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;200&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;User updated.&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#eceff4"&gt;});});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="delete"&gt;Delete&lt;/h4&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; endpoint to delete a user record from the users table &lt;span style="color:#81a1c1;font-weight:bold"&gt;in&lt;/span&gt; the database&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; expects an id &lt;span style="color:#81a1c1;font-weight:bold"&gt;in&lt;/span&gt; the URL&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt; Doesn&lt;span style="color:#a3be8c"&gt;&amp;#39;t complain if the id doesn&amp;#39;&lt;/span&gt;t exist&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;delete&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;/user/:id&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; id &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;params&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;id&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; sql &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#bf616a"&gt;`&lt;/span&gt;DELETE FROM users WHERE rowid &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;id&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; db&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;run&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;sql&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;status&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;500&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;message&lt;span style="color:#eceff4"&gt;);&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;status&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;200&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;();&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#eceff4"&gt;});});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="hardening"&gt;Hardening&lt;/h3&gt;
&lt;p&gt;To keep things simple (since I was just trying to show basic examples of using sqlite) I used string interpolation when making the SQL to run against the database. That&amp;rsquo;s not a great technique because of the danger of SQL injection; so we should routinely use parameterized queries instead. Here&amp;rsquo;s how adding a user looks if we use parameterized queries:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; endpoint to add a user record to the users table&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; expects a name &lt;span style="color:#81a1c1;font-weight:bold"&gt;and&lt;/span&gt; email &lt;span style="color:#81a1c1;font-weight:bold"&gt;in&lt;/span&gt; the JSON body of the requestapp&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;post&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;/users&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; name &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;body&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; email &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;body&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;email&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; sql &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#bf616a"&gt;`&lt;/span&gt;INSERT INTO users &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; email&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; VALUES &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#bf616a"&gt;?&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#bf616a"&gt;?&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; db&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;run&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;sql&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;[&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; email&lt;span style="color:#eceff4"&gt;],&lt;/span&gt; function&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;status&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;500&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;message&lt;span style="color:#eceff4"&gt;);&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;status&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;201&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;json&lt;span style="color:#eceff4"&gt;({&lt;/span&gt; rowid&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; this&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;lastID &lt;span style="color:#eceff4"&gt;});&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#eceff4"&gt;});});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With parameterized queries, whatever the user passes in ends up in the database rather than being executed as part of the query string.&lt;/p&gt;
&lt;p&gt;For example, imagine if an API user tried to add a user with this body:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{ &amp;#34;name&amp;#34;: &amp;#34;John&amp;#34;, &amp;#34;email&amp;#34;: &amp;#34;john@example.com\&amp;#34;); DROP TABLE users; --&amp;#34;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then my original add code:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;post&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;/users&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; name &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;body&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; email &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;body&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;email&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; sql &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#bf616a"&gt;`&lt;/span&gt;INSERT INTO users &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; email&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; VALUES &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;${name}&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;${email}&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; db&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;run&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;sql&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; function&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;status&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;500&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;message&lt;span style="color:#eceff4"&gt;);&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;status&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;201&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;json&lt;span style="color:#eceff4"&gt;({&lt;/span&gt; rowid&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; this&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;lastID &lt;span style="color:#eceff4"&gt;});&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#eceff4"&gt;});});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;would result in executing this against the database:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;INSERT INTO users (name, email) VALUES (&amp;#34;John&amp;#34;, &amp;#34;john@example.com&amp;#34;); DROP TABLE users; --&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;which would delete the entire users table from the database. This is &lt;a href="https://xkcd.com/327/"&gt;widely known as the &amp;ldquo;Bobby Tables&amp;rdquo; problem&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;With the parameterized version, you just end up with an ugly user record.&lt;/p&gt;
&lt;p&gt;Changing all of these doesn&amp;rsquo;t add much code, but does make it a little bit harder to follow, hence showing you the old version first.&lt;/p&gt;
&lt;h3 id="rest-api-conventions"&gt;REST API conventions&lt;/h3&gt;
&lt;p&gt;You may have noticed in this code I&amp;rsquo;ve used a variety of HTTP request types - GET, POST, PUT, DELETE etc. There&amp;rsquo;s no rules for these things, but if someone else (including future you) is going to have to maintain or use your API, it&amp;rsquo;s a good idea to follow the conventions.&lt;/p&gt;
&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Situation&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Request&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;URL&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Return&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Add a record&lt;/td&gt;&lt;td&gt;POST&lt;/td&gt;&lt;td&gt;/users&lt;/td&gt;&lt;td&gt;record id&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Replace a whole record&lt;/td&gt;&lt;td&gt;PUT&lt;/td&gt;&lt;td&gt;/users/:id&lt;/td&gt;&lt;td&gt;the whole record&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Replace part of a record&lt;/td&gt;&lt;td&gt;PATCH&lt;/td&gt;&lt;td&gt;/users/:id&lt;/td&gt;&lt;td&gt;the whole record&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Get all the records&lt;/td&gt;&lt;td&gt;GET&lt;/td&gt;&lt;td&gt;/users&lt;/td&gt;&lt;td&gt;all the records&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Get a particular record&lt;/td&gt;&lt;td&gt;GET&lt;/td&gt;&lt;td&gt;/users/:id&lt;/td&gt;&lt;td&gt;that record&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Delete a record&lt;/td&gt;&lt;td&gt;DELETE&lt;/td&gt;&lt;td&gt;/users/:id&lt;/td&gt;&lt;td&gt;nothing&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;You might have noticed that I haven&amp;rsquo;t done the PATCH - the difference between that and the PUT is that with the PATCH we don&amp;rsquo;t supply the whole record, just the fields we want to change. I&amp;rsquo;m not going to worry about that for this API since our record is so small.&lt;/p&gt;
&lt;p&gt;But I also don&amp;rsquo;t return the whole record after a PUT. Unfortunately, it means a second request - but there&amp;rsquo;s probably not much of a performance hit since it will be in the cache.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; endpoint to update a user record &lt;span style="color:#81a1c1;font-weight:bold"&gt;in&lt;/span&gt; the users table &lt;span style="color:#81a1c1;font-weight:bold"&gt;in&lt;/span&gt; the database&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; expects a name&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;and&lt;/span&gt; email &lt;span style="color:#81a1c1;font-weight:bold"&gt;in&lt;/span&gt; the JSON body of the request&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; expects an id &lt;span style="color:#81a1c1;font-weight:bold"&gt;in&lt;/span&gt; the URLapp&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;put&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;/user/:id&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; id &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;params&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;id&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; name &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;body&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; email &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;body&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;email&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; updateSql &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#bf616a"&gt;`&lt;/span&gt;UPDATE users SET name &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#bf616a"&gt;?&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; email &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#bf616a"&gt;?&lt;/span&gt; WHERE rowid &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#bf616a"&gt;?`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; db&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;run&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;updateSql&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;[&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; email&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; id&lt;span style="color:#eceff4"&gt;],&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;status&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;500&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;message&lt;span style="color:#eceff4"&gt;);&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; selectSql &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#bf616a"&gt;`&lt;/span&gt;SELECT rowid&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;*&lt;/span&gt; FROM users WHERE rowid &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#bf616a"&gt;?`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; db&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;selectSql&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;[&lt;/span&gt;id&lt;span style="color:#eceff4"&gt;],&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; row&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;status&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;500&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;message&lt;span style="color:#eceff4"&gt;);&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;status&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;200&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;json&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;row&lt;span style="color:#eceff4"&gt;);&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#eceff4"&gt;});});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;a href="https://github.com/IanKulin/sqlite-rest-demo/blob/main/app.js"&gt;Link to the completed project on Github&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>Gogs, Gitea, Forgejo</title><link>https://blog.iankulin.com/gogs-gitea-forgejo/</link><pubDate>Mon, 18 Dec 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/gogs-gitea-forgejo/</guid><description>&lt;img src="https://blog.iankulin.com/images/img_7071-1.png" width="640" alt=""&gt;
&lt;p&gt;I&amp;rsquo;ve been really pleased with &lt;a href="https://blog.iankulin.com/tags/gogs/"&gt;Gogs&lt;/a&gt; - it&amp;rsquo;s lightweight, was simple to spin up, and has worked perfectly. But then this morning on Mastodon, there&amp;rsquo;s a &lt;a href="https://mastodon.social/@Codeberg@social.anoxinon.de/111471407276450348"&gt;post from @Codeberg.org&lt;/a&gt; describing a security vulnerability in their Git hosting project Forgejo. This issue also apparently affects Gitea and Gogs - what&amp;rsquo;s up with that?&lt;/p&gt;
&lt;p&gt;I actually already did spend a bit of time comparing Gogs and Gitea before deciding on Gogs, since I&amp;rsquo;d heard of people running Gitea over the past year or so, but only seen that Gogs seemed to be popular with self-hosters in a Lemmy post I&amp;rsquo;d read. My first impression was that Gitea was more focused on CI/CD and seemed to have a more complicated install process.&lt;/p&gt;
&lt;p&gt;What I didn&amp;rsquo;t do, was think about the project management and teams. It turns out that &lt;a href="https://about.gitea.com/"&gt;Gitea&lt;/a&gt; was forked from &lt;a href="https://gogs.io/"&gt;Gogs&lt;/a&gt; by contributors in 2016 due to &lt;a href="https://blog.gitea.com/welcome-to-gitea/"&gt;disagreements about the project management&lt;/a&gt;. Then at the end of 2022 &lt;a href="https://forgejo.org/"&gt;Forgejo&lt;/a&gt; was forked from Gitea due to &lt;a href="https://forgejo.org/2022-12-15-hello-forgejo/"&gt;Gitea moving the trademarks and domain into a company&lt;/a&gt; providing Gitea support.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://forgejo.org/2023-11-release-v1-20-5-1/"&gt;CVE announcement from Forgeo&lt;/a&gt;, while a little snarky about their ancestors, does give the impression of a functional organisation that&amp;rsquo;s able to deal with issues as they come up. It&amp;rsquo;s a credit to the group to be in that position after just a year, and their &lt;a href="https://codeberg.org/forgejo/forgejo"&gt;repo&lt;/a&gt; (which is dogfooded) seems plenty active.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve only just started on Gogs, so it&amp;rsquo;s still easy to move if that&amp;rsquo;s what I decide. I guess my learning from stumbling upon this security announcement is more that I should:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;take into account more than just project features when making these decisions&lt;/li&gt;
&lt;li&gt;I need to be subscribed to the channels where I&amp;rsquo;d learn about security issues in the projects I&amp;rsquo;m using and their major dependencies.&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Git - pushing to two remotes</title><link>https://blog.iankulin.com/git-pushing-to-two-remotes/</link><pubDate>Fri, 15 Dec 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/git-pushing-to-two-remotes/</guid><description>&lt;p&gt;I am loving running a local Gogs instance - it&amp;rsquo;s nice pushing my git repos to a totally private hub that I know is backed up with all my other self-hosted infrastructure.&lt;/p&gt;
&lt;p&gt;Of course, there&amp;rsquo;s good reasons to have code in GitHub as well - my build-in-public philosophy, the vague possibility that some of it might be useful to someone, my contribution to our future AI overlords, and when I need to make some code linkable - for example from one of these posts. And of course there&amp;rsquo;s this bit of social-engineering which I assume was inspired by the bathroom decor in &lt;a href="https://i.pinimg.com/originals/94/23/85/9423854153f55938c454a061ad5462fe.gif"&gt;Veronica Mars&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-11-25-at-5.45.50-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Git is an amazing tool, so of course this is possible. Normally my workflow is that I &lt;code&gt;git init&lt;/code&gt; whenever I&amp;rsquo;m working on a new something, then at some point I think &amp;ldquo;I should really push all this so it&amp;rsquo;s backed up&amp;rdquo;. I create the repository for it on GitHub or Gogs via the web interface, then come back to my project and:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;git remote add origin git@github.com:IanKulin/test.git&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This is making the connection between my local project and the GitHub repo. I&amp;rsquo;d never really thought about what &lt;code&gt;origin&lt;/code&gt; meant in this context the hundreds of times I&amp;rsquo;ve previously typed it in, but actually it&amp;rsquo;s just the name we are giving to this connection. It&amp;rsquo;s just a convention to call it &amp;lsquo;origin&amp;rsquo;, it could just as easily be called &amp;lsquo;fred&amp;rsquo; or &amp;lsquo;github&amp;rsquo;. Since I am now planning to push to two separate remotes, it&amp;rsquo;s going to make sense to give them meaningful names. So in that case, we can do this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git remote add github git@github.com:IanKulin/test.git
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git remote add gogs http://ct-gogs/iankulin/test.git
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then, we can push with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git push github main
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git push gogs main
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You might be wondering what happens if you just do a &lt;code&gt;git push&lt;/code&gt; at this stage (or as I like to call it &amp;ldquo;&lt;em&gt;Pressing the &amp;lsquo;Publish Branch&amp;rsquo; button on the VS Code source control panel&amp;rdquo;&lt;/em&gt;). The answer is that at the command line you&amp;rsquo;ll get an error saying you haven&amp;rsquo;t specified the destination, or in VS Code, it will ask you which one.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-11-25-at-9.41.08-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;We can set the default remote with the -u flag when we&amp;rsquo;re pushing&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git push -u gogs main
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-11-25-at-9.46.26-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Now the button in VS Code will say something &amp;ldquo;Sync Changes&amp;rdquo; and when you press it, it will only push to the remote we used in the last &lt;code&gt;-u&lt;/code&gt; push. Same thing if we &lt;code&gt;git push&lt;/code&gt; at the command line - it will work, but only push to the remote we used in the last &lt;code&gt;-u&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s also worth noting that when we&amp;rsquo;ve set the default remote with the &lt;code&gt;-u&lt;/code&gt; flag in a &lt;code&gt;push&lt;/code&gt;, it is also the default remote for pulling from. Essentially this remote becomes the source-of-truth.&lt;/p&gt;
&lt;p&gt;For me this setup is usually fine - I&amp;rsquo;m generally working on my local gogs remote, that&amp;rsquo;s the source of truth so I specify it as the default with the &lt;code&gt;push -u&lt;/code&gt;. Then, when I&amp;rsquo;m done, I manually push to github so I can share it. If it was a project I needed to work on with anyone else, that would have to be the other way around - I&amp;rsquo;d use GitHub (or GitLab, Bitbucket etc) as the source of truth, and probably not even worry about hosting a copy on my home network unless I was worried about the repo being deleted.&lt;/p&gt;</description></item><item><title>Concurrency and channels in Go</title><link>https://blog.iankulin.com/concurrency-and-channels-in-go/</link><pubDate>Tue, 12 Dec 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/concurrency-and-channels-in-go/</guid><description>&lt;img src="https://blog.iankulin.com/images/portal-logo.jpg" width="400" alt=""&gt;
&lt;p&gt;In the long ago times, I&amp;rsquo;d done several years of commercial programming before I ever had to worry about dealing with multiple things happening at the same time. Perhaps because of the rarity of this problem, doing it in traditional languages was not always elegant.&lt;/p&gt;
&lt;p&gt;In the modern world of everything happening on the network, and systems being build out of micro-services and APIs, the beginning programmer probably has to deal with this stuff in Programming 102. Luckily, modern languages have these considerations built in, and one language with a particular reputation for that is Go.&lt;/p&gt;
&lt;p&gt;In Go, we have &lt;em&gt;Goroutines&lt;/em&gt;. This is basically a way of calling a function in such a way that the function goes away and does it&amp;rsquo;s thing and the rest of the program doesn&amp;rsquo;t wait for it. To do this, you just pop a go directive in front of the function call. Consider this little program:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;package&lt;/span&gt; main
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;import&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	&lt;span style="color:#a3be8c"&gt;&amp;#34;fmt&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	&lt;span style="color:#a3be8c"&gt;&amp;#34;math/rand&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	&lt;span style="color:#a3be8c"&gt;&amp;#34;time&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;func&lt;/span&gt; &lt;span style="color:#88c0d0"&gt;waitAndReportWorker&lt;/span&gt;&lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	&lt;span style="color:#81a1c1;font-weight:bold"&gt;for&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;		sleepTime &lt;span style="color:#81a1c1"&gt;:=&lt;/span&gt; time&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;&lt;span style="color:#88c0d0"&gt;Duration&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;rand&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;&lt;span style="color:#88c0d0"&gt;Intn&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;5&lt;/span&gt;&lt;span style="color:#eceff4"&gt;))&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;*&lt;/span&gt; time&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;Second
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;		time&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;&lt;span style="color:#88c0d0"&gt;Sleep&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;sleepTime&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;		fmt&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;&lt;span style="color:#88c0d0"&gt;Printf&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Worker slept for %s&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; sleepTime&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;func&lt;/span&gt; &lt;span style="color:#88c0d0"&gt;main&lt;/span&gt;&lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	&lt;span style="color:#88c0d0"&gt;waitAndReportWorker&lt;/span&gt;&lt;span style="color:#eceff4"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If we run this, main hands control over to the worker, which sleeps for a bit, prints a message then repeats (normally the worker would some, like, actual work; but for our demo purposes, having a little nap then reporting it it fine).&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-11-23-at-8.34.12-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-11-23-at-8.34.12-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;We can convert it to a Goroutine, just by putting a go in front of the function call,&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;func&lt;/span&gt; main&lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	go waitAndReportWorker&lt;span style="color:#eceff4"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Yay! Our first baby Goroutine. Sadly, this program will exit before the worker ever reports, so let&amp;rsquo;s add an infinite loop after we&amp;rsquo;ve launched the Goroutine. And we&amp;rsquo;ll do something in the loop so you can see that there things are happening concurrently in our program.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-11-23-at-8.46.11-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;h3 id="collisions"&gt;Collisions&lt;/h3&gt;
&lt;p&gt;In my confected example, it&amp;rsquo;s extremely unlikely that the message from the worker would collide with the message being printed in the main loop, but it&amp;rsquo;s possible. But if we scaled up to a worldwide networked system processing millions of something a minute, it becomes almost guaranteed. Maybe a couple of sentences being mangled in output to the terminal is no big drama, but if we were writing something to a memory location, a file, a heart surgery robot interface, a database etc it could be bad. So we need to avoid that.&lt;/p&gt;
&lt;p&gt;The way Go deals with this is with &lt;em&gt;channels&lt;/em&gt;. A channel is like a portal between the main program in sequential procedural program land, to the worker function. When the worker needs to interact in some way with the main program it passes something back through the portal, and Go deals with it to avoid the dreaded collision. The portal/channel works the other way as well - the main program can pass information through the portal to the worker function.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s have a look at the changes for this, then tease them out a little:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;func&lt;/span&gt; waitAndReportWorker&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;resultChan chan&lt;span style="color:#81a1c1"&gt;&amp;lt;-&lt;/span&gt; string&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	&lt;span style="color:#81a1c1;font-weight:bold"&gt;for&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;		sleepTime &lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; time&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;Duration&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;rand&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;Intn&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;5&lt;/span&gt;&lt;span style="color:#eceff4"&gt;))&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;*&lt;/span&gt; time&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;Second
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;		time&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;Sleep&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;sleepTime&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;		resultChan &lt;span style="color:#81a1c1"&gt;&amp;lt;-&lt;/span&gt; fmt&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;Sprintf&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#ebcb8b"&gt;\n&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;Worker slept for &lt;/span&gt;&lt;span style="color:#a3be8c"&gt;%s&lt;/span&gt;&lt;span style="color:#ebcb8b"&gt;\n&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; sleepTime&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;func&lt;/span&gt; main&lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	resultChan &lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; make&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;chan string&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	go waitAndReportWorker&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;resultChan&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	&lt;span style="color:#81a1c1;font-weight:bold"&gt;for&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;		time&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;Sleep&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;250&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;*&lt;/span&gt; time&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;Millisecond&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;		fmt&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;Print&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Nothing happening here &amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;		result &lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;&amp;lt;-&lt;/span&gt;resultChan
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;		fmt&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;Println&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;result&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We create the channel with the &lt;code&gt;make&lt;/code&gt; function. The type for the channel is the type that we&amp;rsquo;re going to be passing through it. We pass the channel to our worker, where the function signature is:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;func waitAndReportWorker(resultChan chan&amp;lt;- string)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;I like this little arrow, it&amp;rsquo;s showing which way the portal works. In this case its a portal (channel) for passing a string out of the worker back to the main program. Channels can go the other way, ie. to pass things into the worker, or they can be bi-directional, which I don&amp;rsquo;t really think I&amp;rsquo;d do - I&amp;rsquo;d just add another channel.&lt;/p&gt;
&lt;p&gt;In our worker, we stuff something into the channel with that same arrow:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;resultChan &amp;lt;- fmt.Sprintf(&amp;quot;\nWorker slept for %s\n&amp;quot;, sleepTime)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;On the other end of our portal/channel (I wish they&amp;rsquo;d just called them portals - it&amp;rsquo;s no quirkier than the date formatting) in the main program we use another arrow to pull the value out:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;result := &amp;lt;-resultChan&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;If we run this code, it works, sort of.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-11-24-at-4.51.21-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;If you look at the output at the bottom, you can see that extracting the string out of our channel is a blocking operation. The program is waiting there until it gets a value. That&amp;rsquo;s no use - we could have done that without mucking about with channels.&lt;/p&gt;
&lt;p&gt;Of course, there is a way around this. What we really want to to is check if there&amp;rsquo;s a value in the channel. If there is, process it, or if not, travel around our loop again. What we do is put the retrieval of the channel value as a case in a select block.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;func&lt;/span&gt; main&lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	resultChan &lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; make&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;chan string&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	go waitAndReportWorker&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;resultChan&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	&lt;span style="color:#81a1c1;font-weight:bold"&gt;for&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;		select &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;		&lt;span style="color:#81a1c1;font-weight:bold"&gt;case&lt;/span&gt; result &lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;&amp;lt;-&lt;/span&gt;resultChan&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;			fmt&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;Println&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;result&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;		default&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;			time&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;Sleep&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;250&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;*&lt;/span&gt; time&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;Millisecond&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;			fmt&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;Print&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Nothing happening here &amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;		&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This version of the program will work exactly how we want. The worker goroutine will execute independently of the main loop which runs permanently, but then when the worker goroutine has something to say, it uses the channel to pass it back to the main routine which deals with it at the first opportunity.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-11-24-at-6.04.35-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Even though this is all working how we&amp;rsquo;d like, there is bit of programming craftsmanship needed. You may already know &lt;code&gt;make()&lt;/code&gt; from using it for slices. When we&amp;rsquo;re using it we&amp;rsquo;re allocating some resources - so now we have the responsibility to release them.&lt;/p&gt;
&lt;p&gt;To release the channel we made above, we close it:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;close(resultChan)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s add that for completeness. I&amp;rsquo;ll the time to exit my infinite loop. In practice You&amp;rsquo;ll have some other condition.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;package&lt;/span&gt; main
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;import&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	&lt;span style="color:#a3be8c"&gt;&amp;#34;fmt&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	&lt;span style="color:#a3be8c"&gt;&amp;#34;math/rand&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	&lt;span style="color:#a3be8c"&gt;&amp;#34;time&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;func&lt;/span&gt; &lt;span style="color:#88c0d0"&gt;waitAndReportWorker&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;resultChan &lt;span style="color:#81a1c1;font-weight:bold"&gt;chan&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;&amp;lt;-&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;string&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	&lt;span style="color:#81a1c1;font-weight:bold"&gt;for&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;		sleepTime &lt;span style="color:#81a1c1"&gt;:=&lt;/span&gt; time&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;&lt;span style="color:#88c0d0"&gt;Duration&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;rand&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;&lt;span style="color:#88c0d0"&gt;Intn&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;5&lt;/span&gt;&lt;span style="color:#eceff4"&gt;))&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;*&lt;/span&gt; time&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;Second
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;		time&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;&lt;span style="color:#88c0d0"&gt;Sleep&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;sleepTime&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;		resultChan &lt;span style="color:#81a1c1"&gt;&amp;lt;-&lt;/span&gt; fmt&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;&lt;span style="color:#88c0d0"&gt;Sprintf&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;\nWorker slept for %s\n&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; sleepTime&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;func&lt;/span&gt; &lt;span style="color:#88c0d0"&gt;main&lt;/span&gt;&lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	resultChan &lt;span style="color:#81a1c1"&gt;:=&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;make&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;chan&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;string&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	&lt;span style="color:#81a1c1;font-weight:bold"&gt;go&lt;/span&gt; &lt;span style="color:#88c0d0"&gt;waitAndReportWorker&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;resultChan&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	startTime &lt;span style="color:#81a1c1"&gt;:=&lt;/span&gt; time&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;&lt;span style="color:#88c0d0"&gt;Now&lt;/span&gt;&lt;span style="color:#eceff4"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	&lt;span style="color:#81a1c1;font-weight:bold"&gt;for&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;		&lt;span style="color:#81a1c1;font-weight:bold"&gt;select&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;		&lt;span style="color:#81a1c1;font-weight:bold"&gt;case&lt;/span&gt; result &lt;span style="color:#81a1c1"&gt;:=&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;&amp;lt;-&lt;/span&gt;resultChan&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;			fmt&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;&lt;span style="color:#88c0d0"&gt;Println&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;result&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;		&lt;span style="color:#81a1c1;font-weight:bold"&gt;default&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;			time&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;&lt;span style="color:#88c0d0"&gt;Sleep&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;250&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;*&lt;/span&gt; time&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;Millisecond&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;			fmt&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;&lt;span style="color:#88c0d0"&gt;Print&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Nothing happening here &amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;		&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;		&lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; time&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;&lt;span style="color:#88c0d0"&gt;Since&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;startTime&lt;span style="color:#eceff4"&gt;).&lt;/span&gt;&lt;span style="color:#88c0d0"&gt;Seconds&lt;/span&gt;&lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;&amp;gt;=&lt;/span&gt; &lt;span style="color:#b48ead"&gt;10&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;			&lt;span style="color:#81a1c1;font-weight:bold"&gt;break&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;		&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	&lt;span style="color:#81a1c1"&gt;close&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;resultChan&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is pretty much the minimal set up you need to get going with concurrency with Go:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the channel&lt;/li&gt;
&lt;li&gt;a goroutine&lt;/li&gt;
&lt;li&gt;a select in a loop&lt;/li&gt;
&lt;li&gt;some cleanup by closing the channel&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href="https://github.com/IanKulin/gochanneldemo"&gt;code on github&lt;/a&gt;&lt;/p&gt;</description></item><item><title>New Self-Hosted Service Workflow</title><link>https://blog.iankulin.com/new-self-hosted-service-workflow/</link><pubDate>Sun, 03 Dec 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/new-self-hosted-service-workflow/</guid><description>&lt;p&gt;I&amp;rsquo;ve developed a bit of a workflow for setting up a new service of some type on the homelab. Installing it is the obvious thing, but I also have a few quality of life things I do to make it a full production-quality part of my installation. I thought it might be helpful to run through those things using a recent example of adding &lt;a href="https://www.audiobookshelf.org/"&gt;audiobookshelf&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="audiobookshelf"&gt;audiobookshelf&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://www.audiobookshelf.org/"&gt;audiobookshelf&lt;/a&gt; is a web based system for viewing, playing, downloading and/or generally managing your audio books. I&amp;rsquo;ve been an &lt;a href="https://www.audible.com.au/"&gt;Audible&lt;/a&gt; user/subscriber, but recently got grumpy at them about something - I think I had paused my subscription, and my downloaded books were still available on my phone. I was halfway through one, upgraded the app, and then wasn&amp;rsquo;t able to play the book without re-subscribing. That might not be exactly right, but it was some type of frustrating carry on like that.&lt;/p&gt;
&lt;p&gt;In any case, that made me decide I couldn&amp;rsquo;t trust them, and it was time to reassert my digital sovereignty by downloading the books I&amp;rsquo;d paid for (and the ones they&amp;rsquo;d given me), removing the &lt;a href="https://en.wikipedia.org/wiki/Digital_rights_management"&gt;DRM&lt;/a&gt;, and hosting it myself. The first two steps of that process were easily carried out with a brilliant bit of software called &lt;a href="https://openaudible.org/"&gt;OpenAudible&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="do-it-on-dev"&gt;Do it on dev&lt;/h3&gt;
&lt;img src="https://blog.iankulin.com/images/img_7003.jpg" width="900" alt=""&gt;
&lt;p&gt;Since I have the luxury of having separate production and development servers, I generally play around with new things I&amp;rsquo;m trying out on the dev instance of Proxmox. Note that this is almost entirely unnecessary - since everything is virtualised in Proxmox on the production server, there&amp;rsquo;s hardly any damage I could cause in one VM or container that would adversely affect anything else.&lt;/p&gt;
&lt;p&gt;Nevertheless, whether it&amp;rsquo;s caution, or a need to justify the size of the homelab, I always start building new things on the dev server. Once it&amp;rsquo;s all working perfectly, it&amp;rsquo;s a simple matter (that we&amp;rsquo;ll get to later) to move it as-is to the production server.&lt;/p&gt;
&lt;h3 id="installation-stack"&gt;Installation Stack&lt;/h3&gt;
&lt;p&gt;My default setup now is a Docker container, inside an LXC container on Proxmox. Although this originally felt like a comical number of levels of abstraction, each layer is doing something for me, and now it just feels like the cost of doing business.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Proxmox - virtualising everything insulates services from each other, makes moving them around easier, backing them up and restoring them trivial, and provides a level of high availability.&lt;/li&gt;
&lt;li&gt;LXC - lighter than a full VM, more VM like than Docker, and quicker to play with. Does add a bit of complexity we&amp;rsquo;ll get to later.&lt;/li&gt;
&lt;li&gt;Docker - OCI compliant containers are the bomb. This is how we do software now. I pushed back as long as I could but the logic is too strong. There are problems still to solve around &lt;a href="https://www.cisa.gov/sbom"&gt;SBOM&lt;/a&gt;, but the reduction in the work of managing installations is compelling.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I create a non-root user, and the &lt;code&gt;docker-compose.yml&lt;/code&gt; and the directories for any config or data all go in that user&amp;rsquo;s home directory. I don&amp;rsquo;t prefer &lt;a href="https://docs.docker.com/storage/volumes/"&gt;Docker volumes&lt;/a&gt; for the data any more since the &lt;a href="https://blog.iankulin.com/docker-volume-backup-is-more-complicated-than-it-should-be/"&gt;downsides&lt;/a&gt; annoy me and the upsides must be in order to solve problems I haven&amp;rsquo;t encountered yet.&lt;/p&gt;
&lt;p&gt;Since there are a few little gotchas using LXC, when I&amp;rsquo;m trying something for the very first time, and I&amp;rsquo;m not even sure if it&amp;rsquo;s going to end up being used, I&amp;rsquo;ll do it in an VM first. I have a bunch of VM&amp;rsquo;s on the dev machine in varying states, so I normally pick one of them that already had Docker installed. This also gives me an idea for the amount of RAM and disk space the container is going to need. Changing the memory size once it&amp;rsquo;s in production is no biggie, but expanding the disk space is a bit of stuffing around.&lt;/p&gt;
&lt;p&gt;When I&amp;rsquo;m ready to make the container, it&amp;rsquo;s always the latest Debian stable, unprivileged, nesting turned on. Very few web services require more than 1GB RAM, and I guess the disk usage from the earlier trials then add a bit. I have lots of disk space and CPU time - it&amp;rsquo;s usually memory that&amp;rsquo;s the first bottleneck you&amp;rsquo;ll run into on little homelab servers. I&amp;rsquo;m sure I&amp;rsquo;ve heard &lt;a href="https://2.5admins.com/"&gt;Jim Salter and Allan Jude&lt;/a&gt; recommend that you should keep the VM memory low to leave more for the host so the it can effectively cache for all the guests.&lt;/p&gt;
&lt;p&gt;I always use docker-compose. Too many times I&amp;rsquo;ve wanted to upgrade a container, and have to waste time figuring out what the run command was. The compose file is good documentation for where your data is as well if you are, like me, avoiding volumes.&lt;/p&gt;
&lt;h3 id="the-steps"&gt;The Steps&lt;/h3&gt;
&lt;h4 id="some-installs"&gt;Some installs&lt;/h4&gt;
&lt;p&gt;With the fresh LXC created (latest Debian stable, unprivileged, nesting turned on), and started, I use the Proxmox console to log in, do some &lt;code&gt;apt&lt;/code&gt; updates, use &lt;code&gt;adduser&lt;/code&gt; to add my user, &lt;code&gt;apt install sudo&lt;/code&gt; and then &lt;code&gt;usermod&lt;/code&gt; to add my user to the sudo group.&lt;/p&gt;
&lt;p&gt;I then switch to a real terminal and ssh in as that user to install Docker. While that&amp;rsquo;s happening, I log into my router and reserve the IP address for the new container. This will follow when I move the container to the production server since it takes it&amp;rsquo;s MAC address with it.&lt;/p&gt;
&lt;p&gt;My pattern for SSH keys, which might not be the most secure, is that I have a key per device. So there&amp;rsquo;s one from my laptop, one for the terminal on my phone, and one for a VM that I sometimes use as an entry point to my home network via Tailnet. My theory with all this is that if any of those devices are compromised (for example my laptop is stolen) I can revoke that key from each of my services.&lt;/p&gt;
&lt;h4 id="nas-mount"&gt;NAS Mount&lt;/h4&gt;
&lt;p&gt;Often the service I&amp;rsquo;m installing needs access to the NAS - and that&amp;rsquo;s the case for audibookshelf which obviously needs access to my collection of audio books on my four bay Synology. I use an &lt;code&gt;/etc/fstab&lt;/code&gt; entry to mount the folder I&amp;rsquo;m interested in. I&amp;rsquo;ve set up the NAS to share these over SMB. The entry for audiobookshelf looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;//192.168.100.32/media/books/audio/ /mnt/media cifs username=abs_user,password=SeCrErpaSSword,file_mode=0660,dir_mode=07
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;There&amp;rsquo;s a bit going on here, let&amp;rsquo;s pull it apart:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;//192.168.100.32/media/books/audio/&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The directory on the NAS where my audiobooks are stored. I&amp;rsquo;ve been a bit slack here. It would have been better for that directory to have been it&amp;rsquo;s own share to reduce the attack surface.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;/mnt/media&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the directory in the LXC container that we&amp;rsquo;re mounting the books to. If I could go back in time to when I started by Linux &amp;amp; self-hosting journey, I would not have used the word media, since in Linux that more refers to things like USB drives and less like entertainment to consume. &lt;a href="https://www.karlton.org/2017/12/naming-things-hard/"&gt;Naming things is hard&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;cifs&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The protocol being used for the share. I&amp;rsquo;ve got this shared folder set up as SMB, so I use CIFS. Some of my shares are NFS, so you could have &lt;code&gt;nfs&lt;/code&gt; at this position in the entry.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;username=abs_user,password=SeCrErpaSSword&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It seems bad to have these credentials in /etc/fstab where any user on this system can read them, but I am the only user on this system and I don&amp;rsquo;t know what other convenient way I could get around this.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;file_mode=0660&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Read/write for user and group&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;dir_mode=07&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Read/write/execute on directories for user &amp;amp; group&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once that&amp;rsquo;s in the &lt;code&gt;/etc/fstab&lt;/code&gt;, you need to mount it with a &lt;code&gt;mount -a&lt;/code&gt;, then you should see the share by &lt;code&gt;ls&lt;/code&gt;-ing the mount point.&lt;/p&gt;
&lt;h4 id="docker-compose"&gt;Docker compose&lt;/h4&gt;
&lt;p&gt;Obviously this will vary with whatever service you&amp;rsquo;re running. Here&amp;rsquo;s mine for audiobookshare.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;version: &amp;#39;3&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;services:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; audiobookshelf:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; image: ghcr.io/advplyr/audiobookshelf
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; container_name: audiobookshelf
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ports:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &amp;#34;80:80&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; volumes:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - ./config:/config
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - ./metadata:/metadata
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - /mnt/media:/audiobooks
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; restart: always
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The notable things here are that I store all the container data - in this case &lt;code&gt;/config&lt;/code&gt; and &lt;code&gt;/metadata&lt;/code&gt; in subdirectories from the current directory, which is actually the user&amp;rsquo;s home directory. This LXC container is only for running this single service, so as soon as I &lt;code&gt;ssh&lt;/code&gt; in, everything I need to know or find out is easily discoverable, and easily accessible if I want to &lt;code&gt;scp&lt;/code&gt; it without a convoluted path.&lt;/p&gt;
&lt;p&gt;Another benefit of running in individual LXC&amp;rsquo;s is that each service has its own IP address - so I can use port 80 for every service.&lt;/p&gt;
&lt;h4 id="tailscale"&gt;Tailscale&lt;/h4&gt;
&lt;p&gt;Now that we can have up to 100 Tailscales on the free tier, every real service gets one. For the install, I just follow the &lt;a href="https://tailscale.com/kb/1174/install-debian-bookworm/"&gt;Debian Tailscale installation instructions&lt;/a&gt; since I&amp;rsquo;m using a Debian LXC. And now when we try &lt;code&gt;tailscale up&lt;/code&gt; we run into the LXC problem. I&amp;rsquo;ve already documented how to overcome that in &lt;a href="https://blog.iankulin.com/getting-tailscale-working-in-lxc-containers/"&gt;an earlier post&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The combination of using Tailscale, and having access to port 80 means that the web address for this service will just be whatever hostname I gave it, in this case http://ct327-audiobookshelf&lt;/p&gt;
&lt;h4 id="ansible"&gt;Ansible&lt;/h4&gt;
&lt;p&gt;Some of the next steps are so common, I&amp;rsquo;ve set up Ansible playbooks for them, but to allow me to apply them to the new server, they need to be added into my Ansible infrastructure. First the hosts file where they get a host entry and some variables.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-11-18-at-5.48.08-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-11-18-at-5.48.08-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Then in the encrypted &lt;code&gt;vault.yml&lt;/code&gt; file for the secrets. I&amp;rsquo;ve written about these before &lt;a href="https://blog.iankulin.com/first-ansible-playbook/"&gt;here&lt;/a&gt; and &lt;a href="https://blog.iankulin.com/ansible-with-secrets/"&gt;here&lt;/a&gt;. Since I have &lt;code&gt;hosts:all&lt;/code&gt; in the playbook that runs all my &lt;a href="https://gist.github.com/IanKulin/41dbf097ac6bddd9e315859d3a06fe02"&gt;&lt;code&gt;apt&lt;/code&gt; updates&lt;/a&gt;, this now means the LXC container will get all it&amp;rsquo;s updates.&lt;/p&gt;
&lt;p&gt;Now we can automate some tasks:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Make this server use our &lt;code&gt;apt-cache&lt;/code&gt; server to make updates a bit faster and efficient. Described &lt;a href="https://blog.iankulin.com/caching-apt-updates/"&gt;here&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Install a &lt;a href="https://blog.iankulin.com/simple-api-endpoint-in-go/"&gt;little endpoint&lt;/a&gt; so the available memory and disk space can be monitored.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Once that endpoint is installed, I can add a couple of entries to my &lt;a href="https://blog.iankulin.com/uptime-kuma-nfty/"&gt;Uptime Kuma&lt;/a&gt; instance to keep track of the server health and notify me with &lt;a href="https://blog.iankulin.com/uptime-kuma-nfty/"&gt;ntfy&lt;/a&gt; - so that&amp;rsquo;s monitoring covered off.&lt;/p&gt;
&lt;h4 id="backups"&gt;Backups&lt;/h4&gt;
&lt;p&gt;Backups in Proxmox are easy. I already have a general backup job set up for the prod DataCenter - it just snapshots every VM and LXC to the NAS at 1:00am each day. That&amp;rsquo;s plenty for this service - the only thing that would get lost would be a day&amp;rsquo;s worth of metadata, most of which is automatically pulled from web services anyway.&lt;/p&gt;
&lt;p&gt;This backup is of the LXC container with all the audiobookshelf config and code - not my book library. There is a backup process for it that&amp;rsquo;s a complicated collection of and external USB drive and &lt;code&gt;rsync&lt;/code&gt;-ing to a remote that might be a story for another day.&lt;/p&gt;
&lt;h3 id="done"&gt;Done&lt;/h3&gt;
&lt;p&gt;And that&amp;rsquo;s it. Now my audiobookshelf is running in an LXC container, serving the books off my NAS. The service is monitored for health, and there&amp;rsquo;s a backup plan in place. I can kick back and catch up on some technical reading.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/img_7018.jpg" alt=""&gt;&lt;/p&gt;</description></item><item><title>ViewTube</title><link>https://blog.iankulin.com/viewtube/</link><pubDate>Mon, 27 Nov 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/viewtube/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-11-18-at-5.17.47-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Whenever I encounter one of those &amp;ldquo;What are you self-hosting?&amp;rdquo; threads, I know I&amp;rsquo;m about to waste an hour looking at, and often trying out, software I probably don&amp;rsquo;t really need, and that was the case with &lt;a href="https://lemmy.world/post/8385160"&gt;this post&lt;/a&gt; on the &lt;a href="https://lemmy.world/c/selfhost@lemmy.ml"&gt;lemmy.world Selfhosted&lt;/a&gt; community.&lt;/p&gt;
&lt;p&gt;The basic idea of ViewTube is that it&amp;rsquo;s a self-hosted front end for YouTube, which just happens to strip out all the advertising and tracking. You can create your own local accounts which allows you to subscribe to channels and which keeps your progress so you don&amp;rsquo;t start over if you go back to a video - although I couldn&amp;rsquo;t see a history list. Forgetting your history might be a feature in an app designed to prevent tracking.&lt;/p&gt;
&lt;p&gt;It only took five minutes to get it running to try out, and most of that was downloading the docker images. I just made a directory in a VM and dropped this docker compose into it.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;version: &amp;#39;3&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;services:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; viewtube:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; restart: unless-stopped
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; # Or use mauriceo/viewtube:dev for the development version
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; image: mauriceo/viewtube:latest
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; # ViewTube will not start until the database and redis are ready
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; depends_on:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - viewtube-mongodb
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - viewtube-redis
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; # Make sure all services are in the same network
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; networks:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - viewtube
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; volumes:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; # This will map ViewTube&amp;#39;s data directory to the local folder ./data/viewtube/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - ./data/viewtube:/data
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; environment:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - VIEWTUBE_DATABASE_HOST=viewtube-mongodb
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - VIEWTUBE_REDIS_HOST=viewtube-redis
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ports:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - 8066:8066
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; viewtube-mongodb:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; restart: unless-stopped
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; image: mongo:4.4
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; networks:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - viewtube
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; volumes:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - ./data/db:/data/db
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; viewtube-redis:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; restart: unless-stopped
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; image: redis:7
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; networks:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - viewtube
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; volumes:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - ./data/redis:/data
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;networks:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; viewtube:
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The only change in here from the &lt;a href="https://viewtube.wiki/installation/docker"&gt;official one&lt;/a&gt; was to change to an older version since I hadn&amp;rsquo;t passed through the CPU in host mode, so there was no &lt;a href="https://old.reddit.com/r/homelab/comments/yvo4jm/how_do_i_enable_avx_on_my_server/"&gt;AVX support which is required by newer versions&lt;/a&gt; of MongoDB.&lt;/p&gt;</description></item><item><title>Displaying markdown as HTML</title><link>https://blog.iankulin.com/displaying-markdown-as-html/</link><pubDate>Wed, 08 Nov 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/displaying-markdown-as-html/</guid><description>&lt;p&gt;In the spirit of over-complicating things, when I wanted to collect all the links to the services on my homelab into one place, I decided I needed to write them in markdown, and have them converted on the fly into HTML by a server. Then when I couldn&amp;rsquo;t find exactly what I was after (&lt;a href="http://harpjs.com/"&gt;Harp&lt;/a&gt; was closest) of course, I decided to write it.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/distracted.jpg" width="1000" alt=""&gt;
&lt;h3 id="markdown"&gt;Markdown&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Markdown"&gt;Markdown&lt;/a&gt; has definitely been having it&amp;rsquo;s moment over the last couple of years. It&amp;rsquo;s a simple open format mark-up language that is quite readable in it&amp;rsquo;s source form. Although it&amp;rsquo;s now very fashionable as an input for static site generators, most people will have run in to it when adding simple formatting to forum comments or on instant messaging platforms.&lt;/p&gt;
&lt;p&gt;It supports text formatting such as bold, italic and underlining as well as links, and in some extended versions, tables and so on.&lt;/p&gt;
&lt;h3 id="middleware"&gt;Middleware&lt;/h3&gt;
&lt;p&gt;My plan for tackling this is to have a simple Node.js/Express web server that&amp;rsquo;s serving static files from a public sub-directory. As it receives requests for each file, it checks if it&amp;rsquo;s a markdown file (which normally if served directly to a browser would trigger it to be downloaded instead of displayed). If it is markdown, it&amp;rsquo;s translated into HTML and passed to the browser.&lt;/p&gt;
&lt;p&gt;This is easily accomplished in a simple Express server which has the concept of &amp;lsquo;&lt;a href="https://expressjs.com/en/guide/using-middleware.html"&gt;middleware&lt;/a&gt;&amp;rsquo;. This are just layers of processing that each request goes through. If a layer can deal with a request it does, otherwise it passes it off to the next layer. You&amp;rsquo;ll often see this type of pattern (usually with more layers) in an Express app, where each of the &lt;code&gt;app.use&lt;/code&gt; declarations is another middleware layer:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; app &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; express&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;use&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;mdParser&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;use&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;express&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;static&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;publicDirectory&lt;span style="color:#eceff4"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In this case, &lt;code&gt;mdParser&lt;/code&gt; is my middleware function that checks if a file is markdown, then if it is returns a HTML version of the file to the browser, or if not, just lets the request go through to the next layer. A simple version might look like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; express &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;express&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; fs &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;fs&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; path &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;path&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; showdown &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;showdown&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; converter &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;new&lt;/span&gt; showdown&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;Converter&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; publicDirectory &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#39;public&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; staticRoot &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; path&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;join&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;__dirname&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; publicDirectory&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;// middleware for processing markdown files
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;function&lt;/span&gt; mdParser&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; next&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;url&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;endsWith&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;.md&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;))&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; mdFilePath &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; path&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;join&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;staticRoot&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; req&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;url&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; fs&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;readFile&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;mdFilePath&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#39;utf8&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; data&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;err&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;status&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;404&lt;/span&gt;&lt;span style="color:#eceff4"&gt;).&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;File not found&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; htmlContent &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; converter&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;makeHtml&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;data&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;htmlContent&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; next&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The actual heavy lifting of converting the markdown into HTML is done in line 20 with a library called &lt;a href="https://showdownjs.com/"&gt;ShowDown&lt;/a&gt;. There are a few of these floating around, I tried &lt;a href="https://marked.js.org/"&gt;Marked&lt;/a&gt; first, but it didn&amp;rsquo;t immediately work how I expected without reading any documentation, so I moved on ¯\_(ツ)_/¯&lt;/p&gt;
&lt;h3 id="templating"&gt;Templating&lt;/h3&gt;
&lt;p&gt;This simple version works - the markdown is correctly displayed in the browser, but there&amp;rsquo;s a couple of things going on that are not great.&lt;/p&gt;
&lt;p&gt;The first is that it&amp;rsquo;s not actually well formed HTML. If we load a markdown file containing this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;# Test.md
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;* A sample mark down file
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It looks like this:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-22-at-7.42.46-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-22-at-7.42.46-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;But if we view the page source, it&amp;rsquo;s this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;h1 id=&amp;#34;testmd&amp;#34;&amp;gt;Test.md&amp;lt;/h1&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;ul&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;li&amp;gt;A sample mark down file&amp;lt;/li&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/ul&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;No &lt;code&gt;DOCTYPE, &amp;lt;html&amp;gt;, &amp;lt;head&amp;gt;&lt;/code&gt; etc. Since forever, browsers have been expected to deal gracefully with malformed HTML, and they generally do, but as someone who still feels bound by the ethics printed on my 1991 &lt;a href="https://www.acs.org.au/content/dam/acs/rules-and-regulations/Code-of-Ethics.pdf"&gt;ACS membership certificate&lt;/a&gt;, I can&amp;rsquo;t accept this low standard.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a second related problem I don&amp;rsquo;t like, that&amp;rsquo;s that the title of this page (displayed in the browser tab, and used if we bookmark the page) is &amp;ldquo;http://127.0.0.1:3000&amp;rdquo; instead of what I would like it to be - probably &amp;ldquo;Test&amp;rdquo;. This is not Showdown&amp;rsquo;s fault, it doesn&amp;rsquo;t really have any way of guessing what we&amp;rsquo;d like for the title.&lt;/p&gt;
&lt;p&gt;As usual, these are a class of problem that&amp;rsquo;s long been solved, in this case with templates. Essentially what I need to do is take the generated (but not correctly formed) HTML output from Showdown, and insert it in the middle of some boilerplate HTML. Perhaps the template could look like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;!DOCTYPE html&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;html lang=&amp;#34;en&amp;#34;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;head&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;meta charset=&amp;#34;UTF-8&amp;#34;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;title&amp;gt;{{title}}&amp;lt;/title&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/head&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;body&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;main&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {{content}}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/main&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/body&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/html&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I&amp;rsquo;d put the title I wanted for the page in &lt;code&gt;{{title}}&lt;/code&gt; and the converted markdown into &lt;code&gt;{{content}}&lt;/code&gt;. These double curly braces are a reasonably common convention for templating.&lt;/p&gt;
&lt;p&gt;If I load the template file (which can include all sorts of lovely CSS and JS) into &lt;code&gt;templateData&lt;/code&gt; at start up, I can just use a string replace when I need to serve the file at request time:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;useTemplate&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; Replace placeholders with title &lt;span style="color:#81a1c1;font-weight:bold"&gt;and&lt;/span&gt; content 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; title &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; path&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;basename&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;mdFilePath&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; templatedHtml &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; templateData&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;replace&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;{{title}}&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; title&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;replace&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;{{content}}&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; htmlContent&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;templatedHtml&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;htmlContent&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I&amp;rsquo;m just using the file name for the title here, I&amp;rsquo;ll think about &lt;a href="https://github.com/IanKulin/mdserver/issues/1"&gt;how to improve that&lt;/a&gt; in a later installment.&lt;/p&gt;
&lt;p&gt;Now the output looks like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;!DOCTYPE html&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;html lang=&amp;#34;en&amp;#34;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;head&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;meta charset=&amp;#34;UTF-8&amp;#34;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;title&amp;gt;test.md&amp;lt;/title&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/head&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;body&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;main&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;h1 id=&amp;#34;testmd&amp;#34;&amp;gt;Test.md&amp;lt;/h1&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;ul&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;li&amp;gt;A sample mark down file&amp;lt;/li&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/ul&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/main&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/body&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/html&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Which, while &lt;a href="https://github.com/IanKulin/mdserver/issues/2"&gt;not well indented&lt;/a&gt;, at least meets the HTML specification.&lt;/p&gt;
&lt;h3 id="done"&gt;Done&lt;/h3&gt;
&lt;p&gt;I really enjoyed making this - it&amp;rsquo;s one of those compact sized projects you can start and finish on a Saturday between house jobs, and although small, it does address a genuine use case - if I&amp;rsquo;d found this when I was searching for something I would have used it as is.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://github.com/IanKulin/mdserver/blob/e9a09c4381e9bda373b86701f90cf165ea0d0e7e/server.js"&gt;code&amp;rsquo;s up on github&lt;/a&gt; if you want a look. To make it a finished product it probably needs some hardening. Also, since I need to learn how to build Docker containers, this would be a good project for that, so stand by for a future installment.&lt;/p&gt;</description></item><item><title>Basic VPS disk speed</title><link>https://blog.iankulin.com/basic-vps-disk-speed/</link><pubDate>Sat, 09 Sep 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/basic-vps-disk-speed/</guid><description>&lt;p&gt;I couldn&amp;rsquo;t help but measure some VPS disk speeds while I was busting out the &lt;code&gt;fio&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/vps.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Binary Lane only claims &amp;ldquo;pure SSD drives&amp;rdquo; but seems pretty great. The difference between Digital Ocean SSD and NVME is disappointing. Obviously you&amp;rsquo;re sharing a drive with other users, so perhaps this depends on what else is going on.&lt;/p&gt;</description></item><item><title>Nginx config on Debian/Ubuntu</title><link>https://blog.iankulin.com/nginx-config-on-debian-ubuntu/</link><pubDate>Wed, 16 Aug 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/nginx-config-on-debian-ubuntu/</guid><description>&lt;p&gt;A quick look at the arrangements around the config settings for nginx. This is based on what I can see in Debian and Ubuntu, but likely it will be most &lt;code&gt;apt&lt;/code&gt; flavoured distros. Others may well be different, I know CentOS is.&lt;/p&gt;
&lt;h3 id="context"&gt;Context&lt;/h3&gt;
&lt;p&gt;If the way the configs for nginx are arranged seems a little complicated, it&amp;rsquo;s helpful to keep in mind there&amp;rsquo;s a couple of challenges that are being addressed with that complexity.&lt;/p&gt;
&lt;h4 id="separating-concerns"&gt;Separating concerns&lt;/h4&gt;
&lt;p&gt;Separating out distro, and sysadmin concerns. - A very common pattern in Linux configurations is that the main config (often a .conf file) is kept with the application&amp;rsquo;s other files in an /etc/&lt;app name&gt; directory, and a line in that configuration file includes all the configuration files in a subsidiary directory.&lt;/p&gt;
&lt;p&gt;For example, here&amp;rsquo;s a tree of &lt;code&gt;/etc/ssh&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-09-at-8.35.03-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;If we look in the &lt;code&gt;ssh_config&lt;/code&gt; file, there will be a line including all the configs found in the &lt;code&gt;ssh_config.d&lt;/code&gt; directory.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-09-at-8.42.11-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The benefit of this pattern (having a main config file including all the config files in a sub-directory) is that the distro maintainers can make changes (in future updates) to the main config file to make everything in their distro work together, and the system administrator can make their changes for the local system in the sub-directory without them being written over during an update.&lt;/p&gt;
&lt;p&gt;Nginx configuration follows this pattern. The main configuration file &lt;code&gt;nginx.conf&lt;/code&gt;, includes the &lt;code&gt;*.conf&lt;/code&gt; files in the &lt;code&gt;conf.d&lt;/code&gt; sub-directory.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-09-at-8.48.19-am.png" alt=""&gt;&lt;/p&gt;
&lt;h4 id="maintaining-sites"&gt;Maintaining Sites&lt;/h4&gt;
&lt;p&gt;It&amp;rsquo;s common for web servers running on an individual machine, to be serving content for more than one domain or web site. For example you might be running apache2 on a VPS that is serving your business web site, as well as the web sites of a couple of clients. This would be achieved by having the DNS records for, say itfreaks.com, realfakedoors.ca, and joesusedcars.com all pointing to the same IP address of your server.&lt;/p&gt;
&lt;p&gt;Since each of these sites may need slightly different setups, it would make sense for them to have separate config files as well. If you look at the screenshot above, you can see that nginx is going to look in the &lt;code&gt;/etc/nginx/sites-enabled/&lt;/code&gt; sub-directory for these, but that&amp;rsquo;s not quite the whole story. Let&amp;rsquo;s look at the tree of a fresh install of niginx.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-09-at-9.03.01-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;You can see that the &lt;code&gt;sites-enabled&lt;/code&gt; sub-directory includes a symlink to a config file called default in the &lt;code&gt;sites-available&lt;/code&gt; sub-directory. The intention here is that you put your config files for each site in /&lt;code&gt;etc/nginx/sites-available&lt;/code&gt;, and link to them from &lt;code&gt;/etc/nginx/sites-available&lt;/code&gt;. Then when you want to activate a site, you put a link in sites-enable, or delete the link when you want to disable it, but this is done without losing the work in the site config file.&lt;/p&gt;
&lt;p&gt;Usually, you use the site name for these files - there&amp;rsquo;s no need for the &lt;code&gt;.conf&lt;/code&gt; extension. So if we look at an installation that has a live site with an IP address, it could look like this.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-09-at-9.16.36-am.png" alt=""&gt;&lt;/p&gt;</description></item><item><title>Where to go after Reddit</title><link>https://blog.iankulin.com/where-to-go-after-reddit/</link><pubDate>Tue, 01 Aug 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/where-to-go-after-reddit/</guid><description>&lt;p&gt;A big chunk of my mindless doomscrolling used to go to Reddit, but also, Reddit posts from the various communities were frequently the useful results when googling error messages. I lurked in many a sub-reddit, but only posted in a couple - usually r/self-hosted or r/Homelab.&lt;/p&gt;
&lt;p&gt;The problematic treatment of the communities in the leadup to their IPO has been well publicised, and the short blackout by some subreddits seemed to have zero effect on the company&amp;rsquo;s approach to it&amp;rsquo;s users (which is in fact what they have to sell). Those subreddits, and many others are still working, but (and perhaps I&amp;rsquo;m imagining this) seem somehow thinner. Additionally, I feel like it&amp;rsquo;s a fragile arrangement - the company has shown how they will deal with their communities, so depending on them in the long term does not seem wise, or even, somehow, ethical - like I&amp;rsquo;m crossing a picket line.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s a great pity. The format of asynchronous chats around bite-size topics that are promoted or demoted based on user interests is a great format for technology questions.&lt;/p&gt;
&lt;p&gt;In any case, if we are to abandon Reddit, in some sort of fuck-you to u/spez, where to go?&lt;/p&gt;
&lt;p&gt;Currently for me, the answer is probably Lemmy. This is a &amp;lsquo;federated&amp;rsquo; system like Masterdon. You can spin up your own instance, and other instances can decide to block you if they are not happy with your moderation policies - sort of the same as email. Communities (ie subreddits) live on a particular server, but you can subscribe to them from whichever server you have your account on. For example, I&amp;rsquo;m a member of the lemmy.world server where a lot of subreddits and redditors have washed up, but there&amp;rsquo;s a homelab community over at &lt;a href="https://lemmy.ml/c/homelab"&gt;lemmy.ml/c/homelab&lt;/a&gt;. When I&amp;rsquo;m signed in at &lt;a href="https://lemmy.world/"&gt;lemmy.world&lt;/a&gt;, I can access the homelab community - reading and posting - just as if it was hosted where I&amp;rsquo;m logged in.&lt;/p&gt;
&lt;p&gt;The Lemmy system will be familiar to ex-redditors. There&amp;rsquo;s upvotes and downvotes, mods, a sidebar with rules etc. I have noticed that many Lemmy communities have decided to try and use the change to make things a bit more civil. Stackoverflow behavior was never an issue in the selfhosted or homelab subreddits, but it certainly has been an issue elsewhere on the site. This slightly-nicer-on-Lemmy phenomenon is interesting, perhaps &lt;a href="https://www.garbageday.email/"&gt;Ryan Broderick&lt;/a&gt; will make sense of it. An extreme example might be that lemmynsfw.com has decide their rule number two is &amp;lsquo;respect and consent&amp;rsquo;. I don&amp;rsquo;t know (but it&amp;rsquo;s easy to imagine) this might not have been the case for NSFW subreddits.&lt;/p&gt;
&lt;p&gt;Not all communities are translated directly across to Lemmy, an example of that, that I&amp;rsquo;ve followed is that Jim Salter (who was a r/ZFS mod) has set up a Discourse server for &lt;a href="https://discourse.practicalzfs.com/c/openzfs/4"&gt;ZFS&lt;/a&gt; and &lt;a href="https://discourse.practicalzfs.com/c/openzfs/proxmox/7"&gt;Proxmox&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I mentioned internet technology journalist Ryan Broderick earlier. One of his theories is that the internet is going through a period where we will not really be having a &lt;a href="https://threadreaderapp.com/thread/1174694510527488000.html"&gt;common internet experience&lt;/a&gt;. With Twitter and Reddit being fractured like they have been in the recent past, that seems like a supportable theory. If it means the little different islands of, say, self-hosted fans are each smaller numbers of people, it does devalue the experience a little - expose each group of users to a smaller range of ideas and thoughts. The upside might be this opportunity to re-think what each community should feel like to be part of.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s been some speculation that the &lt;a href="https://www.youtube.com/watch?v=cXlxMP9PU8I"&gt;sudden decline in Stack Overflow&lt;/a&gt; is because ChatGPT has gobbled up all of it&amp;rsquo;s content, so it&amp;rsquo;s better to just ask ChatGPT your web development questions. It&amp;rsquo;s certainly the case that when I&amp;rsquo;ve been talking to it, ChatGPT has always been present to deal with, and never treated me like an idiot regardless of the noob mistakes I&amp;rsquo;m making. If new Lemmy (or other versions of) subreddits are going to be nicer, I&amp;rsquo;m all for it.&lt;/p&gt;
&lt;p&gt;Doubtless users will slowly vote with their feet, and the situation will evolve over time, but for the moment, I&amp;rsquo;ll be at:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://lemmy.world/"&gt;lemmy.world&lt;/a&gt; - general stuff&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lemmy.world/c/selfhosted"&gt;lemmy.world/c/selfhosted&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lemmy.ml/c/webdev"&gt;lemmy.ml/c/webdev&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lemmy.ml/c/homelab"&gt;lemmy.ml/c/homelab&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://discourse.practicalzfs.com/c/openzfs/proxmox/"&gt;discourse.practicalzfs.com/c/openzfs/proxmox/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I&amp;rsquo;m still reading Twitter/X - not so much for the tech stuff - most of the tech folk I follow have moved to Masterdon, but for a couple of sassy-quipping influencers who haven&amp;rsquo;t made the leap, and for the education accounts I follow who don&amp;rsquo;t seemed to have migrated at all.&lt;/p&gt;</description></item><item><title>Complicating the Temperature API</title><link>https://blog.iankulin.com/complicating-the-temperature-api/</link><pubDate>Wed, 28 Jun 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/complicating-the-temperature-api/</guid><description>&lt;p&gt;I&amp;rsquo;ve been slammed with other work, so my web dev learning has fallen well behind. Luckily, the YouTube procrastination algorithm noticed this and suggested I watch a video from &lt;a href="https://www.youtube.com/@codewithcon"&gt;CodeWithCon&lt;/a&gt; titled &lt;a href="https://www.youtube.com/watch?v=KNa-wMpry00&amp;amp;list=PLkJHe6eU_tzeoe7vKUEa4MrS74CpVEwdI&amp;amp;index=3&amp;amp;t=305s"&gt;Learn Backend in 10 MINUTES&lt;/a&gt;.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/KNa-wMpry00?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;

&lt;p&gt;Since I was watching a video of a guy learning to land a C152 at St Baths (a skill I do &lt;em&gt;not&lt;/em&gt; need) at the time, it was hard to argue with myself that I didn&amp;rsquo;t have ten minutes to learn all of backend programming.&lt;/p&gt;
&lt;p&gt;I mean, &lt;em&gt;all&lt;/em&gt; of backend programming in 10 minutes is a big claim, but the video did do a surprising good job of simple REST APIs in &lt;a href="https://nodejs.org/en"&gt;Node&lt;/a&gt; using the &lt;a href="http://expressjs.com/"&gt;Express&lt;/a&gt; framework.&lt;/p&gt;
&lt;p&gt;I abandoned iOS programming a year ago when I started to think about the sort of applications I wanted to develop, and saw they would need to run against cloud databases, and so I was going to have to learn backend web dev at some stage anyway, and if so, learning that, then writing the front-ends for web seemed like a lower friction, and wider audience approach.&lt;/p&gt;
&lt;p&gt;I have &lt;em&gt;sort&lt;/em&gt; of created an API to solve my &lt;a href="https://blog.iankulin.com/outside-temperature-from-an-api-in-a-shell-script/"&gt;temperature logging problem&lt;/a&gt;. A Python script runs as a cron job every 5 minutes on a VPS, calls a weather API, parses the json and drops the values I want into a text file on an NGINX server which can be called with a straightforward GET.&lt;/p&gt;
&lt;p&gt;While that was great to learn a bit of Python, it&amp;rsquo;s not pretty, or standard. It does solve the problem I intended (I wanted that weather data for three servers running at home, but didn&amp;rsquo;t want to hammer the weather API I was using for free) it has a few other problems. As the cron job on the VPS runs each five minutes, the data there can be up to five minutes behind the API, and since the cron jobs on my servers are running on the same five minute intervals, and the call to the Australian VPS is quicker than the API call to the US based API, I&amp;rsquo;m always returning the VPS data from five minutes ago - so now my data is up to ten minutes old.&lt;/p&gt;
&lt;p&gt;Does that matter for this application? No, but the whole exercise was for learning, and this is a good enough reason to improve it my making it even more unnecessarily complicated.&lt;/p&gt;
&lt;p&gt;I think my new system will be that the homelab servers will still poll the VPS, but the VPS will be a Node.js endpoint. When it receives a GET from one of the servers, it will check the age of it&amp;rsquo;s current weather data. If it&amp;rsquo;s less than a minute, it will return that, if it&amp;rsquo;s older than a minute, it will call the weather API, store that and return it.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/20230624-weather.drawio-1.png" width="512" alt=""&gt;
&lt;p&gt;Apart from reducing the latency of the outside temperature data, this has a couple of other benefits. The first is that my VPS won&amp;rsquo;t go on for ever requesting the weather API data after I&amp;rsquo;ve reloaded the operating system on the home servers and completely forgotten about this project. The second is that the temperatures in the data I&amp;rsquo;m getting back look like they only change every 20 minutes, so probably they are stale before I ever get them from Open Weather. There are live weather station web pages that I could scrape for better data, so doing things in node on the VPS leaves a good option open for that future improvement.&lt;/p&gt;
&lt;p&gt;To chunk the project down to really small bite sizes, I&amp;rsquo;ll to it in two parts.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The first will just be to replicate the current system - return a text file when receiving a GET - in Node. That way I will have dealt with the issue of running Node behind NGINX on the VPS.&lt;/li&gt;
&lt;li&gt;The second part will be to expand that to call the weather API from inside the Node program when it&amp;rsquo;s needed.&lt;/li&gt;
&lt;li&gt;A possible third part would be to convert it all to JSON instead of text, and then deal with that in the Python scripts running on the servers.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That&amp;rsquo;s the plan.&lt;/p&gt;</description></item><item><title>HDD Swap on A1278 MacBook Pro</title><link>https://blog.iankulin.com/hdd-swap-on-a1278-macbook-pro/</link><pubDate>Wed, 10 May 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/hdd-swap-on-a1278-macbook-pro/</guid><description>&lt;p&gt;My MacBook died, I guess about three years ago. It was randomly difficult for a week or so, but then just behaving as if it had no hard drive at all. It&amp;rsquo;s been in a drawer ever since waiting for me to replace the hard drive and see if I could sell it, which I never quite got to.&lt;/p&gt;
&lt;p&gt;I mentioned a while ago that I&amp;rsquo;d &lt;a href="https://blog.iankulin.com/linux-on-hp-mini-110/"&gt;borrowed an old Atom powered HP Mini 110&lt;/a&gt; to play with a Linux desktop machine, partly for fun &amp;amp; learning, and partly for a first-class SPICE experience (also fun). Meanwhile I&amp;rsquo;ve got an old but still sexy Intel MacBook Pro sitting in a drawer - that doesn&amp;rsquo;t make sense!&lt;/p&gt;
&lt;p&gt;So I ordered an 2.5&amp;quot; SDD in order to resurrect the MacBook. This era (2012) of MacBooks are quite repairable - even the RAM is just regular SODIMM.&lt;/p&gt;
&lt;h3 id="instructions"&gt;Instructions&lt;/h3&gt;
&lt;p&gt;Lay it upside down and remove the screws with a tiny (I guess #00) Philips screwdriver. Note that the screws don&amp;rsquo;t come out perpendicular to the table you&amp;rsquo;re working on - the top four almost do, but the others are perpendicular to the tangent at the point of the screw hole. ie, the case is curved, so take that into account. It doesn&amp;rsquo;t matter much for removing them, but now is the time to notice.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;ve got the laptop turned around so you can read the writing, then the long screws are the three top right ones. If you lay them out in the order you remove them, you won&amp;rsquo;t have to remember that.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/img_5087.jpg" width="1008" alt=""&gt;
&lt;img src="https://blog.iankulin.com/images/img_5088.jpg" width="1008" alt=""&gt;
&lt;p&gt;Pick the cover up from the back and it just lifts off.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/img_5067.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The HDD is in the bottom right corner there. It&amp;rsquo;s locked in with that black plastic retainer you can see above the drive. Use your little Philips screwdriver to undo the two screws holding it in place (they don&amp;rsquo;t come right out), then lift out the retainer and put it down somewhere carefully - you will need to put it back the right way later.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/img_5069.jpg" width="1008" alt=""&gt;
&lt;img src="https://blog.iankulin.com/images/img_5072.jpg" width="1008" alt=""&gt;
&lt;p&gt;That plastic tab is for lifting the far side of the drive out. Only lift it far enough to loosen the remove the SATA plug from the drive, then lift the whole drive out.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/img_5073.jpg" width="1008" alt=""&gt;
&lt;img src="https://blog.iankulin.com/images/img_5075.jpg" width="1008" alt=""&gt;
&lt;p&gt;Once it&amp;rsquo;s unplugged, the drive will lift away from the bottom.&lt;/p&gt;
&lt;p&gt;There are four little lug things screwed into the mounting screw holes on the drive. You&amp;rsquo;ll need to remove them and shift them over to the new drive. They nestle into the little round shock mounts at the case edge. You need a tiny torx driver for those lugs. I&amp;rsquo;m not sure what size, but the driver I bought for taking Nokia 5110&amp;rsquo;s apart fits perfectly.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/img_5081.jpg" width="1008" alt=""&gt;
&lt;p&gt;Re-installation is just the opposite of taking it all apart. Be gentle with the SATA connector. I tried moving that sticky tab over to the new drive, but it wasn&amp;rsquo;t interested in re-sticking. So I McGyvered a bit of packing tape.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/img_5085.jpg" width="1008" alt=""&gt;</description></item><item><title>Outside Temperature From an API in a Shell Script</title><link>https://blog.iankulin.com/outside-temperature-from-an-api-in-a-shell-script/</link><pubDate>Wed, 03 May 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/outside-temperature-from-an-api-in-a-shell-script/</guid><description>&lt;p&gt;I&amp;rsquo;m interested in &lt;a href="https://blog.iankulin.com/linux-shell-script-for-temperature-logging/"&gt;collecting some internal temperature data&lt;/a&gt; from my servers to look at the effect of adding an NMVe drive. Last week we had a couple of warm days immediately followed by a couple of cool ones. I imagine a 20° ambient temperature change could effect the server temperatures, so I thought it would be good to add that to my temperature logs.&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t have a weather station or other automated system for collecting the temperature, but there are several commercial sources for this data which, while probably not as good as a sensor in the server room, will be fine for our purposes.&lt;/p&gt;
&lt;p&gt;One of the more well known weather APIs was &lt;a href="https://darksky.net/dev"&gt;Dark Sky&lt;/a&gt;, they got bought up by Apple and now similar data is available in the &lt;a href="https://developer.apple.com/weatherkit/get-started/"&gt;WeatherKit API&lt;/a&gt;. I hold a developer program membership, so that would be free to use for the frequency I need, but the API and sign up looked a bit complex, so I looked elsewhere.&lt;/p&gt;
&lt;p&gt;OpenWeather have a &lt;a href="https://openweathermap.org/current"&gt;simple API&lt;/a&gt; (including one intended to make changing over from Dark Sky easy), &lt;a href="https://openweathermap.org/price"&gt;a good free tier&lt;/a&gt;, and simple sign up - no credit card required. On the free tier I can pull the current weather for a location 22 times a minute continuously. Since I&amp;rsquo;m only collecting my server temps on a five minute cycle, that will be more than fine.&lt;/p&gt;
&lt;p&gt;Even thought the API would allow it, it seems wasteful, and greedy (since I&amp;rsquo;m not paying for it), to pull the same data three times (for each of the three servers), so to complicate things (and learn some interesting stuff) I decided to poll the OpenWeather API once every five minutes from my VPS, process that current weather JSON down to just the temperature I was after, then expose that as a http endpoint. Then each of my servers would poll the VPS to get that outside temp as part of their logging.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/20230425-weather.drawio-1.png" width="435" alt=""&gt;
&lt;p&gt;This will all extend involve some scripting that I haven&amp;rsquo;t encountered yet.&lt;/p&gt;
&lt;h3 id="vps--weather-api"&gt;VPS / Weather API&lt;/h3&gt;
&lt;p&gt;The OpenWeather API couldn&amp;rsquo;t be more straightforward, you sign up with an email and get an API token, then it&amp;rsquo;s just this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;https://api.openweathermap.org/data/2.5/weather?lat={lat}&amp;amp;lon={lon}&amp;amp;appid={API key}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;There&amp;rsquo;s a couple of options for language and units, I went with &lt;em&gt;metric&lt;/em&gt;, then you get have some JSON.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;coord&amp;#34;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;lon&amp;#34;: 118,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;lat&amp;#34;: -33.93
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;weather&amp;#34;: [
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;id&amp;#34;: 803,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;main&amp;#34;: &amp;#34;Clouds&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;description&amp;#34;: &amp;#34;broken clouds&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;icon&amp;#34;: &amp;#34;04d&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;base&amp;#34;: &amp;#34;stations&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;main&amp;#34;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;temp&amp;#34;: 12.59,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;feels_like&amp;#34;: 11.68,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;temp_min&amp;#34;: 12.59,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;temp_max&amp;#34;: 12.59,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;pressure&amp;#34;: 1007,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;humidity&amp;#34;: 68,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;sea_level&amp;#34;: 1007,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;grnd_level&amp;#34;: 976
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;visibility&amp;#34;: 10000,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;wind&amp;#34;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;speed&amp;#34;: 7.39,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;deg&amp;#34;: 307,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;gust&amp;#34;: 11.23
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;clouds&amp;#34;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;all&amp;#34;: 64
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;dt&amp;#34;: 1682401802,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;sys&amp;#34;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;country&amp;#34;: &amp;#34;AU&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;sunrise&amp;#34;: 1682375848,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;sunset&amp;#34;: 1682415263
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;timezone&amp;#34;: 28800,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;id&amp;#34;: 2070753,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;name&amp;#34;: &amp;#34;Gnowangerup&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;cod&amp;#34;: 200
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;From this, I want to extract the temperature, and the unix timestamp &amp;ldquo;dt&amp;rdquo;. Here&amp;rsquo;s my bash script.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#5e81ac;font-style:italic"&gt;#!/bin/bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;weather_text&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;curl -s &lt;span style="color:#a3be8c"&gt;&amp;#34;https://api.openweathermap.org/data/2.5/weather?lat=-33.93&amp;amp;lon=118.00&amp;amp;appid=somegiantrandomUIDtypenumber&amp;amp;units=metric&amp;#34;&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;temp_text&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;echo&lt;/span&gt; $weather_text &lt;span style="color:#eceff4"&gt;|&lt;/span&gt; awk -F&lt;span style="color:#a3be8c"&gt;&amp;#39;&amp;#34;temp&amp;#34;:&amp;#39;&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#39;{print $2}&amp;#39;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;|&lt;/span&gt; cut -d&lt;span style="color:#a3be8c"&gt;&amp;#39;,&amp;#39;&lt;/span&gt; -f1&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;time_text&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;echo&lt;/span&gt; $weather_text &lt;span style="color:#eceff4"&gt;|&lt;/span&gt; awk -F&lt;span style="color:#a3be8c"&gt;&amp;#39;&amp;#34;dt&amp;#34;:&amp;#39;&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#39;{print $2}&amp;#39;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;|&lt;/span&gt; cut -d&lt;span style="color:#a3be8c"&gt;&amp;#39;,&amp;#39;&lt;/span&gt; -f1&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;log_file&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/home/ian/iankulin.com/www/gnp_temp.txt&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;printf&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;%s,%s&amp;#34;&lt;/span&gt; $temp_text $time_text &amp;gt; $log_file
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Of note, and that I haven&amp;rsquo;t already discussed:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;weather_text&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;curl &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;s &lt;span style="color:#a3be8c"&gt;&amp;#34;https://api.openweathermap.org/data/2.5/weather?lat=-33.93&amp;amp;lon=118.00&amp;amp;appid=somegiantrandomUIDtypenumber&amp;amp;units=metric&amp;#34;&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;curl&lt;/code&gt; basically sends out a network request the same as if you had typed it into the top of your browser. If it was a web page, it would return the text of the HTML, but in this case it returns the JSON I showed before - although less formatted.&lt;/p&gt;
&lt;p&gt;weather_text is a variable to which we are assigning the return value of the curl - ie the string of JSON. Note the backticks `` the curl is enclosed in. This is how the script knows to execute the command and assign the results rather than assigning some text beginning with &lt;code&gt;curl&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;temp_text=`echo $weather_text | awk -F&amp;#39;&amp;#34;temp&amp;#34;:&amp;#39; &amp;#39;{print $2}&amp;#39; | cut -d&amp;#39;,&amp;#39; -f1`
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Oh man, this took me on a journey. Firstly, keep in mind I&amp;rsquo;ve prettified the JSON above, actually the string looked like this, so it wasn&amp;rsquo;t possible to process it on a line by line.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&amp;#34;coord&amp;#34;:{&amp;#34;lon&amp;#34;:118,&amp;#34;lat&amp;#34;:-33.93},&amp;#34;weather&amp;#34;:[{&amp;#34;id&amp;#34;:803,&amp;#34;main&amp;#34;:&amp;#34;Clouds&amp;#34;,&amp;#34;description&amp;#34;:&amp;#34;broken clouds&amp;#34;,&amp;#34;icon&amp;#34;:&amp;#34;04d&amp;#34;}],&amp;#34;base&amp;#34;:&amp;#34;stations&amp;#34;,&amp;#34;main&amp;#34;:{&amp;#34;temp&amp;#34;:12.59,&amp;#34;feels_like&amp;#34;:11.68,&amp;#34;temp_min&amp;#34;:12.59,&amp;#34;temp_max&amp;#34;:12.59,&amp;#34;pressure&amp;#34;:1007,&amp;#34;humidity&amp;#34;:68,&amp;#34;sea_level&amp;#34;:1007,&amp;#34;grnd_level&amp;#34;:976},&amp;#34;visibility&amp;#34;:10000,&amp;#34;wind&amp;#34;:{&amp;#34;speed&amp;#34;:7.39,&amp;#34;deg&amp;#34;:307,&amp;#34;gust&amp;#34;:11.23},&amp;#34;clouds&amp;#34;:{&amp;#34;all&amp;#34;:64},&amp;#34;dt&amp;#34;:1682401802,&amp;#34;sys&amp;#34;:{&amp;#34;country&amp;#34;:&amp;#34;AU&amp;#34;,&amp;#34;sunrise&amp;#34;:1682375848,&amp;#34;sunset&amp;#34;:1682415263},&amp;#34;timezone&amp;#34;:28800,&amp;#34;id&amp;#34;:2070753,&amp;#34;name&amp;#34;:&amp;#34;Gnowangerup&amp;#34;,&amp;#34;cod&amp;#34;:200}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We are assigning to the variable &lt;code&gt;temp_text&lt;/code&gt; the contents of this command, where $weather_text is the JSON string.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;echo $weather_text | awk -F&amp;#39;&amp;#34;temp&amp;#34;:&amp;#39; &amp;#39;{print $2}&amp;#39; | cut -d&amp;#39;,&amp;#39; -f1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The vertical lines are called &lt;em&gt;pipes&lt;/em&gt; &lt;code&gt;|&lt;/code&gt; they send the output of the command on their left into the command to their right. So there&amp;rsquo;s three different things happening here. The &lt;code&gt;echo&lt;/code&gt; just outputs the JSON, then we process it twice more.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;awk -F&amp;#39;&amp;#34;temp&amp;#34;:&amp;#39; &amp;#39;{print $2}&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;a href="https://www.geeksforgeeks.org/awk-command-unixlinux-examples/"&gt;awk&lt;/a&gt; is one of the great text processing commands along with &lt;code&gt;grep&lt;/code&gt; and &lt;code&gt;sed&lt;/code&gt;. The way it is being used here is to break the string into multiple parts, where the parts are delimited by the text &lt;code&gt;&amp;quot;temp&amp;quot;:&lt;/code&gt; which in our case is just two parts. Then we are outputting the second part ready for the next processing. So at this stage, the text would look like this.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;12.59,&amp;#34;feels_like&amp;#34;:11.68,&amp;#34;temp_min&amp;#34;:12.59,&amp;#34;temp_max&amp;#34;:12.59,&amp;#34;pressure&amp;#34;:1007,&amp;#34;humidity&amp;#34;:68,&amp;#34;sea_level&amp;#34;:1007,&amp;#34;grnd_level&amp;#34;:976},&amp;#34;visibility&amp;#34;:10000,&amp;#34;wind&amp;#34;:{&amp;#34;speed&amp;#34;:7.39,&amp;#34;deg&amp;#34;:307,&amp;#34;gust&amp;#34;:11.23},&amp;#34;clouds&amp;#34;:{&amp;#34;all&amp;#34;:64},&amp;#34;dt&amp;#34;:1682401802,&amp;#34;sys&amp;#34;:{&amp;#34;country&amp;#34;:&amp;#34;AU&amp;#34;,&amp;#34;sunrise&amp;#34;:1682375848,&amp;#34;sunset&amp;#34;:1682415263},&amp;#34;timezone&amp;#34;:28800,&amp;#34;id&amp;#34;:2070753,&amp;#34;name&amp;#34;:&amp;#34;Gnowangerup&amp;#34;,&amp;#34;cod&amp;#34;:200}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then I need to do the same sort of thing again - split the string using a delimiter, and just keep the part with the termperature in it. This time we&amp;rsquo;ll use a comma , as the delimiter, and only keep the part in front of it.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cut -d&amp;#39;,&amp;#39; -f1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We&amp;rsquo;re saying cut this string in to bits were the delimiter &lt;code&gt;-d&lt;/code&gt; is a comma, then output the first field.&lt;/p&gt;
&lt;p&gt;You might be wondering why I didn&amp;rsquo;t just use &lt;code&gt;awk&lt;/code&gt; again - I could have, but &lt;code&gt;cut&lt;/code&gt; is simpler. The reason I didn&amp;rsquo;t use &lt;code&gt;cut&lt;/code&gt; both times is that it can only take a single character as a delimiter. In fact, the first version I wrote of this script only used &lt;code&gt;cut&lt;/code&gt;, and I had the delimiters as colon for the first cut and comma for the second. As I was writing it, I was thinking that I should stress in the blog post about it that it was quite fragile - a small change in the JSON (for example adding a field, or changing the order - both things that would not cause a problem to a good Swift or JS JSON library) would break it. Then the weather changed and so was two layers of clouds, and the script broke and output the time as &lt;code&gt;{&amp;quot;all&amp;quot;&lt;/code&gt; instead of a number.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-25-at-3.00.13-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;printf&lt;/code&gt; just outputs the two values - temperature and timestamp as plain text with a comma between them into a text file that&amp;rsquo;s in the root of the Nginx webserver.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-04-25-at-8.06.34-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-25-at-8.06.34-pm.png" width="794" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Now that&amp;rsquo;s in place, I just edited &lt;code&gt;/etc/crontab&lt;/code&gt; to have the new script run every five minutes to update the file with the temperature and timestamp.&lt;/p&gt;
&lt;h3 id="server-temp-logging"&gt;Server Temp Logging&lt;/h3&gt;
&lt;p&gt;We&amp;rsquo;ve already seen most of this, but I&amp;rsquo;ve made a couple of additions.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#5e81ac;font-style:italic"&gt;#!/bin/bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;#check drivetemp has been loaded - needed for ssd temp&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; ! lsmod &lt;span style="color:#eceff4"&gt;|&lt;/span&gt; grep -wq drivetemp&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; modprobe drivetemp
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;#collect the temp data&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;pch_name&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;cat /sys/class/hwmon/hwmon0/name&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;pch_temp&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;cat /sys/class/hwmon/hwmon0/temp1_input&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cpu_name&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;cat /sys/class/hwmon/hwmon1/name&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cpu_temp&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;cat /sys/class/hwmon/hwmon1/temp1_input&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ssd_name&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;cat /sys/class/hwmon/hwmon2/name&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ssd_temp&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;cat /sys/class/hwmon/hwmon2/temp1_input&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;#this should contain the current outside temp and unix time&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;outside_temp&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;curl -s &lt;span style="color:#a3be8c"&gt;&amp;#34;https://iankulin.com/gnp_temp.txt&amp;#34;&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;log_file&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/var/log/temps.csv&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;# Print the temperatures to a log file&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;printf&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;$(&lt;/span&gt;date +&lt;span style="color:#a3be8c"&gt;&amp;#39;%d/%m/%Y,%T&amp;#39;&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;)&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;,%s,%d,%s,%d,%s,%d,out,%s\n&amp;#34;&lt;/span&gt; $pch_name $pch_temp $cpu_name $cpu_temp $ssd_name $ssd_temp $outside_temp &amp;gt;&amp;gt; $log_file
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We&amp;rsquo;ve already discussed how the curl works - this one is picking up the script we wrote to run on the VPS earlier. More interesting is checking for the &lt;code&gt;drivetemp&lt;/code&gt; module.&lt;/p&gt;
&lt;p&gt;The drivetemp module needs to be loaded into the Linux kernel before we can read the SSD temperature with the line.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ssd_temp=`cat /sys/class/hwmon/hwmon2/temp1_input`
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Once it&amp;rsquo;s loaded, it stays there, unless computer is shutdown for any reason. There&amp;rsquo;s a &lt;a href="https://www.baeldung.com/linux/run-script-on-startup"&gt;number of places&lt;/a&gt; we can execute things on startup, but really this &lt;code&gt;drivetemp&lt;/code&gt; module is only needed for this script, so we should do it here. As far as I can make out, telling Linux to load a module that&amp;rsquo;s already loaded does not do any harm, and at once every five minutes it&amp;rsquo;s hardly going to cause a performance issue. Nevertheless, some sort of programmer ethics compels me to only do it if its needed.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;#check drivetemp has been loaded - needed for ssd temp&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;!&lt;/span&gt; lsmod &lt;span style="color:#81a1c1"&gt;|&lt;/span&gt; grep &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;wq drivetemp&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; then
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; modprobe drivetemp
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;fi
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;lsmod&lt;/code&gt; returns a list of the loaded modules, this is passed to the &lt;code&gt;grep&lt;/code&gt;. &lt;code&gt;grep&lt;/code&gt; looks through lines of input and usually returns any lines that match. However in this case, we&amp;rsquo;re using the &lt;code&gt;-q&lt;/code&gt; (quiet) option. With this option on, instead of lines of text, you get nothing on the standard output, instead it sets the exit code to 0 (true) if it&amp;rsquo;s found, or 1 (false) if not.&lt;/p&gt;
&lt;p&gt;Since I&amp;rsquo;m interested in only running the &lt;code&gt;modprobe&lt;/code&gt; if &lt;code&gt;drivetemp&lt;/code&gt; is &lt;em&gt;not&lt;/em&gt; found, I have to negate the result of the &lt;code&gt;grep&lt;/code&gt; with &lt;code&gt;!&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;After that, all the temperature data is collected, then written out to a log fie for later processing.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-25-at-9.00.47-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;h3 id="the-results"&gt;The Results&lt;/h3&gt;
&lt;p&gt;Here&amp;rsquo;s 24 hours of the five minute temperature logs. For each server I averaged the three different temperatures (PCH, CPU core, and SSD drive) and graphed them along with the outside temperature from OpenWeather. &lt;code&gt;pve-prod1&lt;/code&gt; is the only one doing any real work here. It hosts my Jellyfin media server on a VM, and another VM with a collection of utilities such as Uptime Kuma. The Y axis is degrees centigrade.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/20230427-server-temps.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/img_4315.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The spike in &lt;code&gt;pve-dev&lt;/code&gt;1 at 2100 was caused by me stress testing one core to 100% load for ten minutes. I think I can see &lt;code&gt;pve-prod2&lt;/code&gt; (which sits directly on top of &lt;code&gt;pve-dev1&lt;/code&gt;) warming up a little as well. But strangely, and perhaps I&amp;rsquo;m imagining it, it seems like &lt;code&gt;pve-prod1&lt;/code&gt; (which sits on top of the stack) was a bit cooler in that time?&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t remember if I watched some TV between 6 and 8pm, but it looks like I did, and the spike at 2am will be the nightly snapshots being taken and sent off to the NAS.&lt;/p&gt;
&lt;p&gt;You can see that &lt;code&gt;pve-prod2&lt;/code&gt; and &lt;code&gt;pve-dev1&lt;/code&gt; were turned on to run this test, and it takes about 40 minutes for them to warm up. It&amp;rsquo;s interesting to notice the bigger amplitude of the production machine compared to the others just idling. And also interesting that &lt;code&gt;pve-dev1&lt;/code&gt; (which wasn&amp;rsquo;t running any load till I ran the stress test on it) was just generally warmer that &lt;code&gt;pve-prod1&lt;/code&gt; which was running a small work load.&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t remember if I watched some TV between 6 and 8pm, but it looks like I did, and the spike at 2am will be the nightly snapshots being taken and sent off to the NAS.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s have a look at pve-dev1 while the stress test was running.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/pve-dev1-temp.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;It makes sense that the PCH which is mm away from, and directly connected to, the CPU would warm up as the CPU was hammered with square root calculations, and since the drive temp is a up a little so I guess that reflects the ambient temperature inside the case.&lt;/p&gt;
&lt;p&gt;The CPU temperature hadn&amp;rsquo;t plateaued yet, so it might be interesting to run it until it does one day and see what that looks like.&lt;/p&gt;</description></item><item><title>Linux on HP Mini 110</title><link>https://blog.iankulin.com/linux-on-hp-mini-110/</link><pubDate>Mon, 17 Apr 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/linux-on-hp-mini-110/</guid><description>&lt;p&gt;I&amp;rsquo;ve been furthering my Linux education by playing with some desktop distros in VMs, but it&amp;rsquo;s not a great experience accessing them through the Proxmox web GUI. The alternative to this is to use a good &lt;a href="https://en.wikipedia.org/wiki/Simple_Protocol_for_Independent_Computing_Environments"&gt;SPICE&lt;/a&gt; client on the remote desktop, but there is &lt;a href="https://forum.proxmox.com/threads/access-vm-thru-spice-on-osx.66727/"&gt;not a simple good solution&lt;/a&gt; for this for MacOS.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been playing with the idea of picking up an old i3/i5 Thinkpad - these are around the AUD130 mark on eBay, to run a Linux distro with the main idea being to use it to SPICE into my VMs.&lt;/p&gt;
&lt;p&gt;This weekend at my parents house, I&amp;rsquo;ve been going through the cupboard secure wiping a couple of the discarded laptops, and found a fancy looking HP Mini 110-1131dx.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.notebookcheck.net/HP-Mini-110-Series.24414.0.html"&gt;&lt;img src="https://blog.iankulin.com/images/9563377_ra.jpg" width="500" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.notebookcheck.net/HP-Mini-110-Series.24414.0.html"&gt;&lt;img src="https://blog.iankulin.com/images/9563377cv3a.jpg" width="500" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This was a netbook, quite a cute little thing, and like most HP hardware - well made and popular enough that I should be able to googlesolve any issues I encounter. The Atom N270 that mousepowers it is a single core 32bit baby, but there&amp;rsquo;s also a moderate graphics accelerator chip - the &lt;a href="https://www.notebookcheck.net/Intel-Graphics-Media-Accelerator-950.2177.0.html"&gt;GMA 950&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Mint or Ubuntu would probably be my first choices for a desktop distro, but given the very low specs of the HP 110, I&amp;rsquo;m guessing that&amp;rsquo;s not going to be a good experience even if you go back to the most recent 32 bit versions. After a bit of googling around, I decided &lt;a href="https://antixlinux.com/"&gt;antiX&lt;/a&gt; or &lt;a href="https://lubuntu.net/"&gt;Lubuntu&lt;/a&gt; might be good choices (I&amp;rsquo;m partial to the &lt;code&gt;apt get&lt;/code&gt; family of distros).&lt;/p&gt;
&lt;h3 id="antix"&gt;antiX&lt;/h3&gt;
&lt;p&gt;As with most modern distros, the install was a painless experience - booting from the USB and following prompts. The UI was pleasant and crisp, and I especially liked the background on the desktop with some live statistics.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.linuxinsider.com/story/antix-linux-not-pretty-but-highly-functional-86942.html"&gt;&lt;img src="https://blog.iankulin.com/images/86942_antix-3-small.jpg" width="620" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;From the base install, the wireless would not work properly. On the HP 100, there&amp;rsquo;s a little momentary switch on the front left of the keyboard with an indicator light. It was correctly indicating that the wireless was disabled (by glowing orange) and if I flicked it, I could see in the settings the bluetooth was going off and on, but not the wireless.&lt;/p&gt;
&lt;h2 id="lubuntu"&gt;Lubuntu&lt;/h2&gt;
&lt;p&gt;Again, a painless install experience. The ISO was about twice the size at 2.7GB and the install took a lot longer, although much of it was familiar to me from the numerous Debian and Ubuntu server installs I&amp;rsquo;ve done. Once it was installed and booted, the desktop seemed a bit chunky and dated compared to the flatter antiX, and although slower it was very usable.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/img_4846b.jpg" width="1000" alt=""&gt;
&lt;p&gt;The wifi didn&amp;rsquo;t work, although the indicator was blue suggesting it was turned on. In the menu was an option to check for needed propitiatory drivers, and when I plugged into the Ethernet and ran this, it decided there was a Broadcom chipset wifi that it knew the drivers for. I allowed it to fetch and install them, and the wireless came to life.&lt;/p&gt;
&lt;p&gt;The proprietary drivers not being installed is a common and reasonable thing, and almost certainly the issue with my antiX install, so it seemed like it was probably worth having another go at that plugged into the ethernet and enabling whatever is needed to allow the non-FOSS stuff.&lt;/p&gt;
&lt;h2 id="antix-wifi"&gt;antiX wifi&lt;/h2&gt;
&lt;p&gt;My first thought was that perhaps I could just enable the non-free option for the Debian sources. The way that the apt package manager works is that there&amp;rsquo;s a list of sources it checks with. Some distros are strict about non-FOSS stuff and this needs changed in /etc/apt/sources to &lt;a href="https://serverfault.com/questions/240920/how-do-i-enable-non-free-packages-on-debian"&gt;make it check the non-free parts of the repository&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;antiX had a slightly different setup, with a whole directory of sources, but the non-free option was set. I also mucked around with the &lt;code&gt;rfkill&lt;/code&gt; command which kept saying the softblock was on - although I could see, by using rfkill list that the hardware button was working exactly how it should - flick it once and the hardware block for wifi and bluetooth was activated, flick it again and it went off.&lt;/p&gt;
&lt;p&gt;I also jumped off the cliff of just trying commands that I found on the internet that were suggested for similar sounding situations, and that I only had the shakiest idea of what they did. I&amp;rsquo;m certain that the problem at this stage is that I need to install those Broadcom 43 drivers. Without something poping up to ask me if I want to do that (which is exactly the sort of thing a lean distro wouldn&amp;rsquo;t have) I&amp;rsquo;m a bit lost.&lt;/p&gt;
&lt;p&gt;Antix seemed so right for my purposes, I might come back and try it again when my Linux knowledge is a bit better. In the mean time, I need a popular distro, lighter weight than Lubuntu, so I&amp;rsquo;ll give Mint a shot.&lt;/p&gt;
&lt;h2 id="mint-xfce"&gt;Mint Xfce&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://distrowatch.com/"&gt;Distrowatch&lt;/a&gt; currently lists Mint as #3, so it meets my &amp;ldquo;popular&amp;rdquo; criterion, and is has a Xfce (lightweight desktop environment) version, so perhaps it will run crisply on the Atom, but still hold my hand to install these wifi drivers.&lt;/p&gt;
&lt;p&gt;As with all the other distros, it went on smoothly. Once I booted in to it, some sort of system checker popped up to complain about the Broadcom drivers, and offered to extract them from the install USB - that didn&amp;rsquo;t work for me since I&amp;rsquo;d used &lt;a href="https://www.ventoy.net/en/index.html"&gt;Ventoy&lt;/a&gt; for the install, and the ISO was not loaded after I&amp;rsquo;d rebooted. Heading back up to the office to reconnect to an Ethernet cable allowed the system to download the drivers and five minutes later I was on wifi.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not sure if I&amp;rsquo;ve used it enough to be sure, but the performance with Mint Xfce seems similar to Lubuntu - ie not as good as antiX. Also, there&amp;rsquo;s a scary message saying long term support runs out in nine days since I had to go back to version 19.3 to find a 32 bit version.&lt;/p&gt;</description></item><item><title>rsync / Synology / @eaDir</title><link>https://blog.iankulin.com/rsync-synology-eadir/</link><pubDate>Tue, 28 Mar 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/rsync-synology-eadir/</guid><description>&lt;p&gt;The reason I&amp;rsquo;ve been figuring out rsync is to setup my backup strategy. Eventually this will partly be managed with scheduled tasks (ie cron jobs) running rsync. I wanted the SSH in and try this out, since I didn&amp;rsquo;t know some basic things like the mount points of the shares.&lt;/p&gt;
&lt;h3 id="mount-points"&gt;Mount points&lt;/h3&gt;
&lt;p&gt;My first issue was to find the paths to all my data. This turned out not to be a drama. Each of the volumes you create when the NAS is set up are just in the root directory. This includes any USB drives plugged in.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-25-at-8.08.10-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Inside each of those &lt;em&gt;volumes&lt;/em&gt; are any &lt;em&gt;shares&lt;/em&gt; you&amp;rsquo;ve created. At the moment I want to rsync my movies which are in a &amp;lsquo;media&amp;rsquo; share on volume1 to the usb drive, so the directories I&amp;rsquo;ll be using are:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/volume1/media/video/Movies/&lt;/code&gt;&lt;br&gt;
&lt;code&gt;/volumeUSB1/usbshare/media/video/Movies&lt;/code&gt;&lt;/p&gt;
&lt;h3 id="rsync-attempt"&gt;rsync attempt&lt;/h3&gt;
&lt;p&gt;rsync has a cool feature whereby you can do a &amp;lsquo;dry run&amp;rsquo; where it goes through the motions of the command you&amp;rsquo;ve given it, but doesn&amp;rsquo;t change any files. If you combine this with the verbose output, you can clearly see what it&amp;rsquo;s going to do before you let it start changing things. That&amp;rsquo;s an especially good idea when you&amp;rsquo;re dealing with large amounts of data, so my first pass at this included the -n option.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rsync -avin /volume1/media/video/Movies/ /volumeUSB1/usbshare/media/video/Movies --del
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The situation with these two lots of data is that I&amp;rsquo;ve copied my media off the USB drive onto the NAS, then when I installed Jellyfin to access it, I discovered lots of misnamed items (had the years incorrect mostly) and I&amp;rsquo;ve been combining some directories, and renaming others and so on. So I expected this first run or rsync to pull up a heap of changes to make, which it did - thousands of lines of them.&lt;/p&gt;
&lt;p&gt;I noticed a lot of them included this weird directory that I didn&amp;rsquo;t recognise.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;gt;f+++++++++ @eaDir/Tora Tora Tora (1970 PG)@SynoEAStream
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;gt;f+++++++++ @eaDir/Tora Tora Tora (1970 PG)@SynoResource
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I&amp;rsquo;ve since learned it might be extended attributes, people started noticing it around the introduction of DSM7. &lt;a href="https://tech.webit.nu/synology-nas-those-eadir-folders/"&gt;I don&amp;rsquo;t seem to be the only user who hates&lt;/a&gt; Synology messing with my data. There&amp;rsquo;s some consensus they are created by the indexing service (which I&amp;rsquo;ve turned off as much as is possible in the GUI) and when the &lt;a href="https://www.reddit.com/r/synology/comments/exh5ho/preventing_eadir_from_being_created/"&gt;drives are externally mounted&lt;/a&gt; - which of course I have been doing quite a bit while moving things around.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll tackle removing them all and trying to prevent their reoccurence another day, but for the moment, I&amp;rsquo;ll just tell rsync to ignore them using the &lt;code&gt;--exclude&lt;/code&gt; option.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rsync -avin --exclude &amp;#39;*@eaDir*&amp;#39; /volume1/media/video/Movies/ /volumeUSB1/usbshare/media/video/Movies --del
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>SSH with Keys to Synology</title><link>https://blog.iankulin.com/ssh-with-keys-to-synology/</link><pubDate>Mon, 27 Mar 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/ssh-with-keys-to-synology/</guid><description>&lt;p&gt;The Synology operating system DSM (I&amp;rsquo;m on DSM 7.1.1) is Linux, but its highly customised for the purpose of making running a complicated Linux NAS doable for less technical users.&lt;/p&gt;
&lt;p&gt;Due to that, some things that are routine in a regular distro, require a few more steps to jump through to get them to work. SSH-ing in to a Synology with keys is one of those things.&lt;/p&gt;
&lt;h3 id="should-you"&gt;Should you?&lt;/h3&gt;
&lt;p&gt;Before you do start fiddling around, it&amp;rsquo;s probably worth mentioning that almost all the things you might want to do on the Synology can be accomplished through their web interface, or by installing a &amp;lsquo;package&amp;rsquo; from the &lt;em&gt;Package Center&lt;/em&gt;. For example, if you need to run a cron job, that&amp;rsquo;s done through the &lt;em&gt;Control Panel&lt;/em&gt; &amp;lsquo;&lt;em&gt;Task Scheduler&lt;/em&gt;&amp;rsquo;. If you need TailScale installed to easily access it over Wireguard, there&amp;rsquo;s a TailScale package. In general it&amp;rsquo;s probably easier and safer to do things their way.&lt;/p&gt;
&lt;h3 id="enabling-ssh"&gt;Enabling SSH&lt;/h3&gt;
&lt;p&gt;Before you can SSH into the Synology, you need to enable the SSH service. This is straightforward with the web interface. In &lt;em&gt;Control Panel&lt;/em&gt;, look for &lt;em&gt;Terminal &amp;amp; SMNP&lt;/em&gt; and tick the box, and click &lt;em&gt;Apply&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-25-at-6.01.35-pm.png" alt=""&gt;&lt;/p&gt;
&lt;h3 id="home-directory"&gt;Home directory&lt;/h3&gt;
&lt;p&gt;If you SSH to the Synology now, it works, but you&amp;rsquo;ll notice that there&amp;rsquo;s a warning message saying &amp;ldquo;Could not chdir to home directory /var/services/homes/&lt;user name&gt;: No such file or directory&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-25-at-7.12.36-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The reason for this is that unlike most distros, when you create a user in DSM, there&amp;rsquo;s no home directory created for them. There must be some bash config somewhere since I get that nice prompt, but no user home directory.&lt;/p&gt;
&lt;p&gt;Lot&amp;rsquo;s of times, you could just ignore that warning, you can still probably do what you wanted to, but it is going to be an issue for installing SSH keys - when you do the &lt;code&gt;ssh-copy-id&lt;/code&gt; it will want to create a .ssh file in the user&amp;rsquo;s home directory, and if they haven&amp;rsquo;t got one, that is not going to work. You&amp;rsquo;ll get a similar sort of error saying something like&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;sh: line 0: cd: /var/services/homes/&amp;lt;user_name&amp;gt;: No such file or directory mkdir: cannot create directory '.ssh': Permission denied&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Again, there&amp;rsquo;s a setting in the web interface to create home directories for the users. We&amp;rsquo;re in the &lt;em&gt;Control Panel&lt;/em&gt; again, but this time look for &lt;em&gt;User &amp;amp; Group&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-25-at-6.20.35-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Tick the box for &lt;em&gt;Enable user home service&lt;/em&gt;, and hit &lt;em&gt;Apply&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Now you&amp;rsquo;ll be able to copy the keys as usual with ssh-copy-id.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-03-25-at-7.25.59-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-25-at-7.25.59-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Nostalgia</title><link>https://blog.iankulin.com/nostalgia/</link><pubDate>Tue, 14 Mar 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/nostalgia/</guid><description>&lt;p&gt;I&amp;rsquo;m not super interested in FreeDOS, but did enjoy this video from Jim Hall since I lived through all this, and was working in IT (well, &amp;lsquo;data processing&amp;rsquo; actually) during the introduction of the IBM PC.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/3E5Hog5OnIM?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;

&lt;p&gt;My first DOS was 2.11, but spent a lot more time on 3.12, and later 4.01. Windows wasn&amp;rsquo;t really ready for anyone until 3.1 which is when I dived in there. I seem to remember purchasing a PC with a whole megabyte of RAM in anticipation!&lt;/p&gt;
&lt;p&gt;Jim mentions AsEasyAs in the video, which was a &amp;lsquo;shareware&amp;rsquo; spreadsheet I used extensively at home (work had Lotus 123 - AsEasyAs was a play on that name).&lt;/p&gt;
&lt;p&gt;It couldn&amp;rsquo;t conceivably serve any useful purpose, but I do have a small hankering to find the executables on old drives and see if I can set up my &lt;a href="https://en.wikipedia.org/wiki/Clipper_(programming_language)"&gt;Clipper&lt;/a&gt; environment and compile some of my first commercial software.&lt;/p&gt;
&lt;p&gt;I do wish I had a few screenshots from over the years - you never know what you&amp;rsquo;re going to enjoy looking back on. For example, I&amp;rsquo;ve spent hundreds of hours over the last few years in various versions of the Wordpress editor and never thought to record that.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-12-at-1.28.53-pm.jpg" alt=""&gt;&lt;/p&gt;</description></item><item><title>NAS Storage Calculations</title><link>https://blog.iankulin.com/nas-storage-calculations/</link><pubDate>Sat, 11 Mar 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/nas-storage-calculations/</guid><description>&lt;p&gt;I&amp;rsquo;ve been really happy with my two bay Synology NAS - a DS216j. The Synology&amp;rsquo;s seem to have great reputation for just pushing on. Mine is loaded up with two 8TB Seagate Barracudas in RAID 1 leaving me with a one drive failure redundancy.&lt;/p&gt;
&lt;p&gt;I guess a more hard-core host-er than me would be building their own array and using Unraid or ZFS or something. I&amp;rsquo;m pretty comfortable with the Synology off the shelf system; it&amp;rsquo;s a good match for my (low) level of expertise, and more robust than my previous storage system of a USB external drive.&lt;/p&gt;
&lt;p&gt;As I start to move real world applications out of the cloud and on to self-hosting, I need to be serious about availability and data security. The general standard for this in the self-hosting community seems to be three versions of data:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;production version&lt;/li&gt;
&lt;li&gt;local backup&lt;/li&gt;
&lt;li&gt;remote backup&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I feel like the local and remote backups don&amp;rsquo;t &lt;em&gt;have&lt;/em&gt; to be NAS - a large enough external drive might be a reasonable cost saving. I would like the local backup to be able to be swapped into production though, so even if it was an external USB drive and would provide a degraded service, it should be able to be used to maintain services.&lt;/p&gt;
&lt;p&gt;A few days ago, there was an 8 bay, hot swappable Synology on eBay that got me a bit excited thinking about running different pools with a variety of RAIDs or just packing it with low cost smaller HDDs. Luckily I didn&amp;rsquo;t win it, but it triggered me to think about exactly what I need and what the trade-offs are.&lt;/p&gt;
&lt;h3 id="drive-quality"&gt;Drive Quality&lt;/h3&gt;
&lt;p&gt;The drives I&amp;rsquo;ve got in my NAS are second-hand, brand name non-NAS drives. They had just under 9000 hours on them, and the company selling them had hundreds of identical drives. From this, I&amp;rsquo;m assuming they came out of a data centre who replace drives at the one year mark.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s possible to buy &amp;ldquo;NAS&amp;rdquo; drives (and also &amp;ldquo;surveillance&amp;rdquo; drives) which are sold for uses where they are in use 24/7. They cost more.&lt;/p&gt;
&lt;p&gt;Is it possible these two types of drives (and perhaps even the USB external drives) are all the same drives, just marketed differently? Well, it&amp;rsquo;s possible. But it&amp;rsquo;s also possible they are mechanically identical, but have been sent to different market segments based on their initial test results.&lt;/p&gt;
&lt;h3 id="raid"&gt;RAID&lt;/h3&gt;
&lt;p&gt;RAID is a way of combining physical disks into one logical volume, usually in a way that reduces the capacity but allows for a drive failure without data loss. There&amp;rsquo;s several different &amp;rsquo;levels&amp;rsquo; of RAID. My current setup is RAID1 - I have two 8TB disks which present to the system as a single 8TB disk, but if one drive fails, I still have access to all of my data. If you have more than a couple of disks, RAID5 is a better option - if you had 3 x 8TB drives, you&amp;rsquo;d end up with 16TB usable space, and still be able to tolerate one drive failure. If you&amp;rsquo;re super cautious, RAID6 will allow two drive failures before you&amp;rsquo;re in danger. Of course this comes at a cost, if we had a 4x 8TB drive setup, there&amp;rsquo;d only be 16TB available, but any two of the drives could die without stopping the system.&lt;/p&gt;
&lt;h3 id="scoping-out-the-options"&gt;Scoping out the options&lt;/h3&gt;
&lt;p&gt;I&amp;rsquo;ve decided I need about 12TB - I currently have about 3TB of media locally and 0.5TB of general data on my laptop, that&amp;rsquo;s backed up to an external drive about weekly, along with about 20GB in DropBox. The bottom Dropbox plan is about AUD190 for 2TB, and I&amp;rsquo;m only using a fraction of it, so as part of my self-hosting that will get canned. 12TB seems like a lot of headroom from 4TB which is about where I&amp;rsquo;m sitting. I&amp;rsquo;d like to offer a couple of TB to whichever relative ends up hosting my remote backup. And finally it&amp;rsquo;s a multiple of 6TB which is a common ex-enterprise second hand drive size on ebay, so I know I can get year old ones for about $100&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll run through the thinking of each of the options I&amp;rsquo;ve considered.&lt;/p&gt;
&lt;h3 id="ds412"&gt;DS412+&lt;/h3&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-10-at-5.14.28-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I had a couple of Synologys with more bays than this in my eBay watchlist, but as you add drives, you add power consumption and heat, so I think realistically at the 12TB point, 4 bays is the most you could justify. There&amp;rsquo;s a bit of a price step up as well when you go to five bays and leave the serious home user segment of the market.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s worth mentioning how the Synology model numbers work in the DS range. My existing unit is a DS216j. The 2 is for two bays and it&amp;rsquo;s 2016 model. So the DS412+ is from 2012 and has four bays. The &amp;lsquo;j&amp;rsquo; on mine denotes its a mouse-power CPU - in this case a Marvell Armada 385 - some sort of low power ARM. The DS412+ is rocking a bigger mouse - the Intel Atom D2700, and it has a bit more RAM.&lt;/p&gt;
&lt;p&gt;The processor is not a big deal to me. Some folk host a lot of apps - media servers etc on their NAS. I&amp;rsquo;m not planning to do that. As long as it can run Tailscale in a container (which the &amp;lsquo;j&amp;rsquo; models can) we&amp;rsquo;re good to go.&lt;/p&gt;
&lt;p&gt;My theory with drive quality is that the lower the quality of the drive, the higher level RAID I need. So If I scope this unit out with the $100 used, non-NAS drives, I can install four disks as RAID6, have two fail on the same day, and still be operational.&lt;/p&gt;
&lt;p&gt;This second-hand (about ten years old) unit was on &amp;ldquo;buy now&amp;rdquo; for $416, the four year old drives adds $440 making it $856 unit - around $71/TB. The quoted power draw is 44W which works out at 3.7W/TB - the highest of everything I considered.&lt;/p&gt;
&lt;h3 id="ds420j"&gt;DS420j&lt;/h3&gt;
&lt;p&gt;Thinking about likely points of failure with the eleven year old NAS (if I used the DS412j) made me wonder if a unit failure might be higher on the probability list than a second-hand drive failure. The answer is who knows? - probably both events are quite unlikely. However I have some redundancy built in to the drives, but a single point of failure in the NAS unit. It made me wonder what a new 4 bay Synology costs, and the answer is not much more.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/27459.jpg" width="229" alt=""&gt;
&lt;p&gt;This DS420j is again 4 bays, and like the older 4 nay unit it&amp;rsquo;s hot-swapable. This means that in the event of a drive failure, you can leave the NAS running, remove the faulty drive and insert a new one. The unit will then (slowly) rebuild the RAID array, but while you are removing a drive and rebuilding the RAID the system is still fully operational.&lt;/p&gt;
&lt;p&gt;Listed at $439, that works out to $880 total if I used the same second-hand 6TB drives as in the calculation above. So with my RAID 6 (two drive failures can be tolerated without losing data) the cost per TB is $73 - only a couple more than in the first example.&lt;/p&gt;
&lt;p&gt;Apart from the peace of mind of running a newer unit, there&amp;rsquo;s a big difference in power consumption. The DS420j uses 44W, this one is 22W with all the drives spinning, or if you allow them to hibernate as low as 8W. So the max power burn is half at 1.8W per TB&lt;/p&gt;
&lt;h3 id="trading-raid"&gt;Trading RAID&lt;/h3&gt;
&lt;p&gt;RAID 6 - where I&amp;rsquo;m installing 4 x 6TB drives, and only ending up with 12TB of usable space is very conservative - and I was doing that because I was using the second hand drives. What if I bought cheap, but still brand name HDDs, but only three of them and configured as RAID 5 so I&amp;rsquo;d still get 12TB of usable disk, and a single drive can fail without affecting my data?&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-03-10-at-5.40.03-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-10-at-5.40.03-pm.png" width="239" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;There are no three bay Synologys, so I&amp;rsquo;d use the same NAS as above - the DS420j. I can buy three Seagate Skyhawk 6TB disks for $660, so the total comes to $1100 or $92/TB - quite a bit more than four of the older drives in RAID6. With less drives spinning, we can probably assume a total power consumption of around 15W - 1.25W/TB&lt;/p&gt;
&lt;h3 id="even-less-disks"&gt;Even less disks&lt;/h3&gt;
&lt;p&gt;What if we reduce the number of disks even further? If we double the drive capacity to 12TB we could run 2 x 12TB drives as RAID1, have a smaller NAS, save some power, and still have a single drive fail with no data loss. We might want to go to an even higher quality of drive, perhaps one of the ones rated for NAS use - The cheapest new brand name 12TB NAS drive on eBay was a Seagate IronWolf. Two of those costs $756. The two bay DS220j NAS only adds $241 for a total of $997 - $83/TB. Power looks great at 1.1W/TB.&lt;/p&gt;
&lt;p&gt;These smaller NAS&amp;rsquo;s are not hot-swappable. You have to power down the NAS to replace a drive. This is not as cool as just clicking a button and sliding a drive out while all your services are still up, but it&amp;rsquo;s not really a significant factor in my decision making.&lt;/p&gt;
&lt;h3 id="disk-singular"&gt;Disk singular&lt;/h3&gt;
&lt;p&gt;I wouldn&amp;rsquo;t do this for my production storage, but it&amp;rsquo;s worth mentioning the single disk NAS. You&amp;rsquo;d want the best quality of drive possible, and probably schedule to swap it out after three years or so. A Synology single drive NAS with a NAS rated drive is a big step up in quality and convenience from an external USB drive. With that same IronWolf 12TB drive and a DS120j you&amp;rsquo;d be out of pocket $578 or $48/TB, and power is down to 0.83W/TB.&lt;/p&gt;
&lt;h3 id="ye-olde-usb-drive"&gt;Ye Olde USB Drive&lt;/h3&gt;
&lt;p&gt;I&amp;rsquo;ve not had a USB drive failure, but mine generally live a happy life powered down in a cool dry drawer until they are fished out for a backup session. Just by way of a comparison to the options above, a WD &amp;ldquo;Elements&amp;rdquo; USB drive costs the same as the single disk NAS at $580 ($48/TB) but the power is down at 0.67W/TB. The cheaper Seagate &amp;ldquo;One Touch Desktop Hub&amp;rdquo; drive works out at $35/TB and 0.83W/TB&lt;/p&gt;
&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class="has-text-align-center" data-align="center"&gt;&lt;strong&gt;NAS&lt;/strong&gt;&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;&lt;strong&gt;Disks&lt;/strong&gt;&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;&lt;strong&gt;$/TB&lt;/strong&gt;&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;&lt;strong&gt;W/TB&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="has-text-align-center" data-align="center"&gt;DS412+&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;4 x 6TB used&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;$856&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;71&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;3.7&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="has-text-align-center" data-align="center"&gt;DS420j&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;4 x 6TB used&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;$880&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;73&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;1.8&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="has-text-align-center" data-align="center"&gt;DS420j&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;3 x 6TB new&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;$1,100&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;92&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;1.25&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="has-text-align-center" data-align="center"&gt;DS220j&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;2 x 12TB NAS&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;$997&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;83&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;1.1&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="has-text-align-center" data-align="center"&gt;DS120j&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;1 x 12TB NAS&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;$578&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;48&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;0.83&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="has-text-align-center" data-align="center"&gt;WD Elements&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;1 x 12TB USB&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;$580&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;48&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;0.67&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="has-text-align-center" data-align="center"&gt;Seagate&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;1 x 12TB USB&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;$423&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;35&lt;/td&gt;&lt;td class="has-text-align-center" data-align="center"&gt;0.83&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Clearly out of the first two options, you&amp;rsquo;d choose the second. $2 extra per TB of storage is easily worth it to start with a new NAS compared with an eleven year old one. After that the price per TB doesn&amp;rsquo;t go down till you hit the single drive devices.&lt;/p&gt;
&lt;p&gt;Someone else may well start with different assumptions that I have made here, especially in the way I&amp;rsquo;ve decided to increase the drive quality as I&amp;rsquo;ve reduced the redundancy. For instance, you may be happy with a couple of second hand 12TB drives in a DS220j at $54/TB instead of rooting for new NAS drives. This would be along the lines of my original purchase of a DS216j and two 8TB second hand drives for $58/TB.&lt;/p&gt;
&lt;h2 id="the-plan"&gt;The Plan&lt;/h2&gt;
&lt;p&gt;Based on all this, I went with option two - the new DS420j and four old drives in a RAID6. It turned out a bit cheaper since there was a discount code for the NAS, and the price per drive was a touch less when buying four.&lt;/p&gt;
&lt;p&gt;For a local backup, I&amp;rsquo;ll use a single second hand 12TB drive in a DS120j, and for the remote, mainly because I want to share some storage with the home owner, and I feel that has to be on RAID, I&amp;rsquo;ll buy a pair of 14TB second hand drives to put in the DS216j for the remote, so I can open up a 2TB pool for them to use as a local backup for laptops or what have you.&lt;/p&gt;</description></item><item><title>Could it be a permissions problem?</title><link>https://blog.iankulin.com/could-it-be-a-permissions-problem/</link><pubDate>Sun, 05 Mar 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/could-it-be-a-permissions-problem/</guid><description>&lt;p&gt;Unix, and therefore Linux, was built from the ground up as a multi-user system. Thanks to this, great security is baked in, for example every file has permission attributes for it&amp;rsquo;s owner, the group the owner is a member of, and then everyone. For example, it might be a good idea if I can read, write and execute my own files, but the other members of my group can just read them, and any other user on the system has none of those rights.&lt;/p&gt;
&lt;p&gt;I &lt;a href="https://blog.iankulin.com/folder-ownership-problems-with-jellyfin/"&gt;talked a bit about this&lt;/a&gt; when I was solving the first round of problems with getting Jellyfin working. I actually solved all of those problems - they were permissions related. Once I&amp;rsquo;d figured out the group id of the jellyfin user and applied that when mounting the NAS I had a week of blissful media consumption on the TV (via Google TV Chromecast), on my laptop, and phone. The eventual plan for this little box is to move it offsite though, so I needed TailScale, which has worked perfectly and effortlessly everywhere else I&amp;rsquo;d tried it, but it turns out it is not happy living in an LXC container on Proxmox which is where my Jellyfin instance was running.&lt;/p&gt;
&lt;p&gt;Googling around, it sounds like it is possible, but more work than I was prepared to invest, and didn&amp;rsquo;t actually needed to. The little i3 it is going to live on has plenty of headroom to run a full VM so I decided to do that.&lt;/p&gt;
&lt;p&gt;I started from scratch with a Debian VM and had it working perfectly, but then an hour or so after I&amp;rsquo;d downloaded all the metadata for my content, some, but not all of the posters would disappear. Once again, the problem turned out to be permissions (Jellyfin wanted write access to the media locations even though I&amp;rsquo;d told it not to save metadata there). I solved that with the &lt;a href="https://pimylifeup.com/chmod-777/"&gt;cursed 777&lt;/a&gt; then did something terrible to the jellyfin process so now it would not start again.&lt;/p&gt;
&lt;p&gt;Sadly, I had not saved a snapshop before I started messing around with things I only half understand.&lt;/p&gt;
&lt;p&gt;In the process of looking for a solution to the jellyfin process retrying starting five times then dying after I&amp;rsquo;d tried to restart it for a reason I can&amp;rsquo;t even recall, I stumbled on a StackExchange that was my exact problem. In the thread of answers, one of them was just &amp;ldquo;Use the container version&amp;rdquo;. I took slight offence at this, as did OP, but when the commenter pointed out that these fiddly installation problems that lead you all over the place and are painful because your configuration is different to everyone else&amp;rsquo;s, and the problem could lie there - is the exact problem containers are intended to solve.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve taken that advice on board and installed the official container version. First problem I&amp;rsquo;ve run into - Jellyfin can&amp;rsquo;t see my media folder - permissions.&lt;/p&gt;
&lt;p&gt;ANY problem you have running something on Linux, you should always start thinking about it in terms of permissions. Who is the user, what are they acessing?&lt;/p&gt;</description></item><item><title>Classes in JavaScript</title><link>https://blog.iankulin.com/classes-in-javascript/</link><pubDate>Sat, 07 Jan 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/classes-in-javascript/</guid><description>&lt;p&gt;First lesson with classes today. First of all I was pleased to see they exists since we&amp;rsquo;ve just been plucking objects out of thing air like:&lt;code&gt;}&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; userIan &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#39;Ian&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; language&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#39;Indonesian&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;but with classes we can declare a class and instantiate an object of it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;class&lt;/span&gt; User &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; constructor&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; language&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; this&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; name&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; this&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;language &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; language&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; ian &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; new User&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;Ian&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#39;Indonesian&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;console&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;log&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;ian&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;There&amp;rsquo;s (at least) single inheritance:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;class&lt;/span&gt; User &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; constructor&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; language&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; this&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; name&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; this&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;language &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; language&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;class&lt;/span&gt; Administrator &lt;span style="color:#81a1c1;font-weight:bold"&gt;extends&lt;/span&gt; User &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; constructor&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; language&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; permissions&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; super&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; language&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; this&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;permissions &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; permissions
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; ian &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; new Administrator&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;Ian&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#39;Indonesian&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#39;-rw-r--r--@&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;console&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;log&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;ian&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Of course it&amp;rsquo;s JS, so there&amp;rsquo;s no named arguments, and if you miss one off in a call to the constructor there&amp;rsquo;s no issue until you try to use it.&lt;/p&gt;
&lt;p&gt;Methods are declared without a keyword:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;class&lt;/span&gt; User &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; constructor&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; language&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; this&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; name&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; this&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;language &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; language&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; asString&lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; &lt;span style="color:#bf616a"&gt;`&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;this&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;}&lt;/span&gt; language&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;this&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;language&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; ian &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; new User&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;Ian&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#39;Indonesian&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;console&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;log&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;ian&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;asString&lt;span style="color:#eceff4"&gt;());&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Or we could do that as. computed property using &amp;lsquo;get&amp;rsquo;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;class&lt;/span&gt; User &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; constructor&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; language&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; this&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; name&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; this&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;language &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; language&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; get asString&lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; &lt;span style="color:#bf616a"&gt;`&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;this&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;}&lt;/span&gt; language&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;this&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;language&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; ian &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; new User&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;Ian&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#39;Indonesian&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;console&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;log&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;ian&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;asString&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You don&amp;rsquo;t have to have a constructor, just declare the fields:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;class&lt;/span&gt; User &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; name&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; language&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; get asString&lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; &lt;span style="color:#bf616a"&gt;`&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;this&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;}&lt;/span&gt; language&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;this&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;language&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; ian &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; new User&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ian&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Ian&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ian&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;language &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Indonesian&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;console&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;log&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;ian&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;asString&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Probably a good idea to have default values in that case though:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;class&lt;/span&gt; User &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; name &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; language &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#39;English&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; get asString&lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; &lt;span style="color:#bf616a"&gt;`&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;this&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;}&lt;/span&gt; language&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;this&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;language&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; ian &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; new User&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ian&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Ian&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;console&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;log&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;ian&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;asString&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>Things I love about Swift after a week of JavaScript</title><link>https://blog.iankulin.com/things-i-love-about-swift-after-a-week-of-javascript/</link><pubDate>Fri, 06 Jan 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/things-i-love-about-swift-after-a-week-of-javascript/</guid><description>&lt;p&gt;So, a week into JavaScript, what am I missing? The techie in me wants to say things like Automatic Reference Counting, but actually, at my junior level, I don&amp;rsquo;t run into memory management issues on the day-to-day, so what really do I miss?&lt;/p&gt;
&lt;h4 id="determinism"&gt;Determinism&lt;/h4&gt;
&lt;p&gt;When I build an iOS app, it&amp;rsquo;s frozen in time. The functions inside are always going to stay the same. There might be a future version of iOS that won&amp;rsquo;t run it, but as long as it runs, any pure functions inside it will return the same value. The process of compiling it locks that in. Likewise, any libraries that are complied with it.&lt;/p&gt;
&lt;p&gt;As an interpreted language, that is not true for JavaScript. It can be interpreted differently. It might run differently on different versions of a browser, or browsers from different developers. For example, it would be possible to write a function that adds 1+1 that has a different result on the current version of the Opera Mini browser to Chrome do to the different way it deals with &lt;code&gt;const&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s a fair point to say this also does not affect me on the day-to-day, but it does worry me that something I put out there might fail unpredictably in the future. I guess &lt;a href="https://webassembly.org/"&gt;web assembly&lt;/a&gt; fixes this, but then I could be writing in Swift!&lt;/p&gt;
&lt;h4 id="ide"&gt;IDE&lt;/h4&gt;
&lt;p&gt;I really love &lt;a href="https://code.visualstudio.com/"&gt;VS Code&lt;/a&gt;, it makes me feel differently (in a good way) about Microsoft. It&amp;rsquo;s fun to write code in, and has a amazing extensions that provide all sorts of good experiences, but it&amp;rsquo;s not a complete IDE. Likely there&amp;rsquo;s things I can set up to get closer to debugging in VS Code while running my apps in a browser and I just haven&amp;rsquo;t figured them out yet. When I&amp;rsquo;m just playing with JS using node is a good solution but I haven&amp;rsquo;t arrived at a smooth solution for browser. Currently I&amp;rsquo;m using lots of console.log()s and the developer console in the browsers.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s fair to say Swift developers don&amp;rsquo;t universally love the Xcode experience, and there is definitely room to improve the extension environment, but it is a complete package for developing iOS apps.&lt;/p&gt;
&lt;h4 id="deprecated--deleted"&gt;Deprecated = Deleted&lt;/h4&gt;
&lt;p&gt;When Swift kills off an old language feature in a big number version change, it stays dead. It&amp;rsquo;s not there any more, it haunt&amp;rsquo;s no one. That&amp;rsquo;s not possible with an interpreted language with millions of installed apps. Browsers have to support bad old ideas in JavaScript basically forever. I have to have &lt;a href="https://caniuse.com/"&gt;CanIUse&lt;/a&gt; open in a tab (although I guess there&amp;rsquo;s a VS Code extension for that!).&lt;/p&gt;
&lt;h4 id="in-javascripts-favour"&gt;In JavaScript&amp;rsquo;s favour&lt;/h4&gt;
&lt;p&gt;I love the low barrier to entry to JavaScript. Web development is open to anyone with an internet connection and a device. No MacBook, no Apple Developer subscription, no iPhone needed. Most anyone wanting to try it can just open the console in their browser.&lt;/p&gt;
&lt;p&gt;In theory an interpreted language has the advantage of not needing the build step. When I first encountered Ruby and Python this was part of their charm for me. This does apply to JavaScript - you can just pull up the console and start playing. In the intervening years, compiled languages have made up this space a bit though, so it&amp;rsquo;s not the big selling point it used to be. Swift in Xcode is compiled somehow as you type so you&amp;rsquo;re seeing errors flagged as they occur. Building the little apps I&amp;rsquo;ve written is very quick, and tools like Playgrounds fill part of this need.&lt;/p&gt;</description></item><item><title>Second Guessing</title><link>https://blog.iankulin.com/second-guessing/</link><pubDate>Thu, 05 Jan 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/second-guessing/</guid><description>&lt;p&gt;In the last post, I was pleased with myself for accidentally anticipating an improvement to a tutorial project which turned out to be the next task, today I&amp;rsquo;m pleased with myself for discussing the pros and cons of &lt;code&gt;onclick=&lt;/code&gt; vs &lt;code&gt;addEventListener()&lt;/code&gt; then having that same discussion turn up in the next tutorial. I take it as an indication that I am correctly immersing myself in the subject.&lt;/p&gt;
&lt;p&gt;My approach to this learning is to watch all the tutorials, and any that involve code, type along with them. Usually they are followed with a task to extend them so I do those or invent my own. I&amp;rsquo;ve also started trying to reproduce versions of other websites I see that I&amp;rsquo;m interested in.&lt;/p&gt;
&lt;p&gt;I also listen to a few podcasts about the subject. Often good podcasts seem to get more detailed and technical over time, so I search for any that do occasional episodes on beginner topics. Even if a lot of it is over my head, you do start to pick up on the industry buzz about things, and learn some of the names that can be helpful for searches later. I&amp;rsquo;m currently cherry picking old episodes of &lt;a href="https://topenddevs.com/podcasts/javascript-jabber"&gt;JavaScript Jabber&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The last thing is these posts. I&amp;rsquo;m fully aware that most likely they&amp;rsquo;ll never be read by anyone but me, but the process of writing them does help clarify my thoughts, and the knowledge of a potential audience gives me a sense of accountability I wouldn&amp;rsquo;t have if I was just saving them to a directory. They do even occasionally help me - I&amp;rsquo;ve been back to some of the git ones when I&amp;rsquo;ve forgotten things!&lt;/p&gt;</description></item><item><title>99 CSS Layout challenge</title><link>https://blog.iankulin.com/99-css-layout-challenge/</link><pubDate>Fri, 23 Dec 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/99-css-layout-challenge/</guid><description>&lt;p&gt;In the &lt;a href="https://zerotomastery.io/"&gt;Zero To Mastery&lt;/a&gt; &lt;a href="https://www.udemy.com/course/the-complete-web-developer-zero-to-mastery/"&gt;Complete Web Developer&lt;/a&gt; course, I&amp;rsquo;m up to the first practical challenge - to use CSS to layout a reasonably standard looking web page using flex-box and grid to make it responsive.&lt;/p&gt;
&lt;p&gt;Frustratingly, both for writing this, and while I was trying to build the page, I&amp;rsquo;m unable to screenshot the example of the page I was supposed to be building, and instead had to keep opening the video and seeking the two second flash of the completed project, and eventually being reduced to photographing my laptop screen like a boomer relative sending me a meme:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/img_3601.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;What you can&amp;rsquo;t see in that image (because it was never shown for this version) is a foooter bar containing a piece of centred text.&lt;/p&gt;
&lt;p&gt;The starting point was some html with a a handful of elements, and some unnecessarily complicated css colours.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2022-12-20-at-8.06.13-am-1.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-12-20-at-8.06.13-am-1.png" width="800" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;!DOCTYPE html&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;html&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;head&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;title&amp;gt;Layout Master&amp;lt;/title&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;link rel=&amp;#34;stylesheet&amp;#34; type=&amp;#34;text/css&amp;#34; href=&amp;#34;./style.css&amp;#34;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/head&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;body&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;div class=&amp;#34;zone green&amp;#34;&amp;gt;Header&amp;lt;/div&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;div class=&amp;#34;zone red&amp;#34;&amp;gt;Cover&amp;lt;/div&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;div class=&amp;#34;zone blue&amp;#34;&amp;gt;Project Grid&amp;lt;/div&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;div class=&amp;#34;zone yellow&amp;#34;&amp;gt;Footer&amp;lt;/div&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/body&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/html&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The hint that was given, was to use a mix of flex-box and grid. Since it was stated in the same breath that everything you can do in flex, you can do in grid, I decided to just do grid. I&amp;rsquo;m not anticipating having to maintain anyone&amp;rsquo;s flex code, so I felt I could reduce the amount I need to learn by eliminating flex-box.&lt;/p&gt;
&lt;p&gt;I changed up the HTML a bit to make it more semantic, but still ended up with a couple of divs for the cover and projects.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-html" data-lang="html"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#5e81ac;font-style:italic"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;html&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;lang&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;en&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;head&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;meta&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;charset&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;UTF-8&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;meta&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;http-equiv&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;X-UA-Compatible&amp;#34;&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;content&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;IE=edge&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;meta&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;name&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;viewport&amp;#34;&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;content&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;width=device-width, initial-scale=1.0&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;link&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;href&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;style.css&amp;#34;&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;rel&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;stylesheet&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;title&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;Document&lt;span style="color:#eceff4"&gt;&amp;lt;/&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;title&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;&amp;lt;/&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;head&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;body&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;header&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;ul&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;li&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;a&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;href&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;about.html&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;About&lt;span style="color:#eceff4"&gt;&amp;lt;/&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;a&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;li&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;li&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;a&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;href&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;products.html&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;Products&lt;span style="color:#eceff4"&gt;&amp;lt;/&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;a&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;li&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;li&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;a&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;href&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;ourteam.html&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;Our Team&lt;span style="color:#eceff4"&gt;&amp;lt;/&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;a&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;li&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;li&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;a&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;href&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;contact.html&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;Contact&lt;span style="color:#eceff4"&gt;&amp;lt;/&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;a&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;li&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;/&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;ul&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;/&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;header&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;main&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;div&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;class&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;cover&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;img&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;src&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;img/undraw.png&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;/&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;div&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;div&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;class&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;projects&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;img&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;src&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;img/monitor_coding_2.png&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;img&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;src&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;img/desktop_analytics_2.png&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;img&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;src&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;img/files_2.png&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;img&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;src&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;img/data_storage_2_2.png&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;img&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;src&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;img/monitor_settings_2.png&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;img&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;src&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;img/server_2_2.png&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;img&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;src&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;img/server_3.png&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;img&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;src&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;img/server_safe_2.png&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;/&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;div&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;/&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;main&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;footer&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Made by IanKulin
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;&amp;lt;/&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;footer&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;&amp;lt;/&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;body&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;&amp;lt;/&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;html&lt;/span&gt;&lt;span style="color:#eceff4"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I&amp;rsquo;ll work through the CSS piece by piece. The general stuff was importing the font, the reset and setting the body defaults.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@import url(&amp;#39;https://fonts.googleapis.com/css?family=Roboto&amp;amp;display=swap&amp;#39;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;* {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; margin: 0;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; padding: 0;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; box-sizing: border-box;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;body {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; color: white;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; font-family: &amp;#39;Roboto&amp;#39;, sans-serif;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The header/nav bar was actually the toughest job here - it needs four links; three left aligned and one right aligned. I have no doubt that this is not the most elegant solution.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;header {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; background: #33BCFF;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; height: 3rem;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; display: grid;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; align-content: center;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This grid and align was just to get the text vertically centered.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;header ul {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; display: grid;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; grid-template-columns: repeat(3, fit-content(150px)) 1fr;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; grid-gap: 10px;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; justify-items: end;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; padding: 20px;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The links are in an unordered list, so that&amp;rsquo;s made to be a grid with three columns closely fitted to the text size, and one more column that takes up the rest of the screen width. There&amp;rsquo;s a 10 pixel gap between the columns and the content in the grid is aligned to the end. That makes no difference for the first three links since they are the size of their containers, but the right end one is pushed to the edge of the screen minus the padding.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;header li {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; list-style-type: none;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;header a:link, header a:visited {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; color: white;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; text-decoration: none; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;header a:hover {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; color: black;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Even though it&amp;rsquo;s convenient to have the links in a list, we don&amp;rsquo;t want the bullet points, so &lt;code&gt;list-style-type: none;&lt;/code&gt; eliminates them. The &lt;code&gt;text-decoration: none;&lt;/code&gt; gets rid of the underline, then we add in a hover style so the user gets some feedback that this is, in fact, clickable.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.cover {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; height: 300px;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; display: grid;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; place-items: center;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.cover img {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; height: 280px;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The cover image was a bit more straightforward. I used a grid to centre it in both axes, and made the image a bit smaller than the div.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.projects {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; /* the grid container */
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; background-color: #33BCFF;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; display: grid;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; place-items: center;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.projects img {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; /* the grid items */
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; height: 200px;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; width: 200px;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; margin: 1rem;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; background-color: #333;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; padding: 2rem;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The computer icons are displayed in another grid. This time the columns are auto-fill based on the browser width, but with a minimum width of the icons.&lt;/p&gt;
&lt;p&gt;Finally, the footer is yet another grid, just to center the text on both axes.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;footer {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; background-color: #F1948A;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; height: 3rem;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; display: grid;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; place-items: center;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;a href="https://gist.github.com/IanKulin/7a30de3a7b4266850a9c16258604198b"&gt;source&lt;/a&gt;&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/eSpsqGfL9hM?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;
</description></item><item><title>HTML 002 - Tags for structure</title><link>https://blog.iankulin.com/html-002-tags-for-structure/</link><pubDate>Wed, 21 Dec 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/html-002-tags-for-structure/</guid><description>&lt;p&gt;I briefly mentioned &lt;a href="https://blog.iankulin.com/html-001/"&gt;earlier&lt;/a&gt; that our HTML tags should flag WHAT this part of the document is rather than how to display it (we&amp;rsquo;ll look at how to use CSS for making the content look how we want later). This idea is called semantic HTML. This post will look at some of the tags (often called &lt;a href="https://www.w3schools.com/html/html5_semantic_elements.asp"&gt;semantic tags&lt;/a&gt;) we use to convey knowledge of what part of each document an element is.&lt;/p&gt;
&lt;h4 id="why-do-i-care"&gt;Why do I care?&lt;/h4&gt;
&lt;p&gt;It&amp;rsquo;s fair to ask &amp;ldquo;why bother?&amp;rdquo; If you know how you want your page to &lt;em&gt;look&lt;/em&gt;, why not just put that in the html and be done with it? There&amp;rsquo;s a couple of good answers to this:&lt;/p&gt;
&lt;p&gt;Maintainability - when we separate how the pages should look from what&amp;rsquo;s in them, that can be reused. If we have a single .css file that says which font paragraphs use, and what colour headings should be, it can be used across the site so all the pages look the same, and if the graphics designer changes their mind about the font it can be changed in a single line in one file instead of having to edit every .html file for the site.&lt;/p&gt;
&lt;p&gt;Accessibility - not every human user will be using a conventional web-browser to consume the pages, and even if they are, they might be using accessibility features to suit their abilities. I already mentioned the obvious issues for screen readers that describe things in audio, but imagine if you do not use a mouse, how helpful it might be to be able to skip from section to section in a document using a keyboard shortcut - that&amp;rsquo;s made more possible by dividing our content into sections.&lt;/p&gt;
&lt;p&gt;Skynet - Your web pages are not only consumed by humans. Search engine web crawlers and, increasingly, AI models hungry for knowledge will also use them as input. If the different parts of your pages are marked up semantically it will improve your search engine optimisation and further the rise of our robot overlords. For example, a top-level heading &lt;h1&gt; can be assumed by the machine to be a good indication of the content of the page. The text between the &lt;summary&gt; tags could be assumed to be good to show as a snippet in some search results.&lt;/p&gt;
&lt;h4 id="headings"&gt;Headings&lt;/h4&gt;
&lt;p&gt;We&amp;rsquo;ve already met the &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; tag which is intended to be the top level heading - there will probably only be one of these in your document and it will signify the content for the entire page. There&amp;rsquo;s a decreasingly important range of sub-headings from &lt;code&gt;&amp;lt;h2&amp;gt;&lt;/code&gt; down to &lt;code&gt;&amp;lt;h6&amp;gt;&lt;/code&gt;. The headings will be rendered differently to indicate their importance but, thinking semantically, The different levels should indicate the role of the document parts following them.&lt;/p&gt;
&lt;h4 id="body-parts"&gt;Body Parts&lt;/h4&gt;
&lt;p&gt;A number of semantic tags indicate different types of content, and many are self explanatory:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;footer&gt;&lt;/footer&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;header&gt;&lt;/header&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;nav&gt;&lt;/nav&gt; - collection of navigation links
&lt;/li&gt;
&lt;li&gt;
&lt;main&gt;&lt;/main&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;summary&gt;&lt;/summary&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;/p&gt; - paragraph
&lt;/li&gt;
&lt;li&gt;
&lt;aside&gt;&lt;/aside&gt; - text that relates to, but is not essential to understanding the main text.
&lt;/li&gt;
&lt;li&gt;
&lt;main&gt;&lt;/main&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;figure&gt;&lt;/figure&gt; - illustrates a point in the text. Often it will contain an &lt;img&gt; and a &lt;figcaption&gt;
&lt;/li&gt;
&lt;li&gt;&lt;detail&gt;&lt;/detail&gt; - often contains some deeper explanation which can be hidden if not needed&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A couple of others are a bit vaguer, but still have semantic meaning.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;section&gt;&lt;/section&gt; - the exact meaning of a section will depend on the type of content. Maybe it's a chapter of a book, maybe a group of related auto-parts.
&lt;/li&gt;
&lt;li&gt;
&lt;article&gt;&lt;/article&gt; - could be a self-contained blog post, or you know, and article in a news page.
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="whats-not-semantic"&gt;What&amp;rsquo;s not semantic&lt;/h4&gt;
&lt;p&gt;There&amp;rsquo;s a couple of tags you&amp;rsquo;ll see widely used for recent historical reasons, as well as web-dev inertia. They are containers that can be used to attach CSS styles to, but don&amp;rsquo;t convey any semantic meaning to the browser or future code maintainers.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;div&gt;&lt;/div&gt; - short for division. Used around blocks of text, usually with a class attribute so some style can be applied to it.
&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;/span&gt; - similar usage as &lt;div&gt; but used for smaller, inline parts of texts.&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Visual Studio Code</title><link>https://blog.iankulin.com/visual-studio-code-2/</link><pubDate>Mon, 12 Dec 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/visual-studio-code-2/</guid><description>&lt;p&gt;I&amp;rsquo;ve gone over to the dark side a little. As I think about the sort of apps I want to make, I realise I am going to need to be able to do back-end web development. My apps are going to need a secure REST api to a database. I guess that means node.js. I&amp;rsquo;m also conscious that my ticket app needs to run on android, and a short cut around all of that might be to make the whole thing a web app from the start, but with the premium experience on iOS.&lt;/p&gt;
&lt;p&gt;So with that in mind, I dropped $18 on the &amp;ldquo;&lt;a href="https://www.udemy.com/course/the-complete-web-developer-zero-to-mastery/"&gt;The Complete Web Developer in 2023: Zero to Mastery&lt;/a&gt;&amp;rdquo; course on Udemy. I&amp;rsquo;m not sure how I think I have the time for that, but anyway&amp;hellip;.&lt;/p&gt;
&lt;p&gt;They recommend, and their examples are using, the &lt;a href="https://www.sublimetext.com/"&gt;Sublime Text&lt;/a&gt; editor, which has an even simpler (and therefore cooler) look than VS Code, but I was already impressed with VSCode so I&amp;rsquo;ve been using that, and gotta say, love it. Things just keep working how I expect, and a fair bit of that is due to good plugins.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-12-10-at-2.55.24-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;This &lt;a href="https://zerotomastery.io/"&gt;ZTM&lt;/a&gt; course is a bit different to what I&amp;rsquo;ve been getting from the 100 Days of SwiftUI, but like that, it starts of by saying you need to do the things to be accountable or you&amp;rsquo;ll likely drop out along the way. That&amp;rsquo;s a fair point, and I thought about adding it to my posting, but really it&amp;rsquo;s more of a side project so I&amp;rsquo;m not sure you would see that here.&lt;/p&gt;
&lt;p&gt;I could (and might) start another blog for it, but I&amp;rsquo;ve committed to myself to post here on something (at least vaguely) iOS development related, and some week&amp;rsquo;s that&amp;rsquo;s a struggle, so I guess we&amp;rsquo;ll see.&lt;/p&gt;</description></item><item><title>Clean code</title><link>https://blog.iankulin.com/clean-code/</link><pubDate>Fri, 09 Dec 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/clean-code/</guid><description>&lt;p&gt;I&amp;rsquo;ve been listening to the &lt;a href="https://www.youtube.com/watch?v=YVrHPCZnC50"&gt;latest episode of the Empower Apps&lt;/a&gt; podcast, this one with &lt;a href="https://twitter.com/Jilsco9"&gt;Jill Scott&lt;/a&gt; talking about &amp;ldquo;Humane&amp;rdquo; development - in the sense of being humane to whoever (probably you) is going to be reading this code in the future. It helped me clarify my thoughts about a couple of things.&lt;/p&gt;
&lt;p&gt;None of these ideas are particularly new or groundbreaking, and although I think of them as my personal style, they are very common, and in Swift could be regarded as part of the culture. Some of these concepts support each other, some represent a trade off between two opposing ideas that require us to make a choice.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/onion-belt.jpg" alt=""&gt;&lt;/p&gt;
&lt;h4 id="the-custom-at-the-time"&gt;The Custom at the Time&lt;/h4&gt;
&lt;p&gt;If other people or bots are going to read your code, or you need to comprehend theirs, there is a lot of value in following the conventions in the language of community you work in. It helps in a couple of ways - 1) you are not expending energy deciding if equals signs should have a space each side, and 2) fluency of reading and writing will improve.&lt;/p&gt;
&lt;h4 id="natural-language"&gt;Natural Language&lt;/h4&gt;
&lt;p&gt;If it&amp;rsquo;s possible to make choices in a piece of code to make it read more like a description of what is happening, then usually do that. Swift (and probably other modern languages - I wouldn&amp;rsquo;t know) has some great language features to support this. For example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;answer = resultOf(6, plus: 7)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I also appreciate the Swift convention of using auxiliary verbs. I think &lt;code&gt;isPaused&lt;/code&gt; or &lt;code&gt;hasCompleted&lt;/code&gt; is clearer that &lt;code&gt;paused&lt;/code&gt; or &lt;code&gt;completed&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id="decomposed"&gt;Decomposed&lt;/h4&gt;
&lt;p&gt;There&amp;rsquo;s probably a reason why paragraphs exist in writing, and are about the length they usually are, deep in the science of how memory works in humans and the interplay between working memory and what else goes on in comprehending something.&lt;/p&gt;
&lt;p&gt;I start to get uncomfortable if a chunk of code I&amp;rsquo;m trying to understand or write is more than a couple of 13&amp;quot; laptop screens long. I thought &lt;a href="https://davidstechtips.com/2012/05/folding-code-in-xcode/comment-page-1/"&gt;code folding&lt;/a&gt; would help but haven&amp;rsquo;t really found that. I aim to have each chunk (I&amp;rsquo;m using &amp;ldquo;chunk&amp;rdquo; for function, method, computed property etc) express a single idea. If part of it is getting long, I consider if that can be removed somewhere else and replaced with a helpful function name in the piece I&amp;rsquo;m working on.&lt;/p&gt;
&lt;h4 id="less-magic"&gt;Less Magic&lt;/h4&gt;
&lt;p&gt;When I&amp;rsquo;m working with some code, I don&amp;rsquo;t want it to need too much context to understand. This principle means anything used in this code should come in through the obvious interface. Global variables, environment variables, or stuff captured from the enclosing scope are undesirable. If I use them, I try and put them near the top since that&amp;rsquo;s where people (me) look when they encounter something part way through the code and don&amp;rsquo;t know where it came from.&lt;/p&gt;
&lt;p&gt;In an ideal world, I could grab a piece of code and paste it into a &lt;a href="https://gist.github.com/discover"&gt;gist&lt;/a&gt; to share here and it would be comprehensible.&lt;/p&gt;
&lt;h4 id="evaporated-comments"&gt;Evaporated Comments&lt;/h4&gt;
&lt;p&gt;I doubt I invented this, but I haven&amp;rsquo;t seen it mentioned anywhere else either. The way I most commonly use comments is to clarify my thoughts before I write any code, something like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;func&lt;/span&gt; sendFile&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;fileName&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#bf616a"&gt;String&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; link&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; NetworkTube&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;-&amp;gt;&lt;/span&gt; SendResultCode &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; check link is operational
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; attempt to open file
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; step through each line sending it&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; wait &lt;span style="color:#81a1c1;font-weight:bold"&gt;for&lt;/span&gt; ack 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; close file
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; code
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then as I flesh out the function, I delete the comment if the code is straight forward. Usually this results in all of the comments being deleted. In my Swift code, comments are quite rare. Where there are comments, it&amp;rsquo;s probably a sign I need to name things better.&lt;/p&gt;
&lt;p&gt;I just went back through the code for that apps I actually use on my phone, and these are the only comments I could find outside of a header.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; fractionDue&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; Double &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; when a habit is overdue&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;or&lt;/span&gt; due now&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; the fractionDue is &lt;span style="color:#b48ead"&gt;1.0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; when it&lt;span style="color:#a3be8c"&gt;&amp;#39;s not due at all - just been done, the fractioDue is 0.0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; isDueNow &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; &lt;span style="color:#b48ead"&gt;1.0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; let daysSinceDone &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; Date&lt;span style="color:#eceff4"&gt;()&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;timeIntervalSince&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;lastDone&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;/&lt;/span&gt; &lt;span style="color:#b48ead"&gt;86&lt;/span&gt;_400
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;assert&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;daysBetweenCompletions &lt;span style="color:#81a1c1"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#b48ead"&gt;0.0&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; daysSinceDone &lt;span style="color:#81a1c1"&gt;/&lt;/span&gt; daysBetweenCompletions
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is a computed variable in a &amp;ldquo;Habit&amp;rdquo; struct. I wanted to have little ticks next to each habit when they were done, which would slowly fade to be completely gone when this habit was due again. To achieve this I needed to calculate an opacity value for the tick. I think it&amp;rsquo;s fair to say this needs re-working. I don&amp;rsquo;t recall if I wrote the comment first, or put it there later recognising the code wasn&amp;rsquo;t self explanatory - it could have been either.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not sure I could fix this to the point it wouldn&amp;rsquo;t need a comment at all. There&amp;rsquo;s a couple of name changes that would help. I think &lt;code&gt;daysSinceDone&lt;/code&gt; would be better as &lt;code&gt;daysSinceLastCompleted&lt;/code&gt;, and instead of calling the property &lt;code&gt;fractionDue&lt;/code&gt;, I might call it &lt;code&gt;freshness&lt;/code&gt;. Instead of dividing the time interval by 86,400 I could have a &lt;code&gt;millisecondsToDays()&lt;/code&gt;function.&lt;/p&gt;</description></item><item><title>SwiftUI provides</title><link>https://blog.iankulin.com/swiftui-provides/</link><pubDate>Wed, 07 Dec 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/swiftui-provides/</guid><description>&lt;img src="https://blog.iankulin.com/images/img_3476.png" width="284" alt=""&gt;
&lt;p&gt;A few hours after I speculated about pausing work on the tickets app because outputting the tickets was too far out of my expertise, a helpful instance of the &lt;a href="https://en.wikipedia.org/wiki/Frequency_illusion"&gt;Baader–Meinhof phenomenon&lt;/a&gt; threw up some help in the form of this tweet from &lt;a href="https://twitter.com/flowritescode"&gt;@FloWritesCode&lt;/a&gt;. It turns out this was an addition in iOS16 announced at WWDC that makes this straightforward.&lt;/p&gt;
&lt;p&gt;As soon as I googled around about it I also found good solutions that wrapped the old code to provide similar functionality. So that&amp;rsquo;s a lesson for me about not assuming something&amp;rsquo;s hard before I&amp;rsquo;ve spent some time investigating it. I took that lesson and applied it to rendering to a PDF, and of course, @twostraws &lt;a href="https://www.hackingwithswift.com/quick-start/swiftui/how-to-render-a-swiftui-view-to-a-pdf"&gt;has a code example&lt;/a&gt; for that from three days ago!&lt;/p&gt;
&lt;p&gt;Obviously I&amp;rsquo;m going to have some fiddling around to lay it out and so on, and I still need to figure out the share behaviour (which I&amp;rsquo;m expecting to be straightforward) but it feels like SwiftUI is just going to help me express my intents in code. I&amp;rsquo;m really enjoying this language, framework and community!&lt;/p&gt;</description></item><item><title>Committed</title><link>https://blog.iankulin.com/committed/</link><pubDate>Tue, 06 Dec 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/committed/</guid><description>&lt;p&gt;I quite like logging into GitHub and seeing my commit history as the graph with the green dots. Once I get up to a year it would be a great thing to have on a T-Shirt.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-12-03-at-7.36.29-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;d expect to be seeing the busy weekends, but Tuesday nights seem to be oddly productive. It could just be a start of the week energy thing - I have some other community obligations on a couple of Monday nights a month.&lt;/p&gt;
&lt;p&gt;git is an amazing tool - better that the commercial tool I used when I was programming a long way back. I really admire what the many open source programmers have put in to it. I also owe some gratitude to Microsoft for making GitHub available to newbs like me at no cost - it&amp;rsquo;s an great service.&lt;/p&gt;
&lt;p&gt;According to the graph, my first commits were on July 10. My first blog post was on the 5th, and I&amp;rsquo;m on a 154 once per day posting streak - although there&amp;rsquo;s a few two per day scattered through there when I&amp;rsquo;ve been on holidays. I&amp;rsquo;m only up to day 64 of the 100 Days of SwiftUI - so I clearly I haven&amp;rsquo;t been getting in an hour each day on that religiously, but overall, I&amp;rsquo;m pretty happy with my commitment to myself.&lt;/p&gt;</description></item><item><title>Ticket to ride</title><link>https://blog.iankulin.com/ticket-to-ride/</link><pubDate>Mon, 05 Dec 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/ticket-to-ride/</guid><description>&lt;p&gt;A &lt;a href="https://blog.iankulin.com/project-based-learning/"&gt;couple of days ago&lt;/a&gt; I was lauding the learning benefits of writing your own projects over completing tutorial projects - since your own projects push your boundaries further. Of course, its also the case that the project requirements might so completely exceed your current ability that it grinds to a halt. That&amp;rsquo;s the case with my &lt;a href="https://blog.iankulin.com/tickets-on-myself/"&gt;behaviour ticket app&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The part of the app for collecting the data is pretty much done and how I imagined it, but the output needs to be pretty tickets that can be printed on paper. I managed to write the ticket data to a CSV file and export that to the files app with a .fileExporter, but really what I wanted is to have one of those share screens where you can chose to AirDrop, Print etc, and for the tickets to have been rendered to a PDF or series of images to be shared. That will have to wait. I&amp;rsquo;m just up to a bit in the #100Days about writing images so I&amp;rsquo;ll push on with that for a bit and come back to my app.&lt;/p&gt;
&lt;p&gt;In the meantime, it&amp;rsquo;s worth going over how to create and export the text file briefly.&lt;/p&gt;
&lt;p&gt;First of all, I stole this TextFile code from Paul Hudson (&lt;a href="https://www.hackingwithswift.com/quick-start/swiftui/how-to-create-a-document-based-app-using-filedocument-and-documentgroup"&gt;here&lt;/a&gt;) that wraps some complexity neatly into a struct.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;//&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;// TextFile.swift&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;// Stolen from Paul Hudson @twostraws&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;// https://www.hackingwithswift.com/quick-start/swiftui/how-to-create-a-document-based-app-using-filedocument-and-documentgroup&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;//&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;import&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;SwiftUI&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;import&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;UniformTypeIdentifiers&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;struct&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;TextFile&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; FileDocument &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#616e87;font-style:italic"&gt;// tell the system we support only plain text&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;static&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; readableContentTypes &lt;span style="color:#eceff4"&gt;=&lt;/span&gt; &lt;span style="color:#eceff4"&gt;[&lt;/span&gt;UTType&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;plainText&lt;span style="color:#eceff4"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#616e87;font-style:italic"&gt;// by default our document is empty&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; text &lt;span style="color:#eceff4"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#616e87;font-style:italic"&gt;// a simple initializer that creates new, empty documents&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;init&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;initialText&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;String&lt;/span&gt; &lt;span style="color:#eceff4"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; text &lt;span style="color:#eceff4"&gt;=&lt;/span&gt; initialText
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#616e87;font-style:italic"&gt;// this initializer loads data that has been saved previously&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;init&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;configuration&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; ReadConfiguration&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;throws&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;let&lt;/span&gt; data &lt;span style="color:#eceff4"&gt;=&lt;/span&gt; configuration&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;file&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;regularFileContents &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; text &lt;span style="color:#eceff4"&gt;=&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;String&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;decoding&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; data&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;as&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;UTF8&lt;/span&gt;&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;self&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;throw&lt;/span&gt; CocoaError&lt;span style="color:#eceff4"&gt;(.&lt;/span&gt;fileReadCorruptFile&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#616e87;font-style:italic"&gt;// this will be called when the system wants to write our data to disk&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;func&lt;/span&gt; &lt;span style="color:#88c0d0"&gt;fileWrapper&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;configuration&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; WriteConfiguration&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;throws&lt;/span&gt; &lt;span style="color:#eceff4"&gt;-&amp;gt;&lt;/span&gt; FileWrapper &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;let&lt;/span&gt; data &lt;span style="color:#eceff4"&gt;=&lt;/span&gt; Data&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;text&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;utf8&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; FileWrapper&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;regularFileWithContents&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; data&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now here&amp;rsquo;s all the modifiers I&amp;rsquo;ve got attached to the list of tickets.&lt;/p&gt;
&lt;p&gt;The toolbar button just assigns a string to the instance of Paul&amp;rsquo;s TextFile(). The string is built by just stepping through the tickets in a for loop and appending csv strings for each ticket to the big string that becomes the file.&lt;/p&gt;
&lt;p&gt;The .fileExporter then does the heavy lifting. It slides up a view of the files in the &amp;ldquo;On My Phone&amp;rdquo; folder, lets the user name the file and saves it.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;.&lt;/span&gt;navigationTitle&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Tickets&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;.&lt;/span&gt;fileExporter&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;isPresented&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#bf616a"&gt;$&lt;/span&gt;showingExporter&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; document&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; exportFile&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; contentType&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#eceff4"&gt;.&lt;/span&gt;plainText&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; result &lt;span style="color:#81a1c1;font-weight:bold"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;switch&lt;/span&gt; result &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;case&lt;/span&gt; &lt;span style="color:#eceff4"&gt;.&lt;/span&gt;success&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;let&lt;/span&gt; url&lt;span style="color:#eceff4"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;print&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Saved to &lt;/span&gt;&lt;span style="color:#a3be8c"&gt;\(&lt;/span&gt;url&lt;span style="color:#a3be8c"&gt;)&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;case&lt;/span&gt; &lt;span style="color:#eceff4"&gt;.&lt;/span&gt;failure&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;let&lt;/span&gt; error&lt;span style="color:#eceff4"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;print&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;error&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;localizedDescription&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;.&lt;/span&gt;toolbar &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Button &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; exportFile&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;text &lt;span style="color:#eceff4"&gt;=&lt;/span&gt; ticketsExportText&lt;span style="color:#eceff4"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; showingExporter &lt;span style="color:#eceff4"&gt;=&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; label&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Image&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;systemName&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;square.and.arrow.up&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;.&lt;/span&gt;padding&lt;span style="color:#eceff4"&gt;(.&lt;/span&gt;horizontal&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>Pi Server</title><link>https://blog.iankulin.com/pi-server/</link><pubDate>Sun, 04 Dec 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/pi-server/</guid><description>&lt;p&gt;I have a a couple of Raspberry Pi&amp;rsquo;s on my home network. One is a radio interface on the &lt;a href="https://www.allstarlink.org/"&gt;AllStar network&lt;/a&gt;, and the other is just a toy server - I can&amp;rsquo;t actually recall why I bought it. Both of them are Model 3B&amp;rsquo;s - I&amp;rsquo;d love a 4, but they are scarce and expensive.&lt;/p&gt;
&lt;p&gt;This doesn&amp;rsquo;t have much to do with Swift, although it&amp;rsquo;s possible to run &lt;a href="https://lickability.com/blog/swift-on-raspberry-pi/"&gt;Swift on a Pi&lt;/a&gt;, or even &lt;a href="https://medium.com/@jhheider/installing-vapor-and-swift-on-the-raspberry-pi-45a6c7baef35"&gt;Vapor&lt;/a&gt;. Mine is set up as a generic web server that I use as the back end for my tiny projects. It runs &lt;a href="https://nodejs.org/en/about/"&gt;Node.js&lt;/a&gt;, &lt;a href="https://www.apache.org/"&gt;apache&lt;/a&gt; and &lt;a href="https://www.lighttpd.net/"&gt;lighttpd&lt;/a&gt; webservers, &lt;a href="https://www.php.net/"&gt;PHP&lt;/a&gt;, &lt;a href="https://www.mysql.com/"&gt;MySQL&lt;/a&gt;, &lt;a href="https://www.sqlite.org/index.html"&gt;SQLite&lt;/a&gt;, and, when I get to that stage of my programmming, &lt;a href="https://pimylifeup.com/raspberry-pi-postgresql/"&gt;Postgres&lt;/a&gt;. I could do all that on my MacBook, but it&amp;rsquo;s somehow more fun on the Pi.&lt;/p&gt;
&lt;p&gt;A recent addition is to run &lt;a href="https://pi-hole.net/"&gt;Pi-hole&lt;/a&gt; - a DNS sink to block advertisements on all devices on my network. It was a painless addition, and I enjoy looking at the stats as well as ads being blocked at the network level instead of depending on browser addons.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-12-02-at-7.17.04-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not sure that I&amp;rsquo;ll stick with Raspberry Pi. With the current shortage, it probably makes more sense to repurpose an old laptop, or buy a refurb mini pc which for a similar $400 price tag would be substantially more powerful. One con of that approach is you can spend several weekends getting everything running linux properly, whereas with the Pis that&amp;rsquo;s all done for you. Another is the idle power consumption would be much higher than my slow Pi 3Bs.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-12-02-at-8.02.59-pm-1.jpg" alt=""&gt;&lt;/p&gt;</description></item><item><title>Towards MVVM</title><link>https://blog.iankulin.com/towards-mvvm/</link><pubDate>Sat, 03 Dec 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/towards-mvvm/</guid><description>&lt;p&gt;On one of the more mediocre &lt;a href="https://firesideswift.fireside.fm/96"&gt;episodes of Fireside Swift&lt;/a&gt;, McSwiftface and Zach talk about the &lt;a href="https://en.wikipedia.org/wiki/SOLID"&gt;SOLID principles&lt;/a&gt; of class design, although I don&amp;rsquo;t hold the principles as the article of religious fervour that many interviewers apparently do, they are a useful touchstone for considering class quality. OOP had been in swing (in a commercial way) for a few years by then - I was writing in Delphi and C++. The spaghetti code era was a long way behind us and the idea of separation of responsibilities was well established.&lt;/p&gt;
&lt;p&gt;I have been thinking about architecture a bit anyway - the introduction of Core Data into the &lt;a href="https://www.hackingwithswift.com/100/swiftui"&gt;#100Day&lt;/a&gt; apps I&amp;rsquo;m up to (day 63) means that there&amp;rsquo;s complicated looking code scattered around my views. In the &lt;a href="https://cs193p.sites.stanford.edu/"&gt;cs193p lectures&lt;/a&gt;, MVVM is right near the start, and I made &lt;a href="https://blog.iankulin.com/tags/mvvm/"&gt;some early forays&lt;/a&gt; into it, but so far no talk of architecture in 100Days (although I know it&amp;rsquo;s coming soon.&lt;/p&gt;
&lt;p&gt;I have certainly had the experience before of needing a layer between my apps and the database tech so they can be swapped out, and it&amp;rsquo;s basically a reflex to me to always wrap any commercial external code I&amp;rsquo;m introducing in any reasonable size program to abstract it a bit and make the (likely) task of having to replace it in the future a lot easier.&lt;/p&gt;
&lt;p&gt;Once again, YouTube serves me up a timely video.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/ehV2gp5uVhs?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;

&lt;p&gt;When I watch this, I can see why Paul Hudson might have left it for later in the course. There is a lot of complexity. It&amp;rsquo;s not simple code, it&amp;rsquo;s scalable code. This is good for real life apps, but it does not follow that it&amp;rsquo;s good for learning programming.&lt;/p&gt;</description></item><item><title>Deep Linking</title><link>https://blog.iankulin.com/deep-linking/</link><pubDate>Fri, 02 Dec 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/deep-linking/</guid><description>&lt;p&gt;I was listening to an &lt;a href="https://firesideswift.fireside.fm/100"&gt;old episode of Fireside Swift&lt;/a&gt; today discussing NFC tags. I have a bundle of this tags in a drawer here somewhere - I thought it would be cool to tap one as I came home to turn off the CCTV and some other home automation things. But it turns out my phone (an SE2) has the capability for this, but only inside an app - not just from anywhere, whereas the proper phones can just tap anytime, and if the NFC payload is set up correctly, follow a URL, including by &amp;ldquo;deep linking&amp;rdquo; into an app.&lt;/p&gt;
&lt;p&gt;Many years ago I worked on a project involving &lt;a href="https://www.trovan.com/en/RFID-FAQ/rfid-animals"&gt;injectable NFC animal tags&lt;/a&gt; in emus, and fancied have one in my arm. The standard&amp;rsquo;s moved on since then so probably good I didn&amp;rsquo;t. Even though it seems convenient to tag emu&amp;rsquo;s in their necks this is not a good idea as the tags move around in the fat layer and it can take some searching to find them with the scanner.&lt;/p&gt;
&lt;p&gt;The podcast made me wonder about deep linking, and because the YouTube algorithm reads minds now (and possibly because I&amp;rsquo;ve enjoyed other Swift Arcade videos). I got served up this video this afternoon.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/WmM4ryGcmSg?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;

&lt;p&gt;Unfortunately, that&amp;rsquo;s from UIKit days, for a more up to date look, &lt;a href="https://betterprogramming.pub/scalable-navigation-with-deep-links-in-swiftui-96cea1764994"&gt;this post&lt;/a&gt; by Riccardo Cipolleschi. Is a better bet. I&amp;rsquo;ll come back to it when I get a new phone ;-)&lt;/p&gt;</description></item><item><title>Project Based Learning</title><link>https://blog.iankulin.com/project-based-learning/</link><pubDate>Thu, 01 Dec 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/project-based-learning/</guid><description>&lt;p&gt;A couple of times in conversations on &lt;a href="https://firesideswift.fireside.fm/"&gt;Fireside Swift&lt;/a&gt; and &lt;a href="https://podcasts.apple.com/au/podcast/swift-over-coffee/id1435076502"&gt;Swift Over Coffee&lt;/a&gt; the presenters have talked about the danger of just doing more and more tutorials to learn programming, and the benefit, in contrast, of building your own real app. Although I am very much still benefiting from the 100DaysOfSwiftUI I have been seeing some of the upside of working on a real app in the last day and a half.&lt;/p&gt;
&lt;p&gt;From my search history, I&amp;rsquo;ve learned about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Exporting files&lt;/li&gt;
&lt;li&gt;Regex&lt;/li&gt;
&lt;li&gt;Disabling autocorrect in a search box so the search doesn&amp;rsquo;t re-run incorrectly the second you click out of it&lt;/li&gt;
&lt;li&gt;You can&amp;rsquo;t use .onDelete without a ForEach&lt;/li&gt;
&lt;li&gt;There are good websites for converting CSV to JSON, and they mostly run in the browser which is good if you&amp;rsquo;re using sensitive data&lt;/li&gt;
&lt;li&gt;FileDocument&lt;/li&gt;
&lt;li&gt;URLs to the sandbox&lt;/li&gt;
&lt;li&gt;How UUIDs are really unique (spoiler, there&amp;rsquo;s only a tiny chance they&amp;rsquo;re not)&lt;/li&gt;
&lt;li&gt;The frustratingly large number of ways to format dates&lt;/li&gt;
&lt;li&gt;Sorting a FetchRequest in reverse order&lt;/li&gt;
&lt;li&gt;Change in initial values of pickers in iOS16&lt;/li&gt;
&lt;li&gt;It&amp;rsquo;s possible, but not as easy as I was imagining to add your own data to the Environment&lt;/li&gt;
&lt;li&gt;How to extract parts of strings&lt;/li&gt;
&lt;li&gt;The Array(Set(array)) method of eliminating duplicates - I actually knew, but couldn&amp;rsquo;t remember the details&lt;/li&gt;
&lt;li&gt;Using a toggle switch&lt;/li&gt;
&lt;li&gt;Jumping around in NavigationViews&lt;/li&gt;
&lt;li&gt;Making a bottom tab bar&lt;/li&gt;
&lt;li&gt;Dynamic filtering of a FetchRequest - revisited the &lt;a href="https://www.hackingwithswift.com/books/ios-swiftui/dynamically-filtering-fetchrequest-with-swiftui"&gt;@twostraws method&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Adding a search bar to a list&lt;/li&gt;
&lt;li&gt;Using url response to see why a fetch wasn&amp;rsquo;t working&lt;/li&gt;
&lt;li&gt;Refreshed the trick to convert from snake_case in JSON&lt;/li&gt;
&lt;li&gt;Copying files to a remote SSH&lt;/li&gt;
&lt;li&gt;Generating fake JSON data&lt;/li&gt;
&lt;li&gt;The range of an Int32&lt;/li&gt;
&lt;li&gt;Installing apache on the Pi&lt;/li&gt;
&lt;li&gt;Looked up what the mergeByPropertyObjectTrump rules actually are&lt;/li&gt;
&lt;li&gt;How to get the XCode preview back when you accidentally close it&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I think it&amp;rsquo;s significant that it&amp;rsquo;s a real app. When I&amp;rsquo;m just noodling around making a fun app, if something seems hard when you look into it, there&amp;rsquo;s the temptation to just do something else. In a real app you need to push through. For example I was bogged on having a search bar that live updated the list of results returned from Core Data as the user types. But this was a core part of the user experience required in this app, so I had to push through and learn the answers.&lt;/p&gt;</description></item><item><title>Tickets on Myself</title><link>https://blog.iankulin.com/tickets-on-myself/</link><pubDate>Sun, 27 Nov 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/tickets-on-myself/</guid><description>&lt;p&gt;Way back on Day 47 I wrote a little habit tracking app. It was the challenge at the end of a JSON tutorial, so the persistence is done by writing the JSON to UserDefaults as a string. Basic as it is, it&amp;rsquo;s installed on my phone and I check it a couple of times a day, and haven&amp;rsquo;t missed a day of coding, or the weekly bin day since. It&amp;rsquo;s strangely motivating.&lt;/p&gt;
&lt;p&gt;I feel I&amp;rsquo;ve got enough Core Data skills now to write a small real app. This will be for teachers to record the reward tickets they issue for students. Two entities, Tickets and Students.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/img_2792.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/img_2793.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;School staff need to be able to search for a student, and add a ticket for them. The tickets are for behaviours in particular categories and sub-categories, and can have a text description.&lt;/p&gt;
&lt;p&gt;Eventually these tickets will have to be exportable somehow. Also it would be tedious to enter all the student data manually, so that needs to be importable. For the time being, the categories and sub-categories can just be a list that grows as they are entered, but eventually they should be importable as well. That data would be part of a bundle along with teacher names etc - again, importable somehow.&lt;/p&gt;
&lt;p&gt;All this importing could be from a simple webserver inside the school network for the moment - I learned how to download a JSON file in an earlier tutorial, and can setup apache or similar on my pi here at home for testing. That probably does not scale to a commercial solution, but it gets around having to build a real back end with authentication etc for now.&lt;/p&gt;
&lt;p&gt;Getting the data off is a similar problem. If I&amp;rsquo;m going to use the app for real at my school, it needs to integrate with an existing paper system, so exporting a PDF to my macBook would be lovely, or a text file to iCloud would be a good start.&lt;/p&gt;</description></item><item><title>Something weird 'append</title><link>https://blog.iankulin.com/something-weird-about-append/</link><pubDate>Mon, 21 Nov 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/something-weird-about-append/</guid><description>&lt;p&gt;I&amp;rsquo;m noodling around making sure I understand how Core Data works. Thought I&amp;rsquo;d start with a master/detail app with an array of structs, then replicate it in a Core Data implementation. I&amp;rsquo;m using an array of this struct for my data:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;struct Garden &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; id &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; UUID&lt;span style="color:#eceff4"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; name &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; address &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; plants&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#eceff4"&gt;[&lt;/span&gt;Plant&lt;span style="color:#eceff4"&gt;]&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#eceff4"&gt;[]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And I thought this code to load up some sample data was pretty sweet.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#bf616a"&gt;Button&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Sample Data&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; someGarden &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; Garden&lt;span style="color:#eceff4"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; someGarden&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;White Lodge&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; someGarden&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;address &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;72 Anderson Street, &lt;/span&gt;&lt;span style="color:#ebcb8b"&gt;\n&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;Rothwell QLD 4022&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; gardens&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;append&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;someGarden&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; someGarden&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Gordon Terrace&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; someGarden&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;address &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;95 Learmouth Street&lt;/span&gt;&lt;span style="color:#ebcb8b"&gt;\n&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;Tahara Vic 3301&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; gardens&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;append&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;someGarden&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; someGarden&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Powlett Cottage&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; someGarden&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;address &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;11 Bayfield Street&lt;/span&gt;&lt;span style="color:#ebcb8b"&gt;\n&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;White Beach Tas 7184&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; gardens&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;append&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;someGarden&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; someGarden&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Adams Garden&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; someGarden&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;address &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;71 Swanston Street&lt;/span&gt;&lt;span style="color:#ebcb8b"&gt;\n&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;Kanya Vic 3381&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; gardens&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;append&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;someGarden&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;But, no. This is what happens:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-11-18-at-7.00.49-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The reason I thought this was okay was that structs are value types in Swift. When I pass a struct in a function or method, it&amp;rsquo;s actually a copy at the other end. I could prove this by mutating the array copy and checking the original.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#bf616a"&gt;Button&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Sample Data&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; someGarden &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; Garden&lt;span style="color:#eceff4"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; someGarden&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;White Lodge&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; someGarden&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;address &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;72 Anderson Street, &lt;/span&gt;&lt;span style="color:#ebcb8b"&gt;\n&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;Rothwell QLD 4022&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; gardens&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;append&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;someGarden&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; gardens&lt;span style="color:#eceff4"&gt;[&lt;/span&gt;&lt;span style="color:#b48ead"&gt;0&lt;/span&gt;&lt;span style="color:#eceff4"&gt;]&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Mutated&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;print&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Array: \(gardens[0].name)&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;print&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Local: \(someGarden.name)&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Prints:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Array: Mutated
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Local: White Lodge
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So clearly I do understand structs/reference types correctly. They are different structs. Also, if structs were somehow reference types, they should have all had the data from the last garden, not the first one.&lt;/p&gt;
&lt;p&gt;You&amp;rsquo;d might be thinking (as I did), perhaps the error is in the view code. That&amp;rsquo;s easily checked. I forced the second one by creating a new struct, it works correctly and is in the correct position so clearly the view code is working.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-11-18-at-7.18.59-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The code mutating someGarden is getting run, so it&amp;rsquo;s not like the compiler&amp;rsquo;s eliminated those lines somehow. Also it is getting the number of entries correct - they are just somehow linking back to the first entry.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-11-18-at-7.31.44-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;It seems impossible! This is always the way with good bugs.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m almost tempted to ask for help.&lt;/p&gt;
&lt;p&gt;Before one does that, it&amp;rsquo;s always a good idea to boil things down to the essence of the problem. Playgrounds is an excellent tool for such an endeavor. I just need the simplest version of an array of structs. Here&amp;rsquo;s what I came up with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;import Foundation
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;struct Animal &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; name&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#bf616a"&gt;String&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; animals&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#eceff4"&gt;[&lt;/span&gt;Animal&lt;span style="color:#eceff4"&gt;]&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#eceff4"&gt;[]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; animal1 &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; Animal&lt;span style="color:#eceff4"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;animal1&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Cat&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;animals&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;append&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;animal1&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;animal1&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Dog&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;animals&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;append&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;animal1&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;print&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;animals&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Produces the output:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[Page_Contents.Animal(name: &amp;#34;Cat&amp;#34;), Page_Contents.Animal(name: &amp;#34;Dog&amp;#34;)]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Grrr. It turns out Swift works perfectly.&lt;/p&gt;
&lt;p&gt;In the screenshot above, where I&amp;rsquo;ve broken on line 52, I couldn&amp;rsquo;t actually see how to inspect the gardens array. Perhaps I&amp;rsquo;d be better with the classic print() debugging instead.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[DataDemo.Garden(id: 0725A8FC-D410-4A2C-B925-C34340F25B05, name: &amp;#34;White Lodge&amp;#34;, address: &amp;#34;72 Anderson Street, \nRothwell QLD 4022&amp;#34;, plants: []), DataDemo.Garden(id: CF30FA66-A2F2-44FB-A6E0-A22E93492578, name: &amp;#34;Gordon Terrace&amp;#34;, address: &amp;#34;95 Learmouth Street\nTahara Vic 3301&amp;#34;, plants: []), DataDemo.Garden(id: 0725A8FC-D410-4A2C-B925-C34340F25B05, name: &amp;#34;Powlett Cottage&amp;#34;, address: &amp;#34;11 Bayfield Street\nWhite Beach Tas 7184&amp;#34;, plants: []), DataDemo.Garden(id: 0725A8FC-D410-4A2C-B925-C34340F25B05, name: &amp;#34;Adams Garden&amp;#34;, address: &amp;#34;71 Swanston Street\nKanya Vic 3381&amp;#34;, plants: [])]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Okay, so the bug I&amp;rsquo;ve been trying to fix, is not the bug at all. The array is working exactly as intended, it&amp;rsquo;s my view that&amp;rsquo;s the issue somehow.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-11-18-at-7.18.59-pm-1.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;And it&amp;rsquo;s not just a general problem with the view - it&amp;rsquo;s a problem with the view that goes away if I create a new Garden struct instead of recycling one&amp;hellip; Simultaneously I have these two thoughts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When I create a new struct, it gets a fresh UUID&lt;/li&gt;
&lt;li&gt;What was that console spam message I&amp;rsquo;ve been ignoring? Oh yeah, it was &lt;code&gt;ForEach, UUID, HStack&amp;gt;&amp;gt;: the ID 0725A8FC-D410-4A2C-B925-C34340F25B05 occurs multiple times within the collection, this will give undefined results!&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can guess the rest, my ForEach is using the UUID:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;List {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ForEach(gardens, id: \.id) {garden in
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; HStack {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(garden.name)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Spacer()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(garden.address)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If I fix the ids:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-11-18-at-8.16.17-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;And that&amp;rsquo;s how you waste a hour fixing a bug the framework warned you about in the first five minutes.&lt;/p&gt;</description></item><item><title>Tough Day</title><link>https://blog.iankulin.com/tough-day/</link><pubDate>Sun, 20 Nov 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/tough-day/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/bad-day-at-sea-uspaceken.jpg" alt=""&gt;
&lt;em&gt;&lt;a href="https://www.reddit.com/r/Art/comments/7i7crd/bad_day_at_sea_ii_30_x_40_oil/"&gt;Bad Day at Sea - reddit u/SpaceKen&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Day 61 of &lt;a href="https://www.hackingwithswift.com/100/swiftui/61"&gt;#100DaysOfSwiftUI&lt;/a&gt; is a tough day. It&amp;rsquo;s the first real big test of Core Data understanding, and I&amp;rsquo;m finding I didn&amp;rsquo;t actually understand how the code for Core Data works. For the first time, I&amp;rsquo;m thinking of going back and redoing the days leading up to it.&lt;/p&gt;
&lt;p&gt;To try and get it straight in my mind, here&amp;rsquo;s how I think Core Data works:&lt;/p&gt;
&lt;p&gt;1. it&amp;rsquo;s a way of persisting instances of objects - not a relational database (even though that&amp;rsquo;s what&amp;rsquo;s lying underneath it). I&amp;rsquo;m sure this is part of what is obstructing my thinking. Relational databases were my bread and butter for many years.&lt;/p&gt;
&lt;p&gt;2. Core Data won&amp;rsquo;t just persist any old objects, you need to define these objects in the Data Model. They will be &amp;ldquo;NSManagedObjects&amp;rdquo;&lt;/p&gt;
&lt;p&gt;3. In the Data Model you can specify the &amp;ldquo;CodeGen&amp;rdquo;. If it&amp;rsquo;s left on the default &amp;ldquo;Class Definition&amp;rdquo; a hidden source file you can&amp;rsquo;t access is generated deep in the build file so the objects exist - and therefore you can use them.&lt;/p&gt;
&lt;p&gt;4. If you set CodeGen to &amp;ldquo;Manual/None&amp;rdquo;, it is now your responsibility to create that file. You usually select this, then tell XCode to generate them for you and add them to your project navigator by going in to Editor | Create NSManagedObject subclass. This then allows you to edit those files.&lt;/p&gt;
&lt;p&gt;5. To do anything these objects, you need a managaged object context. I&amp;rsquo;m not real clear on what this is. You create it in the App file, from a data controller that contains an NSPersistantObject. Then you save it to some super global called the environment.&lt;/p&gt;
&lt;p&gt;6. To add objects to the collection, you instantiate them using an initialiser that accepts the managed object context as an argument. Then to really persist them (as opposed to just the memory version) you call save() on the object context.&lt;/p&gt;
&lt;p&gt;7. To access the objects, you build a fetch request access a collection of them.&lt;/p&gt;
&lt;p&gt;8. It gets tricky when there are relationships between the objects.&lt;/p&gt;
&lt;p&gt;My plan is to work through my own example of arrays of items vs a Core Data version of the same thing to ensure I&amp;rsquo;ve got it all straight in my head before coming back to the Day 61 challenge. I don&amp;rsquo;t feel too bad about this - Paul makes a point that its a tough challenge, and I don&amp;rsquo;t want to just &amp;ldquo;get it working&amp;rdquo; and move on with that uneasy feeling I might have code with errors I just haven&amp;rsquo;t discovered yet.&lt;/p&gt;</description></item><item><title>iOS Dev Twitter</title><link>https://blog.iankulin.com/ios-dev-twitter/</link><pubDate>Fri, 18 Nov 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/ios-dev-twitter/</guid><description>&lt;p&gt;One of &lt;a href="https://www.youtube.com/c/SeanAllen/videos"&gt;Sean Allen&amp;rsquo;s&lt;/a&gt; many pieces of excellent advice was to follow a few Swift/iOS dev people on Twitter. I took that advice, and it is now a source of joy to flick through every couple of days and get a feel for what&amp;rsquo;s happening, and to discover new things. It&amp;rsquo;s how I learned &lt;a href="https://iosdevweekly.com/"&gt;iOS Dev Weekly&lt;/a&gt; existed, and discovered &lt;a href="https://designcode.io/instructor/meng"&gt;Meng To&lt;/a&gt;, and put faces/ideas to Swift and iOS people that&amp;rsquo;s I&amp;rsquo;d heard mentioned or interviewed in podcasts such as &lt;a href="https://ericasadun.com/"&gt;Erica Sadun&lt;/a&gt; and &lt;a href="https://sarunw.com/"&gt;Sarun W.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Whether it&amp;rsquo;s illusory or not, it does feel like a community, and a pleasant one. I imagine there are thousands of little corners of Twitter like this, where people feel a sense of belonging based on some shared interest or ideas. No sane person would argue that Twitter has been 100% beneficial to the world, but it has enabled groups like iOS Dev Twitter to form and thrive, and it would be sad to lose that.&lt;/p&gt;</description></item><item><title>FriendFace Feedback</title><link>https://blog.iankulin.com/friendface-feedback/</link><pubDate>Tue, 15 Nov 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/friendface-feedback/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-11-12-at-4.38.24-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;After each app, I use my HackingWithSwift+ membership to view Paul&amp;rsquo;s version of the app as a way to judge my performance. Yesterday&amp;rsquo;s app was &amp;ldquo;&lt;a href="https://www.hackingwithswift.com/guide/ios-swiftui/5/3/challenge"&gt;FriendFace&lt;/a&gt;&amp;rdquo; - download some JSON of a number of people (including their friends) and display it.&lt;/p&gt;
&lt;h4 id="uuid"&gt;UUID&lt;/h4&gt;
&lt;p&gt;In my struct, I&amp;rsquo;d just specified the User.id as a string, Paul uses UUID - this makes no difference to the app as it stands, but is much better if we ever needed to add users.&lt;/p&gt;
&lt;h4 id="example"&gt;Example&lt;/h4&gt;
&lt;p&gt;For my details preview, I created a fake user in the preview. Paul does something slightly nicer - with a static let example = User(&amp;hellip;) as a property of the User struct. Again - not a biggy for this app, but handy if you need to use it for other previews.&lt;/p&gt;
&lt;h4 id="fetchusers"&gt;fetchUsers()&lt;/h4&gt;
&lt;p&gt;I had a test in the .task() to prevent the double loading of data, Paul puts his here inside a guard. I think mine is simpler to read and understand, but Paul&amp;rsquo;s would be better if there was a chance we might call fetchUsers() from other places in the app. If that&amp;rsquo;s likely, this would be the third time out of three differences where Paul is anticipating a bigger app - it might just be a reflex for such an accomplished developer. However, I&amp;rsquo;m going to award this one to me.&lt;/p&gt;
&lt;h4 id="docatch"&gt;do/catch&lt;/h4&gt;
&lt;p&gt;Paul&amp;rsquo;s JSON fetching and decoding is in a single do/catch block - as mine was in my first drafting, but then it didn&amp;rsquo;t work correctly so I changed it. I think we can assume Paul&amp;rsquo;s works - he&amp;rsquo;s a quality coder, and this code has had a lot of eyes on it by now. So this is something I need to understand better.&lt;/p&gt;
&lt;p&gt;Apart from the two items above, the rest of fetchUsers was essentially the same.&lt;/p&gt;
&lt;h4 id="list"&gt;List&lt;/h4&gt;
&lt;p&gt;Paul used little red or green circles to indicate the active status of users in his list - much nicer than my version of having the text. Apart from that, it was a NavigationView iwht a List with a NavigationLink to the detail view, so same same.&lt;/p&gt;
&lt;h4 id="userdetails"&gt;UserDetails&lt;/h4&gt;
&lt;p&gt;Instead of a form, Paul uses a List with a .listStyle of .grouped which probably follows the HIG better.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/BmPPhnIxoHM?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;
</description></item><item><title>Profile Photo Rabbit Hole</title><link>https://blog.iankulin.com/profile-photo-rabbit-hole/</link><pubDate>Sun, 13 Nov 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/profile-photo-rabbit-hole/</guid><description>&lt;p&gt;I&amp;rsquo;m on &lt;a href="https://www.hackingwithswift.com/guide/ios-swiftui/5/3/challenge"&gt;day 60 of #100Days&lt;/a&gt;, and have just wasted most of an evening&amp;rsquo;s coding time going down a rabit hole I didn&amp;rsquo;t need to. The app for this challenge is called &amp;ldquo;FriendFace&amp;rdquo; and is pretty straightforward: download a heap of JSON which is an array of users. Show it in a list that can be clicked through to see the details of that user.&lt;/p&gt;
&lt;p&gt;I did that, and instead of moving onto the next project, decided I&amp;rsquo;d like to show a profile picture of each person. There&amp;rsquo;s no data for that, so I&amp;rsquo;ll use a fake photo. I use the Stable Diffusion AI for many of the pictures in this blog, so I assumed there would be an API for grabbing a fake profile pic from somewhere. It turns out there are several, but they are paid services.&lt;/p&gt;
&lt;p&gt;Never mind, I know &lt;a href="https://unsplash.com/"&gt;unsplash&lt;/a&gt; is a great source of free images, and they definitely have a portrait category. I&amp;rsquo;ll get them from there. Instead of using a REST api, I just wanted to grab them from a URL. After a bit of googling around, it turns out you can just use the url &lt;a href="https://source.unsplash.com/collection/9948714?372"&gt;https://source.unsplash.com/collection/9948714?372&lt;/a&gt; and change the number after the question mark to get a different portrait.&lt;/p&gt;
&lt;p&gt;Now all I need is a way to generate a number 1-1000 such that it&amp;rsquo;s always the same for each individual user. That&amp;rsquo;s basically a hash, and Swift has a hashValue property on it&amp;rsquo;s string. I tried user.name.hashValue % 1_000, but kept getting different pictures. Pulled up a playground and tried some print() debugging, and the hashValue was different each run. It would be the same if I hashed the same string twice in a row in code, but not between runs. A few googles later, I&amp;rsquo;ve learned this is deliberate.&lt;/p&gt;
&lt;p&gt;So I need to write my own hash. This is not cryptography - I can just sum all the ascii values of the characters in the string and modulo them. I&amp;rsquo;m a bit hazy on how to get every character in a Swift string because of the unicode thing, but rmaddy has &lt;a href="https://stackoverflow.com/questions/51606011/4-bit-hash-from-string-in-swift"&gt;this answer&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;extension &lt;span style="color:#bf616a"&gt;String&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; fourBitHash&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; Int &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;self&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;utf8&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;reduce&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;0&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#b48ead"&gt;0&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;+&lt;/span&gt; Int&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#b48ead"&gt;1&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;%&lt;/span&gt; &lt;span style="color:#b48ead"&gt;16&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;let colorIndex &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;John R Smith&amp;#34;&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;fourBitHash
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;print&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;colorIndex&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Perfect. I adapt this into my code as:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;func&lt;/span&gt; simpleHash&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;_ string&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#bf616a"&gt;String&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;-&amp;gt;&lt;/span&gt; Int &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; string&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;utf8&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;reduce&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;0&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#b48ead"&gt;0&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;+&lt;/span&gt; Int&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#b48ead"&gt;1&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;%&lt;/span&gt; &lt;span style="color:#b48ead"&gt;1000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And we&amp;rsquo;re in business, but hang on, I&amp;rsquo;m still getting a different picture by repeatedly going into the view detail for the same user. I try pasting in the url a few times, and sure enough, unsplash are serving up random portraits for the same URL&amp;hellip;.&lt;/p&gt;
&lt;p&gt;Okay, so I need a different source for pics. More googling, and I discover &lt;a href="https://randomuser.me/"&gt;RandomUser.me&lt;/a&gt; They only have 100 profiles for men, and another 100 for women - but this app is only for me, and I&amp;rsquo;ll probably get bored after I&amp;rsquo;ve clicked on three or four so that will be fine. I throw that into my AsyncImage and we&amp;rsquo;re in business.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;AsyncImage(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; url: URL(string: &amp;#34;https://randomuser.me/api/portraits/women/\(simpleHash(user.name)).jpg&amp;#34;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; scale: 3
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;) { image in image
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .resizable()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .scaledToFit()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;} placeholder: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ProgressView()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I&amp;rsquo;d prefer to show the correct gender (I&amp;rsquo;m not being deliberately binary, I&amp;rsquo;d just like to double the number of photos) , and there is no gender field in my data. So, I guess I&amp;rsquo;ll need an API that guesses gender based on a name input based on a giant lookup table or an AI model.&lt;/p&gt;
&lt;p&gt;More &lt;a href="https://stackoverflow.com/questions/1685559/find-the-gender-from-a-name"&gt;googling&lt;/a&gt;, and I find &lt;a href="https://stackoverflow.com/users/1608667/stromgren"&gt;stomgren&amp;rsquo;s&lt;/a&gt; genderize api. An input of:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;[https://api.genderize.io/?name=ian](https://api.genderize.io/?name=ian)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;returns:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;count&amp;#34;: 306685,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;gender&amp;#34;: &amp;#34;male&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;name&amp;#34;: &amp;#34;ian&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;probability&amp;#34;: 1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;or&lt;/p&gt;
&lt;p&gt;&lt;code&gt;https://api.genderize.io/?name=kim&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;count&amp;#34;: 83361,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;gender&amp;#34;: &amp;#34;female&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;name&amp;#34;: &amp;#34;kim&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;probability&amp;#34;: 0.7
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Perfect. So, I can just add this to my code to pull up the picture, or, you know what, I&amp;rsquo;m procrastinating and it&amp;rsquo;s time for bed.&lt;/p&gt;</description></item><item><title>Project 12 Feedback</title><link>https://blog.iankulin.com/project-12-feedback/</link><pubDate>Fri, 11 Nov 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/project-12-feedback/</guid><description>&lt;p&gt;As usual, I watch Paul&amp;rsquo;s solution video, and compare it to mine.&lt;/p&gt;
&lt;h4 id="task-1"&gt;Task 1&lt;/h4&gt;
&lt;p&gt;This was passing in the predicate as a String. I passed the whole thing, but as I figured out along the way, Paul meant just the operator word. He also added some buttons to test it better, which I didn&amp;rsquo;t think of till Task 3 - it would have saved me some simulator runs.&lt;/p&gt;
&lt;h4 id="task-2"&gt;Task 2&lt;/h4&gt;
&lt;p&gt;This was changing to enums. As I mentioned, I am a fan. Meanwhile, my solution was exactly the same as Paul&amp;rsquo;s, down to where he put the enum.&lt;/p&gt;
&lt;h4 id="task-3"&gt;Task 3&lt;/h4&gt;
&lt;p&gt;Another FilteredList argument - this time the sort descriptors. I did get a bit bogged down in this. I followed the build error and was trying to use NSSortDescriptors and tying myself up in knots to get it to work. When I reread the task, I saw Paul&amp;rsquo;s hint to use SortDescriptor&lt;Singer&gt; then it all came together - and we had the same solution.&lt;/p&gt;</description></item><item><title>You Can Take Big Steps When You Feel Safe</title><link>https://blog.iankulin.com/you-can-take-big-steps-when-you-feel-safe/</link><pubDate>Wed, 09 Nov 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/you-can-take-big-steps-when-you-feel-safe/</guid><description>&lt;p&gt;&lt;a href="https://www.deviantart.com/jhonair/art/Forest-of-giantess-604262747"&gt;&lt;img src="https://blog.iankulin.com/images/forest-of-giantess-jhonair.png" alt="" title="Forest-of-giantess By JhonAir"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.hackingwithswift.com/100/swiftui/58"&gt;Day 58&lt;/a&gt; of &lt;a href="https://www.hackingwithswift.com/100/swiftui"&gt;#100Days&lt;/a&gt; feels like complex topics are being dropped in pretty fast. We tackle one:many data relationships and how to set them up in CoreData, using CoreData constraints and setting a merge policy to manage conflicts, and even the underscore to access the actual property inside a wrapped property struct (needed for dynamic filtering in a view).&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve &lt;a href="https://blog.iankulin.com/top-four-reasons-why-twostraws-is-a-good-teacher/"&gt;mentioned before&lt;/a&gt; that I think Paul Hudson is an excellent teacher, and an example of this is that even though this was a day with a lot of challenging material, I&amp;rsquo;m not worried. I followed the discussion and tried the code, and more importantly I&amp;rsquo;m anticipating these new skills will be practiced in the next app, and probably shortly after I&amp;rsquo;ll be writing an app using them.&lt;/p&gt;
&lt;p&gt;When learners feel safe and supported, they are comfortable taking bigger risks. This has the effect of growing their Zone of Proximal Development and allows faster learning.&lt;/p&gt;
&lt;p&gt;Some of the complexity around CoreData relates to it&amp;rsquo;s pre-SwiftUI age - it has a lot of power, and does a lot for the developer but is full of non-intuitive bits. The rest of the complexity is really just related to it&amp;rsquo;s job - any object graph persistence that&amp;rsquo;s going to allow us to think of, and work with, our data as native objects is going to have to expose some of the complexity of what&amp;rsquo;s happening underneath in order to provide the flexibility needed. What&amp;rsquo;s not so evident in this implementation is Swifts progressive disclosure of complexity. It&amp;rsquo;s easy to imagine a modern rewrite of a more Swift-like object persistence framework being less scary.&lt;/p&gt;
&lt;p&gt;Since CoreData is using SQLite underneath, an interesting question is what the same code would look like if you pulled in an SQLite library and handled things manually - to approach the same functionality - ie not refetching when a view is recreated if the data hasn&amp;rsquo;t changed, lazy list building etc. My guess is: a lot more complex.&lt;/p&gt;</description></item><item><title>CoreData and the Preview</title><link>https://blog.iankulin.com/coredata-and-the-preview/</link><pubDate>Fri, 04 Nov 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/coredata-and-the-preview/</guid><description>&lt;p&gt;I&amp;rsquo;ve noticed Paul is inclined to ignore the preview and run his code in the simulator to check its operation. That&amp;rsquo;s valid, but it seems quicker, and reassuring, to see it in the preview as I type.&lt;/p&gt;
&lt;p&gt;This led to a small problem with &lt;a href="https://www.hackingwithswift.com/books/ios-swiftui/how-to-combine-core-data-and-swiftui"&gt;Day 53&lt;/a&gt; that uses CoreData. When I added a student in the preview, it looked like this, and was immediately followed with a crash report.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-10-30-at-11.32.07-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;It ran fine in the simulator, and in the text for Day 53 there was a comment about adding the managed object context to the preview, but without a hint about how.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-10-30-at-11.34.07-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I tried a few things - it felt like I should be able to get it from the @Environment somehow, but ended up using &lt;a href="https://www.hackingwithswift.com/forums/100-days-of-swiftui/day-53-question-how-to-set-up-a-managed-object-context-in-xcode-s-swiftui-previews/16686"&gt;this&lt;/a&gt; solution from fellow HWS student &lt;a href="https://www.hackingwithswift.com/users/Fly0strich"&gt;@Fly0strich&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-10-30-at-11.47.51-am.jpg" alt=""&gt;&lt;/p&gt;</description></item><item><title>Top Four Reasons why @TwoStraws is a Good Teacher</title><link>https://blog.iankulin.com/top-four-reasons-why-twostraws-is-a-good-teacher/</link><pubDate>Tue, 01 Nov 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/top-four-reasons-why-twostraws-is-a-good-teacher/</guid><description>&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2022-10-29-at-1.28.59-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-10-29-at-1.28.59-pm.png" width="241" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4 id="good-questions"&gt;Good Questions&lt;/h4&gt;
&lt;p&gt;At various points in the &lt;a href="https://www.hackingwithswift.com/100/swiftui"&gt;100 Days of SwiftUI&lt;/a&gt; course, you get asked sets of questions to check you&amp;rsquo;ve understood the preceding material. They&amp;rsquo;re usually presented as two different statements, one of which is true, and the other false. It&amp;rsquo;s actually a really good technique - the student feels like they&amp;rsquo;ve got a couple of opportunities to figure it out, plus they are forced to read both statements and think about them. Paul does a similar thing in the Unwrapped app - there, the questions are often presented as &amp;ldquo;Is this valid Swift code&amp;rdquo; and the user needs to scan through it all looking for mistakes. It&amp;rsquo;s checking your understanding, and making you a thoughtful debugger!&lt;/p&gt;
&lt;h4 id="zone-of-proximal-development"&gt;Zone of Proximal Development&lt;/h4&gt;
&lt;p&gt;You know how if something is too easy, it&amp;rsquo;s not engaging? It&amp;rsquo;s why you don&amp;rsquo;t choose Snap when you sit down to play cards. It&amp;rsquo;s so far below your skill level your brain is not interested in it. There&amp;rsquo;s a similar problem at the other end - if I ask you do try something that&amp;rsquo;s so hard for you that you&amp;rsquo;ll never be able to achieve it, you want want to do it again. For good learning to take place, it&amp;rsquo;s important to pitch the difficulty of activities just ever so slightly in advance of what the student can comfortably do. This is the zone where the most learning takes place in the shortest amount of time.&lt;/p&gt;
&lt;h4 id="learning-in-context"&gt;Learning in Context&lt;/h4&gt;
&lt;p&gt;Logically, you could teach iOS development with a semester of pure Swift teaching before you got to your first app. Probably there are courses that do that, but if you want to engage your learners do it with as much real life context and hands-on activity as possible. #100Days is all about this.&lt;/p&gt;
&lt;h4 id="beginner-mind"&gt;Beginner Mind&lt;/h4&gt;
&lt;p&gt;In the videos/lessons, Paul often anticipates how the learner might expect something to work, or how they might tackle a problem, before explaining the problem with that thinking or showing a better way of doing things. This is a great trait of a teacher. Often it&amp;rsquo;s difficult for experts (which Paul undoubtedly is) to recall how things looked to them as they were learning. Anticipating the state of mind of the learner, and moving them from that point is both comforting for the learner, and avoids confusion.&lt;/p&gt;
&lt;h4 id="currency"&gt;Currency&lt;/h4&gt;
&lt;p&gt;As soon as you start googling problems and reading blog posts or StackOverflow answers, it becomes apparent that the rapid development of Swift and SwiftUI has a downside - a lot of the helpful information put out there is out of date. Like everyone, I&amp;rsquo;m amazed at the work Paul puts in to producing his massive amount of content, and then keeping it up to date. If there&amp;rsquo;s a Hacking With Swift result in a search you&amp;rsquo;ve made, that&amp;rsquo;s the one to click on.&lt;/p&gt;</description></item><item><title>Git - make all the commits into a single commit</title><link>https://blog.iankulin.com/git-make-all-the-commits-into-a-single-commit/</link><pubDate>Sat, 29 Oct 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/git-make-all-the-commits-into-a-single-commit/</guid><description>&lt;p&gt;When I&amp;rsquo;m following a tutorial app, I generally pause and type up the code as I go, and make local commits with appropriate messages. This is almost completely unnecessary, but it seems like a good habit and doesn&amp;rsquo;t cost me anything - I just tick the box for creating the git when I start the project, then it&amp;rsquo;s a couple of keystrokes (option-command-C) and I&amp;rsquo;m done.&lt;/p&gt;
&lt;p&gt;Most of the apps have a follow-along portion, then some challenges which involve minor changes to the app. When I get to the challenges I like to throw it up on Github - it&amp;rsquo;s conceivable it could help someone one day, or at the least, I&amp;rsquo;m helping to train &lt;a href="https://github.com/features/copilot"&gt;Microsoft&amp;rsquo;s AI&lt;/a&gt; to write shitty beginner code in exchange for free git server access.&lt;/p&gt;
&lt;p&gt;The early commits I do are no help to anyone, even me by that stage, and it feels somehow untidy to leave them in there, so I started to wonder if there was some branchy/rebasey way to eliminate them before I push it up.&lt;/p&gt;
&lt;p&gt;This and the related problems of just eliminating some of the recent commit history are clearly topics of interest - there&amp;rsquo;s many stackover flow posts and blog articles. But shout out to &lt;a href="https://stackoverflow.com/users/825/pat-notz"&gt;Pat Noz&lt;/a&gt;, for his suggestion - &lt;a href="https://stackoverflow.com/questions/1657017/how-to-squash-all-git-commits-into-one"&gt;just delete the .git directory and start over&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-10-27-at-8.39.02-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;When you &lt;code&gt;git init&lt;/code&gt;, a hidden folder is created in the directory you init in called &lt;code&gt;.git&lt;/code&gt; - you don&amp;rsquo;t normally see these hidden folders, but if you press &lt;code&gt;command-shift-.&lt;/code&gt; you can see it. This directory holds all the data that allows the magic of git to happen. If you delete just this directory and it&amp;rsquo;s contents, it&amp;rsquo;s like you never used git on this code.&lt;/p&gt;
&lt;p&gt;So in Pat&amp;rsquo;s words:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rm -rf .git
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git init
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git add .
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git commit
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Simple. Elegant. Obvious once you&amp;rsquo;ve had the suggestion.&lt;/p&gt;</description></item><item><title>Day 47 - Habits App</title><link>https://blog.iankulin.com/day-47-habits-app/</link><pubDate>Thu, 27 Oct 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/day-47-habits-app/</guid><description>&lt;p&gt;I&amp;rsquo;ve been mucking around with the Habits app too long - it&amp;rsquo;s started to look like procrastination. It already meets the &lt;a href="https://www.hackingwithswift.com/100/swiftui/47"&gt;specification&lt;/a&gt;, so I&amp;rsquo;m calling it an MVP and moving on.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/IanKulin/Habitual"&gt;&lt;img src="https://blog.iankulin.com/images/github-mark-32px.png" width="32" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/img_3110.png" width="266" alt=""&gt;
&lt;p&gt;This is the first app of mine I&amp;rsquo;ve loaded onto my phone and started using, and there are a couple of things I&amp;rsquo;d like to do with it. It currently just lets you specify how many days between an activity repeating - so if you say you should go to the gym every second day, and you complete that activity on Monday, &amp;ldquo;Gym&amp;rdquo; will make it&amp;rsquo;s way to the top of the list on Wednesday. While it&amp;rsquo;s waiting in the list for Wednesday to come around, it will show the &amp;ldquo;Due&amp;rdquo; time as being exactly 48 hours after you last pressed &amp;ldquo;done&amp;rdquo; on it. But if the habit you want is to go to the gym after work at 6:00pm that&amp;rsquo;s when you want it to be due. I&amp;rsquo;d like that.&lt;/p&gt;
&lt;p&gt;This does overlap a bit with the idea of a &amp;ldquo;ToDo&amp;rdquo; list, so maybe it&amp;rsquo;s a feature creep it shouldn&amp;rsquo;t have. Espeically since the reason I wanted it was because in the process of adding test activities I added &amp;ldquo;Take out the bins&amp;rdquo; and that has to be done on Wednesday nights.&lt;/p&gt;
&lt;p&gt;Paul&amp;rsquo; has moved away from all the data being in an @State in the View to having a data model object in a separate file. It feels like only a few steps until he jumps out from behind a bush and says &amp;ldquo;You&amp;rsquo;ve been writting MVVM already!&amp;rdquo; although currently its a bit more like just MV.&lt;/p&gt;
&lt;p&gt;The list of things I&amp;rsquo;m going to come back to is growing - I also have still not finished writing the custom picker so I can match the design work I paid for on the &lt;a href="https://blog.iankulin.com/design-help/"&gt;times table app&lt;/a&gt;. Nevertheless, I&amp;rsquo;ve so far spent 120 days on the &lt;a href="https://www.hackingwithswift.com/100/swiftui"&gt;#100DaysOfSwiftUI&lt;/a&gt; and I&amp;rsquo;m only up to day 47, so time to get moving.&lt;/p&gt;</description></item><item><title>Updating stored JSON due to a struct change</title><link>https://blog.iankulin.com/updating-stored-json-due-to-a-struct-change/</link><pubDate>Tue, 25 Oct 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/updating-stored-json-due-to-a-struct-change/</guid><description>&lt;p&gt;I mentioned yesterday &amp;ldquo;&lt;em&gt;I could use a renamed old version of my struct to load the existing data, and convert it across to the new model.&lt;/em&gt;&amp;rdquo;. Since I&amp;rsquo;ve been testing the app on my phone, and using plausible data, it was going to be painful enough to lose it that I thought I should go through those steps.&lt;/p&gt;
&lt;p&gt;First, I make a copy of the old struct, and renamed it with the app version number that used it. No need to bring all the computed properties into this struct, just the bits that get encoded into the JSON.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;struct&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;V01HabitItem&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; Identifiable&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; Codable&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;Equatable&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; id &lt;span style="color:#eceff4"&gt;=&lt;/span&gt; UUID&lt;span style="color:#eceff4"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;let&lt;/span&gt; name&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;String&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; started &lt;span style="color:#eceff4"&gt;=&lt;/span&gt; Date&lt;span style="color:#eceff4"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; timesDone &lt;span style="color:#eceff4"&gt;=&lt;/span&gt; &lt;span style="color:#b48ead"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; lastDone&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; Date
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; daysBetweenCompletions &lt;span style="color:#eceff4"&gt;=&lt;/span&gt; &lt;span style="color:#b48ead"&gt;1.0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then I could go ahead and change the official struct to add the new properties it needs. Now when the JSON is attempted to be decoded into the new struct, it will fail, so we need to detect that and try with the old version of the struct.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;class&lt;/span&gt; &lt;span style="color:#8fbcbb"&gt;Habits&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; ObservableObject &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;@&lt;/span&gt;Published &lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; items &lt;span style="color:#eceff4"&gt;=&lt;/span&gt; &lt;span style="color:#eceff4"&gt;[&lt;/span&gt;HabitItem&lt;span style="color:#eceff4"&gt;]()&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;didSet&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;let&lt;/span&gt; encoded &lt;span style="color:#eceff4"&gt;=&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;try&lt;/span&gt;&lt;span style="color:#eceff4"&gt;?&lt;/span&gt; JSONEncoder&lt;span style="color:#eceff4"&gt;().&lt;/span&gt;encode&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;items&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; UserDefaults&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;standard&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;set&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;encoded&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; forKey&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Habits&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;print&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;JSON encoding fail&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;init&lt;/span&gt;&lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;let&lt;/span&gt; savedItems &lt;span style="color:#eceff4"&gt;=&lt;/span&gt; UserDefaults&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;standard&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;data&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;forKey&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Habits&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;let&lt;/span&gt; decodedItems &lt;span style="color:#eceff4"&gt;=&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;try&lt;/span&gt;&lt;span style="color:#eceff4"&gt;?&lt;/span&gt; JSONDecoder&lt;span style="color:#eceff4"&gt;().&lt;/span&gt;decode&lt;span style="color:#eceff4"&gt;([&lt;/span&gt;HabitItem&lt;span style="color:#eceff4"&gt;].&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;self&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; from&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; savedItems&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; items &lt;span style="color:#eceff4"&gt;=&lt;/span&gt; decodedItems
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;print&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;JSON decoding fail - trying v0.1&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; v01Items &lt;span style="color:#eceff4"&gt;=&lt;/span&gt; &lt;span style="color:#eceff4"&gt;[&lt;/span&gt;V01HabitItem&lt;span style="color:#eceff4"&gt;]()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;let&lt;/span&gt; decodedItems &lt;span style="color:#eceff4"&gt;=&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;try&lt;/span&gt;&lt;span style="color:#eceff4"&gt;?&lt;/span&gt; JSONDecoder&lt;span style="color:#eceff4"&gt;().&lt;/span&gt;decode&lt;span style="color:#eceff4"&gt;([&lt;/span&gt;V01HabitItem&lt;span style="color:#eceff4"&gt;].&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;self&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; from&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; savedItems&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; v01Items &lt;span style="color:#eceff4"&gt;=&lt;/span&gt; decodedItems
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; v01Items&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;forEach &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; oldHabit &lt;span style="color:#81a1c1;font-weight:bold"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; items&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;append&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;HabitItem&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; id&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; oldHabit&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;id&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; name&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; oldHabit&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; started&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; oldHabit&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;started&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; timesDone&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; oldHabit&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;timesDone&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; lastDone&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; oldHabit&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;lastDone&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; daysBetweenCompletions&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; oldHabit&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;daysBetweenCompletions
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;print&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;JSON decoding fail&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; items &lt;span style="color:#eceff4"&gt;=&lt;/span&gt; &lt;span style="color:#eceff4"&gt;[]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Once it&amp;rsquo;s decoded, we need to ForEach through and create instances of the new struct from the old ones.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s made my init() a bit messy, normally I&amp;rsquo;d move that out - maybe have a separate file with the old struct and a function for decoding it and copying into the new struct, but this is not a production app, it&amp;rsquo;s only on my phone and on two simulations on my macBook, so this code won&amp;rsquo;t stay in once I&amp;rsquo;ve updated each instance by running it once.&lt;/p&gt;
&lt;p&gt;If it works correctly, I should get the debug message and the data will still be there on the first run, then on a second run the message should not appear (since the new struct version will have been written to the file.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-10-22-at-7.41.44-pm.jpg" alt=""&gt;&lt;/p&gt;</description></item><item><title>List Apps</title><link>https://blog.iankulin.com/list-apps/</link><pubDate>Thu, 20 Oct 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/list-apps/</guid><description>&lt;p&gt;When I was first programming professionally, it wasn&amp;rsquo;t long before I noticed that there were patterns to the sort of bread-and-butter things I was writing most times - the majority of the small business applications I wrote tracked several entities; for each entity there needed to be add/edit/delete screens, there would be some business rules around those things, and some reports and search functionality.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://www.hackingwithswift.com/guide/ios-swiftui/4/3/challenge"&gt;Day 47 Milestone app&lt;/a&gt; is for tracking habits - presented in a list; you need to be able to add and delete them, view details and do some business logic on them. Not only does this sound a lot like the earlier expense tracking app, but also not that different to the app idea I&amp;rsquo;ve got for tracking when I charge each of the rechargeable batteries in my house.&lt;/p&gt;
&lt;p&gt;Eventually these list type apps written in SwiftUI will need a better (perhaps MVVM) architecture, but at the simple end, it seems these apps are going to be some variation on this recipe:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The item is going to be a struct with an id&lt;/li&gt;
&lt;li&gt;It will conform to Identifiable and Codable&lt;/li&gt;
&lt;li&gt;The collection of items will be a class that inherits from ObservableObject&lt;/li&gt;
&lt;li&gt;The collection property will be @Published&lt;/li&gt;
&lt;li&gt;The collection property will have an init() that reads the items from somewhere and a didset() that writes them&lt;/li&gt;
&lt;li&gt;The instance of that collection class will be an @StateObject&lt;/li&gt;
&lt;li&gt;The items will be shown as a List inside a NavigationView&lt;/li&gt;
&lt;li&gt;It will probably have a toolbar with a control to add items&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Patterns in development like this are convenient - they lead to high quality programs developed quickly, and being compliant with the &lt;a href="https://developer.apple.com/design/human-interface-guidelines/guidelines/overview/"&gt;Apple HIG&lt;/a&gt; means all the behaviours will be familiar to users as well as being accessible.&lt;/p&gt;
&lt;p&gt;The danger is that they look and feel like every other list type app, so developers need to think carefully about what all the common use cases are, as well as appealing design, to ensure the users&amp;rsquo; needs are being addressed and the application can justify it&amp;rsquo;s existence alongside all the other offerings.&lt;/p&gt;</description></item><item><title>When it Works</title><link>https://blog.iankulin.com/when-it-works/</link><pubDate>Mon, 17 Oct 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/when-it-works/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-10-15-at-10.43.18-am.jpg" alt="Screenshot of swiftui code and the iphone simulator with a roughly drawn face"&gt;&lt;/p&gt;
&lt;p&gt;The little joy of something working. It&amp;rsquo;s one of the things that makes coding enjoyable. Like a good video game you have an overarching goal, but on the way you need to solve a large number of problems of variable complexity, and you get a little bit of dopamine for each one.&lt;/p&gt;
&lt;p&gt;I think in every language I&amp;rsquo;ve ever learned, as soon as I know how to draw something on the screen, I start to get the urge to create a simple drawing application. When I was starting on Visual C++ and the MFC (Microsoft Foundation Classes) the &lt;a href="https://www.amazon.com/Beginning-Visual-C-Ivor-Horton/dp/1861000081"&gt;book&lt;/a&gt; I used to get started built a drawing application as an example. It hooks into the benefit of being able to quickly see the evidence you&amp;rsquo;ve achieved something.&lt;/p&gt;</description></item><item><title>Musings on Data</title><link>https://blog.iankulin.com/musings-on-data/</link><pubDate>Sun, 16 Oct 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/musings-on-data/</guid><description>&lt;p&gt;I&amp;rsquo;ve been feeling my enthusiasm for the online courses I started waning a little, additionally, I have enough progress under my belt that I could actually start working on some of the projects in my &amp;ldquo;App Ideas&amp;rdquo; notebook. I&amp;rsquo;m not sure if starting on them is a smart idea, or just a way of procrastinating. One thing most of those apps have in common, and that I haven&amp;rsquo;t substantively learned yet is persisting data. Their data requirements vary from a sort of specialised todo list only required on that device, to relational data that needs to be live synced across devices including macOS and needs to support rolling back transactions to resolve conflicts.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://blog.iankulin.com/iexpense-challenges/"&gt;iExpense&lt;/a&gt; app from the &lt;a href="https://www.hackingwithswift.com/books/ios-swiftui/archiving-swift-objects-with-codable"&gt;100 Days course&lt;/a&gt; included encoding data to JSON then saving it in UserDefaults, but that&amp;rsquo;s the extent of my data expertise so far. And, that would be fine for a prototype version of a couple of the app ideas for now.&lt;/p&gt;
&lt;p&gt;I know these things exist:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Core Data (and, I imagine CloudKit for saving the SQLite in iCloud)&lt;/li&gt;
&lt;li&gt;FireBase - the Google database engine that seems popular for serious apps that need to share data&lt;/li&gt;
&lt;li&gt;It&amp;rsquo;s possible to just save files in the apps sandbox (which I don&amp;rsquo;t know how to do, but sounds a bit more legit that using UserDefaults)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In programming terms, I grew up on large relational databases - on disk. RAM used to be much more precious and capricious, and saving a file much slower - so I have some thinking adjustments to do. For my Todo type app that&amp;rsquo;s not likely to have more than 100 records, it&amp;rsquo;s probably legit to just encode the whole thing to JSON and write it to a file for every add/edit/delete.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not sure what I&amp;rsquo;ll do - the logical thing is to buckle down and work through the 100 days until I get to databases in the tutorials, but if working on apps is more engaging and leads to more programming that could be better. My jobby-job was particularly demanding this week, so perhaps the weekend will fix this problem for me.&lt;/p&gt;</description></item><item><title>Using AI to Generate Icons</title><link>https://blog.iankulin.com/using-ai-to-generate-icons/</link><pubDate>Tue, 11 Oct 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/using-ai-to-generate-icons/</guid><description>&lt;p&gt;Since I have minimal design skills, I went back to &lt;a href="https://www.fiverr.com/"&gt;Fiverr&lt;/a&gt; (the digital gig economy platform) to get some icons done for CodeTrimmer - explaining that I wanted something like a &amp;ldquo;pair of scissors floating over some computer code&amp;rdquo;. At the same time I&amp;rsquo;ve been playing with &lt;a href="https://github.com/divamgupta/diffusionbee-stable-diffusion-ui"&gt;DiffusionBee&lt;/a&gt; - a free Apple silicon version of the &lt;a href="https://stability.ai/blog/stable-diffusion-public-release"&gt;Stable Diffusion&lt;/a&gt; artifical intellligence that generates images from text prompts. The image above was created on an M1 Macbook using DiffusionBee.&lt;/p&gt;
&lt;p&gt;Since generating an image from a text prompt is exactly what I&amp;rsquo;d just done with Fiverr, I thought I&amp;rsquo;d give the AI a chance. Even if the results are not great, they will do for the toy applications I&amp;rsquo;m creating as I&amp;rsquo;m learning.&lt;/p&gt;
&lt;p&gt;The model in Stable Diffusion is trained on images scraped from the internet, so it&amp;rsquo;s strong on pop culture and painting styles. For instance &amp;ldquo;Jack Black as Aquaman&amp;rdquo; is likely to give good results, also &amp;ldquo;Kermit dressed as Yoda in front of a sunset landscape&amp;rdquo;. The license for the outputs of the AI is quite permissive, including for commercial purposes as long as it&amp;rsquo;s not used for evil (that&amp;rsquo;s my paraphrasing, the actual &lt;em&gt;CreativeML Open RAIL-M&lt;/em&gt; license is &lt;a href="https://github.com/CompVis/stable-diffusion/blob/main/LICENSE"&gt;here&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s a couple of outputs for the different versions of the &amp;ldquo;scissors floating over code, macOS, icon&amp;rdquo; prompt.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-10-05-at-10.56.41-am.jpg" alt="screen cap of stable diffusion output"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-10-05-at-10.35.25-am.jpg" alt="screen cap of stable diffusion output"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/kermit-dressed-as-yoda-in-front-of-a-sunset-landscape.jpg" alt="Kermit dressed as Yoda - Stable Diffusion"&gt;&lt;/p&gt;
&lt;p&gt;Like any tool, there is a skill to using it well, and with image generation from text prompts, the skill is in creating the prompts, hence a sudden plethora of articles about this all over the internet, and even a &lt;a href="https://promptbase.com/"&gt;market&lt;/a&gt; for them. So perhaps with a bit more skill I&amp;rsquo;d get something closer to what I&amp;rsquo;d like, although if you are prepared to accept something a bit abstract a couple of these would be fine.&lt;/p&gt;
&lt;p&gt;If I had a small amount of design skills these images might serve as a great creative thinking starting point - for example I like the criss-crossy background of one to represent code. I like the drop shadow and flat green of the ipod shuffle one - so these ideas could be combined to make a pair of flat green handled scissors floating over the lined background.&lt;/p&gt;
&lt;p&gt;If you are not allowed to use AI&amp;rsquo;s for evil (I&amp;rsquo;m looking at you &lt;a href="https://en.wikipedia.org/wiki/Skynet_(Terminator)"&gt;Skynet&lt;/a&gt;), it seems like using them as a tool for creating okay images for apps and other digital content is likely to become a practical use for them.&lt;/p&gt;</description></item><item><title>Color Picker (website)</title><link>https://blog.iankulin.com/color-picker-website/</link><pubDate>Tue, 04 Oct 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/color-picker-website/</guid><description>&lt;p&gt;I&amp;rsquo;ve started work on trying to recreate a &lt;a href="https://blog.iankulin.com/design-help/"&gt;UI provided by a designer&lt;/a&gt;, and in the process needed to identify some colours from a PNG image. I found this great website for this exact purpose.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-30-at-4.36.17-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The site is ImageColorPicker. To use it, you &amp;ldquo;upload&amp;rdquo; your image (actually the image is not going anywhere - it&amp;rsquo;s all done in-browser). Then click on any area you want to identify the colour of.&lt;/p&gt;
&lt;p&gt;It gives the RGB values out of 255, so I divide each one by 255 to get the CGFloat values that the SwiftUI Color() type will take - for example I used the colour above as &lt;code&gt;Color(red: 0.89, green: 0.96, blue: 1.0)&lt;/code&gt;&lt;/p&gt;</description></item><item><title>Design Help</title><link>https://blog.iankulin.com/design-help/</link><pubDate>Mon, 03 Oct 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/design-help/</guid><description>&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2022-09-29-at-5.50.43-pm-1.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-29-at-5.50.43-pm-1.png" width="253" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I think I mentioned when I&amp;rsquo;d completed the &lt;a href="https://blog.iankulin.com/times-tables-day-35-challenge/"&gt;TimesTable app&lt;/a&gt;, that I was not happy with the design. It&amp;rsquo;s ugly, I don&amp;rsquo;t like the way the feedback about a wrong answer is shown at the same time as the next question, the number of questions setting would be rarely changed and looks uncomfortable where it is, I&amp;rsquo;m not sure the purpose of the picker for which times table would be clear, and it&amp;rsquo;s not appealing to children.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve used &lt;a href="https://www.fiverr.com/search/gigs?query=ux%20design&amp;amp;source=sorting_by&amp;amp;ref_ctx_id=ea3c1ec986bfad92b64b621e1b254729&amp;amp;search_in=everywhere&amp;amp;search-autocomplete-original-term=ux%20design&amp;amp;filter=new"&gt;Fiverr&lt;/a&gt; before to have various bits of graphic design done, so I thought I&amp;rsquo;d have a look, and sure enough there is a heap of people offering UI &amp;amp; UX design. Prices vary widely - from about $10 to $2000AUD. The lower end ones are generally newer entrants. There was a suspicious number of incredibly beautiful looking Ukrainian women wanting UX design gigs - I assumed they were fake and picked a couple of others.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the results so far (one guy is still making changes).&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/main-screen-copy.jpg" width="473" alt=""&gt;
&lt;p&gt;This one combined the number input with the question text - that&amp;rsquo;s smart. To change the number of questions you click on the circle at the top, then it shows you which question you are up to, plus the circle edge is indicating your progress. I love all of that, but do not like having &amp;ldquo;Question&amp;rdquo; written there. There&amp;rsquo;s no solution offered for showing you&amp;rsquo;ve got the question correct or wrong, but maybe I could do that in the sum area (with an animated tick, or a crossing out and re-writing). The picker for which table to practice is pretty similar in function. I didn&amp;rsquo;t like the backspace button - but the designer pushed back saying it met the brief of appealing to kids. I love the layered rounded rectangles, and the inset for the question progress. I&amp;rsquo;m not so keen on having &amp;ldquo;Times Tables&amp;rdquo; written at the top, ot the line underneath it. In my version, the title is animated a little to indicated right/wrong answers so perhaps that&amp;rsquo;s why this designer kept it.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/frame-36-copy.png" width="444" alt=""&gt;
&lt;p&gt;This designer&amp;rsquo;s first draft was black and white, so apart from moving the backspace (which I like) it was just a less ugly version of my UI. When I asked about how this was appealing to kids, he hit on these colours, which I think are great. The non-centred number of questions is driving me mental, but the designer is currently working on shrinking that somehow and moving it up to the light bulb in the top right. His idea for that was that it would give the user a hint when they clicked on it. That seemed like a change in functionality rather than a UX improvement to me. It turned out he was also a budding mobile developer, so he&amp;rsquo;d actually built the entire app - I&amp;rsquo;m not sure if in Flutter or Swift - either way, it looks like I could actually build it with my current skills (although perhaps not that picker). Unlike the first design with the inset and overlapping shapes that&amp;rsquo;s going to require some googling if it&amp;rsquo;s possible at all.&lt;/p&gt;
&lt;p&gt;So far, this has been a good experiment. Both designers quickly came up with something much prettier than I had, and although neither of them look perfect to me, now that I&amp;rsquo;ve seen them I have ideas to improve them.&lt;/p&gt;
&lt;p&gt;Now, the interesting challenge is to see how close I can get to implementing them!&lt;/p&gt;</description></item><item><title>Calculator</title><link>https://blog.iankulin.com/calculator/</link><pubDate>Thu, 29 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/calculator/</guid><description>&lt;p&gt;The app I&amp;rsquo;m working on currently (for multiplication tables practice) has a number type keypad and display a bit like a calculator - but for entering the answers. It&amp;rsquo;s been quite fun to think through all the little problems to make it work how you&amp;rsquo;d expect, so I was quite interested to watch an iOS Academy video where Afraz Siddiqui builds a partially finished SwiftUI version of the iOS Calculator app.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=cMde7jhQlZI"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-25-at-2.42.34-pm.jpg" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;It is a great example for beginners to see the power of building views quickly in SwiftUI, but the most fun part for me was thinking of all the edge cases that would hadn&amp;rsquo;t been dealt with in his quick build out of the logic (which wasn&amp;rsquo;t the point of the video).&lt;/p&gt;
&lt;p&gt;A couple of times I&amp;rsquo;ve thought about building copies of existing Apple iOS apps. I think part of the appeal is that the design work is done for me, so I&amp;rsquo;m just thinking about under the hood. Long term, I need to know more about design and to be better at it - I bet there&amp;rsquo;s a &lt;a href="https://designcode.io/ui-design"&gt;Meng To&lt;/a&gt; course that would be a good starting point.&lt;/p&gt;</description></item><item><title>Gitting up to date</title><link>https://blog.iankulin.com/gitting-up-to-date/</link><pubDate>Tue, 27 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/gitting-up-to-date/</guid><description>&lt;p&gt;I&amp;rsquo;ve started the habit of branching my code for each feature or batches of features. This is not really needed, I&amp;rsquo;ve developing solo, and the code on &lt;code&gt;main&lt;/code&gt; is not in production. I could just go on committing, but part of my process is about becoming competent with git.&lt;/p&gt;
&lt;p&gt;There are a couple of git commands (&lt;code&gt;merge&lt;/code&gt; and &lt;code&gt;rebase&lt;/code&gt;) that mush code between branches together in different ways. The video below (from &lt;a href="https://www.udemy.com/user/manuel-lorenz/"&gt;Manuel Lorenz&lt;/a&gt; at &lt;a href="https://academind.com/"&gt;Academind&lt;/a&gt;) is a particularly clear look at these two commands.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/CRlGDDprdOQ?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;

&lt;p&gt;The discussion that follows is essentially just a re-hash of the video above, so if you think you&amp;rsquo;ve got it, you can leave now!&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the situation we start with. A &amp;ldquo;feature&amp;rdquo; branch was created at the point in time that the most recent commit on the master branch was &amp;ldquo;m2&amp;rdquo;. A couple of commits have been made to the feature branch, but meanwhile a further commit has been made on the master branch:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-24-at-7.00.08-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;So, what are the options now? (this is just a rehash of the video above in my words, so if you already watched that, you may leave :- ) I&amp;rsquo;m going to use &amp;ldquo;master&amp;rdquo; in my discussions here since that&amp;rsquo;s what Manuel uses, but if you&amp;rsquo;re new to git and you&amp;rsquo;ve only ever seen &amp;ldquo;main&amp;rdquo; - they are just different names for the default branch, &amp;ldquo;main&amp;rdquo; is the current (and slightly better) preference for that name.&lt;/p&gt;
&lt;h3 id="merge"&gt;Merge&lt;/h3&gt;
&lt;p&gt;We could checkout the master branch, and merge feature into it with&lt;/p&gt;
&lt;p&gt;&lt;code&gt;git merge feature&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;That works, and our commit history for the master branch will look like this:&lt;/p&gt;
&lt;p&gt;M1 - M2 - F1 - F2 - M3 - Merge&lt;/p&gt;
&lt;h3 id="squash"&gt;Squash&lt;/h3&gt;
&lt;p&gt;To bundle up the feature branch and bring it across to master, we can (from the master branch) do:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;git merge --squash feature&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;That add all the feature changes to the current branch, but they&amp;rsquo;re not committed yet, so we&amp;rsquo;ll also:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;git commit -m &amp;quot;add feature&amp;quot;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The history of the master branch now will be:&lt;/p&gt;
&lt;p&gt;M1 - M2 - M3 - add feature&lt;/p&gt;
&lt;p&gt;So this is sort of neater, instead of a chronological history in the commits, we&amp;rsquo;re conceptually saying the feature work was all done in a single commit after the M3 change. But&amp;hellip; what if we wanted to keep the feature commit history?&lt;/p&gt;
&lt;h3 id="rebase"&gt;Rebase&lt;/h3&gt;
&lt;p&gt;Just to recap, although our master branch is up to m3, the feature branch was &lt;em&gt;based&lt;/em&gt; off m2. So if we had a look at the history of the feature branch (using git log) it looks like this:&lt;/p&gt;
&lt;p&gt;M2 - F1 - F2&lt;/p&gt;
&lt;p&gt;Rebasing it will look at the changes in feature then apply them to the current m3 commit in master. If we change to the feature branch and enter:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;git rebase master&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Now the feature history will be&lt;/p&gt;
&lt;p&gt;M1 - M2 - M3 - F1 - F2&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s sort of cool and all, basically the code in the feature branch is in the state that master would be after we&amp;rsquo;ve joined them back up, so we can go ahead and test and so on. But we haven&amp;rsquo;t actually joined up feature and master yet. To do that, we could to checkout the master branch, and rebase it from feature with:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;git rebase feature&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Then master will have the same history as feature: M1 - M2 - M3 - F1 - F2 That can just be committed and we&amp;rsquo;re good to go. Since the whole commit history is now in the master branch its fine to go ahead and delete the feature branch.&lt;/p&gt;
&lt;h3 id="gotcha"&gt;Gotcha&lt;/h3&gt;
&lt;p&gt;If you&amp;rsquo;re working in a team around a shared repository, it makes a lot of sense to rebase your local project from the current main/master in the shared repo. That way you can test for any problems your code might have with the current version, but, it&amp;rsquo;s bad form to rebase any commits that get pushed up. A good explanation for why this is can be found in the &lt;a href="https://git-scm.com/book/en/v2/Git-Branching-Rebasing"&gt;git docs&lt;/a&gt; - scroll down to &amp;ldquo;the perils of rebasing&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Instead what we should have done in the example above if we were planning on pushing the master would have been to rebase our feature branch as we did, but then change to the master branch and merge the feature branch in with&lt;/p&gt;
&lt;p&gt;&lt;code&gt;git merge feature&lt;/code&gt;&lt;/p&gt;</description></item><item><title>iOS Academy</title><link>https://blog.iankulin.com/ios-academy-2/</link><pubDate>Mon, 26 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/ios-academy-2/</guid><description>&lt;p&gt;A YouTube channel worth subscribing to is &lt;a href="https://www.linkedin.com/in/afrazsiddiqui"&gt;Afraz Siddiqui&amp;rsquo;s&lt;/a&gt; &lt;a href="https://www.youtube.com/c/iOSAcademy/videos"&gt;iOS Academy&lt;/a&gt;. He does a great videos on iOS development. My favouriets might be the shorter focussed ones, like this one on the new SwiftUI chart views.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/KVz_I10R-wA?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;
</description></item><item><title>Animation Feedback</title><link>https://blog.iankulin.com/animation-feedback/</link><pubDate>Sat, 24 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/animation-feedback/</guid><description>&lt;p&gt;I had a look at Paul&amp;rsquo;s version of the challenge to animate the Guess The Flags app, and like me he&amp;rsquo;d hit on altering the &lt;em&gt;amount&lt;/em&gt; of each animation depending on if it was the clicked on flag, but he&amp;rsquo;d done it with a lot less code - using the ternary operator in each of the modifiers - versus my approach of filling in an Int array for each of the effects and altering them depending on the user selection.&lt;/p&gt;
&lt;p&gt;Another round to Paul!&lt;/p&gt;</description></item><item><title>Gists for embedding code</title><link>https://blog.iankulin.com/gists-for-embedding-code/</link><pubDate>Fri, 23 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/gists-for-embedding-code/</guid><description>&lt;p&gt;So, I might have found a slightly better method for sharing code in posts that I complained about the &lt;a href="https://blog.iankulin.com/wordpress-code-blocks/"&gt;other day&lt;/a&gt;. GitHub has a thing called &lt;a href="https://gist.github.com/"&gt;Gists&lt;/a&gt;. It&amp;rsquo;s like a tiny repository you can paste a code snippet into (or upload a source file). Once that&amp;rsquo;s done, you can just paste the URL of the Gist into &lt;a href="https://wordpress.com/support/gist/"&gt;Wordpress&lt;/a&gt; - it recognises it and does this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ForEach&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;0.&lt;/span&gt;&lt;span style="color:#eceff4"&gt;.&amp;lt;&lt;/span&gt;&lt;span style="color:#b48ead"&gt;3&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; number &lt;span style="color:#81a1c1;font-weight:bold"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Button &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#616e87;font-style:italic"&gt;// flag was tapped&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; flagTapped&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;number&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; label&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; FlagView&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;flagOf&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; countries&lt;span style="color:#eceff4"&gt;[&lt;/span&gt;number&lt;span style="color:#eceff4"&gt;])&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;.&lt;/span&gt;rotation3DEffect&lt;span style="color:#eceff4"&gt;(.&lt;/span&gt;degrees&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;flagSpinAmount&lt;span style="color:#eceff4"&gt;[&lt;/span&gt;number&lt;span style="color:#eceff4"&gt;]),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; axis&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;x&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#b48ead"&gt;0&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; y&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#b48ead"&gt;1&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; z&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#b48ead"&gt;0&lt;/span&gt;&lt;span style="color:#eceff4"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;.&lt;/span&gt;opacity&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;flagOpacity&lt;span style="color:#eceff4"&gt;[&lt;/span&gt;number&lt;span style="color:#eceff4"&gt;])&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;.&lt;/span&gt;scaleEffect&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;flagScale&lt;span style="color:#eceff4"&gt;[&lt;/span&gt;number&lt;span style="color:#eceff4"&gt;])&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;.&lt;/span&gt;animation&lt;span style="color:#eceff4"&gt;(.&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;default&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; value&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; flagSpinAmount&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I came across this being used on a &lt;a href="https://blog.rosay.io/create-a-camera-app-with-swiftui-60876fcb9118"&gt;blog post&lt;/a&gt; (about using the camera in apps) from &lt;a href="https://rosay.io/"&gt;Gaspard Rosay&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>Wordpress Code Blocks</title><link>https://blog.iankulin.com/wordpress-code-blocks/</link><pubDate>Wed, 21 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/wordpress-code-blocks/</guid><description>&lt;p&gt;Non-iOS post warning :- )&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not really happy with the way I&amp;rsquo;m sharing code in these posts. I started off with the regular Wordpress code blocks:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;func&lt;/span&gt; isPossible&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;word&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#bf616a"&gt;String&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;-&amp;gt;&lt;/span&gt; Bool &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; tempWord &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; rootWord
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;for&lt;/span&gt; letter &lt;span style="color:#81a1c1;font-weight:bold"&gt;in&lt;/span&gt; word &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; let pos &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; tempWord&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;firstIndex&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;of&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; letter&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; tempWord&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;remove&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;at&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; pos&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;These seem a bit large to me, but it comes with a font size choice, which I like setting to &amp;ldquo;Tiny&amp;rdquo;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;func&lt;/span&gt; isPossible&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;word&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#bf616a"&gt;String&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;-&amp;gt;&lt;/span&gt; Bool &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; tempWord &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; rootWord
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;for&lt;/span&gt; letter &lt;span style="color:#81a1c1;font-weight:bold"&gt;in&lt;/span&gt; word &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; let pos &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; tempWord&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;firstIndex&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;of&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; letter&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; tempWord&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;remove&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;at&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; pos&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;There&amp;rsquo;s a reason why coloured syntax highlighting exists in IDE&amp;rsquo;s, so obviously I&amp;rsquo;d want that for my posts. If you move to a paid tier on Wordpress, as well as eliminating the advertisements from your posts, you get a new coloured code block called &amp;ldquo;SyntaxHighlighter Code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func isPossible(word: String) -&amp;gt; Bool {
 var tempWord = rootWord
 for letter in word {
 if let pos = tempWord.firstIndex(of: letter) {
 tempWord.remove(at: pos)
 } else {
 return false
 }
 }
 return true
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It has a few options, line numbers, making links clickable, and highlighting lines (as I&amp;rsquo;ve done above), but no size, and no control of the font or colours, which are so dreadful I&amp;rsquo;ve mostly given up on using them. It does have a number of languages to choose from which is impressive, but the highlighting is not as good as Xcode, here&amp;rsquo;s how that snippet looks with the theme I&amp;rsquo;m using.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-18-at-7.34.14-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;So the Wordpress block is picking out keywords and types, but not properties. In the ideal world my code examples here would look exactly like this. I could just use screenshots like this, but there&amp;rsquo;s a couple of minor issues with that. The first is the problems with scaling on different devices, and the second is that non-Apple viewers don&amp;rsquo;t have a simple way of selecting text from an image.&lt;/p&gt;
&lt;p&gt;The idea solution would be that the SyntaxHighlighter code block had a few more options. Wordpress is known for the large number of plugins available, so there&amp;rsquo;s possibly a plugin that solves this problem, so a possible solution is for me to learn more about Wordpress which is not a big priority for me at the moment. Related to that is the possibility of using &amp;ldquo;Additional CSS classes&amp;rdquo; which is one of the options for the code block.&lt;/p&gt;
&lt;p&gt;I do note that when code is copied out from Xcode it includes the font and colour information (I guess as rich text?). If I copy the code above and pasted it into word and change the background colour, it looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-18-at-7.43.31-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;So that raises the prospect of pasting it into a different Wordpress block that displays rich text, but if there is such a thing, I can&amp;rsquo;t see how to access it.&lt;/p&gt;
&lt;p&gt;HTML has evolved in part to solve this sort of problem, and there is an HTML block for Wordpress. If I save the Word doc above into HTML and paste it into the HTML block I get this, which is about 75% of the way towards what I&amp;rsquo;m after.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;func&lt;/strong&gt; isPossible(word: String) -&amp;gt; Bool {&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;var&lt;/strong&gt; tempWord = rootWord&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;for&lt;/strong&gt; letter &lt;strong&gt;in&lt;/strong&gt; word {&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;if&lt;/strong&gt; &lt;strong&gt;let&lt;/strong&gt; pos = tempWord.firstIndex(of: letter) {&lt;/p&gt;
&lt;p&gt;tempWord.remove(at: pos)&lt;/p&gt;
&lt;p&gt;} &lt;strong&gt;else&lt;/strong&gt; {&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;return&lt;/strong&gt; &lt;strong&gt;false&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;return&lt;/strong&gt; &lt;strong&gt;true&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;The HTML source is not pretty, but I can&amp;rsquo;t see why this couldn&amp;rsquo;t work if I wrote something to convert the pasted rich text into nicer HTML that looks closer to the Xcode IDE version.&lt;/p&gt;
&lt;p&gt;Other people have solved this problem. I notice Paul Hudson has exactly the presentation I&amp;rsquo;d like on his pages:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-18-at-8.11.37-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;His HTML for this (correctly) leaves the work for the CSS. I had a quick look, and other than knowing it was written by &lt;a href="https://getbootstrap.com/"&gt;BootStrap&lt;/a&gt;, it was mostly incomprehensible to me. Better HTML and CSS is on my list of coding goals, but my current level of knowledge is stuck on 1996 HTML. I&amp;rsquo;d be happy to chuck up a page with some blinking text, a visitor counter and an under construction gif for any clients looking for that.&lt;/p&gt;</description></item><item><title>How to upgrade XCode and Swift</title><link>https://blog.iankulin.com/how-to-upgrade-xcode-and-swift/</link><pubDate>Sun, 18 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/how-to-upgrade-xcode-and-swift/</guid><description>&lt;p&gt;With the September release of XCode 14 and Swift 5.7 it was time for my first update - I looked in &amp;ldquo;About&amp;rdquo; for an update link but there wasn&amp;rsquo;t one - so if you&amp;rsquo;re as dense as me, the tip is to head to the &lt;a href="https://www.google.com/url?sa=t&amp;amp;rct=j&amp;amp;q=&amp;amp;esrc=s&amp;amp;source=web&amp;amp;cd=&amp;amp;cad=rja&amp;amp;uact=8&amp;amp;ved=2ahUKEwjN8OGn85r6AhXlWHwKHf85DzAQFnoECBUQAQ&amp;amp;url=https%3A%2F%2Fapps.apple.com%2Fus%2Fapp%2Fxcode%2Fid497799835%3Fmt%3D12&amp;amp;usg=AOvVaw2fEvMbfRtGhB4SPHYB54NX"&gt;Mac App Store&lt;/a&gt; and have a look at the Updates page.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-17-at-8.53.34-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Your current XCode version can, of course be found in the &lt;em&gt;XCode | About&lt;/em&gt; dialogue. Mine was on 13.4.1. There&amp;rsquo;s a couple of ways of finding the Swift version - If you&amp;rsquo;ve got an XCode project open, click on the .xcodeproj in the explorer,and have a look in &lt;em&gt;Build Settings&lt;/em&gt; for &lt;em&gt;Swift Compiler - Language&lt;/em&gt; for the major version.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-17-at-8.56.44-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Or for a bit more accuracy, try &lt;code&gt;xcrun swift -version&lt;/code&gt; at the command line. I got a few errors, then the version 5.6.1&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-17-at-8.55.13-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I won&amp;rsquo;t bang on about the &lt;a href="https://www.youtube.com/watch?v=tYBZ8AVH0Q0"&gt;changes for XCode&lt;/a&gt; or &lt;a href="https://www.swift.org/blog/swift-5.7-released/"&gt;Swift&lt;/a&gt; since that&amp;rsquo;s already been done well elsewhere, and the Swift changes are mostly beyond my expertise level.&lt;/p&gt;</description></item><item><title>How to download a file from GitHub</title><link>https://blog.iankulin.com/how-to-download-a-file-from-github/</link><pubDate>Sat, 17 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/how-to-download-a-file-from-github/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-13-at-9.12.31-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;A quick tip - since it was not immediately obvious to me. If you need to download a file from GitHub (as opposed to cloning it etc), look for the &amp;ldquo;Raw&amp;rdquo; button - that&amp;rsquo;s a link to the file, then right click and do whatever your browser needs to download a web resource.&lt;/p&gt;</description></item><item><title>Project 4 Challenges</title><link>https://blog.iankulin.com/project-4-challenges/</link><pubDate>Fri, 16 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/project-4-challenges/</guid><description>&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2022-09-13-at-7.22.43-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-13-at-7.22.43-pm.png" width="197" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve completed the Project 4 challenges (source) of the 100 Days of SwiftUI, no biggie - the increase in difficulty between each step of Paul&amp;rsquo;s bootcamp is small enough that it&amp;rsquo;s never too stressful, but large enough you feel like you&amp;rsquo;re progressing all the time.&lt;/p&gt;
&lt;p&gt;Since I&amp;rsquo;ve paid to be a member of Hacking with Swift, one of the perks is to see Paul&amp;rsquo;s video solutions. I&amp;rsquo;ve not worries about it before, but I should - looking at them and comparing to my efforts is probably good feedback. So here&amp;rsquo;s the differences in our answers to the challenges.&lt;/p&gt;
&lt;h4 id="sections"&gt;Sections&lt;/h4&gt;
&lt;p&gt;When I changed the VStacks to Sections, I put the section title text at the top:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Section(header: Text(&amp;#34;When do you want to wake up?&amp;#34;)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; HStack {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; DatePicker(&amp;#34;Please enter a time&amp;#34;, selection: $wakeUp, 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; displayedComponents: .hourAndMinute).labelsHidden()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Paul had his in a trailing closure at the bottom:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Section {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; DatePicker(&amp;#34;Please enter a time&amp;#34;, selection: $wakeUp, 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; displayedComponents: .hourAndMinute).labelsHidden()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;header: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(&amp;#34;When do you want to wake up?&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I thought mine was nicer, but then the very next thing he shows in the video is:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Section(&amp;#34;When do you want to wake up?&amp;#34;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; DatePicker(&amp;#34;Please enter a time&amp;#34;, selection: $wakeUp, 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; displayedComponents: .hourAndMinute).labelsHidden()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So there - I have learned something for an expert&amp;hellip;&lt;/p&gt;
&lt;p&gt;The next job was to change the stepper to a picker for the number of cups of coffee. I had something like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Section(header: Text(&amp;#34;Daily coffee intake?&amp;#34;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Picker(coffeeAmount == 1 ? &amp;#34;1 cup&amp;#34; : &amp;#34;\(coffeeAmount) cups&amp;#34;, 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; selection: $coffeeAmount) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ForEach(1...20, id: \.self) { i in
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; if i == 1 {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(&amp;#34;1 cup&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } else {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(&amp;#34;\(i) cups&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And Paul:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Section(&amp;#34;Daily coffee intake?&amp;#34;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Picker(&amp;#34;Number of cups&amp;#34;, selection: $coffeeAmount) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ForEach(1..&amp;lt;21) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(String($0))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;so yeah, then when I looked at my output properly (see simulator image above) I noticed mine didn&amp;rsquo;t make sense anyway. My bad - that&amp;rsquo;s a horrid careless error.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not sure about the difference in the ranges 1&amp;hellip;20 and 1..&amp;lt;21 the ..&amp;lt; is a great habit for avoiding off by one errors with collections, perhaps that&amp;rsquo;s the reason for Paul&amp;rsquo;s choice there.&lt;/p&gt;
&lt;p&gt;The third thing to do was to get rid of the alert, and show the result live as changes were made. I did mine by by adding a text field whose contents were a call to a slightly modified calculateBedtime() function. Paul moved that code up to a new computed property. I&amp;rsquo;m not sure I see any difference there except style.&lt;/p&gt;
&lt;p&gt;So, that&amp;rsquo;s a worthwhile thing to do - to look at Paul&amp;rsquo;s solution and compare it to mine, so I&amp;rsquo;ll go on and do that in future.&lt;/p&gt;</description></item><item><title>Machine Learning</title><link>https://blog.iankulin.com/machine-learning/</link><pubDate>Thu, 15 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/machine-learning/</guid><description>&lt;p&gt;A few years ago when I still used a Tom-Tom for car navigation, I was a little freaked out when it started offering suggestions on where to go to when I started the car - guessing, usually correctly, where I wanted to go. Like - how did it know I was leaving school for band practice two towns over?&lt;/p&gt;
&lt;p&gt;Clearly, is must have been collecting data on times/days and departure locations to learn some of my habits. It felt quite invasive, but I thought it must have been on-device since I had the wifi turned off in the unit.&lt;/p&gt;
&lt;p&gt;In &lt;a href="https://www.hackingwithswift.com/100/swiftui/27"&gt;Day 27&lt;/a&gt; of 100 Days of Swift UI we CoreML and use a dataset to train a model, then incorporate it in an App. The most shocking thing about it was how straightforward it was. In the example, the model was trained in CreateML from XCode and only used on the device, rather than trained on it, but current iPhones have the power to do that.&lt;/p&gt;
&lt;p&gt;Obviously there&amp;rsquo;s some amazing things that can be done with machine learning, but I&amp;rsquo;m actually more excited about the small things - perhaps just offering better defaults for user inputs.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a great intro to machine learning for Apple developers &lt;a href="https://developer.apple.com/machine-learning/"&gt;here&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>Before SwiftUI</title><link>https://blog.iankulin.com/before-swiftui/</link><pubDate>Wed, 14 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/before-swiftui/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-10-at-9.28.24-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m on Day 26 of 100 Days, and didn&amp;rsquo;t grok the dates on my first read through, so I&amp;rsquo;ve read a couple of other explanations and sat down with a coffee and thought I&amp;rsquo;d see what YouTube had for me on the subject. I seen a few great &lt;a href="https://www.youtube.com/c/iOSAcademy/videos"&gt;iOS Academy&lt;/a&gt; videos, so &lt;a href="https://www.youtube.com/watch?v=HSFTzcYzuEQ"&gt;this one&lt;/a&gt; seemed like a good choice.&lt;/p&gt;
&lt;p&gt;I haven&amp;rsquo;t seen enough to say if it is a good or great explanation of dates, calendars and date components in Swift yet, but man, getting to the stage of writing useful code when using storyboards and UIKit takes a while! It&amp;rsquo;s literally 3:42 in to the video before there&amp;rsquo;s enough infrastructure done for &amp;ldquo;hello world&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Takeaways: (1) Watching the launch of SwiftUI must have been mind-blowing. (2) I super appreciate the existence of Playgrounds for learning Swift.&lt;/p&gt;</description></item><item><title>Rock, Paper Scissors (2)</title><link>https://blog.iankulin.com/rock-paper-scissors-2/</link><pubDate>Tue, 13 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/rock-paper-scissors-2/</guid><description>&lt;p&gt;When I was forced by a deadline into delivering this project, I noted in its &lt;a href="https://blog.iankulin.com/rock-paper-scissors-1/"&gt;post&lt;/a&gt; that there was a number of improvements to make:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;&lt;em&gt;The rock paper scissors could be some better data structure than an array and some ints.&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;I don’t love the try to win, try to lose aspect, but the client specified it&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Having the didUserWin and didComputerWin funcs is a cop out – that should probably be a single function returning a win/lose/draw type&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;I also am unhappy with nesting them in the view namespace to use my #consts&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;duplicating the last part of the view but making the elements .hidden() to keep the same spacing seems like a kludge&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;when I added the link to the Hacking With SwiftUI page with the app brief just now, I noticed I haven’t done the scoring the way it was asked for&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here&amp;rsquo;s the progress&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The rock paper scissors could be some better data structure than an array and some ints&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Done. Made a sweet swifty enum. Read about it here. That also solved #4&lt;/p&gt;
&lt;p&gt;&lt;em&gt;I don’t love the try to win, try to lose aspect, but the client specified it&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;We can&amp;rsquo;t always help what a client wants. Deal with it.&lt;/p&gt;
&lt;p&gt;_Having the didUserWin and didComputerWin funcs is a cop out – that should probably be a single function returning a win/lose/dra_w&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;enum&lt;/span&gt; GameResult &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;case&lt;/span&gt; win
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;case&lt;/span&gt; loss
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;case&lt;/span&gt; draw
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;func&lt;/span&gt; gameResult&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;user&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; FingerShape&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; computer&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; FingerShape&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;-&amp;gt;&lt;/span&gt; GameResult &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;switch&lt;/span&gt; user &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;case&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;rock&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;switch&lt;/span&gt; computer &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;case&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;rock&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;draw
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;case&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;paper&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;loss
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;case&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;scissors&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;win
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;case&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;paper&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;switch&lt;/span&gt; computer &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;case&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;rock&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;win
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;case&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;paper&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;draw
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;case&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;scissors&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;loss
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;case&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;scissors&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;switch&lt;/span&gt; computer &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;case&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;rock&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;loss
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;case&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;paper&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;win
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;case&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;scissors&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;draw
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;em&gt;duplicating the last part of the view but making the elements .hidden() to keep the same spacing seems like a kludge&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This was an issue - not just because of the mess of code, but because (according to Paul) it&amp;rsquo;s a workload issue - the view managing part of SwiftUI has to keep creating and destroying parts of our view instead of recycling them. In an earlier lesson, he encouraged the use of the ternary operator in modifiers to avoid this - but .hidden() doesn&amp;rsquo;t have any arguments. Here&amp;rsquo;s the sort of code I&amp;rsquo;m talking about - I&amp;rsquo;ve got this sort of thing several places.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;if revealResult {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(computerSelection.rawValue)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .font(.system(size: 200))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(winText).font(.title)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Spacer()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Button(&amp;#34;Play again&amp;#34;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; goalIsWinThisTurn = Bool.random()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; revealResult = false
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; if showScores {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; score = 0
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; numberOfPlays = 0
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; showScores.toggle()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .buttonStyle(CustomButtonStyle())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;else {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(computerSelection.rawValue)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .font(.system(size: 200))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .hidden()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(winText)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .font(.title)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .hidden()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Spacer()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Button(&amp;#34;Play again&amp;#34;) {}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .buttonStyle(CustomButtonStyle())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .hidden()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I was surprised when googling something like &amp;ldquo;swiftui view visibility not hidden()&amp;rdquo; I found, instead of a stack overflow question, a &lt;a href="https://developer.apple.com/tutorials/swiftui-concepts/choosing-the-right-way-to-hide-a-view?changes=_3"&gt;nice Apple tutorial&lt;/a&gt; at the top of the links.&lt;/p&gt;
&lt;p&gt;Within was my answer - most views have an .opacity() modifier. With that, the code above becomes:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Group {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(computerSelection.rawValue)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .font(.system(size: 200))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(winText).font(.title)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Spacer()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Button(&amp;#34;Play again&amp;#34;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; goalIsWinThisTurn = Bool.random()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; revealResult = false
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; if showScores {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; score = 0
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; numberOfPlays = 0
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; showScores.toggle()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .buttonStyle(CustomButtonStyle())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.opacity(revealResult ? 1 : 0)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Noice. I then when on a ternary insertion spree that dropped my view body down from 74 lines of code to 52 and improved readability substantially.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/IanKulin/RockPaper/blob/fe8e2eea247e9c1ab13daa3e39929100991f69c5/RockPaper/ContentView.swift"&gt;Current source&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Swift enums</title><link>https://blog.iankulin.com/swift-enums/</link><pubDate>Mon, 12 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/swift-enums/</guid><description>&lt;p&gt;I&amp;rsquo;ve started on the refactoring for &lt;a href="https://blog.iankulin.com/rock-paper-scissors-1/"&gt;Rock, paper, scissors&lt;/a&gt;. One of the things I didn&amp;rsquo;t like was using Ints to signal which shape (I&amp;rsquo;m calling the rock, or paper, or scissors hand shape a &lt;em&gt;shape&lt;/em&gt;) was being handed around. The Int I was using was also the index into an array of the emoji&amp;rsquo;s - so if I did an off-by-one I was risking an out of bounds on the array.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m pleased with this solution:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2022-09-08-at-6.39.53-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-08-at-6.39.53-pm.png" width="921" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re a refugee from C, there&amp;rsquo;s a lot happening here:&lt;/p&gt;
&lt;p&gt;1 - In C enums are actually int&amp;rsquo;s inside, and you can mess with which int goes with which value. In Swift, they can be any type, but the type is specified when you define the enum. They are extracted with &lt;em&gt;.rawvalue&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;2 - The Swift String type is a beast. Yes it does emojis, and a lot of other very cool unicode stuff that has a cost.&lt;/p&gt;
&lt;p&gt;3 - enums can have methods. I know right?&lt;/p&gt;
&lt;p&gt;When I read the &lt;a href="https://docs.swift.org/swift-book/"&gt;Swift book&lt;/a&gt;, I felt it was a travesty against nature for enums being allowed to have methods, but the code above felt like the most natural thing ever. I must be acclimatising.&lt;/p&gt;
&lt;p&gt;Apart from eliminating the array out of bounds possibility, this also made the code more readable, and meant that I could remove the default case from all my switch statements.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/IanKulin/RockPaper/commit/ff3d81599b92ba39cfa2a41c5c8c213f1b442735?diff=split"&gt;source&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I might not be happy with the name of this type - &lt;em&gt;Shape&lt;/em&gt;. I have spend a bit of time thinking about it, and even looked up Rock Paper Scissors on Wikipedia to see what terminology they used. It&amp;rsquo;s meant to represent the shape of the hands made by the players in each round of the game. It felt unclear enough that I added a comment to make it clearer - a sure sign it&amp;rsquo;s not. It may change to &lt;em&gt;FingerShape&lt;/em&gt; when I do the next lot of refactoring.&lt;/p&gt;</description></item><item><title>.git stuffed</title><link>https://blog.iankulin.com/git-stuffed/</link><pubDate>Sun, 11 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/git-stuffed/</guid><description>&lt;p&gt;I&amp;rsquo;m in a bit of a swing with my git process. I usually develop locally committing as needed, then when I reach some sort of first milestone, create an empty repo on GitHub the push up to it by:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git remote add origin git@github.com:IanKulin/RockPaper.git
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git branch -M main
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git push -u origin main
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;or, I start on GitHub, create a new repo with a readme.md in it, and then use the -f (force) flag when I push to it and override the contents. I think forgetting about this might have been the source of tonight&amp;rsquo;s problems with &amp;ldquo;unrelated histories&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-07-at-8.26.16-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll try and reproduce it on the weekend - I thought I could just pull down the readme from github and the push up all my local changes, but I for the unrelated histories error. I started googling and entering git commands I knew nothing about for a while before just deleting the .git folder, and the github repo, and starting again.&lt;/p&gt;</description></item><item><title>Rock, Paper, Scissors (1)</title><link>https://blog.iankulin.com/rock-paper-scissors-1/</link><pubDate>Sat, 10 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/rock-paper-scissors-1/</guid><description>&lt;p&gt;As I mentioned yesterday, I needed to make some progress to blog about, and I had a half working version of a Rock, Paper, Scissors for &lt;a href="https://www.hackingwithswift.com/100/swiftui/25"&gt;Day 25&lt;/a&gt; so I pushed myself to get that working.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s lots in the code below I don&amp;rsquo;t love.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The rock paper scissors could be some better data structure than an array and some ints.&lt;/li&gt;
&lt;li&gt;I don&amp;rsquo;t love the try to win, try to lose aspect, but the client specified it&lt;/li&gt;
&lt;li&gt;Having the didUserWin and didComputerWin funcs is a cop out - that should probably be a single function returning a win/lose/draw type&lt;/li&gt;
&lt;li&gt;I also am unhappy with nesting them in the view namespace to use my #consts&lt;/li&gt;
&lt;li&gt;duplicating the last part of the view but making the elements .hidden() to keep the same spacing seems like a kludge&lt;/li&gt;
&lt;li&gt;when I added the link to the Hacking With SwiftUI page with the app brief just now, I noticed I haven&amp;rsquo;t done the scoring the way it was asked for&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href="https://github.com/IanKulin/RockPaper/commit/b1497cccf2dc8af953b946458af797dc5ad12dc9?diff=unified#diff-223dd39ecc4f631b084c99b065a71ea40dc2deba8e36e7f5f939802e60c80186"&gt;source on github&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;struct ContentView: View {
@State var score = 0
@State var goalIsWinThisTurn = Bool.random()
@State var userSelection = 0
@State var computerSelection = 0
@State var winText = &amp;quot;&amp;quot;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// game has two modes - revealResult - buttons don't work, and we can see the computer result
// or not readyToPlay - user can chose their play
@State var revealResult = false

let rock = 0
let paper = 1
let scissors = 2
let rpsEmoji = \[&amp;quot;🪨&amp;quot;, &amp;quot;📃&amp;quot;, &amp;quot;✂️&amp;quot;\]

 
var body: some View {
 ZStack{
 LinearGradient(colors: \[.white, .gray\], startPoint: .top, endPoint: .bottom)
 VStack{
 Button(&amp;quot;Score: \\(score)&amp;quot;){
 score = 0
 }
 .font(.title)
 .foregroundColor(.primary)
 
 Spacer()
 if goalIsWinThisTurn {
 Text(&amp;quot;Try to win&amp;quot;)
 .font(.title)
 }
 else {
 Text(&amp;quot;Try to lose&amp;quot;)
 .font(.title)
 }
 
 Spacer()
 HStack{
 Spacer()
 Button(rpsEmoji\[rock\]) {
 processButton(selection: rock)
 }
 Spacer()
 Button(rpsEmoji\[paper\]) {
 processButton(selection: paper)
 }
 Spacer()
 Button(rpsEmoji\[scissors\]){
 processButton(selection: scissors)
 }
 Spacer()
 }.font(.system(size: 60))
 
 Spacer()
 if revealResult {
 Text(rpsEmoji\[computerSelection\])
 .font(.system(size: 200))
 Text(winText).font(.title)
 Spacer()
 Button(&amp;quot;Play again&amp;quot;) {
 goalIsWinThisTurn = Bool.random()
 revealResult = false
 }
 .buttonStyle(CustomButtonStyle())
 }
 else {
 Text(rpsEmoji\[computerSelection\])
 .font(.system(size: 200))
 .hidden()
 Text(winText)
 .font(.title)
 .hidden()
 Spacer()
 Button(&amp;quot;Play again&amp;quot;) {
 goalIsWinThisTurn = Bool.random()
 revealResult = false
 }
 .buttonStyle(CustomButtonStyle())
 .hidden()
 }
 Spacer()
 }
 }
}

func didUserWin(user: Int, computer: Int) -&amp;gt; Bool {
 switch(user) {
 case rock:
 switch(computer) {
 case scissors: return true
 default: return false
 }
 case paper:
 switch(computer) {
 case rock: return true
 default: return false
 }
 case scissors:
 switch(computer) {
 case paper: return true
 default: return false
 }
 default:
 assert(false)
 return false
 }
}

func didComputerWin(user: Int, computer: Int) -&amp;gt; Bool {
 switch(computer) {
 case rock:
 switch(user) {
 case scissors: return true
 default: return false
 }
 case paper:
 switch(user) {
 case rock: return true
 default: return false
 }
 case scissors:
 switch(user) {
 case paper: return true
 default: return false
 }
 default:
 assert(false)
 return false
 }
}

func processButton(selection: Int) {
 if !revealResult {
 computerSelection = Int.random(in: 0...2)
 userSelection = selection
 if didUserWin(user: userSelection, computer: computerSelection) {
 winText = &amp;quot;You win&amp;quot;
 if goalIsWinThisTurn {
 score += 1
 winText = &amp;quot;You win&amp;quot;
 } else {
 score -= 1
 winText = &amp;quot;You win, sorry&amp;quot;
 }
 } else if didComputerWin(user: userSelection, computer: computerSelection) {
 if goalIsWinThisTurn {
 score -= 1
 winText = &amp;quot;You lose, sorry&amp;quot;
 } else {
 score += 1
 winText = &amp;quot;You lose!&amp;quot;
 }
 } else {
 winText = &amp;quot;Draw&amp;quot;
 }
 revealResult = true
 }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;struct CustomButtonStyle : ButtonStyle {
func makeBody(configuration: Configuration) -&amp;gt; some View {
configuration.label
.font(.title)
.padding(10)
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(5)
}
}&lt;/p&gt;</description></item><item><title>A deadline is a good thing</title><link>https://blog.iankulin.com/a-deadline-is-a-good-thing/</link><pubDate>Fri, 09 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/a-deadline-is-a-good-thing/</guid><description>&lt;p&gt;I usually have a few days of blog posts written in advance so I can schedule one to come out each day, and not sweat if I&amp;rsquo;m caught up in real life. There&amp;rsquo;s no real reason why I should have that strict publishing schedule, but it is part of my internal discipline to ensure that, at least on average I&amp;rsquo;m making some sort of report-able progress or effort each day.&lt;/p&gt;
&lt;p&gt;And of course, there&amp;rsquo;s the psychological weight of my streak!&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-07-at-7.45.40-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m bushed from work tonight, and probably would not have done any programming work beyond watching a related YouTube, but I&amp;rsquo;d run out of scheduled posts, and had a half started project as the next thing to do.&lt;/p&gt;
&lt;p&gt;The pressure of having to come up with a post here made me drag it out and do a MVP of it to met the specifications. Yay deadlines.&lt;/p&gt;</description></item><item><title>Learn to Code</title><link>https://blog.iankulin.com/learn-to-code/</link><pubDate>Thu, 08 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/learn-to-code/</guid><description>&lt;p&gt;This blog exists for a couple of reasons - firstly Paul Hudson insisted on posting progress in the 100 days of SwiftUI on social media, and secondly, when I try to explain something, I&amp;rsquo;m forced to understand it clearly - so I know this is a good learning technique.&lt;/p&gt;
&lt;p&gt;This video from &lt;a href="https://fireship.io/"&gt;Fireship&lt;/a&gt; says this idea is called the &lt;a href="https://www.lifehack.org/862931/feynman-technique"&gt;Feynman Technique&lt;/a&gt;&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/NtfbWkxJTHw?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;
</description></item><item><title>.self in ForEach</title><link>https://blog.iankulin.com/self-in-foreach/</link><pubDate>Wed, 07 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/self-in-foreach/</guid><description>&lt;p&gt;I&amp;rsquo;m on Day 25 of Hacking With SwiftUI, and &lt;a href="https://www.hackingwithswift.com/guide/ios-swiftui/2/2/key-points"&gt;Paul is making a point&lt;/a&gt; about how SwiftUI can loop over an array to build a view. He starts with this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;let agents = [&amp;#34;Cyril&amp;#34;, &amp;#34;Lana&amp;#34;, &amp;#34;Pam&amp;#34;, &amp;#34;Sterling&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;VStack {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ForEach(0..&amp;lt;agents.count) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text(agents[$0])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;But then proposes an alternative:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;let agents = [&amp;#34;Cyril&amp;#34;, &amp;#34;Lana&amp;#34;, &amp;#34;Pam&amp;#34;, &amp;#34;Sterling&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;VStack {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ForEach(agents, id: \.self) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text($0)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;He explains the use of \.self here by saying&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;So, we come back to how Swift can identify values in our array. When we were using a range such as &lt;code&gt;0..&amp;lt;5&lt;/code&gt; or &lt;code&gt;0..&amp;lt;agents.count&lt;/code&gt;, Swift knew for sure that each item was unique because it would use the numbers from the range – each number was used only once in the loop, so it would definitely be unique.&lt;/p&gt;
&lt;p&gt;In our array of strings that’s no longer possible, but we can clearly see that each value is unique: the values in &lt;code&gt;[&amp;quot;Cyril&amp;quot;, &amp;quot;Lana&amp;quot;, &amp;quot;Pam&amp;quot;, &amp;quot;Sterling&amp;quot;]&lt;/code&gt; don’t repeat. So, what we can do is tell SwiftUI that the strings themselves – “Cyril”, “Lana”, etc – are what can be used to identify each view in the loop uniquely.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I&amp;rsquo;m having a couple of problems with this.&lt;/p&gt;
&lt;h4 id="grumble-one"&gt;Grumble One&lt;/h4&gt;
&lt;p&gt;The first is that Swift can&amp;rsquo;t really use the &amp;ldquo;strings themselves&amp;rdquo;. So this doesn&amp;rsquo;t work:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ForEach(agents, String($0)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text($0)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It complains that there&amp;rsquo;s no initialiser to take those arguments. So \.self must be something fancier. This is something I can presumably investigate by looking at the initialisers for ForEach.&lt;/p&gt;
&lt;h4 id="grumble-two"&gt;Grumble Two&lt;/h4&gt;
&lt;p&gt;And the second grumble is that the first formulation &lt;code&gt;ForEach(0..&amp;lt;agents.count)&lt;/code&gt; throws a compiler warning &amp;ldquo;&lt;code&gt;Non-constant range: argument must be an integer literal&lt;/code&gt;&amp;rdquo;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-03-at-11.44.26-am.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Which is true, if I swap it for the number &amp;lsquo;4&amp;rsquo; it stops complaining. I guess in the initialiser for ForEach there&amp;rsquo;s something specifying this although it&amp;rsquo;s not clear to me why. Again it&amp;rsquo;s an initialiser mystery because I can make the warning go away by adding the id reference to self.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-03-at-2.06.48-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Googling for the warning finds a &lt;a href="https://www.hackingwithswift.com/forums/swiftui/compiler-warning-non-constant-range-argument-must-be-an-integer-literal/14878"&gt;page on the Hacking with Swift forums&lt;/a&gt; that throws a little bit of light on the issue by looking at the inits.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-03-at-2.13.44-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The difference between these two that we&amp;rsquo;re interested in is that in the first, the type is &lt;code&gt;Range&lt;/code&gt;, and the second &lt;code&gt;Data&lt;/code&gt;. &lt;a href="https://www.hackingwithswift.com/users/roosterboy"&gt;@RoosterBoy&lt;/a&gt;&amp;rsquo;s explanation for why we should use literal ints for the range is that it&amp;rsquo;s to be safe from the array changing size during the loop - which is slightly unsatisfying because that could still happen with the literal int range.&lt;/p&gt;
&lt;h4 id="grumble-one-again"&gt;Grumble One again&lt;/h4&gt;
&lt;p&gt;These initialisers also shed some light on my first problem, the references used by SwiftUI should be KeyPaths. &lt;a href="https://sarunw.com/posts/what-is-keypath-in-swift/"&gt;Sarun&amp;rsquo;s explanation&lt;/a&gt; of them makes them sound like actual references - ie memory addresses for the properties. That would make sense since SwiftUi wants them to keep track of things.&lt;/p&gt;
&lt;p&gt;So I tried this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ForEach(agents, id: \String.$0) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text($0)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;but the compiler is still unhappy - so clearly I am not knowledgeable enough yet to solve this one.&lt;/p&gt;</description></item><item><title>I git it*</title><link>https://blog.iankulin.com/i-git-it/</link><pubDate>Tue, 06 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/i-git-it/</guid><description>&lt;img src="https://blog.iankulin.com/images/6y8jpj5f4el91.jpg" width="500" alt=""&gt;
&lt;p&gt;This meme&amp;rsquo;s been trending in the &lt;a href="https://old.reddit.com/r/ProgrammerHumor/comments/x3udcz/is_it_really_that_much_easy_because_in_the/"&gt;r/ProgrammerHumor subreddit&lt;/a&gt;, and although &amp;ldquo;to do literally anything&amp;rdquo; is a stretch, my git / github workflow is pretty routine now using the &lt;a href="https://xkcd.com/1597/"&gt;relevant xkcd&lt;/a&gt; method, but actually with quite a bit of understanding from the first half of the excellent &lt;a href="https://git-scm.com/book/en/v2"&gt;Pro Git book&lt;/a&gt;. I highly recommend it.&lt;/p&gt;
&lt;p&gt;I had in my goals to set up XCode for push (I think I probably just need to generate a token on GitHub and save it in xcode), so I will do that for completion, but I&amp;rsquo;m also enjoying my &lt;a href="https://blog.iankulin.com/oh-my-zsh/"&gt;pimped out terminal&lt;/a&gt; so I&amp;rsquo;m pretty much a git cli guy now.&lt;/p&gt;
&lt;p&gt;I may have gone overboard creating repos for every tiny app in my learning process - sorry Microsoft you have to host all of that for free. It is probably possible to have one repo, with a heap of separate projects inside it. That would require the ability to clone just a directory and it&amp;rsquo;s sub directories, which is something I don&amp;rsquo;t know how to do yet :-/&lt;/p&gt;
&lt;p&gt;&lt;em&gt;* Yes, I&amp;rsquo;m always going to do bad git puns in post titles.&lt;/em&gt;&lt;/p&gt;</description></item><item><title>Project 3</title><link>https://blog.iankulin.com/project-3/</link><pubDate>Mon, 05 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/project-3/</guid><description>&lt;p&gt;This one&amp;rsquo;s not really a project, just a couple of little updates to earlier work, and a code snippet.&lt;/p&gt;
&lt;h4 id="challenge-1"&gt;Challenge 1&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Go back to project 1 and use a conditional modifier to change the total amount text view to red if the user selects a 0% tip.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The first one is pretty simple - a ternary condition to make the total red if the tip is set to zero.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-09-02-at-5.10.56-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Text(grandTotal, format: currencyCode)
.foregroundColor(tipPercentage == 0 ? .red : .primary)&lt;/p&gt;
&lt;p&gt;The &lt;em&gt;ternary operator&lt;/em&gt; is like a little inline if then else statement. It has the format:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;(w ? t : f)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;where w is the condition, t is the code if the condition is true and f is the code if the condition is false. So the code above checks if the tipPercentage is zero, if it is, the grandTotal text is red, otherwise it&amp;rsquo;s coloured &lt;em&gt;.primary&lt;/em&gt; - one of the semantic colors. The semantic colours are colours set by the system and referred to by their purpose. In this case it will be black (the &amp;ldquo;primary&amp;rdquo; text colour), unless I change the theme to dark mode, in which case it will be white.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/IanKulin/WeSplit/compare/v1.0...v1.1"&gt;Source&lt;/a&gt;&lt;/p&gt;
&lt;h4 id="challenge-2"&gt;Challenge 2&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Go back to project 2 and replace the&lt;/em&gt; &lt;code&gt;Image&lt;/code&gt; &lt;em&gt;view used for flags with a new&lt;/em&gt; &lt;code&gt;FlagImage&lt;/code&gt;&lt;em&gt;&lt;code&gt;()&lt;/code&gt; view that renders one flag image using the specific set of modifiers we had.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Again, pretty straightforward. I just made a new view struct in the main view.&lt;/p&gt;
&lt;p&gt;struct FlagView: View {
var flagOf: String&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; var body: some View {
 Image(flagOf)
 .renderingMode(.original)
 .shadow(radius: 5)
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;Then called it with our flag name.&lt;/p&gt;
&lt;p&gt;ForEach(0..&amp;lt;3) { number in
Button {
// flag was tapped
flagTapped(number)
} label: {
FlagView(flagOf: countries[number])
}
}&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/IanKulin/GuessTheFlag/compare/v1.0...v1.1"&gt;Source&lt;/a&gt;&lt;/p&gt;
&lt;h4 id="challenge-3"&gt;Challenge 3&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Create a custom&lt;/em&gt; &lt;code&gt;ViewModifier&lt;/code&gt; &lt;em&gt;(and accompanying&lt;/em&gt; &lt;code&gt;View&lt;/code&gt; &lt;em&gt;extension) that makes a view have a large, blue font suitable for prominent titles in a view.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;struct ContentView: View {
var body: some View {
VStack{
Text(&amp;ldquo;Hello World&amp;rdquo;)
.titleStyle()
Spacer()
}
}
}&lt;/p&gt;
&lt;p&gt;struct ProminentTitle: ViewModifier {
func body(content: Content) -&amp;gt; some View {
content
.font(.largeTitle)
.foregroundColor(.blue)
.padding()
}
}&lt;/p&gt;
&lt;p&gt;extension View {
func titleStyle() -&amp;gt; some View {
modifier(ProminentTitle())
}
}&lt;/p&gt;</description></item><item><title>Sean != Erica</title><link>https://blog.iankulin.com/sean-erica/</link><pubDate>Sun, 04 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/sean-erica/</guid><description>&lt;p&gt;When Swift was newer, there was a bunch of podcasts about it - in early episodes of &lt;a href="https://podcasts.apple.com/au/podcast/fireside-swift/id1269435221"&gt;Fireside Swift&lt;/a&gt; the existence of a Swift Podcast Network is often mentioned, but now it&amp;rsquo;s more of an established language there&amp;rsquo;s a bit less current content to listen to, and what there is, is less focused on learning Swift and more about what&amp;rsquo;s happening in the community.&lt;/p&gt;
&lt;p&gt;Being firmly in the camp of needing to learn more about the language, I&amp;rsquo;ve listen to a number of older podcasts, or even current ones (such as Fireside) but their older episodes. It is sort of an odd experience traveling on several slightly out of sync timelines, but quite a joy to see what happens to predictions - like the occasion when &lt;a href="https://twitter.com/twostraws"&gt;Paul Hudson&lt;/a&gt; predicts that an &amp;ldquo;Xcode lite&amp;rdquo; on iPad is unlikely to be able to write apps until a more swift like framework for developing interfaces exists.&lt;/p&gt;
&lt;p&gt;One of the podcasts I&amp;rsquo;m working through by every episode is Paul&amp;rsquo;s &lt;a href="https://podcasts.apple.com/au/podcast/swift-over-coffee/id1435076502"&gt;Swift Over Coffee&lt;/a&gt;. The first season he was paired up with &lt;a href="https://seanallen.co/"&gt;Sean Allen&lt;/a&gt;, but I&amp;rsquo;ve just started the second season with &lt;a href="https://ericasadun.com"&gt;Erica Sadun&lt;/a&gt;. When I first came across Sean it took me a while to warm to his enthusiastic voice, but what I loved about him was he was never reluctant to ask Paul to explain something - usually something I needed explained as well. The dynamic of an expert (and expert teacher) co-hosting with a relative newbie was a great combination for me.&lt;/p&gt;
&lt;p&gt;Erica is a giant in the Swift community, and she has a deep understanding and an wide knowledge of Swift topics, so she&amp;rsquo;s going to be great. In the first episode she and Paul riffed on a heap of interesting topics with great enthusiasm and clear enjoyment, but I did miss Sean asking for explanations!&lt;/p&gt;</description></item><item><title>Day 23 - Views and Modifiers - Part 4</title><link>https://blog.iankulin.com/day-23-views-and-modifiers-part-4/</link><pubDate>Sat, 03 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/day-23-views-and-modifiers-part-4/</guid><description>&lt;img src="https://blog.iankulin.com/images/psm_v10_d562_the_hindoo_earth-3.jpg" width="417" alt="This image has an empty alt attribute; its file name is psm\_v10\_d562\_the\_hindoo\_earth-3.jpg"&gt;
&lt;p&gt;Then the last trick for for decomposing the views, is to remember we can pass values when we init a struct. So something like this:&lt;/p&gt;
&lt;p&gt;struct ContentView: View {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var body: some View {
 VStack{
 GreenPaddedText(text: &amp;quot;Hello&amp;quot;)
 GreenPaddedText(text: &amp;quot;world&amp;quot;)
 }
}


struct GreenPaddedText: View {
 var text: String

 var body: some View {
 Text(text)
 .foregroundColor(.green)
 .padding()
 }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;This is probably my favourite - because although in this example I&amp;rsquo;ve created the mini-view struct in the body, if it&amp;rsquo;s a building block I can use elsewhere in a different view, it&amp;rsquo;s super portable.&lt;/p&gt;</description></item><item><title>Day 23 - Views and Modifiers - Part 3</title><link>https://blog.iankulin.com/day-23-views-and-modifiers-part-3/</link><pubDate>Fri, 02 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/day-23-views-and-modifiers-part-3/</guid><description>&lt;img src="https://blog.iankulin.com/images/psm_v10_d562_the_hindoo_earth-3.jpg" width="472" alt=""&gt;
&lt;p&gt;The next part of day 23 started to make my brain hurt a bit. It&amp;rsquo;s easy to imagine that when presenting a complex screen - perhaps some data from a source as a mixture of images and text loaded from a database into a scroll-able view, that the view may start to get complex. Then it becomes good practice to decompose the views to make the code clearer, less error prone, and to avoid any unnecessary repetition.&lt;/p&gt;
&lt;p&gt;Paul&amp;rsquo;s first suggestion is to pull some parts of the view as &lt;em&gt;properties&lt;/em&gt; of the same view.&lt;/p&gt;
&lt;p&gt;struct ContentView: View {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let greenText = Text(&amp;quot;Hello&amp;quot;).foregroundColor(.green)

var body: some View {
 VStack{
 greenText
 greenText
 }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;This works fine, and exactly how you expect, except that if you don&amp;rsquo;t enclose it in the VStack, you just get one Text, but two ContentPreviews. I do not understand why yet, but its probably something to do with the @ViewBuilder property wrapper.&lt;/p&gt;
&lt;p&gt;But&amp;hellip; a property can&amp;rsquo;t refer to another property, so this isn&amp;rsquo;t compilable Swift:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-08-31-at-8.24.46-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;To get around this, we can use a computed property. So this works:&lt;/p&gt;
&lt;p&gt;struct ContentView: View {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@State private var greeting = &amp;quot;Hello&amp;quot;

var greenText: some View {
 Text(greeting).foregroundColor(.green)
}

var body: some View {
 VStack{
 greenText
 greenText
 }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;In fact, this is what I&amp;rsquo;ve been doing so far to decompose views, although I&amp;rsquo;ve been dropping the segments underneath the main body to make things subjectively neater.&lt;/p&gt;
&lt;p&gt;Paul cautions about returning multiple views in this computed property manner. So this does not work:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-08-31-at-8.33.20-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;That makes sense - we were relying on the implied return. Putting them in a VStack would work - because we&amp;rsquo;re just returning a single view (the VStack) which happens to contain multiple views.&lt;/p&gt;
&lt;p&gt;struct ContentView: View {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@State private var greeting = &amp;quot;Hello&amp;quot;

var body: some View {
 VStack{
 greenText
 greenText
 }
}

var greenText: some View {
 VStack{
 Text(greeting).foregroundColor(.green)
 Text(greeting).foregroundColor(.green)
 }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s another view container type called Group which is like a stack, but just contains, rather than arranging, a collection of views, that can be used in the same way.&lt;/p&gt;
&lt;p&gt;Alternatively, and I assume this is related to the problem I had above, we can just wrap the property with the @ViewBuilder attribute.&lt;/p&gt;
&lt;p&gt;struct ContentView: View {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@State private var greeting = &amp;quot;Hello&amp;quot;

var body: some View {
 VStack{
 greenText
 greenText
 }
}

@ViewBuilder var greenText: some View {
 Text(greeting).foregroundColor(.green)
 Text(greeting).foregroundColor(.green)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;</description></item><item><title>Day 23 - Views and Modifiers - Part 2</title><link>https://blog.iankulin.com/day-23-views-and-modifiers-part-2/</link><pubDate>Thu, 01 Sep 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/day-23-views-and-modifiers-part-2/</guid><description>&lt;img src="https://blog.iankulin.com/images/psm_v10_d562_the_hindoo_earth-2.jpg" width="484" alt=""&gt;
&lt;p&gt;Although &amp;ldquo;immutable&amp;rdquo; the view structs can contain some control statements such as if/then and for loops. So this is quite legal, and useful.&lt;/p&gt;
&lt;p&gt;struct ContentView: View {
@State private var likesGreen = true&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var body: some View {
 if likesGreen {
 Text(&amp;quot;Hello World&amp;quot;)
 .background(.green)
 }
 else
 {
 Text(&amp;quot;Hello World&amp;quot;)
 .background(.blue)
 }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;But Paul cautions against this, saying:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://www.hackingwithswift.com/books/ios-swiftui/conditional-modifiers"&gt;You can often use regular if conditions to return different views based on some state, but this actually creates more work for SwiftUI – rather than seeing one Button being used with different colors, it now sees two different Button views, and when we flip the Boolean condition it will destroy one to create the other rather than just recolor what it has.&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Instead, he encourages the use of the ternary operator in modifiers for these situations. The ternary operator is like an if/then/else statement packaged up neatly. So the code above becomes:&lt;/p&gt;
&lt;p&gt;struct ContentView: View {
@State private var likesGreen = true&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var body: some View {
 Text(&amp;quot;Hello World&amp;quot;)
 .background(likesGreen ? .green : .blue)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;He&amp;rsquo;s not arguing that it&amp;rsquo;s neater, but also that it&amp;rsquo;s more efficient - that the first version has to destroy and create a Text when the value of the flag changes, whereas the second one just changes the colour. I assume he&amp;rsquo;s correct, but it&amp;rsquo;s not obvious why that would be so. It must be in the magic of how SwiftUI optimises how and when it renders each part of a view.&lt;/p&gt;</description></item><item><title>Day 23 - Views and Modifiers - Part 1</title><link>https://blog.iankulin.com/day-23-views-and-modifiers-part-1/</link><pubDate>Wed, 31 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/day-23-views-and-modifiers-part-1/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/psm_v10_d562_the_hindoo_earth-1.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I found this one of the trickier days, so I&amp;rsquo;ll write it out to clear up my thinking.&lt;/p&gt;
&lt;p&gt;To draw to to screen in SwiftUI, we don&amp;rsquo;t call a command to draw on a canvas or window. Rather, a &lt;em&gt;view&lt;/em&gt; is defined as an immutable struct of type some View. Here&amp;rsquo;s the simple one from the default Xcode project.&lt;/p&gt;
&lt;p&gt;struct ContentView: View {
var body: some View {
Text(&amp;ldquo;Hello, world!&amp;rdquo;)
.padding()
}
}&lt;/p&gt;
&lt;p&gt;The &lt;em&gt;body&lt;/em&gt; var must be present, and can contain up to ten views. This simple example only contains one view - a text box. There are container view types that can, well, contain other views - for example a &lt;em&gt;HStack&lt;/em&gt; which arranges its content horizontally.&lt;/p&gt;
&lt;p&gt;struct ContentView: View {
var body: some View {
HStack {
Text(&amp;ldquo;Hello, world!&amp;rdquo;)
.padding()
Rectangle()
.frame(width: 100, height: 100, alignment: .center)
}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;produces:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2022-08-31-at-7.20.54-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-08-31-at-7.20.54-pm.png" width="164" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Having defined our view struct, we don&amp;rsquo;t call for it to be rendered on the screen, SwiftUI is just going to do that for us whenever it thinks it needs to. This seems bizarre at first, but you get used to it. It will happen when it needs to - usually because the information that makes up the view has changed. We help that to happen by binding the views to their data in various ways. More on that another day.&lt;/p&gt;
&lt;p&gt;If you look back at the code above, you&amp;rsquo;ll see that views often have &lt;em&gt;modifiers&lt;/em&gt; attached to them. In our example the &lt;em&gt;padding()&lt;/em&gt; on the text and the &lt;em&gt;.frame()&lt;/em&gt; on the rectangle. There&amp;rsquo;s many different modifiers for all of the different view primitives. Many of them are common to different primitives, some are different. It&amp;rsquo;s possible to attach them to container views - in which case they are applied to all the views in the container. For example, if we move the .frame() to the HStack, like this:&lt;/p&gt;
&lt;p&gt;struct ContentView: View {
var body: some View {
HStack {
Text(&amp;ldquo;Hello, world!&amp;rdquo;)
.padding()
Rectangle()
}
.frame(width: 100, height: 100, alignment: .center)
}
}&lt;/p&gt;
&lt;p&gt;We get&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2022-08-31-at-7.33.07-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-08-31-at-7.33.07-pm.png" width="188" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The order of the modifiers is important. You can think of each modifier that&amp;rsquo;s added as wrapping around the view and any previous modifiers. In this example, we&amp;rsquo;ve got the same text field with the &lt;em&gt;.padding()&lt;/em&gt; and &lt;em&gt;.background(.blue)&lt;/em&gt; modifiers. The left one has the padding first, then is wrapped in the blue background. The right one has the blue background applied first, then the padding.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2022-08-31-at-7.39.53-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-08-31-at-7.39.53-pm.png" width="192" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Project 2 - Guess the Flag</title><link>https://blog.iankulin.com/project-2-guess-the-flag/</link><pubDate>Wed, 31 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/project-2-guess-the-flag/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-08-23-at-8.26.59-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Another 100 Days project - the second tutorial one. This was once again a &amp;ldquo;V&amp;rdquo; design pattern (put everything in the view) and as I kept growing it, especially in the challenges, I had a growing sense of unease.&lt;/p&gt;
&lt;p&gt;New things for me was how image assets work - identifying them with strings is convenient, but I&amp;rsquo;m hoping there&amp;rsquo;s safer system later using enums or something to avoid runtime surprises. Also the alert dialog box system. I was wondering how this was going to work in a declarative framework. I do not really approve of modal dialogs in mobile UI&amp;rsquo;s but I guess they have their place. I appreciated the gradients and frosted glass effects -super simple to implement, and if done thoughtfully, pretty.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/IanKulin/GuessTheFlag"&gt;Source/Github&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Download a Directory from a GitHub Repo</title><link>https://blog.iankulin.com/download-a-directory-from-a-github-repo/</link><pubDate>Tue, 30 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/download-a-directory-from-a-github-repo/</guid><description>&lt;p&gt;For &lt;a href="https://www.hackingwithswift.com/books/ios-swiftui/guess-the-flag-introduction"&gt;Challenge 2&lt;/a&gt; in the 100 days, I needed to download a directory of flag images from Paul&amp;rsquo;s GitHub. He has all the projects as sub-directories of a single &amp;ldquo;Hacking With Swift&amp;rdquo; repo. I didn&amp;rsquo;t need to whole thing, just the directory with the images.&lt;/p&gt;
&lt;p&gt;Strangely, git does not have any simple way of doing this. Neither does GitHub - I assumed the web interface would have a &amp;ldquo;download as zip&amp;rdquo; option as it does for tags.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://stackoverflow.com/questions/7106012/download-a-single-folder-or-directory-from-a-github-repo"&gt;One&lt;/a&gt; of the popular solutions on StackOverflow was to use SVN, which GitHub supports and which does have this functionality. I much preferred &lt;a href="https://stackoverflow.com/users/11218031/avinash-thakur"&gt;Avinash Takur&amp;rsquo;s&lt;/a&gt; &lt;a href="https://stackoverflow.com/questions/7106012/download-a-single-folder-or-directory-from-a-github-repo/70729494#70729494"&gt;suggestion&lt;/a&gt; to use GitHub&amp;rsquo;s web based VSCode.&lt;/p&gt;
&lt;p&gt;To access their VSCode, change the .com in the repo url to .dev. For example, instead of https://github.&lt;strong&gt;com&lt;/strong&gt;/twostraws/HackingWithSwift/tree/main/SwiftUI/project2, go to https://github.&lt;strong&gt;dev&lt;/strong&gt;/twostraws/HackingWithSwift/tree/main/SwiftUI/project&lt;code&gt;2&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-08-22-at-6.50.16-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Once that&amp;rsquo;s done, right click on the directory to download it.&lt;/p&gt;</description></item><item><title>Challenge 1</title><link>https://blog.iankulin.com/challenge-1/</link><pubDate>Mon, 29 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/challenge-1/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-08-22-at-8.54.26-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m up to Challenge 1 of 100 Days of SwiftUI (&lt;a href="https://www.hackingwithswift.com/100/swiftui/19"&gt;Day 19&lt;/a&gt;) which is to make your own simple (no MVVM) version of the app built in the previous three days. It&amp;rsquo;s about as simple as can be whilst still feeling like a real app. Something I hadn&amp;rsquo;t done before was limiting the keyboard to numbers or adding a toolbar to close it, so that was nice.&lt;/p&gt;
&lt;p&gt;Something that&amp;rsquo;s not nice, is that when you touch into the text field to change the number, it&amp;rsquo;s not selected ready to type over (the way they always are in browser url fields) so you need to backspace over the previous entry. That&amp;rsquo;s the sort of anoying behaviour I don&amp;rsquo;t like. It seems (after some googling) there&amp;rsquo;s no straightforward way of addressing this in SwiftUI, with the best solution involving importing a package. I will come back to that because it is bugging me.&lt;/p&gt;
&lt;p&gt;struct ContentView: View {
@State private var fromDistance = 0.0
@State private var fromUnits = &amp;ldquo;meters&amp;rdquo;
@State private var toUnits = &amp;ldquo;kilometers&amp;rdquo;
@FocusState private var distanceIsFocused: Bool&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let distanceUnits = \[&amp;quot;meters&amp;quot;, &amp;quot;kilometers&amp;quot;, &amp;quot;yards&amp;quot;, &amp;quot;miles&amp;quot;\]

var toDistance: Double {
 var meters = 0.0
 // convert the from distance to meters
 switch fromUnits {
 case &amp;quot;meters&amp;quot;: meters = fromDistance
 case &amp;quot;kilometers&amp;quot;: meters = fromDistance\*1000
 case &amp;quot;yards&amp;quot;: meters = fromDistance\*0.9144
 case &amp;quot;miles&amp;quot;: meters = fromDistance\*1609.34
 default: assert(false)
 }
 // convert the meters to the from distance
 switch toUnits {
 case &amp;quot;meters&amp;quot;: return meters
 case &amp;quot;kilometers&amp;quot;: return meters/1000
 case &amp;quot;yards&amp;quot;: return meters/0.9144
 case &amp;quot;miles&amp;quot;: return meters/1609.34
 default : assert(false)
 }
 return 0.0
}


var body: some View {
 NavigationView {
 Form {
 
 Section {
 TextField(&amp;quot;Distance&amp;quot;, value: $fromDistance, format: .number)
 .keyboardType(.decimalPad)
 .focused($distanceIsFocused)
 Picker(&amp;quot;Units&amp;quot;, selection: $fromUnits) {
 ForEach(distanceUnits, id: \\.self) {
 Text($0)
 }
 }
 .pickerStyle(.segmented)
 }
 header: {
 Text(&amp;quot;Distance&amp;quot;)
 }
 
 Section {
 if toDistance &amp;lt; 9.9 {
 Text(&amp;quot;\\(toDistance, specifier: &amp;quot;%.4f&amp;quot;)&amp;quot;)
 } else if toDistance &amp;gt; 999 {
 Text(&amp;quot;\\(toDistance, specifier: &amp;quot;%.0f&amp;quot;)&amp;quot;)
 } else {
 Text(&amp;quot;\\(toDistance, specifier: &amp;quot;%.2f&amp;quot;)&amp;quot;)
 }

 Picker(&amp;quot;Units&amp;quot;, selection: $toUnits) {
 ForEach(distanceUnits, id: \\.self) {
 Text($0)
 }
 }
 .pickerStyle(.segmented)
 }
 header: {
 Text(&amp;quot;Converted Distance&amp;quot;)
 }
 
 }
 .navigationTitle(&amp;quot;Distance Conversion&amp;quot;)
 .toolbar {
 ToolbarItemGroup(placement: .keyboard) {
 Spacer()
 Button(&amp;quot;Done&amp;quot;) {
 distanceIsFocused = false
 }
 }
 }
 }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/IanKulin/HSUnitConvert"&gt;Source on Github&lt;/a&gt;&lt;/p&gt;</description></item><item><title>$==Commitment</title><link>https://blog.iankulin.com/commitment/</link><pubDate>Sun, 28 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/commitment/</guid><description>&lt;p&gt;&lt;a href="https://www.hackingwithswift.com/100/swiftui"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-08-21-at-9.47.54-am.jpg" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Paul Hudson&amp;rsquo;s 100 Days of Swift UI course is free - the videos are on YouTube, all the reading and tasks are available on his website for free. Although I assumed he made his living from the prodigious number of Swift books he&amp;rsquo;s written, he doesn&amp;rsquo;t push them in the course (except for his free book &lt;a href="https://www.hackingwithswift.com/quick-start/swiftui"&gt;Swift UI by Example&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;He&amp;rsquo;s so unpushy, I didn&amp;rsquo;t realise till a few days ago that you could pay to become a &lt;a href="https://www.hackingwithswift.com/plus"&gt;&amp;ldquo;subscriber&amp;rdquo; to Hacking with Swift&lt;/a&gt;. Really, he&amp;rsquo;s too nice.&lt;/p&gt;
&lt;p&gt;There are some goodies that come with the + subscription (one I&amp;rsquo;m likely to use later is the book discount) , but really I was very happy to sign up for a year for a couple of reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Paul is a major contributor to the Swift community, and especially to beginners like me. The world is a better place if he can afford to keep doing that.&lt;/li&gt;
&lt;li&gt;This extra commitment, which is a tiny fraction of what I could spend on a bootcamp, makes it more likely I&amp;rsquo;ll complete the course, and puts a time limit on it - if I don&amp;rsquo;t complete it in the year, I&amp;rsquo;ll feel compelled to pay again.&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Int.times()</title><link>https://blog.iankulin.com/int-times/</link><pubDate>Sat, 27 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/int-times/</guid><description>&lt;p&gt;When writing &lt;a href="https://blog.iankulin.com/the-_-underscore/"&gt;yesterday&amp;rsquo;s post&lt;/a&gt; about iterating through a range or collection and using the underscore to throw away the item, I had in the back of my mind that there should be a more straightforward way of doing something a number of times.&lt;/p&gt;
&lt;p&gt;Just to re-iterate (lol), here&amp;rsquo;s the issue. If we want to print &amp;ldquo;Here&amp;rsquo;s the thing&amp;rdquo; three times, in Swift the simplest we can do is:&lt;/p&gt;
&lt;p&gt;for _ in 1&amp;hellip;3 {
print(&amp;ldquo;Here&amp;rsquo;s the thing&amp;rdquo;)
}&lt;/p&gt;
&lt;p&gt;I had the idea, that this should really be a method of the Int type. And in fact I could write it as an extension that took a closure. Then we could just do this:&lt;/p&gt;
&lt;p&gt;3.times {
print(&amp;ldquo;Here&amp;rsquo;s the thing&amp;rdquo;)
}&lt;/p&gt;
&lt;p&gt;That feels much more like the Swift way of doing things (although I probably picked it up during a brief flirtation with Ruby). Of course, I&amp;rsquo;d implement it with a while loop and a counter, so there&amp;rsquo;d still be the counter memory allocated, but only for an Int rather than the Array element type.&lt;/p&gt;
&lt;p&gt;With this system, the problem I was talking about yesterday:&lt;/p&gt;
&lt;p&gt;let thingStrings = [&amp;ldquo;Thing one&amp;rdquo;, &amp;ldquo;Thing two&amp;rdquo;, &amp;ldquo;Thing three&amp;rdquo;]&lt;/p&gt;
&lt;p&gt;for _ in thingStrings {
print(&amp;ldquo;Here&amp;rsquo;s the thing&amp;rdquo;)
}&lt;/p&gt;
&lt;p&gt;would become:&lt;/p&gt;
&lt;p&gt;let thingStrings = [&amp;ldquo;Thing one&amp;rdquo;, &amp;ldquo;Thing two&amp;rdquo;, &amp;ldquo;Thing three&amp;rdquo;]&lt;/p&gt;
&lt;p&gt;thingStrings.count.times {
print(&amp;ldquo;Here&amp;rsquo;s the thing&amp;rdquo;)
}&lt;/p&gt;
&lt;p&gt;Which is, I admit, not amazingly better, but better, especially if the compiler is allocating the memory and filling it with each array value in the first example (which I don&amp;rsquo;t know if it is, but am increasingly interested in finding out).&lt;/p&gt;
&lt;p&gt;Feeling pretty pleased with myself for inventing this new Int method, I had a extra thought that in fact, the Swift community may already of invented this and incorporated it in the language, so I should google it first. It turns out it&amp;rsquo;s not aprt of the official language, but neither (unsurprisingly) am I the first to think of it.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a &lt;a href="https://stackoverflow.com/questions/30554013/what-is-the-shortest-way-to-run-same-code-n-times-in-swift"&gt;Stack Overflow answer&lt;/a&gt; to a question &amp;ldquo;What is the shortest way to run same code n times in Swift?&amp;rdquo;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://stackoverflow.com/questions/30554013/what-is-the-shortest-way-to-run-same-code-n-times-in-swift"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-08-20-at-3.12.32-pm.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;So shout out to &lt;a href="https://stackoverflow.com/users/4358829/matteo-piombo"&gt;Matteo Piombo&lt;/a&gt; for doing the work for my idea seven years before I had it! It&amp;rsquo;s still just for code clarity, but great use of extensions and closures.&lt;/p&gt;
&lt;p&gt;I still maintain that &lt;code&gt;for _ in&lt;/code&gt; is not great, and that &lt;code&gt;for each in&lt;/code&gt; where each was a synonym for the underscore would be the prettiest solution. A likely con of this proposal is that is would be a code breaking change for any code that has already uses &lt;code&gt;for each&lt;/code&gt; which could be quite common.&lt;/p&gt;</description></item><item><title>The _ Underscore</title><link>https://blog.iankulin.com/the-_-underscore/</link><pubDate>Fri, 26 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/the-_-underscore/</guid><description>&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/The_Undertaker"&gt;&lt;img src="https://blog.iankulin.com/images/undertaker_with_fire.jpg" width="84" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve learned (so far) an underscore can be used for a couple of things in Swift, both of them loosely translating to &amp;ldquo;doesn&amp;rsquo;t really matter&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;The first is in a parameter name for a function. Swift has a very cool feature I haven&amp;rsquo;t seen before where an argument can have a different internal and external name. As usual, this will make more sense in code. Imagine this:&lt;/p&gt;
&lt;p&gt;func sumNumbers(firstNumber: Int, secondNumber: Int) -&amp;gt; Int {
return firstNumber + secondNumber
}&lt;/p&gt;
&lt;p&gt;let sum = sumNumbers(firstNumber: 5, secondNumber: 3 )&lt;/p&gt;
&lt;p&gt;Using the magic of internal and external parameter names, we can make the function call read a bit better. The external parameter name goes first and is separated from the internal name by a space.&lt;/p&gt;
&lt;p&gt;func sumNumbers(numbers firstNumber: Int, and secondNumber: Int) -&amp;gt; Int {
return firstNumber + secondNumber
}&lt;/p&gt;
&lt;p&gt;let sum = sumNumbers(numbers: 5, and: 3 )&lt;/p&gt;
&lt;p&gt;That makes the function call a little clearer, but we can go one better by making the external name of the first parameter optional using the underscore:&lt;/p&gt;
&lt;p&gt;func sumNumbers(_ firstNumber: Int, and secondNumber: Int) -&amp;gt; Int {
return firstNumber + secondNumber
}&lt;/p&gt;
&lt;p&gt;let sum = sumNumbers(5, and: 3 )&lt;/p&gt;
&lt;p&gt;So that&amp;rsquo;s the first use - to make a parameter of a function unnamed in the call.&lt;/p&gt;
&lt;p&gt;The second is in loops where we don&amp;rsquo;t care about the item we&amp;rsquo;re extracting. Normally we want to do something with each item in a collection we&amp;rsquo;re iterating through. For example, we might want to print each string in an array:&lt;/p&gt;
&lt;p&gt;let thingStrings = [&amp;ldquo;Thing one&amp;rdquo;, &amp;ldquo;Thing two&amp;rdquo;, &amp;ldquo;Thing three&amp;rdquo;]&lt;/p&gt;
&lt;p&gt;for thing in thingStrings {
print(thing)
}&lt;/p&gt;
&lt;p&gt;But what if we didn&amp;rsquo;t? Perhaps this is what we&amp;rsquo;re doing:&lt;/p&gt;
&lt;p&gt;let thingStrings = [&amp;ldquo;Thing one&amp;rdquo;, &amp;ldquo;Thing two&amp;rdquo;, &amp;ldquo;Thing three&amp;rdquo;]&lt;/p&gt;
&lt;p&gt;for thing in thingStrings {
print(&amp;ldquo;Here&amp;rsquo;s the thing&amp;rdquo;)
}&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;ve sort of created a variable for no purpose - it&amp;rsquo;s not referenced. If that&amp;rsquo;s our intention, it would be better to have the compiler know that so if we inadvertently use it, it would warn us. The underscore comes to help with this, we can just:&lt;/p&gt;
&lt;p&gt;let thingStrings = [&amp;ldquo;Thing one&amp;rdquo;, &amp;ldquo;Thing two&amp;rdquo;, &amp;ldquo;Thing three&amp;rdquo;]&lt;/p&gt;
&lt;p&gt;for _ in thingStrings {
print(&amp;ldquo;Here&amp;rsquo;s the thing&amp;rdquo;)
}&lt;/p&gt;
&lt;p&gt;I have no idea if this results in better binary code - ie if LVM was reserving memory for &lt;code&gt;thing&lt;/code&gt; in the previous example. Knowing Chris Lattner, it probably is smarter than that.&lt;/p&gt;
&lt;p&gt;Although one quickly gets used to reading code like the above example, I don&amp;rsquo;t really like this use of the underscore. The code does not read well, it&amp;rsquo;s not Swifty enough. I&amp;rsquo;d probably say:&lt;/p&gt;
&lt;p&gt;let thingStrings = [&amp;ldquo;Thing one&amp;rdquo;, &amp;ldquo;Thing two&amp;rdquo;, &amp;ldquo;Thing three&amp;rdquo;]&lt;/p&gt;
&lt;p&gt;for eachThing in thingStrings {
print(&amp;ldquo;Here&amp;rsquo;s the thing&amp;rdquo;)
}&lt;/p&gt;
&lt;p&gt;Thinking about this today whilst driving back from shopping, I hit on the idea there should be a reserved word &amp;ldquo;each&amp;rdquo; to achieve the same purpose as the underscore, but perhaps the code above is as good as we can get. It will do until I find out if LVM is optimising it away. (&lt;a href="https://www.hackingwithswift.com/sixty/4/1/for-loops"&gt;Paul says, obliquely, it is not&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not forgetting about the .forEach method on arrays. But that has the same issue - it forces me to name (or underscore) the variable to hold the value we&amp;rsquo;re iterating past.&lt;/p&gt;
&lt;p&gt;let thingStrings = [&amp;ldquo;Thing one&amp;rdquo;, &amp;ldquo;Thing two&amp;rdquo;, &amp;ldquo;Thing three&amp;rdquo;]&lt;/p&gt;
&lt;p&gt;thingStrings.forEach { _ in
print(&amp;ldquo;Here&amp;rsquo;s the thing&amp;rdquo;)
}&lt;/p&gt;</description></item><item><title>Uwrap App</title><link>https://blog.iankulin.com/uwrap-app/</link><pubDate>Thu, 25 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/uwrap-app/</guid><description>&lt;img src="https://blog.iankulin.com/images/img_2549.png" width="269" alt=""&gt;
&lt;p&gt;Part of the &lt;a href="https://twitter.com/twostraws"&gt;@twostraws&lt;/a&gt; programmatic universe is his Swift learning app, &lt;a href="https://apps.apple.com/us/app/unwrap/id1440611372"&gt;Unwrap&lt;/a&gt; that I&amp;rsquo;ve included in my learning goals. It presents little snippets of learning with a 60 second video, and in a written version, then tests the user to check their understanding. It is slightly gamified - you get points for answers, but it&amp;rsquo;s not clear to me how that works beyond the satisfying haptics when your score runs up at the end of a section.&lt;/p&gt;
&lt;p&gt;The tests so far (I&amp;rsquo;m up to Functions) have been code examples along with a &amp;ldquo;true or false&amp;rdquo; question for the set - often &amp;ldquo;This code is valid Swift&amp;rdquo;, but sometimes things like &amp;ldquo;This code prints four messages&amp;rdquo;. At first, I didn&amp;rsquo;t love the tests as they often didn&amp;rsquo;t test the thing I&amp;rsquo;d just learned. For example in the unit about for loops, the hidden error in some code might have been an undeclared variable being used. But the effect of this has been to make me look at each example carefully to look for errors - in the process I&amp;rsquo;ve learned the Swift syntax well (which I would otherwise have relied on Xcode to help me with), and sharpened my ability to spot errors (that I would otherwise have relied on the compiler to help me with).&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/img_2553.png" width="142" alt=""&gt;
&lt;p&gt;The screen shots here are from my SE2 iPhone - so on a sensible sized device the code may be a little easier to read (no it does not to landscape on the phone). I do value having it on the phone though - it&amp;rsquo;s perfect for making good use of tiny bites of time through the day which would be Swift learning free otherwise.&lt;/p&gt;
&lt;p&gt;Even if you&amp;rsquo;re not following one of Paul&amp;rsquo;s Hacking with Swift courses, the Unwrap app is a great way to polish your Swift knowledge.&lt;/p&gt;</description></item><item><title>Codewars / reduce</title><link>https://blog.iankulin.com/codewars-reduce/</link><pubDate>Wed, 24 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/codewars-reduce/</guid><description>&lt;p&gt;&lt;a href="https://www.codewars.com/"&gt;&lt;img src="https://blog.iankulin.com/images/1_0plbhkaulwnsx4u2mqyn2w.png" width="242" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.codewars.com/"&gt;codewars.com&lt;/a&gt; is a &amp;ldquo;coding practice&amp;rdquo; website. You chose a language and a skill level, then it offers up a task (or &lt;em&gt;kata&lt;/em&gt;) for you to write a suitable function. The first one it gave me was seemed too hard, so I changed my level to beginner and skipped to the next one. This was my task:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Given an array of integers, find the one that appears an odd number of times.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; There will always be only one integer that appears an odd number of times.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Examples
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; [7] should return 7, because it occurs 1 time (which is odd).
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; [0] should return 0, because it occurs 1 time (which is odd).
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; [1,1,2] should return 2, because it occurs 1 time (which is odd).
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; [0,1,0,1,0] should return 0, because it occurs 3 times (which is odd).
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; [1,2,2,3,3,3,4,3,3,3,2,2,1] should return 4, because it appears 1 time (which is odd).
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I know there&amp;rsquo;s a cool &amp;ldquo;Set&amp;rdquo; container type in Swift, so my plan was to iterate through the array, then for each number if there&amp;rsquo;s no entry in the set, then add one, but if there is, remove it. That way whatever is left in the set at the end must be in the original array an odd number of times. I was pretty pleased with myself. Here&amp;rsquo;s the code I Playground&amp;rsquo;d up:&lt;/p&gt;
&lt;p&gt;func oddTimesInt(intArray: [Int]) -&amp;gt; Int {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var intSet: Set&amp;lt;Int&amp;gt; = \[\]

for number in intArray {
 if intSet.contains(number) {
 intSet.remove(number)
 } else {
 intSet.insert(number)
 }
}

assert(intSet.count == 1)
if let firstNumber = intSet.first {
 return firstNumber
} else {
 // this is guaranteed not to happen in the specification
 assert(false)
 return 0
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;While you are writing your code in their webpage, you can run it against a test suite. Mine passed the first time; even more pleased with myself.&lt;/p&gt;
&lt;p&gt;When you&amp;rsquo;re ready, you can submit your code. Now it runs against a much bigger test suite. It passed again; now my head is swelling a little at just how canny I am.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a chance to clean up, comment, or to refactor your code before it&amp;rsquo;s finally locked in. Then once you commit that, it shows you other people&amp;rsquo;s solutions. This was the top one:&lt;/p&gt;
&lt;p&gt;func findIt(_ seq: [Int]) -&amp;gt; Int {
seq.reduce(0, ^)
}&lt;/p&gt;
&lt;p&gt;lol. One. Line. That&amp;rsquo;ll learn me.&lt;/p&gt;
&lt;p&gt;So, anyways&amp;hellip;. I guess I learned there&amp;rsquo;s an array method called &lt;em&gt;reduce&lt;/em&gt;, and it reduces an array, but I want to argue my code is easier to understand.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://developer.apple.com/documentation/swift/array/reduce(_:_:)"&gt;According to the docs&lt;/a&gt;, &lt;em&gt;reduce&lt;/em&gt; &amp;ldquo;Returns the result of combining the elements of the sequence using the given closure.&amp;rdquo; Basically, the 0 in the example above is the inital value of the accumulator, then the closure repeatedly operates on the accumulaotr and each value of the array, then the final result in the accumulator is returned. An example will make a lot more sense. Here&amp;rsquo;s the one from the Apple documentation:&lt;/p&gt;
&lt;p&gt;let numbers = [1, 2, 3, 4]
let numberSum = numbers.reduce(0, { x, y in
x + y
})
// numberSum == 10&lt;/p&gt;
&lt;p&gt;The closure in the winning entry used all the closure redaction tricks I think I&amp;rsquo;ve &lt;a href="https://blog.iankulin.com/closures/"&gt;complained&lt;/a&gt; about before. We could make it a bit more readable by putting syntax back.&lt;/p&gt;
&lt;p&gt;func findIt(_ seq: [Int]) -&amp;gt; Int {
seq.reduce(0, {accumulator, element in accumulator ^ element} )
}&lt;/p&gt;
&lt;p&gt;or to go back another step:&lt;/p&gt;
&lt;p&gt;func superXOR(accumulator: Int, element:Int) -&amp;gt; Int {
return accumulator ^ element
}&lt;/p&gt;
&lt;p&gt;func findIt(_ seq: [Int]) -&amp;gt; Int {
seq.reduce(0, superXOR )
}&lt;/p&gt;
&lt;p&gt;The ^ operator is XOR. I know what that does, and could even manually do it on two binary numbers.&lt;/p&gt;
&lt;p&gt;This compact solution is based on knowing that XORing all the integers will leave the odd value, since XORing two identical numbers gives zero, and that carrying forward the XORed value of two different numbers will shake out to leave the number that only appears once. I was not going to think of that solution, even if I&amp;rsquo;d known &lt;em&gt;reduce&lt;/em&gt;() existed.&lt;/p&gt;</description></item><item><title>Sometimes the Gold is in the Comments</title><link>https://blog.iankulin.com/sometimes-the-gold-is-in-the-comments/</link><pubDate>Tue, 23 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/sometimes-the-gold-is-in-the-comments/</guid><description>&lt;p&gt;I&amp;rsquo;m still not 100% clear on @ObservedObject v @StateObject. So when YouTube offered up this video, in which Paul promises during the intro that I&amp;rsquo;ll understand the data bindings by the end, I thought it would be the video for me.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/stSB04C4iS4?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;

&lt;p&gt;I guess I should really have twigged that I&amp;rsquo;d never heard of @ObjectBinding, but I pushed on to the 12 minute mark when he imports the &lt;em&gt;Combine&lt;/em&gt; framework. Hang on, what&amp;rsquo;s that?&lt;/p&gt;
&lt;p&gt;Checking out the comments, it all becomes clear.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-08-14-at-8.26.59-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Sadly, it&amp;rsquo;s a page down in the comments, when it&amp;rsquo;d be great to be pinned to the top.&lt;/p&gt;
&lt;p&gt;When we&amp;rsquo;re publishing content in a tech field that&amp;rsquo;s changing, there probably is probably some benefit to tagging it with versions - for example, my post on git is sort of useful now, but in three years? Who knows. Like this video of Paul&amp;rsquo;s it may be unhelpful and confusing.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll put some thought into how I might do that. Going back and updating things is probably not realistic (this is certainly the case for twostraws&amp;rsquo; videos - he&amp;rsquo;s prolific), but branding content with the versions of things might be. Wordpress dates content at the bottom of the post (on my current theme) so that would give some future traveler some chance, but the more novice they are (and currently my understanding of these topics is only helpful for novices) the less likely they are to be equipped to translate the date into the likelihood of the content being outdated. Perhaps I can use tags somehow. I&amp;rsquo;ll think about it.&lt;/p&gt;</description></item><item><title>Playgrounds are good</title><link>https://blog.iankulin.com/playgrounds-are-good/</link><pubDate>Mon, 22 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/playgrounds-are-good/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/img_2778.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;A couple of times (&lt;a href="https://blog.iankulin.com/protocols/"&gt;Protocols&lt;/a&gt; &amp;amp; &lt;a href="https://blog.iankulin.com/named-loops/"&gt;Named Loops&lt;/a&gt;) in the past few days I&amp;rsquo;ve needed to write and run a couple of tiny C or C++ snippets, and I&amp;rsquo;ve acutely felt the lack of Swift Playgrounds for it. It occurred to me that Playgrounds has been instrumental in my enjoyment of learning Swift - it&amp;rsquo;s just a bit magic to grab the closest device and noodle out an idea or to make sure I&amp;rsquo;ve understood a new concept.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/15152540.jpg" width="89" alt=""&gt;
&lt;p&gt;In a &lt;a href="https://podcasts.apple.com/us/podcast/episode-11-chris-lattner/id1505697997?i=1000478871841"&gt;conversation between Chris Lattner and Paul Hudson&lt;/a&gt; I&amp;rsquo;ve listened to recently, they discuss the value of &lt;a href="https://www.apple.com/swift/playgrounds/"&gt;Playgrounds&lt;/a&gt; and the excellent &lt;a href="https://docs.swift.org/swift-book/"&gt;Swift book&lt;/a&gt; in bringing the community along from Objective C. I could not agree more about the value of these two.&lt;/p&gt;
&lt;p&gt;As an alternative, I downloaded &lt;a href="https://apps.apple.com/in/app/c-programming-language/id499545918"&gt;C Language app&lt;/a&gt; for the iPad. This seems like a reasonable editor that uses an online compiler - it worked for my purpose although it wasn&amp;rsquo;t real snappy.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/img_2779.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;A couple of alternatives in the app store mentioned the ability to compile offline - which didn&amp;rsquo;t make sense to me until I pressed build on this one and realised it was using a server somewhere to do that work. So this may well not be the best one for AUD5.&lt;/p&gt;
&lt;p&gt;I was amazed to find heaps of online compilers for things which would have done the job just as well. I&amp;rsquo;ve bookmarked &lt;a href="https://itsourcecode.com/compile-code-run-using-online-compiler-ide-for-free/"&gt;this one&lt;/a&gt; which does all sorts of languages including C and Swift.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://itsourcecode.com/compile-code-run-using-online-compiler-ide-for-free/"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-08-13-at-12.19.58-pm.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Named Loops</title><link>https://blog.iankulin.com/named-loops/</link><pubDate>Sun, 21 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/named-loops/</guid><description>&lt;img src="https://blog.iankulin.com/images/img_2768.png" width="133" alt=""&gt;
&lt;p&gt;Here’s a neat thing I haven’t seen before. Other languages I’ve worked in haven’t had a neat way to break out of a set of nested loops to a particular loop. It’s not an issue that comes up a lot, but when it has I’ve solved it by creating a continue flag and having that as the first condition of each loop.&lt;/p&gt;
&lt;p&gt;To explain, say if we had these two loops (in C):&lt;/p&gt;
&lt;p&gt;int i;
int j;
char string[] = &amp;ldquo;This string&amp;rdquo;;
int length = strlen(string);&lt;/p&gt;
&lt;p&gt;for (i=0; i&amp;lt;=3; i++) {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (j=0; j&amp;lt;length; j++) {
 printf(&amp;quot;char:%c num:%d\\n&amp;quot;, string\[j\], i);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;and for some unexplained reason, we need to break out of both loops when we encounter a lowercase ‘t’. There is a C command to break out of a loop - &lt;em&gt;break&lt;/em&gt;. But it only breaks out of the current loop:&lt;/p&gt;
&lt;p&gt;int i;
int j;
char string[] = &amp;ldquo;This string&amp;rdquo;;
int length = strlen(string);&lt;/p&gt;
&lt;p&gt;for (i=0; i&amp;lt;=3; i++) {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (j=0; j&amp;lt;length; j++) {
 printf(&amp;quot;char:%c num:%d\\n&amp;quot;, string\[j\], i);
 if (string\[j\] == 't') {
 break;
 } 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;Since the outside loop that is iterating ‘i’ is not broken out of, we still end up looping through to the letter ‘t’ four times. Like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;char:T num:0
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;char:h num:0
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;char:i num:0
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;char:s num:0
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;char: num:0
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;char:s num:0
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;char:t num:0
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;char:T num:1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;char:h num:1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;char:i num:1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;char:s num:1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;char: num:1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;char:s num:1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;char:t num:1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;char:T num:2
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;char:h num:2
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;char:i num:2
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;char:s num:2
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;char: num:2
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;char:s num:2
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;char:t num:2
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;char:T num:3
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;char:h num:3
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;char:i num:3
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;char:s num:3
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;char: num:3
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;char:s num:3
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;char:t num:3
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So in C/C++ I would convert the loops to &lt;em&gt;while&lt;/em&gt;, and set a &lt;em&gt;continue&lt;/em&gt; flag. First the whiles:&lt;/p&gt;
&lt;p&gt;int i;
int j;
char string[] = &amp;ldquo;This string&amp;rdquo;;
int length = strlen(string);&lt;/p&gt;
&lt;p&gt;i = 0;
while (i &amp;lt;=3) {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;j=0;
while (j&amp;lt;length) {
 printf(&amp;quot;char:%c num:%d\\n&amp;quot;, string\[j\], i);
 if (string\[j\] == 't') {
 break;
 } 
 j++;
}

i++;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;Then the flag, which I&amp;rsquo;ve called &lt;em&gt;keepGoing&lt;/em&gt;:&lt;/p&gt;
&lt;p&gt;int i;
int j;
char string[] = &amp;ldquo;This string&amp;rdquo;;
int length = strlen(string) ;&lt;/p&gt;
&lt;p&gt;int keepGoing = true;
i=0;
while (keepGoing==true &amp;amp;&amp;amp; i &amp;lt;=3) {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;j=0;
while (keepGoing==true &amp;amp;&amp;amp; j&amp;lt;length) {
 printf(&amp;quot;char: %c num: %d\\n&amp;quot;, string\[j\], i);
 if (string\[j\]=='t') {
 keepGoing = false;
 }
 j++
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;This gives us the output we want and we can close the ticket. Note that I have&lt;br&gt;
typedef&amp;rsquo;d &lt;em&gt;true&lt;/em&gt; and &lt;em&gt;false&lt;/em&gt; off-screen in the code above.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;char:T num: 0
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;char:h num: 0
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;char:i num: 0
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;char:s num: 0
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;char: num: 0
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;char:s num: 0
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;char:t num: 0
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With Swift, we can just name the loops, then break out to a named loop level:&lt;/p&gt;
&lt;p&gt;littleLoop: for i in 0&amp;hellip;3 {
bigLoop: for char in &amp;ldquo;This string&amp;rdquo; {
print(&amp;ldquo;char:\(char) num:\(i)&amp;rdquo;)
if char==&amp;ldquo;t&amp;rdquo;{
break littleLoop
}
}
}&lt;/p&gt;
&lt;p&gt;Note that I didn&amp;rsquo;t need to name the internal &lt;em&gt;littleLoop&lt;/em&gt;, that was just showing off.&lt;/p&gt;</description></item><item><title>Checkpoint 9</title><link>https://blog.iankulin.com/checkpoint-9/</link><pubDate>Sat, 20 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/checkpoint-9/</guid><description>&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/-JmAbcISEmY?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;

&lt;p&gt;/*
Your challenge is this: write a function that accepts an
optional array of integers, and returns one randomly.
If the array is missing or empty, return a random number
in the range 1 through 100.&lt;/p&gt;
&lt;p&gt;If that sounds easy, it’s because I haven’t explained
the catch yet: I want you to write your function in a
single line of code. No, that doesn’t mean you should
just write lots of code then remove all the line breaks
– you should be able to write this whole thing in one
line of code.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.hackingwithswift.com/quick-start/beginners/checkpoint-9"&gt;https://www.hackingwithswift.com/quick-start/beginners/checkpoint-9&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;*/&lt;/p&gt;
&lt;p&gt;func randomInt(_ numbers:[Int]?) -&amp;gt; Int { numbers?.randomElement() ?? Int.random(in:1&amp;hellip;100) }&lt;/p&gt;
&lt;p&gt;print(randomInt(nil))
print(randomInt([2,5]))&lt;/p&gt;</description></item><item><title>Visual Studio Code</title><link>https://blog.iankulin.com/visual-studio-code/</link><pubDate>Fri, 19 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/visual-studio-code/</guid><description>&lt;img src="https://blog.iankulin.com/images/visual_studio_code_1.35_icon.svg_.png" width="149" alt=""&gt;
&lt;p&gt;I&amp;rsquo;ve noticed &lt;a href="https://code.visualstudio.com/"&gt;Visual Studio Code&lt;/a&gt; in a few videos, and admired what a clean interface it had, and was impressed how opening a terminal window was automatically in the directory you were working in.&lt;/p&gt;
&lt;p&gt;I had a need to write some html/css, and some C++ in the last couple of days, so that seemed like a great excuse to give it a try. I&amp;rsquo;d have to say my opinion of it has only gone up. Clearly, it is right at home with HTML and CSS - code completion and syntax colouring all working nicely. I followed TechWithTim&amp;rsquo;s suggestion to install the &lt;a href="https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer"&gt;Live Server extension&lt;/a&gt; - which was a completely painless experience.&lt;/p&gt;
&lt;p&gt;Same positive experience when I created a .cpp file, and VSC wanted me to take Microsoft&amp;rsquo;s advice about a bunch of extensions, and correctly suggested I build with Mac&amp;rsquo;s clang compiler.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-08-11-at-3.40.15-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;So many things just work how you expect them to. Within seconds I was trying out breakpoints, compiling and managing the git commits. I even tried the Swift extension.&lt;/p&gt;
&lt;p&gt;I have no intention of leaving Xcode, but with a great tool like this, I will never be a &lt;a href="https://blog.iankulin.com/vim/"&gt;vim&lt;/a&gt; guy.&lt;/p&gt;</description></item><item><title>CSS Intro</title><link>https://blog.iankulin.com/css-intro/</link><pubDate>Thu, 18 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/css-intro/</guid><description>&lt;p&gt;When I wrote my last commercial HTML (in 1996 lol) I&amp;rsquo;m pretty sure there was no CSS. It was the land of textured backgrounds, blinking scrolling text, &amp;ldquo;under construction&amp;rdquo; gifs, and links to &lt;a href="https://en.wikipedia.org/wiki/Gopher_(protocol)"&gt;gopher&lt;/a&gt; URLs were not uncommon. So this is an area I need to update my skills a little just to carry on a coherent conversation in the developer world.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve bumped into a couple of &lt;a href="https://www.techwithtim.net/"&gt;Tech With Tim&lt;/a&gt; &lt;a href="https://www.youtube.com/c/TechWithTim/videos"&gt;videos&lt;/a&gt; recently, and I really liked his CSS intro for &amp;ldquo;Non-web developers&amp;rdquo;.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/ZzoAu4VPyho?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;
</description></item><item><title>Checkpoint 8</title><link>https://blog.iankulin.com/checkpoint-8/</link><pubDate>Wed, 17 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/checkpoint-8/</guid><description>&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/Ga800-Qgft4?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;

&lt;p&gt;/*
Your challenge is this: make a protocol that describes a
building, adding various properties and methods, then
create two structs, House and Office, that conform to it.&lt;/p&gt;
&lt;p&gt;Your protocol should require the following:
A property storing how many rooms it has.
A property storing the cost as an integer
(e.g. 500,000 for a building costing $500,000.)
A property storing the name of the estate agent
responsible for selling the building.
A method for printing the sales summary of the building,
describing what it is along with its other properties.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.hackingwithswift.com/quick-start/beginners/checkpoint-8"&gt;https://www.hackingwithswift.com/quick-start/beginners/checkpoint-8&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;*/&lt;/p&gt;
&lt;p&gt;protocol Building {
var rooms: Int {get set}
var cost: Int {get set}
var realEstateAgent: String {get set}
}&lt;/p&gt;
&lt;p&gt;extension Building {
func printSalesSummary(){
print(&amp;ldquo;Rooms: \(rooms) Cost: £\(cost) Agent: \(realEstateAgent)&amp;rdquo;)
}
}&lt;/p&gt;
&lt;p&gt;struct House: Building {
var rooms: Int
var cost: Int
var realEstateAgent: String
var occupants: Int
}&lt;/p&gt;
&lt;p&gt;struct Office: Building {
var rooms: Int
var cost: Int
var realEstateAgent: String
var floorArea: Int
}&lt;/p&gt;
&lt;p&gt;var myOffice = Office(rooms: 5, cost:500_000, realEstateAgent: &amp;ldquo;Bloggs&amp;rdquo;, floorArea: 500)
var myHouse = House(rooms: 2, cost:200_000, realEstateAgent: &amp;ldquo;Fletchers&amp;rdquo;, occupants: 1)&lt;/p&gt;
&lt;p&gt;myOffice.printSalesSummary()
myHouse.printSalesSummary()&lt;/p&gt;</description></item><item><title>Checkpoint 7</title><link>https://blog.iankulin.com/checkpoint-7/</link><pubDate>Mon, 15 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/checkpoint-7/</guid><description>&lt;p&gt;&lt;a href="https://www.hackingwithswift.com/quick-start/beginners/checkpoint-7"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-08-08-at-8.43.44-pm.jpg" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;/*
Your challenge is this: make a class hierarchy
for animals, starting with Animal at the top,
then Dog and Cat as subclasses, then Corgi and
Poodle as subclasses of Dog, and Persian and Lion
as subclasses of Cat.&lt;/p&gt;
&lt;p&gt;But there’s more:
The Animal class should have a legs integer
property that tracks how many legs the animal has.
The Dog class should have a speak() method that
prints a generic dog barking string, but each of
the subclasses should print something slightly
different.
The Cat class should have a matching speak() method,
again with each subclass printing something
different.
The Cat class should have an isTame Boolean property,
provided using an initializer.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.hackingwithswift.com/quick-start/beginners/checkpoint-7"&gt;https://www.hackingwithswift.com/quick-start/beginners/checkpoint-7&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;*/&lt;/p&gt;
&lt;p&gt;class Animal {
var legs = 4
init(legs: Int) {
self.legs = legs
}
}&lt;/p&gt;
&lt;p&gt;class Dog: Animal {
func speak() { print(&amp;ldquo;woof&amp;rdquo;) }
}&lt;/p&gt;
&lt;p&gt;class Corgi: Dog {
override func speak() { print(&amp;ldquo;Your Majesty?&amp;rdquo;) }
}&lt;/p&gt;
&lt;p&gt;class Poodle: Dog {
override func speak() { print(&amp;ldquo;yip&amp;rdquo;) }
}&lt;/p&gt;
&lt;p&gt;class Cat: Animal {
var isTame: Bool&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;init (isTame: Bool, legs: Int) {
 self.isTame = isTame
 super.init(legs: legs)
}

func speak() { print(&amp;quot;meow&amp;quot;) }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;class Persian: Cat {
override func speak() { print(&amp;ldquo;hiss&amp;rdquo;) }&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;init() { super.init (isTame: true, legs: 4 ) }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;class Lion: Cat {
override func speak() { print(&amp;ldquo;rawr&amp;rdquo;) }&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;init() { super.init (isTame: false, legs: 4 ) }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;let lion = Lion()
print(lion.legs)&lt;/p&gt;</description></item><item><title>Scope Creep</title><link>https://blog.iankulin.com/scope-creep/</link><pubDate>Sun, 14 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/scope-creep/</guid><description>&lt;p&gt;&lt;a href="https://www.inc.com/lolly-daskal/7-reasons-why-you-need-to-embrace-procrastination.html"&gt;&lt;img src="https://blog.iankulin.com/images/getty_492816326_104863.jpg" width="441" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In project management, and especially in programming &amp;ldquo;scope creep&amp;rdquo; refers to the common situation where what&amp;rsquo;s required to regard a project as finished keeps growing. Most commonly, in the form of extra features required to be added to an application. I&amp;rsquo;ve especially seen this when clients see early versions of applications and it prompts them to request things that were not in the original specification.&lt;/p&gt;
&lt;p&gt;My iOS development learning journey has been experiencing some of this as well. I started out with only four clear goals:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Complete the Standford CS193p course&lt;/li&gt;
&lt;li&gt;Complete Hacking with SwiftUI&lt;/li&gt;
&lt;li&gt;Get an app in the app store&lt;/li&gt;
&lt;li&gt;Document my progress by blogging my learning&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Some of the things I have been adding are easily justified - getting my head around git &amp;amp; GitHub make a lot of sense to add. Others (I&amp;rsquo;m looking at you the two hours I spent learning about customising my zsh shell), not so much.&lt;/p&gt;
&lt;p&gt;The overarching goal is to be a competent iOS developer (at something around the junior dev level) in a year of part time study. Which I think I&amp;rsquo;m probably on track for, but I do keep thinking of things to add. For example, my HTML knowledge pre-dates CSS, so that probably needs a weekend spent on it so I&amp;rsquo;m at least aware of what I don&amp;rsquo;t know. Similarly, websites are no longer based on PHP - I&amp;rsquo;m sure any CS grads would have some JavaScript skills in their toolkit. Also, as part of developing an application, I should probably be able to standup an SQL server on AWS or similar with the required security.&lt;/p&gt;
&lt;p&gt;Much to do.&lt;/p&gt;</description></item><item><title>@ObservedObject v @StateObject</title><link>https://blog.iankulin.com/observedobject-v-stateobject/</link><pubDate>Sat, 13 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/observedobject-v-stateobject/</guid><description>&lt;p&gt;The Youtube algorithm thinks I need to watch more MVVM videos, and it turns out it&amp;rsquo;s probably right. A day or two ago in an &lt;a href="https://blog.iankulin.com/simple-mvvm/"&gt;MVVM&lt;/a&gt; post using a super simple example, I stored the view model as a property of the view using the @ObservedObject wrapper, as I created it.&lt;/p&gt;
&lt;p&gt;struct ContentView: View {
@ObservedObject var light = LightViewModel()&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var body: some View {
 VStack{
 Spacer()
 if light.isOn(){
 drawLitBulb
 }
 else{
 Image(systemName: &amp;quot;lightbulb.fill&amp;quot;).font(.system(size: 72))
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But then today, Youtube served me up this video from &lt;a href="https://www.youtube.com/c/BeyondOnesAndZeros/videos"&gt;BeyondOnesAndZeros&lt;/a&gt;&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/LntH6moCuo0?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;

&lt;p&gt;They start off with @ObservableObject, but then say that if the View Model is instantiated there, that this is repeated every time the View is recreated (which happens every time it&amp;rsquo;s redrawn).&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t really understand the property wrappers (like @ObservableObject) but I assume this was a type property - ie static. Apparently not. The &lt;a href="https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app"&gt;Apple documentation on managing data&lt;/a&gt; says:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;SwiftUI might create or recreate a view at any time, so it’s important that initializing a view with a given set of inputs always results in the same view. As a result, it’s unsafe to create an observed object inside a view. Instead, SwiftUI provides the &lt;a href="https://developer.apple.com/documentation/swiftui/stateobject"&gt;&lt;code&gt;StateObject&lt;/code&gt;&lt;/a&gt; attribute for this purpose.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So that code should use @StateObject, and @ObservedObject should be used where I pass it down into other view structs in the hierarchy.&lt;/p&gt;
&lt;p&gt;Paul Hudson gives a good example in his explanation on this topic &lt;a href="https://www.avanderlee.com/swiftui/stateobject-observedobject-differences/"&gt;here&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>Rust</title><link>https://blog.iankulin.com/rust/</link><pubDate>Fri, 12 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/rust/</guid><description>&lt;img src="https://blog.iankulin.com/images/rustmemelovetriangle_297886754.jpg" width="375" alt=""&gt;
&lt;p&gt;It&amp;rsquo;s been exciting to see some of the modern language features in Swift - it&amp;rsquo;s a real joy to work in when I think back to my C++ days which was some of the last commercial programming I did.&lt;/p&gt;
&lt;p&gt;The buzz about Carbon got me wondering about other new languages and what might be going on with them. Rust seems to keep popping up in conversations so I thought I&amp;rsquo;d have a quick look.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/br3GIIQeefY?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;

&lt;p&gt;Also read &lt;a href="https://fasterthanli.me/articles/a-half-hour-to-learn-rust"&gt;A half-hour to learn Rust&lt;/a&gt; and &lt;a href="https://faq.sealedabstract.com/rust/"&gt;A Swift Guide to Rust&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Those optionals, type inference, type safety, and exhaustive switch/match statements sure look familiar. Ranges too, and although the infinite range looks cool I&amp;rsquo;m not sure of the use-case.&lt;/p&gt;
&lt;p&gt;Blocks evaluating to a result is cute. This is allowed:&lt;/p&gt;
&lt;p&gt;let x = {
let y = 1; // first statement
let z = 2; // second statement
y + z // this is the *tail* - what the whole block will evaluate to
};&lt;/p&gt;
&lt;p&gt;Notice the missing semicolon - that&amp;rsquo;s sugar for the &amp;lsquo;return&amp;rsquo; that would be otherwise needed. Perhaps only from habit, I do miss the semicolons when writing Swift. I&amp;rsquo;m sure I&amp;rsquo;ll get used to it, but currently I start to feel uncomfortable when spreading an expression out over multiple lines (for clarity) and just expecting LVM to figure it all out.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; Button(&amp;quot;Toggle Light&amp;quot;, action: {
 light.toggle()}
 )
 .padding()
 .font(.title)
 .foregroundColor(.white)
 .background(Color.accentColor)
 .cornerRadius(10)
 .padding()
 Spacer()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Underscore as a &amp;ldquo;throwaway&amp;rdquo; value turns up, but in the guise of default for the match statement.&lt;/p&gt;
&lt;p&gt;Variable bindings, by default (with &amp;rsquo;let&amp;rsquo;) are immutable in Rust, but can be marked as &amp;rsquo;let mut&amp;rsquo; to make them fully variable, as with Swift&amp;rsquo;s &amp;lsquo;var&amp;rsquo;.&lt;/p&gt;
&lt;p&gt;Rust has &amp;ldquo;traits&amp;rdquo;, which currently my knowledge of Swift can&amp;rsquo;t do justice to a compare and contrast, but it definitely has a superclassy feel - like protocols and extensions.&lt;/p&gt;
&lt;p&gt;They both have closures, but again, I&amp;rsquo;m getting out of my current depth on Swift to make any worthwhile comment.&lt;/p&gt;
&lt;p&gt;I do enjoy about Swift how clear it is to read, from what I&amp;rsquo;ve seen of Rust, that&amp;rsquo;s not so much the case there, I guess there&amp;rsquo;s some other trade off involved.&lt;/p&gt;</description></item><item><title>Vim</title><link>https://blog.iankulin.com/vim/</link><pubDate>Wed, 10 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/vim/</guid><description>&lt;img src="https://blog.iankulin.com/images/1-_bwvjb2jzuuzyxgxm6xwqq.png" width="191" alt=""&gt;
&lt;p&gt;I&amp;rsquo;ve been working through the &lt;a href="https://missing.csail.mit.edu/"&gt;Missing Semester&lt;/a&gt; lectures from MIT, and recently completed the lecture about the &lt;a href="https://github.com/vim/vim"&gt;Vim editor&lt;/a&gt;. Vim is a test editor, called from the command line, and optimised for programming - in the sense that it assumes most of the use of the editor is navigating around a big text file making small changes rather than entering large amount of test.&lt;/p&gt;
&lt;p&gt;It uses simple, short key presses (as opposed to mouse movements or using menus or toolbars) to achieve things. This makes it highly efficient for good typists who know all the commands, and slightly incomprehensible to those who do not. An additional level of complexity is the idea of modes. Vim has several modes, the main ones being:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Normal - for navigating around and making those little edits. To get into this mode press the esc key&lt;/li&gt;
&lt;li&gt;Insert - for entering text - ie the mode you&amp;rsquo;d assume you were in when opening an editor and not that you would have to press the letter &amp;lsquo;i&amp;rsquo; to make that happen&lt;/li&gt;
&lt;li&gt;Command - to run commands - like saving or closing VIM To get into this mode press the &amp;lsquo;:&amp;rsquo; key&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Apart from &lt;a href="https://missing.csail.mit.edu/2020/editors/"&gt;this lecture&lt;/a&gt; here are many good guides for learning VIM, a couple I&amp;rsquo;ve looked at are &lt;a href="https://opensource.com/article/19/3/getting-started-vim"&gt;this one from OpenSource.com&lt;/a&gt; and &lt;a href="https://web.stanford.edu/class/cs107/resources/vim.html"&gt;this one from Stanford&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Even though I will never invest the time to become a power user of Vim, it&amp;rsquo;s available most places you&amp;rsquo;ll be using the command line, so the basics are a requirement for any programmer. Plus if you&amp;rsquo;re ever hired by a film production company to advise on a computer hacking scene, you&amp;rsquo;ll need it to scroll though some syntax highlighted Javascript.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.vimcheatsheet.com/"&gt;&lt;img src="https://blog.iankulin.com/images/iigrixvxp5ayn9ox7gr1dfi_rhlrotwllscafjjqjeq.webp" alt=""&gt;&lt;/a&gt;
&lt;em&gt;Great cheat sheet from &lt;a href="https://www.vimcheatsheet.com/"&gt;https://www.vimcheatsheet.com/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;</description></item><item><title>Firebase</title><link>https://blog.iankulin.com/firebase/</link><pubDate>Tue, 09 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/firebase/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-08-06-at-10.01.15-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;In the category of &amp;ldquo;getting ahead of myself&amp;rdquo;, my list of potential future apps to develop includes an entry for a simple accounting system. I guess transactions could live in SQLite, on iCloud for sharing between Mac and mobile devices. Nevertheless, while noodling about it, I thought I should think about it being fully online, and I&amp;rsquo;d heard Google&amp;rsquo;s Firebase (probably referring for specifically to &lt;a href="https://firebase.google.com/products/firestore"&gt;Firestore&lt;/a&gt;) mentioned in several iOS developer podcasts and thought I should take a look.&lt;/p&gt;
&lt;p&gt;Having consumed a few &lt;a href="https://www.youtube.com/watch?v=XhD_Y6kLJqk"&gt;CodeWithChris videos&lt;/a&gt; on the topic, that certainly seems like it would fit the specifications. A simple SQL database on a device is probably a more realistic goal, but it leaves some syncing complications that a cloud based database at least partly addresses.&lt;/p&gt;</description></item><item><title>iOS Academy</title><link>https://blog.iankulin.com/ios-academy/</link><pubDate>Mon, 08 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/ios-academy/</guid><description>&lt;p&gt;I seem to consume a lot of &lt;a href="https://iosacademy.io/"&gt;iOS Academy&lt;/a&gt; videos with great, short (&amp;lt; 10 minute) explanations of various Swift iOS programming topics - particularly little UI topics, like this one on the Grid View.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/A3jOHj9erQ4?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;

&lt;p&gt;I really appreciate the generous content provision in the Swift and iOS development community. Perhaps this is the same for lots of technologies, but for someone who started programming pre-internet, it&amp;rsquo;s a stark difference to how I used to learn - so many magazines, and so many 2&amp;quot; thick books.&lt;/p&gt;</description></item><item><title>MVVM Explained</title><link>https://blog.iankulin.com/mvvm-explained/</link><pubDate>Sun, 07 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/mvvm-explained/</guid><description>&lt;p&gt;The first nine minutes of &lt;a href="https://www.youtube.com/watch?v=sLHVxnRS75w"&gt;this video&lt;/a&gt; from &lt;a href="https://twitter.com/Its_Macco"&gt;Emmanuel Okwara&lt;/a&gt; finally gave me a clear understanding of the difference between MVC and MVVM.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/sLHVxnRS75w?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;

&lt;p&gt;In both MVC and MVVM the data &amp;amp; logic (Model) are separated from the part that the user interacts (View). Usually the View is a screen with controls and so on, but that&amp;rsquo;s not compulsory - for example a voice mail app interface would be all audio and DTMF. The point is that in both, the user interface (view) does not mess directly with the data (model) - it has to go through some sort of gatekeeper.&lt;/p&gt;
&lt;p&gt;The new understanding I got from Emmanuel is that in MVVM, the View Model does not know what is in the View. It does not alter the view, just broadcasts that there&amp;rsquo;s been a change and lets the view go ahead and update itself. It makes sense that this would be the paradigm for SwiftUI&amp;rsquo;s declarative interface style, and is also (I imagine but actually have no idea) the basis for React.js&lt;/p&gt;
&lt;p&gt;One thing Emmanuel mentions that I&amp;rsquo;m not clear on is that each View will have it&amp;rsquo;s own View Controller. Currently all my tiny apps have had one view in the SwiftUI sense. I have pulled out sub views, and some have had views within views - for example with the Navigation View. So I guess my question would be, &amp;ldquo;What constitutes a View in MVVM?&amp;rdquo;&lt;/p&gt;
&lt;p&gt;I noped out at the nine minute mark as soon as Interface Builder showed it&amp;rsquo;s face. I&amp;rsquo;m an iOS15 SwiftUI baby - I will, eventually, need to learn the old magic, but competence developing iOS apps using SwiftUI current methods is the MVP.&lt;/p&gt;
&lt;p&gt;With this understanding, and having finished lecture 4 from the cs193p series, I think a good project for today would be the simplest possible MVVM app with the correct separations and bindings.&lt;/p&gt;</description></item><item><title>Oh My Zsh</title><link>https://blog.iankulin.com/oh-my-zsh/</link><pubDate>Sat, 06 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/oh-my-zsh/</guid><description>&lt;p&gt;I&amp;rsquo;ve been playing in the zsh shell since I started on the &lt;a href="https://blog.iankulin.com/missing-semester/"&gt;Missing Semester&lt;/a&gt;, and was wondering how to get my git branch name in the prompt. A few googles later, I&amp;rsquo;ve installed Oh My Zsh, and added the git and macos plugins. Pretty.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-08-02-at-7.26.08-pm.jpg" alt=""&gt;&lt;/p&gt;</description></item><item><title>Missing Semester</title><link>https://blog.iankulin.com/missing-semester/</link><pubDate>Fri, 05 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/missing-semester/</guid><description>&lt;p&gt;On my git odyssey yesterday, I came across which is an MIT class for CS about practical things CS students don&amp;rsquo;t strictly need for their degree, but will greatly benefit from. I was interested in their git introduction, but they explain &lt;a href="https://missing.csail.mit.edu/"&gt;the course&lt;/a&gt; by saying:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://missing.csail.mit.edu/"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-08-01-at-8.50.12-pm.jpg" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Videos of the lectures, and all the course notes and assignments are freely made available. I&amp;rsquo;ve only watched the first lecture about the shell, and their git lecture. Both were excellent, so I&amp;rsquo;ll add this series to my goals.&lt;/p&gt;</description></item><item><title>Chris Lattner</title><link>https://blog.iankulin.com/chris-lattner/</link><pubDate>Wed, 03 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/chris-lattner/</guid><description>&lt;p&gt;Thank you YouTube algorithm for this recommendation - Chris Lattner, the main author of Swift (amongst other things including LVM) chatting with Lex Fridman. Ignore the clickbait title. There is a good, brief discussion about the tradeoffs in value vs references types which is a topic I&amp;rsquo;ve been thinking a bit about this week.&lt;/p&gt;
&lt;p&gt;Also some interesting comments about how a language delivers it&amp;rsquo;s complexity. Chris gives the funny example of what &amp;ldquo;hello world&amp;rdquo; looks like in Swift vs C++. Here&amp;rsquo;s Swift: &lt;code&gt;Print(&amp;quot;Hello world&amp;quot;)&lt;/code&gt;, here&amp;rsquo;s C++:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c++" data-lang="c++"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#5e81ac;font-style:italic"&gt;#include&lt;/span&gt; &lt;span style="color:#5e81ac;font-style:italic"&gt;&amp;lt;iostream&amp;gt;&lt;/span&gt;&lt;span style="color:#5e81ac;font-style:italic"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;int&lt;/span&gt; &lt;span style="color:#88c0d0"&gt;main&lt;/span&gt;&lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; std&lt;span style="color:#81a1c1"&gt;::&lt;/span&gt;cout &lt;span style="color:#81a1c1"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Hello World!&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; &lt;span style="color:#b48ead"&gt;0&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Especially when part of my interest is in exciting kids in programming, that&amp;rsquo;s a stark difference. Swift does go on to do the hard things - it&amp;rsquo;s used for native apps and has some of the great modern language features, but the simple things are easy. I am very happy with the idea of Swift (especially plus Playgrounds) being a smooth introduction to coding. Less so with SwiftUI - that gets complicated quickly when things go wrong.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/UTFFR61xVbs?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;
</description></item><item><title>Retain Cycle</title><link>https://blog.iankulin.com/retain-cycle/</link><pubDate>Tue, 02 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/retain-cycle/</guid><description>&lt;p&gt;Variables and constants in Swift can be a &lt;em&gt;value type&lt;/em&gt; (their data is copied when they are copied) or a &lt;em&gt;reference type&lt;/em&gt; (a pointer to the data is passed when they are copied.&lt;/p&gt;
&lt;p&gt;Structs, integers, and enums are value types, classes are reference types.&lt;/p&gt;
&lt;p&gt;Memory management of value types is relatively straightforward - there’s a 1:1 relationship between the variable name and its data, so if the variable goes out of scope it can get the chop. With reference types, it’s possible to have several variables (or class or struct properties etc) all pointing to the data, so a more sophisticated system is needed to know when it’s safe to delete the data.&lt;/p&gt;
&lt;p&gt;In Swift (and some other languages), this memory management is done by Automatic Reference Counting ARC. The compiler inserts code that keeps track of what references exist in scope that point to the data in memory that could potentially be freed. When there are zero references exisiting for an object, it can be freed. Here’s an example, meet SomeClass:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;class&lt;/span&gt; SomeClass&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; name&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#bf616a"&gt;String&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; otherClass&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; SomeClass&lt;span style="color:#bf616a"&gt;?&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; init&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#bf616a"&gt;String&lt;/span&gt;&lt;span style="color:#eceff4"&gt;){&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;self&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;name &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; name
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; otherClass &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; nil
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;print&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;init&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; deinit&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;print&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;deinit&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This class has a couple of properties; a name and a link to another object of the same class. The &lt;strong&gt;init&lt;/strong&gt; and &lt;strong&gt;deinit&lt;/strong&gt; methods are called at creation and destruction. I’ve added &lt;strong&gt;print()&lt;/strong&gt; so we can see them.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;func&lt;/span&gt; classCreationFunction&lt;span style="color:#eceff4"&gt;(){&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;print&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;start&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; class1 &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; SomeClass&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;class1&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; class2 &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; SomeClass&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;class2&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; class1&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;otherClass &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; class2
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;print&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;end&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If we run this function, the output will be:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;start
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;init
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;init
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;end
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;deinit
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;deinit
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Two instances created, two released. All this is done for us (the Automatic in ARC), when I was programming in Delphi, it was often the responsibility of the programmer to explicitly deal with this problem.&lt;/p&gt;
&lt;p&gt;A potential problem with ARC is &lt;em&gt;retain cycles&lt;/em&gt;. A retain cycle is where objects (or often a chain of objects) hold references to each other. We’re done with the objects, but because they are holding the references to each other, the references have not been counted down to zero, and therefore ARC does not kill them off. In classCreationFunction above, one instance has a reference to another, and ARC cleans them both up.&lt;/p&gt;
&lt;p&gt;What happens if we have each instance hold a reference to each other? Something like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;func&lt;/span&gt; classCreationFunction&lt;span style="color:#eceff4"&gt;(){&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;print&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;start&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; class1 &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; SomeClass&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;class1&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; class2 &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; SomeClass&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;name&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;class2&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; class1&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;otherClass &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; class2
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; class2&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;otherClass &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; class1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;print&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;end&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The output of this is:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;start
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;init
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;init
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;end
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Two instances of SomeClass created, none destroyed.&lt;/p&gt;</description></item><item><title>Bump One</title><link>https://blog.iankulin.com/bump-one/</link><pubDate>Mon, 01 Aug 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/bump-one/</guid><description>&lt;p&gt;Most of the things I’ve learned so far have been familiar, interesting, or cool - but now I’ve ventured far enough into the Swift Language Programming book to find something that is definitely going to take a couple of readings to piece together.&lt;/p&gt;
&lt;p&gt;I was surprised, then pleased with functions as first class types, and the idea of passing closures around is powerful and useful.&lt;/p&gt;
&lt;p&gt;My current difficulty is getting my head around closures capturing variables. It was tolerable (but not safe) when I just thought of it as a pointer, but when turned out the captured variable continues to exist in some sort of zombie state even after the scope where the variable was contained has ended.&lt;/p&gt;
&lt;p&gt;To go back a bit, nested functions have access to the variables declared in the scope they are nested in.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;func&lt;/span&gt; someFunction&lt;span style="color:#eceff4"&gt;(){&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; someInt &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#b48ead"&gt;4&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; nestedFunction&lt;span style="color:#eceff4"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;print&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;someInt&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;func&lt;/span&gt; nestedFunction&lt;span style="color:#eceff4"&gt;(){&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; someInt &lt;span style="color:#81a1c1"&gt;+=&lt;/span&gt; &lt;span style="color:#b48ead"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I don’t approve of this. It has a global variable flavour. For most purposes I’d rather pass and return the values so the intent is all contained. Nevertheless, I can see it’s a valid approach that might be useful.&lt;/p&gt;
&lt;p&gt;I can’t explain the next bit any better than the Swift book, so here’s it’s opening on &lt;a href="https://docs.swift.org/swift-book/LanguageGuide/Closures.html"&gt;Capturing Values&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/34d3cb1a-730f-4afb-aee3-9c147dd3fabe.jpeg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Here’s my slight edit to the code from the book to get it to print out the incrementing values. This prints ”10” and ”20” to the console.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;func&lt;/span&gt; makeIncrementer&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;forIncrement amount&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; Int&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;-&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;-&amp;gt;&lt;/span&gt; Int &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; runningTotal &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#b48ead"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;func&lt;/span&gt; incrementer&lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;-&amp;gt;&lt;/span&gt; Int &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; runningTotal &lt;span style="color:#81a1c1"&gt;+=&lt;/span&gt; amount
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; runningTotal
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; incrementer
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;let incrementByTen &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; makeIncrementer&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;forIncrement&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#b48ead"&gt;10&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;print&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;incrementByTen&lt;span style="color:#eceff4"&gt;())&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;print&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;incrementByTen&lt;span style="color:#eceff4"&gt;())&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So, makeIncrementer returns its nested incrementing function which has “captured” the runningTotal variable. The scope that runningTotal lives in has gone (when makeIncrementer finished) but runningTotal is still alive. I assume that this deep magic is made possible by our friend Automatic Reference Counting. This variable capture in closures seems like a weapon that needs wielded with great care.&lt;/p&gt;</description></item><item><title>Unwrap</title><link>https://blog.iankulin.com/unwrap/</link><pubDate>Sun, 31 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/unwrap/</guid><description>&lt;p&gt;Unwrap is the Paul Hudson app for Swift learning. It’s good for using those three minute gaps in life to digest a concept. I’ve incorporated it into my goals, as some days its the only progress I make.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/fa3cfadd-f6ef-4a05-9131-be5de8f38291.jpeg" width="501" alt=""&gt;</description></item><item><title>Xcode Refactor/Rename</title><link>https://blog.iankulin.com/xcode-refactor-rename/</link><pubDate>Sat, 30 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/xcode-refactor-rename/</guid><description>&lt;p&gt;This is cool. You can right click on a variable (and I guess any other) name and change it everywhere. No more tedious search and replace.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/SXSQgtGREKw?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;
</description></item><item><title>@ScaledMetric</title><link>https://blog.iankulin.com/scaledmetric/</link><pubDate>Fri, 29 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/scaledmetric/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-07-23-at-9.04.21-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I solved the problem (well, I googled a &lt;a href="https://stackoverflow.com/questions/72568296/sf-symbol-images-different-sizes"&gt;stackoverflow result&lt;/a&gt; to the problem) in the previous post about the different heights of the SF Symbols. The answer was to put them in a frame and lock the height. A problem that then arises from that is that when the user changes the text size, they&amp;rsquo;ll be out of wack. Apple&amp;rsquo;s solution to that, introduced in iOS 14 is the &lt;a href="https://developer.apple.com/documentation/swiftui/scaledmetric"&gt;@ScaledMetric property wrapper&lt;/a&gt; that does some magic I don&amp;rsquo;t fully understand yet.&lt;/p&gt;</description></item><item><title>Memorise Assignment 1</title><link>https://blog.iankulin.com/memorise-assignment-1/</link><pubDate>Thu, 28 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/memorise-assignment-1/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-07-23-at-7.33.03-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;A small milestone achieved - I&amp;rsquo;ve completed the first assignment from the CS193p lecture series - some minor changes to the app being built in the lectures. There was a couple of things I was unhappy with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The text under the SF Symbols you can see in the preview above not being vertically aligned.&lt;/li&gt;
&lt;li&gt;Having duplicated code in my emoji arrays:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; let animalEmojis &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#eceff4"&gt;[&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;🐠&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;🐢&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;🦋&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;🐥&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;🐣&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;🐰&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;🐝&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;🦄&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;🐵&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;🐛&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; let weatherEmojis &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#eceff4"&gt;[&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;🌪&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;🌝&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;🌈&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;🔥&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;🌧&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;🌙&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;🌬&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;☃️&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;☔️&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;🌫&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; let transportEmojis &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#eceff4"&gt;[&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;🚗&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;🚕&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;🚲&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;🚚&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;🛵&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;🚜&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;🛴&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;🛺&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;🚃&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;🚡&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; I&lt;span style="color:#a3be8c"&gt;&amp;#39;m not happy with this duplication //TODO&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#bf616a"&gt;@&lt;/span&gt;State &lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; emojis &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#eceff4"&gt;[&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;🐠&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;🐢&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;🦋&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;🐥&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;🐣&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;🐰&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;🐝&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;🦄&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;🐵&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;🐛&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This second problem is because I couldn&amp;rsquo;t just&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#bf616a"&gt;@&lt;/span&gt;State &lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; emojis &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; animalEmojis
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When I tried it, I encountered the error:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Cannot use instance member &amp;#39;animalEmojis&amp;#39; within property initializer; property initializers run before &amp;#39;self&amp;#39; is available
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is vexing - the constants are defined on the lines above, so surely if this property exists, the ones before it do. Apparently that can&amp;rsquo;t be depended on - probably for some good reason that will be unveiled at some stage. It&amp;rsquo;s not because &lt;code&gt;emojis&lt;/code&gt; is wrapped in the &lt;code&gt;@State&lt;/code&gt; which probably does cause the variable to be created off somewhere else - I tried with just an ordinary var and had the same issue.&lt;/p&gt;
&lt;p&gt;Then I read further down into the assignment, there&amp;rsquo;s a &amp;ldquo;hints section&amp;rdquo; which I should clearly be reading before I being. C&amp;rsquo;s get degrees.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-07-23-at-7.42.54-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Message received. Don&amp;rsquo;t worry about the doubled up string array, and go back and watch the lecture again. There was something about locking an aspect ratio for an SF Symbol at some stage.&lt;/p&gt;</description></item><item><title>SwiftUI Essentials</title><link>https://blog.iankulin.com/swiftui-essentials/</link><pubDate>Wed, 27 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/swiftui-essentials/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-07-23-at-4.12.38-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I hadn&amp;rsquo;t fully gotten my head around what&amp;rsquo;s going on with the declarative nature of SwiftUI, until I&amp;rsquo;d watched this video&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s from the &lt;a href="https://developer.apple.com/videos/play/wwdc2019/216/"&gt;2019 WWDC&lt;/a&gt; which is when (I guess) SwiftUI was new. I still don&amp;rsquo;t have a good handle on how the views are bound to their data, but there is a video from this same series about Data Flows which I imagine will also answer those questions.&lt;/p&gt;
&lt;p&gt;Of course, I could also have been pushing forwards on the two courses I&amp;rsquo;m undertaking - no doubt they are about to teach me these same things, but at least I&amp;rsquo;m procrastinating constructively.&lt;/p&gt;</description></item><item><title>iOS Dev Weekly</title><link>https://blog.iankulin.com/ios-dev-weekly/</link><pubDate>Tue, 26 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/ios-dev-weekly/</guid><description>&lt;p&gt;Dave Verwer&amp;rsquo;s &lt;a href="https://iosdevweekly.com/"&gt;iOS Dev Weekly&lt;/a&gt; digest of links mainly about Swift libraries was mentioned on a podcast I was listening to last night - perhaps the &lt;em&gt;Swift with Sundell&lt;/em&gt; &lt;a href="https://www.swiftbysundell.com/podcast/16/"&gt;chat with Sommer Panage&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;My &lt;a href="https://iosdevweekly.com/issues/568?#start"&gt;first issue&lt;/a&gt; (it&amp;rsquo;s an email newsletter) arrived, and it&amp;rsquo;s pretty great. Not too long, chatty but on topic, and with links to follow for more info. As well as new or improved libraries, other topics are mentioned - I went down a rabbit hole on &lt;a href="https://useyourloaf.com/blog/swiftui-split-view-configuration/"&gt;SwiftUI Split View Configuration&lt;/a&gt;, ending up at this WWDC video about it.&lt;/p&gt;</description></item><item><title>Xcode Tour</title><link>https://blog.iankulin.com/xcode-tour/</link><pubDate>Sun, 24 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/xcode-tour/</guid><description>&lt;p&gt;If you need a solid tour of the basics plus of Xcode, this is a great video from Karin Prater. Its the first video in her &amp;ldquo;Design-oriented course on SwiftUI&amp;rdquo;.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/EDHl1r5vw6Q?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;
</description></item><item><title>SF Symbols</title><link>https://blog.iankulin.com/sf-symbols/</link><pubDate>Thu, 21 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/sf-symbols/</guid><description>&lt;p&gt;A couple of times in the App Development seminar I went to, we used system symbols in the place of images, and in his tutorial on Swift UI Basics, Sean Allen spent a few minutes talking about where they come from and how to choose them.&lt;/p&gt;
&lt;p&gt;First, here&amp;rsquo;s how they look in code - this is from the default Hello World app.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;struct ContentView&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; View &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; body&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; some View &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; VStack &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#bf616a"&gt;Image&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;systemName&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;globe&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;imageScale&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;large&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;foregroundColor&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;accentColor&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Hello world&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2022-07-17-at-7.26.23-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-07-17-at-7.26.23-am.png" width="128" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;systemName&lt;/code&gt; parameter signifies the image is this type, and &lt;code&gt;&amp;quot;globe&amp;quot;&lt;/code&gt; is the name of the image. The code above draws a globe with some lat/long lines. So where does &amp;ldquo;globe&amp;rdquo; come from, and how can I find and choose them?&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s where &amp;ldquo;SF Symbols&amp;rdquo; comes in. This is &lt;a href="https://developer.apple.com/sf-symbols/"&gt;an app&lt;/a&gt; containing a collection of over 4000 (in version 4) symbols that work well with the default San Francisco font, that can be scaled and in many cases coloured. These symbols are a kind of standard that (increasingly) users will recognise - so this also supports good user interfaces.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-07-17-at-6.50.30-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s straightforward to search for symbols and to view them in the colour combinations you&amp;rsquo;re using in your app (if that particular symbol supports it).&lt;/p&gt;</description></item><item><title>Passing Data</title><link>https://blog.iankulin.com/passing-data/</link><pubDate>Wed, 20 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/passing-data/</guid><description>&lt;p&gt;Sean Allen has come to my notice a couple of times, once where he was mentioned as freelance contractor who is a great contributor to the community (I think perhaps that was on &lt;a href="https://podcasts.apple.com/au/podcast/swiftcoders-interviews-with-swift-developers/id1082937962"&gt;Swiftcoders Podcast&lt;/a&gt;), and I&amp;rsquo;ve also bumped into him as co-host (with Paul Hudson) of the early episodes of the &amp;ldquo;&lt;a href="https://podcasts.apple.com/au/podcast/swift-over-coffee/id1435076502"&gt;Swift over Coffee&lt;/a&gt;&amp;rdquo; podcast.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/HXoVSbwWUIk?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;

&lt;p&gt;This video I watched last night is a compilation of the first few videos of &lt;a href="https://seanallen.teachable.com/p/swiftui-fundamentals"&gt;Sean&amp;rsquo;s SwiftUI course&lt;/a&gt;, and it&amp;rsquo;s pretty great. In particular he does a great job of explaining how to start to refactor child views out and call them, and how all the stacks go together to make a pretty interface. What he does not do is vist/explain any of the Swift language fundamentals. If you don&amp;rsquo;t already know what a struc is, and the Swift flavour of them, it may be a challenging place to start.&lt;/p&gt;
&lt;p&gt;In the last couple of tutorials he starts on the way the views are called, and how we can pass values into them. This is great marketing for me - it&amp;rsquo;s exactly where I&amp;rsquo;m up to in my journey - I&amp;rsquo;m perplexed about the structure of a SwiftUI app (where&amp;rsquo;s main?!) and the engine that&amp;rsquo;s watching when the UI needs updated and building the views. For example, I want to write a little hello world that just prints the time on the screen. I got this far:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;struct ContentView&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; View &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; let today &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; Date&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;now
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; body&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; some View &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Text&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;today&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; style&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;time&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;But then a minute later when it needs changed what happens? In traditional programming there would be a loop in main where I could check the minutes have changed, and force the redraw of the label.&lt;/p&gt;
&lt;p&gt;I feel this is the thing that&amp;rsquo;s going to be explained clearly in the next few videos of Sean&amp;rsquo;s course, which is cunningly behind the paywall. I&amp;rsquo;m very tempted, although my learning is already spread over two proper courses plus a shotgun blast of other reading, podcasts and self created programming tasks.&lt;/p&gt;
&lt;p&gt;I did have one moment of confusion when Sean passes down an @State variable to a child view and said the variable in the child view needed to be made @Binding. One of the top comments points this out as being unneeded and Sean agrees.&lt;/p&gt;</description></item><item><title>App Development in Swift Playgrounds</title><link>https://blog.iankulin.com/app-development-in-swift-playgrounds/</link><pubDate>Tue, 19 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/app-development-in-swift-playgrounds/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/img_1938.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;During the week I attended &amp;ldquo;App Development in Swift Playgrounds&amp;rdquo; run by &lt;a href="https://www.mattrichards.net.au/"&gt;Matt Richards&lt;/a&gt; with the support of some of the Apple team and hosted by &lt;a href="https://twitter.com/mssgellis"&gt;Dr Michelle Ellis&lt;/a&gt;. It was aimed a teachers looking at using Playgrounds for digi-tech teaching.&lt;/p&gt;
&lt;p&gt;The day included pulling apart one of the Playgrounds apps and rebuilding it - this being an example of a &amp;ldquo;top-down&amp;rdquo; approach - starting with a complete app and fiddling around with it - to better engage students. The alternative being a bottom-up approach where lesson one would be &amp;ldquo;good morning students, this is a variable, it can hold a value, it has a name we can use to refer to the value&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;This is of particular interest since in my learning journey I am trying to do both - I&amp;rsquo;m working through the &lt;em&gt;100 days of&lt;/em&gt;, as well as having a stab at building my first app. The &lt;a href="https://cs193p.sites.stanford.edu/"&gt;cs193p&lt;/a&gt; course also is tackling the problem from both ends - students start building the app in their first lecture, but also their first home reading assignment is 80% of the &lt;a href="https://docs.swift.org/swift-book/"&gt;Swift Programming Language book&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Clearly this top-down approach is highly engaging, but the cost is that when students (or me in this case) run into problems, they get super-complex super-quick. Even Swift noobs at the course were able to confidently alter the existing app to look quite different, but many (including me) ran aground in the afternoon when they tried to create an app to suit a different purpose.&lt;/p&gt;
&lt;p&gt;In any case, it was a great environment to learn in - being able to put your hand up and get expert assistance is a magical experience when you&amp;rsquo;ve previously been self-teaching and don&amp;rsquo;t even know the form of words you need to google to address your current problem, or that the answers you find at are a higher level than you can comprehend.&lt;/p&gt;
&lt;p&gt;Matt was a big proponent of using the example apps in the Playgrounds App Gallery to pinch ideas of how to accomplish things. For example, my app needed a scrolling list from where an entry could be selected and edited in another view, Matt straight away suggested I should have a look at the &amp;ldquo;Date Planner&amp;rdquo; app that had some similar functionality.&lt;/p&gt;</description></item><item><title>struct</title><link>https://blog.iankulin.com/struct/</link><pubDate>Mon, 18 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/struct/</guid><description>&lt;p&gt;Started on &lt;a href="https://www.hackingwithswift.com/100/swiftui/10"&gt;Day 10 of 100 days of etc etc&lt;/a&gt; today which is about structs. It was immediately clear when I first started looking at Swift and Swift UI that structs were going to be a big deal. I am used to structs being able to contain a collection of other types, but not methods. So I was confused at why tuples existed; that is now cleared up.&lt;/p&gt;
&lt;p&gt;If structs can have methods as well as properties, it answers the question of why tuples exist, but immediately asks the question, why have classes since structs have all this power? I already know (from my podcast consumption) one of the answers for this is that structs are value types rather than references. When you:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;let someConstant = someClass
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;someConstant now contains a copy of the pointer to someClass, as opposed to making a copy as similar code would do for a struct. So this code:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;struct SomeStruct &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; counter&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; Int &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#b48ead"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; structInstance &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; SomeStruct&lt;span style="color:#eceff4"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; someOtherStruct &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; structInstance
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;print&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;structInstance:\(structInstance.counter)&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;print&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;someOtherStruct:\(someOtherStruct.counter)&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;someOtherStruct&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;counter &lt;span style="color:#81a1c1"&gt;+=&lt;/span&gt; &lt;span style="color:#b48ead"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;print&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;structInstance:\(structInstance.counter)&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;print&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;someOtherStruct:\(someOtherStruct.counter)&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;print&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;class&lt;/span&gt; SomeClass &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; counter&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; Int &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#b48ead"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; classInstance &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; SomeClass&lt;span style="color:#eceff4"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; someOtherClass &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; classInstance
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;print&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;classInstance:\(classInstance.counter)&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;print&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;someOtherClass:\(someOtherClass.counter)&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;someOtherClass&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;counter &lt;span style="color:#81a1c1"&gt;+=&lt;/span&gt; &lt;span style="color:#b48ead"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;print&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;classInstance:\(classInstance.counter)&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;print&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;someOtherClass:\(someOtherClass.counter)&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Produces the output:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;structInstance:0
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;someOtherStruct:0
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;structInstance:0
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;someOtherStruct:1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;classInstance:0
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;someOtherClass:0
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;classInstance:1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;someOtherClass:1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The sample code for the Hello World app in playgrounds ONLY contains structs. The app is a struct, containing a view which is a struct. That&amp;rsquo;s basically all there is, so clearly structs are going to be a big deal.&lt;/p&gt;</description></item><item><title>Fireside Swift</title><link>https://blog.iankulin.com/fireside-swift/</link><pubDate>Sun, 17 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/fireside-swift/</guid><description>&lt;p&gt;One of the ways I keep engaged in a topic is to listen to podcasts about it. Currently &lt;a href="https://podcasts.apple.com/au/podcast/fireside-swift/id1269435221"&gt;Fireside Swift&lt;/a&gt; is one of the Swift/SwiftUI/iOS Development podcasts that I have in the rotation.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/firesideswift.jpg" width="283" alt=""&gt;
&lt;p&gt;The blurb for the show is:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&amp;ldquo;Fireside Swift is a popular iOS Development podcast where four buddies discuss a new Swift programming topic each week. They try to stay informal while also conveying the information they know about each topic with bits of humor sprinkled throughout. Have a seat by the fire, and enjoy some nerdy discussion with friends!&amp;rdquo;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Which about sums it up. I listened to one later episode when the four hosts have a big ole chat before getting to the Swift topic, and although I didn&amp;rsquo;t love that - probably because I&amp;rsquo;m coming cold to it, when they got to the Swift topic it was pretty great - a good mix of experience and knowledge easily created a good conversation with some clear explanations.&lt;/p&gt;
&lt;p&gt;Currently there are four hosts, but I&amp;rsquo;ve subscribed and gone back to the beginning when the hosts are just Steve Bernard &amp;amp; Zach Falgout - they are getting into the meat of the Swift topic quicker (while still being very chatty and casual) but they don&amp;rsquo;t always get to the bottom of their subjects, but I&amp;rsquo;m still finding it great listening. In particular they seem to focus on topics that would be of interest to someone with experience in different languages.&lt;/p&gt;</description></item><item><title>Checkpoint 5</title><link>https://blog.iankulin.com/checkpoint-5/</link><pubDate>Sat, 16 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/checkpoint-5/</guid><description>&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;/*&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Your input is this&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; let luckyNumbers &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#eceff4"&gt;[&lt;/span&gt;&lt;span style="color:#b48ead"&gt;7&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#b48ead"&gt;4&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#b48ead"&gt;38&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#b48ead"&gt;21&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#b48ead"&gt;16&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#b48ead"&gt;15&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#b48ead"&gt;12&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#b48ead"&gt;33&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#b48ead"&gt;31&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#b48ead"&gt;49&lt;/span&gt;&lt;span style="color:#eceff4"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Your job is to&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Filter out any numbers that are even
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Sort the array &lt;span style="color:#81a1c1;font-weight:bold"&gt;in&lt;/span&gt; ascending order
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Map them to strings &lt;span style="color:#81a1c1;font-weight:bold"&gt;in&lt;/span&gt; the format &lt;span style="color:#bf616a"&gt;“&lt;/span&gt;&lt;span style="color:#b48ead"&gt;7&lt;/span&gt; is a lucky number&lt;span style="color:#bf616a"&gt;”&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Print the resulting array&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; one item per line
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; So&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; your output should be as follows&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#b48ead"&gt;7&lt;/span&gt; is a lucky number
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#b48ead"&gt;15&lt;/span&gt; is a lucky number
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#b48ead"&gt;21&lt;/span&gt; is a lucky number
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#b48ead"&gt;31&lt;/span&gt; is a lucky number
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#b48ead"&gt;33&lt;/span&gt; is a lucky number
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#b48ead"&gt;49&lt;/span&gt; is a lucky number
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;*/&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;let luckyNumbers &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#eceff4"&gt;[&lt;/span&gt;&lt;span style="color:#b48ead"&gt;7&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#b48ead"&gt;4&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#b48ead"&gt;38&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#b48ead"&gt;21&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#b48ead"&gt;16&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#b48ead"&gt;15&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#b48ead"&gt;12&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#b48ead"&gt;33&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#b48ead"&gt;31&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#b48ead"&gt;49&lt;/span&gt;&lt;span style="color:#eceff4"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;func&lt;/span&gt; isNumberOdd&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;number&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;Int&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;-&amp;gt;&lt;/span&gt; Bool &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; number&lt;span style="color:#81a1c1"&gt;%&lt;/span&gt;&lt;span style="color:#b48ead"&gt;2&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;==&lt;/span&gt; &lt;span style="color:#b48ead"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;let filteredNumbers &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; luckyNumbers&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;filter&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;isNumberOdd&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; this closure effectively does nothing
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;let sortedNumbers &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; filteredNumbers&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;sorted&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;by&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#b48ead"&gt;0&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;&amp;lt;$&lt;/span&gt;&lt;span style="color:#b48ead"&gt;1&lt;/span&gt;&lt;span style="color:#eceff4"&gt;})&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;let mappedNumbers &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; sortedNumbers&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;map&lt;span style="color:#eceff4"&gt;({&lt;/span&gt; &lt;span style="color:#bf616a"&gt;String&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#b48ead"&gt;0&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;+&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34; is a lucky number&amp;#34;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;for&lt;/span&gt; i &lt;span style="color:#81a1c1;font-weight:bold"&gt;in&lt;/span&gt; &lt;span style="color:#b48ead"&gt;0.&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&amp;lt;&lt;/span&gt;mappedNumbers&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;count &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;print&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;mappedNumbers&lt;span style="color:#eceff4"&gt;[&lt;/span&gt;i&lt;span style="color:#eceff4"&gt;])&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>Create an Empty Folder on GitHub</title><link>https://blog.iankulin.com/create-an-empty-folder-on-github/</link><pubDate>Fri, 15 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/create-an-empty-folder-on-github/</guid><description>&lt;p&gt;You can&amp;rsquo;t, but you can create a folder by adding a file through the web interface and using &lt;code&gt;&amp;lt;folder name&amp;gt;/&amp;lt;file name&amp;gt;&lt;/code&gt; - so add a README.md or whatever. Then you can drag your source into the new folder as normal.&lt;/p&gt;
&lt;p&gt;I learned this from Zack West &lt;a href="https://www.alpharithms.com/how-to-create-a-folder-in-github-repos-463022/"&gt;here&lt;/a&gt;, where there is also a better explanation with pictures.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-07-11-at-11.11.39-am.jpg" alt=""&gt;&lt;/p&gt;</description></item><item><title>Minimum Functionality for App Store</title><link>https://blog.iankulin.com/minimum-functionality-for-app-store/</link><pubDate>Fri, 15 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/minimum-functionality-for-app-store/</guid><description>&lt;p&gt;In my &lt;a href="https://blog.iankulin.com/app-idea/"&gt;post about&lt;/a&gt; my first app, EasterDay, I mentioned that it might be too trivial for an App Store submission. I&amp;rsquo;ve just been reading the App Store Review Guidelines, and there is a section on &lt;a href="https://developer.apple.com/app-store/review/guidelines/#minimum-functionality"&gt;Minimum Functionality&lt;/a&gt; that seems like it might be too pointless for acceptance.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-07-11-at-9.22.55-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll push on and build it anyway as I still need the rest of the learning experience, but may not submit it. Of course I&amp;rsquo;ve got other ideas for apps (like everyone who has ever met an iOS developer) but they are outside my expertise for the time being.&lt;/p&gt;</description></item><item><title>awesome-ios list on GitHub</title><link>https://blog.iankulin.com/awesome-ios-list-on-github/</link><pubDate>Thu, 14 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/awesome-ios-list-on-github/</guid><description>&lt;p&gt;I was looking for some more podcasts with Swift fundamentals content when I came across &lt;a href="https://github.com/vsouza/awesome-ios"&gt;this&lt;/a&gt; great community built awesome list.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/vsouza/awesome-ios"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-07-11-at-8.45.25-am.jpg" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a few podcasts on the list I have not come across, so I&amp;rsquo;ll check them out.&lt;/p&gt;</description></item><item><title>Learning Retention</title><link>https://blog.iankulin.com/learning-retention/</link><pubDate>Thu, 14 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/learning-retention/</guid><description>&lt;p&gt;In order to have something to put up on GitHub (as part of working all that out) I went back to re-write the Checkpoint 2 code that I&amp;rsquo;d written, but not saved, three or four days ago.&lt;/p&gt;
&lt;p&gt;The task was to count the unique elements in an array. The teaching had been about the complex data types, so clearly the hint was to cast the array to a set. Although the idea of sets is new to me this year, I&amp;rsquo;ve come across them twice. Once in the 100 days course (the same day as having to write this code) and once from a few days earlier from a &lt;a href="https://firesideswift.fireside.fm/157"&gt;podcast episode&lt;/a&gt;. This is high quality learning - getting the same topic a couple of different ways a few days apart, then having to use the information for real.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;/*
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; This time the challenge is to create an array of strings, then write some code that prints the number of items in the array and also the number of unique items in the array.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; */
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;let strArray = [&amp;#34;one&amp;#34;, &amp;#34;two&amp;#34;, &amp;#34;one&amp;#34;, &amp;#34;three&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;let strSet = Set(strArray)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;print(&amp;#34;Array size:\(strArray.count) unique count:\(strSet.count)&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>Checkpoint 4</title><link>https://blog.iankulin.com/checkpoint-4/</link><pubDate>Wed, 13 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/checkpoint-4/</guid><description>&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;/*&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; The challenge is this&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; write a function that accepts an integer from &lt;span style="color:#b48ead"&gt;1&lt;/span&gt; through &lt;span style="color:#b48ead"&gt;10&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;&lt;span style="color:#b48ead"&gt;000&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;and&lt;/span&gt; returns the integer square root of that number&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt; That sounds easy&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; but there are some catches&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; You can&lt;span style="color:#bf616a"&gt;’&lt;/span&gt;t use Swift&lt;span style="color:#bf616a"&gt;’&lt;/span&gt;s built&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;in&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;sqrt&lt;/span&gt;&lt;span style="color:#eceff4"&gt;()&lt;/span&gt; function &lt;span style="color:#81a1c1;font-weight:bold"&gt;or&lt;/span&gt; similar &lt;span style="color:#bf616a"&gt;–&lt;/span&gt; you need to find the square root yourself&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; If the number is less than &lt;span style="color:#b48ead"&gt;1&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;or&lt;/span&gt; greater than &lt;span style="color:#b48ead"&gt;10&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;&lt;span style="color:#b48ead"&gt;000&lt;/span&gt; you should throw an &lt;span style="color:#bf616a"&gt;“&lt;/span&gt;out of bounds&lt;span style="color:#bf616a"&gt;”&lt;/span&gt; error&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; You should only consider integer square roots &lt;span style="color:#bf616a"&gt;–&lt;/span&gt; don&lt;span style="color:#bf616a"&gt;’&lt;/span&gt;t worry about the square root of &lt;span style="color:#b48ead"&gt;3&lt;/span&gt; being &lt;span style="color:#b48ead"&gt;1.732&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;for&lt;/span&gt; example&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; If you can&lt;span style="color:#bf616a"&gt;’&lt;/span&gt;t find the square root&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; throw a &lt;span style="color:#bf616a"&gt;“&lt;/span&gt;no root&lt;span style="color:#bf616a"&gt;”&lt;/span&gt; error&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;*/&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;enum&lt;/span&gt; IntSqrtError&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; Error &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;case&lt;/span&gt; low&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; high&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; noIntRoot
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;func&lt;/span&gt; calculateIntSqrt&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;_ number&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;Int&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; throws &lt;span style="color:#81a1c1"&gt;-&amp;gt;&lt;/span&gt; Int &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; let lowerBound &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#b48ead"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; let upperBound &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#b48ead"&gt;10&lt;/span&gt;_000
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; number &lt;span style="color:#81a1c1"&gt;&amp;lt;&lt;/span&gt; lowerBound &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;throw IntSqrtError&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;low&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; number &lt;span style="color:#81a1c1"&gt;&amp;gt;&lt;/span&gt; upperBound &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;throw IntSqrtError&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;high&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; brute force &lt;span style="color:#81a1c1"&gt;sqrt&lt;/span&gt; finder
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;for&lt;/span&gt; i &lt;span style="color:#81a1c1;font-weight:bold"&gt;in&lt;/span&gt; lowerBound&lt;span style="color:#81a1c1"&gt;...&lt;/span&gt;number &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; i&lt;span style="color:#81a1c1"&gt;*&lt;/span&gt;i &lt;span style="color:#81a1c1"&gt;==&lt;/span&gt; number &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; i
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; none found &lt;span style="color:#81a1c1;font-weight:bold"&gt;or&lt;/span&gt; we would have returned by now
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; throw IntSqrtError&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;noIntRoot
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;do&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; try &lt;span style="color:#81a1c1"&gt;print&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;calculateIntSqrt&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;5929&lt;/span&gt;&lt;span style="color:#eceff4"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt; catch IntSqrtError&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;low &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;print&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Lower bound error&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt; catch IntSqrtError&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;high &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;print&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Upper bound error&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt; catch IntSqrtError&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;noIntRoot &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;print&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;No integer root&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt; catch &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;assert&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;false&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;print&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Unknown error&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>Checkpoint 4 optimisation</title><link>https://blog.iankulin.com/checkpoint-4-optimisation/</link><pubDate>Wed, 13 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/checkpoint-4-optimisation/</guid><description>&lt;p&gt;The &lt;a href="https://www.hackingwithswift.com/quick-start/beginners/checkpoint-4"&gt;Checkpoint 4&lt;/a&gt; task was to find an integer square root of numbers up to 10000. My &lt;a href="https://blog.iankulin.com/checkpoint-4/"&gt;first pass solution&lt;/a&gt; was:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;func&lt;/span&gt; calculateIntSqrt&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;_ number&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;Int&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; throws &lt;span style="color:#81a1c1"&gt;-&amp;gt;&lt;/span&gt; Int &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; let lowerBound &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#b48ead"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; let upperBound &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#b48ead"&gt;10&lt;/span&gt;_000
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; number &lt;span style="color:#81a1c1"&gt;&amp;lt;&lt;/span&gt; lowerBound &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;throw IntSqrtError&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;low&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; number &lt;span style="color:#81a1c1"&gt;&amp;gt;&lt;/span&gt; upperBound &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;throw IntSqrtError&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;high&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; brute force &lt;span style="color:#81a1c1"&gt;sqrt&lt;/span&gt; finder
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;for&lt;/span&gt; i &lt;span style="color:#81a1c1;font-weight:bold"&gt;in&lt;/span&gt; lowerBound&lt;span style="color:#81a1c1"&gt;...&lt;/span&gt;number &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; i&lt;span style="color:#81a1c1"&gt;*&lt;/span&gt;i &lt;span style="color:#81a1c1"&gt;==&lt;/span&gt; number &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; i
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; none found &lt;span style="color:#81a1c1;font-weight:bold"&gt;or&lt;/span&gt; we would have returned by now
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; throw IntSqrtError&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;noIntRoot
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Although not coded for speed, there are a couple of subtle optimisations here; the first is that it gives up once it gets to the number instead of going up to the end (it assumes the square root of a number can&amp;rsquo;t be greater than the number), and the second is that it counts up from the bottom rather than down from the top - I&amp;rsquo;m assuming the bottom of the range is richer in square roots than the top.&lt;/p&gt;
&lt;p&gt;This code works fine (there was no discernible time difference between 4 and 9999) and meets the specification, so I wouldn&amp;rsquo;t want to bill the client for any further work, or to make the code uglier. Obviously however, it does spark the question of what I&amp;rsquo;d do if the function had to work over a much larger range. When I altered it to allow maxInt and passed 1,000,000,000 to it, it took just under four minutes to find in Playgrounds on an M1 MacBook.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s possible a good mathematics student would know some good tricks for calculating square roots, the first couple of links I googled suggested a method of looking for multiples that themselves were squares. While that&amp;rsquo;s a viable strategy mentally, and for reasonable size numbers, it didn&amp;rsquo;t lend itself to being coded. So I guess the computer science guys would pull out the binary search.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;func&lt;/span&gt; calculateIntSqrt&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;_ number&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;Int&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; throws &lt;span style="color:#81a1c1"&gt;-&amp;gt;&lt;/span&gt; Int &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; lowerBound &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#b48ead"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; upperBound &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#b48ead"&gt;2&lt;/span&gt;_147_483_646
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; number &lt;span style="color:#81a1c1"&gt;&amp;lt;&lt;/span&gt; lowerBound &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;throw IntSqrtError&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;low&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; number &lt;span style="color:#81a1c1"&gt;&amp;gt;&lt;/span&gt; upperBound &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;throw IntSqrtError&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;high&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; guess&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; Int
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt; guessSquared&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; Int
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; ensure the answer is &lt;span style="color:#81a1c1;font-weight:bold"&gt;not&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;in&lt;/span&gt; one of the bounds
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; lowerBound &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; lowerBound&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;&lt;span style="color:#b48ead"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; upperBound &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; number&lt;span style="color:#81a1c1"&gt;+&lt;/span&gt;&lt;span style="color:#b48ead"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;while&lt;/span&gt; upperBound &lt;span style="color:#81a1c1"&gt;&amp;gt;&lt;/span&gt; lowerBound&lt;span style="color:#81a1c1"&gt;+&lt;/span&gt;&lt;span style="color:#b48ead"&gt;1&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; pick a guess number halfway between the bounds
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; guess &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; integerMean&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;lowerBound&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; upperBound&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; guessSquared &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; guess&lt;span style="color:#81a1c1"&gt;*&lt;/span&gt;guess
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; then use it to reduce the &lt;span style="color:#81a1c1"&gt;range&lt;/span&gt; between the bounds
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; guessSquared &lt;span style="color:#81a1c1"&gt;&amp;lt;&lt;/span&gt; number &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; lowerBound &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; guess
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; guessSquared &lt;span style="color:#81a1c1"&gt;&amp;gt;&lt;/span&gt; number &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; upperBound &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; guess
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;else&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; our guess was the integer square root
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; guess
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; none found &lt;span style="color:#81a1c1;font-weight:bold"&gt;or&lt;/span&gt; we would have returned by now
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; throw IntSqrtError&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;noIntRoot
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This version has no discernible speed difference between 4 and 2,147,483,644. It&amp;rsquo;s a great example of the trade-offs to be considered when making programming choices. It&amp;rsquo;s double the lines of code, and wanted more comments to explain the intent.&lt;/p&gt;
&lt;p&gt;Full code available on &lt;a href="https://github.com/IanKulin/HackingWithSwift/blob/main/CheckPoint04b.swift"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>Gitting Started</title><link>https://blog.iankulin.com/gitting-started/</link><pubDate>Wed, 13 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/gitting-started/</guid><description>&lt;p&gt;One of my early goals was to get in the habit of using version control with Git/Github, and I&amp;rsquo;ve got that sorted out today. My source was this excellent, very clear video from &lt;a href="https://www.youtube.com/channel/UCxA99Yr6P_tZF9_BgtMGAWA"&gt;Gwen Faraday&lt;/a&gt;. I highly recommend it if you are just starting.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/RGOj5yH7evk?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;

&lt;p&gt;It possibly helped that I&amp;rsquo;m also on mac, so I didn&amp;rsquo;t have to deal with the &amp;ldquo;or however that&amp;rsquo;s done on your system&amp;rdquo; type problems. Also, where things didn&amp;rsquo;t work as expected, the explanation about what was being done was clear enough that the problem was solvable. For example, the push command Gwen used was:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git push origin master
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;but GitHub had defaulted my initial branch to &amp;ldquo;main&amp;rdquo; rather than &amp;ldquo;master&amp;rdquo;. Easily fixed since she immediately explained what both of those modifiers were. The only other tiny bit of troubleshooting was that my git global config wasn&amp;rsquo;t set up, so my commit was followed by a big message pointing out that my real email address wasn&amp;rsquo;t used for the commit:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Committer: User Name &amp;lt;username@Ians-MacBook-Pro.local&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Your name and email address were configured automatically based
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;on your username and hostname. Please check that they are accurate.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;You can suppress this message by setting them explicitly. Run the
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;following command and follow the instructions in your editor to edit
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;your configuration file:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; git config --global --edit
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;After doing this, you may fix the identity used for this commit with:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; git commit --amend --reset-author
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It didn&amp;rsquo;t make any difference - the file I&amp;rsquo;d created locally pushed up to the GitHub repo just fine. When I did follow those instructions to edit the file, I suddenly needed to know how to use Vim (hint: &amp;ldquo;i&amp;rdquo; to go into insert mode for editing, then &amp;ldquo;:&amp;rdquo; for command mode and &amp;ldquo;x&amp;rdquo; to exit and save).&lt;/p&gt;
&lt;p&gt;The only real complexity in the whole process was generating the SSH key and saving that on GitHub to allow the push from your local directory up to the GitHub repository.&lt;/p&gt;
&lt;p&gt;Ignoring that, the process was:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Creating the repository via on GitHub&lt;/li&gt;
&lt;li&gt;Using &lt;code&gt;git clone&lt;/code&gt; to download the repository to the local machine and set it up for tracking in git&lt;/li&gt;
&lt;li&gt;Edit/create the files, however. Gwen used Visual Studio code, I used my tools&lt;/li&gt;
&lt;li&gt;Check status with &lt;code&gt;git status&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git add .&lt;/code&gt; to stage the new files and freshly edited files&lt;/li&gt;
&lt;li&gt;Commit those changes with &lt;code&gt;git commit -m &amp;quot;commit title&amp;quot; -m &amp;quot;description&amp;quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Then push them up to GitHub with &lt;code&gt;git push origin main&lt;/code&gt; where &amp;ldquo;main&amp;rdquo; is the branch name.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All of that was by about the 25 minute mark in the video, and is probably enough for me to go away and get some practice with. The rest covers getting an already established local git repository to GitHub, branching, forking, undoing.&lt;/p&gt;</description></item><item><title>Tuple Pronunciation</title><link>https://blog.iankulin.com/tuple-pronunciation/</link><pubDate>Tue, 12 Jul 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/tuple-pronunciation/</guid><description>&lt;p&gt;Another advantage of the videos, that hadn&amp;rsquo;t occurred to me when I &lt;a href="https://blog.iankulin.com/cs193p/"&gt;mentioned it the other day&lt;/a&gt;, is learning the correct pronunciation of things you&amp;rsquo;ve only ever read in books.&lt;/p&gt;
&lt;p&gt;Apparently, tuple is pronounced two-pull, and not with the tup to rhyme with cup as I&amp;rsquo;d always imagined. Google has confirmed, so it&amp;rsquo;s not just a UK thing.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-07-09-at-12.06.59-pm.jpg" alt=""&gt;&lt;/p&gt;</description></item></channel></rss>