<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Webdev on blog.iankulin.com</title><link>https://blog.iankulin.com/tags/webdev/</link><description>Recent content in Webdev on blog.iankulin.com</description><generator>Hugo</generator><language>en-AU</language><lastBuildDate>Mon, 12 May 2025 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.iankulin.com/tags/webdev/index.xml" rel="self" type="application/rss+xml"/><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;pre tabindex="0"&gt;&lt;code&gt;describe(&amp;#39;Page Navigation&amp;#39;, () =&amp;gt; {
 it(&amp;#39;should navigate to the customers list when clicking the Home link&amp;#39;, () =&amp;gt; {
 // visit the customer details page
 cy.visit(&amp;#39;http://127.0.0.1:3002/customers/1&amp;#39;);
 
 // find and click the Home link
 cy.get(&amp;#39;a&amp;#39;).contains(&amp;#39;Home&amp;#39;).click();
 
 // verify we navigated to the customers list page
 cy.url().should(&amp;#39;eq&amp;#39;, &amp;#39;http://127.0.0.1:3002/customers&amp;#39;);
 });
});
&lt;/code&gt;&lt;/pre&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;pre tabindex="0"&gt;&lt;code&gt;cy.visit(&amp;#39;http://127.0.0.1:3002/customers/1&amp;#39;);
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You guessed it - we&amp;rsquo;re telling Cypress to visit that page.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;cy.get(&amp;#39;a&amp;#39;).contains(&amp;#39;Home&amp;#39;).click();
&lt;/code&gt;&lt;/pre&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;pre tabindex="0"&gt;&lt;code&gt;cy.url().should(&amp;#39;eq&amp;#39;, &amp;#39;http://127.0.0.1:3002/customers&amp;#39;);
&lt;/code&gt;&lt;/pre&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;pre tabindex="0"&gt;&lt;code&gt; // test for customers list page
 describe(&amp;#34;Customers Page&amp;#34;, () =&amp;gt; {
 it(&amp;#34;should have the home page redirect to customers page&amp;#34;, () =&amp;gt; {
 cy.visit(&amp;#34;http://localhost:3002&amp;#34;);
 cy.url().should(&amp;#34;include&amp;#34;, &amp;#34;/customers&amp;#34;);
 cy.get(&amp;#34;h1&amp;#34;).contains(&amp;#34;Customers&amp;#34;);
 });

 it(&amp;#34;should display a list of customers&amp;#34;, () =&amp;gt; {
 cy.visit(&amp;#34;http://localhost:3002/customers&amp;#34;);
 cy.get(&amp;#34;li&amp;#34;).should(&amp;#34;have.length.at.least&amp;#34;, 5);
 cy.get(&amp;#34;li&amp;#34;).eq(0).contains(&amp;#34;Alice&amp;#34;);
 });

 it(&amp;#34;should have working links to customer details&amp;#34;, () =&amp;gt; {
 cy.visit(&amp;#34;http://localhost:3002/customers&amp;#34;);
 // click the first customer (Alice)
 cy.get(&amp;#34;a&amp;#34;).contains(&amp;#34;Alice Johnson&amp;#34;).click();
 cy.url().should(&amp;#34;include&amp;#34;, &amp;#34;/customers/&amp;#34;);
 cy.get(&amp;#34;h2&amp;#34;).contains(&amp;#34;Alice Johnson&amp;#34;);
 });
 });
&lt;/code&gt;&lt;/pre&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;pre tabindex="0"&gt;&lt;code&gt; it(&amp;#34;should delete a customer when delete link is clicked&amp;#34;, () =&amp;gt; {
 // first check the customer exists
 cy.visit(&amp;#34;http://localhost:3002/customers&amp;#34;);
 cy.get(&amp;#34;a&amp;#34;).contains(&amp;#34;Hannah Abbott&amp;#34;).should(&amp;#34;exist&amp;#34;);

 // visit the customer page and delete
 cy.visit(&amp;#34;http://localhost:3002/customers/8&amp;#34;);
 cy.get(&amp;#34;a&amp;#34;).contains(&amp;#34;Delete customer&amp;#34;).click();

 // verify the customer is deleted
 cy.url().should(&amp;#34;include&amp;#34;, &amp;#34;/customers&amp;#34;);
 cy.get(&amp;#34;a&amp;#34;).contains(&amp;#34;Hannah Abbott&amp;#34;).should(&amp;#34;not.exist&amp;#34;);
 });
&lt;/code&gt;&lt;/pre&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;pre tabindex="0"&gt;&lt;code&gt;// Select by element type
cy.get(&amp;#39;button&amp;#39;)

// Select by class
cy.get(&amp;#39;.my-class&amp;#39;)

// Select by ID
cy.get(&amp;#39;#my-id&amp;#39;)

// Combining selectors
cy.get(&amp;#39;button.primary#submit&amp;#39;)

// Select by attribute
cy.get(&amp;#39;[data-test=&amp;#34;submit-button&amp;#34;]&amp;#39;)
&lt;/code&gt;&lt;/pre&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;pre tabindex="0"&gt;&lt;code&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;/code&gt;&lt;/pre&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;pre tabindex="0"&gt;&lt;code&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;/code&gt;&lt;/pre&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;pre tabindex="0"&gt;&lt;code&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;/code&gt;&lt;/pre&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;pre tabindex="0"&gt;&lt;code&gt;cy.get(selector)
 .invoke(jQueryMethod) // Extract what you need
 .then((result) =&amp;gt; { // Process it with your own logic
 // Custom processing
 });
&lt;/code&gt;&lt;/pre&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;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;span class=&amp;#34;price&amp;#34;&amp;gt;$24.99&amp;lt;/span&amp;gt;
&lt;/code&gt;&lt;/pre&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;pre tabindex="0"&gt;&lt;code&gt;cy.get(&amp;#39;.price&amp;#39;)
 .invoke(&amp;#39;text&amp;#39;)
 .then((priceText) =&amp;gt; {
 const priceValue = parseFloat(priceText.replace(&amp;#39;$&amp;#39;, &amp;#39;&amp;#39;));
 expect(priceValue).to.be.greaterThan(20.0);
 });
&lt;/code&gt;&lt;/pre&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;pre tabindex="0"&gt;&lt;code&gt; describe(&amp;#34;Cascading Deletions&amp;#34;, () =&amp;gt; {
 it(&amp;#34;should delete owned orders when a customer is deleted&amp;#34;, () =&amp;gt; {
 // first make a note of an order for a specific customer
 cy.visit(&amp;#34;http://localhost:3002/customers/4&amp;#34;);
 cy.get(&amp;#34;h2&amp;#34;).contains(&amp;#34;Diana Prince&amp;#34;);

 // note an order ID that belongs to this customer
 cy.get(&amp;#34;ul li a&amp;#34;)
 .first()
 .invoke(&amp;#34;text&amp;#34;)
 .then((orderText) =&amp;gt; {
 // extract the full order text to use for matching later
 const orderTextFull = orderText.trim();
 // extract just the order ID number
 const orderId = orderText.split(&amp;#34; &amp;#34;)[0].trim();

 // delete the customer
 cy.get(&amp;#34;a&amp;#34;).contains(&amp;#34;Delete customer&amp;#34;).click();

 // verify customer is deleted
 cy.url().should(&amp;#34;include&amp;#34;, &amp;#34;/customers&amp;#34;);
 cy.get(&amp;#34;a&amp;#34;).contains(&amp;#34;Diana Prince&amp;#34;).should(&amp;#34;not.exist&amp;#34;);

 // check that the order is also deleted 
 cy.visit(&amp;#34;http://localhost:3002/orders&amp;#34;);
 // make sure we&amp;#39;re matching the exact order (not just a substring)
 cy.get(`a[href=&amp;#34;/orders/${orderId}&amp;#34;]`).should(&amp;#34;not.exist&amp;#34;);
 });
 });
 });
&lt;/code&gt;&lt;/pre&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;pre tabindex="0"&gt;&lt;code&gt;describe(&amp;#39;My app tests&amp;#39;, () =&amp;gt; {
 beforeEach(() =&amp;gt; {
 // This code runs before each test in this block
 cy.visit(&amp;#39;/login&amp;#39;); // for example
 cy.get(&amp;#39;input[name=username]&amp;#39;).type(&amp;#39;user&amp;#39;);
 cy.get(&amp;#39;input[name=password]&amp;#39;).type(&amp;#39;password&amp;#39;);
 cy.get(&amp;#39;button[type=submit]&amp;#39;).click();
 });

 it(&amp;#39;should show the dashboard after login&amp;#39;, () =&amp;gt; {
 cy.url().should(&amp;#39;include&amp;#39;, &amp;#39;/dashboard&amp;#39;);
 cy.contains(&amp;#39;Welcome&amp;#39;).should(&amp;#39;be.visible&amp;#39;);
 });

 it(&amp;#39;should navigate to settings&amp;#39;, () =&amp;gt; {
 cy.get(&amp;#39;nav&amp;#39;).contains(&amp;#39;Settings&amp;#39;).click();
 cy.url().should(&amp;#39;include&amp;#39;, &amp;#39;/settings&amp;#39;);
 });
});
&lt;/code&gt;&lt;/pre&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>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:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;express&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;express&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;session&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;express-session&amp;#39;&lt;/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:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;app&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;express&lt;/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:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;use&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;secret&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;your-secret-key&amp;#39;&lt;/span&gt;, 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;resave&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;saveUninitialized&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;true&lt;/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 style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;get&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;/&amp;#39;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&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:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;views&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;views&lt;/span&gt;&lt;span style="color:#f92672"&gt;++&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;views&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&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&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;send&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;`Views: &lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;views&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&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 style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;listen&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;3000&lt;/span&gt;, () =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;console&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;log&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;Server is running on port 3000&amp;#39;&lt;/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 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:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;express&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;express&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;session&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;express-session&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;FileStore&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;session-file-store&amp;#34;&lt;/span&gt;)(&lt;span style="color:#a6e22e"&gt;session&lt;/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:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;app&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;express&lt;/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:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;use&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;secret&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;your-secret-key&amp;#34;&lt;/span&gt;, &lt;span style="color:#75715e"&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; &lt;span style="color:#a6e22e"&gt;resave&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;saveUninitialized&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;store&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;FileStore&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;session_cookie&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&gt;&lt;/span&gt;&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:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;get&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/&amp;#34;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&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:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;views&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;views&lt;/span&gt;&lt;span style="color:#f92672"&gt;++&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;views&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&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&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;send&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;`Views: &lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;views&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&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 style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;listen&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;3000&lt;/span&gt;, () =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;console&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;log&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;Server is running on port 3000&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;/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;pre tabindex="0"&gt;&lt;code&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;/code&gt;&lt;/pre&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;pre tabindex="0"&gt;&lt;code&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;/code&gt;&lt;/pre&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;pre tabindex="0"&gt;&lt;code&gt;app.get(&amp;#34;/create/:user&amp;#34;, (req, res) =&amp;gt; { const user = req.params.user; const views = 0; req.session.user = user; user_views.push({ user, views }); fs.writeFile(&amp;#34;./user_views.json&amp;#34;, JSON.stringify(user_views), (err) =&amp;gt; { if (err) { res.send(err); return; } }); res.send(`User ${user} created &amp;amp; logged in`);});
&lt;/code&gt;&lt;/pre&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;pre tabindex="0"&gt;&lt;code&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;/code&gt;&lt;/pre&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;pre tabindex="0"&gt;&lt;code&gt;app.get(&amp;#34;/&amp;#34;, (req, res) =&amp;gt; { if (req.session.user) { const user_view = user_views.find((u) =&amp;gt; u.user === req.session.user); user_view.views++; res.send(`User &amp;#34;${req.session.user}&amp;#34; has ${user_view.views} views`); writeUserViewFile(); } else { res.send(&amp;#34;Please log in&amp;#34;); }});
&lt;/code&gt;&lt;/pre&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:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;express&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;express&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;session&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;express-session&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;FileStore&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;session-file-store&amp;#34;&lt;/span&gt;)(&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;fs&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;fs&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;app&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;express&lt;/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:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user_view_file&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;./user_views.json&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;cookie_name&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;session_cookie&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:#66d9ef"&gt;let&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user_views&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; [];
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&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:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;fs&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;existsSync&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;user_view_file&lt;/span&gt;)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;user_views&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;JSON&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;parse&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;fs&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;readFileSync&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;user_view_file&lt;/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 style="color:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;writeUserViewFile&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;fs&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;writeFile&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;user_view_file&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;JSON&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;stringify&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;user_views&lt;/span&gt;), (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;console&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;log&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/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;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:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;findUser&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user_views&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;find&lt;/span&gt;((&lt;span style="color:#a6e22e"&gt;u&lt;/span&gt;) =&amp;gt; &lt;span style="color:#a6e22e"&gt;u&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt; &lt;span style="color:#f92672"&gt;===&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user&lt;/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 style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;use&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;secret&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;your-secret-keyz&amp;#34;&lt;/span&gt;, &lt;span style="color:#75715e"&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; &lt;span style="color:#a6e22e"&gt;resave&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;saveUninitialized&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;store&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;FileStore&lt;/span&gt;(),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;cookie_name&lt;/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;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;get&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/&amp;#34;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user_view&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;findUser&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;user_view&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;views&lt;/span&gt;&lt;span style="color:#f92672"&gt;++&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;send&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;`User &amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34; has &lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;user_view&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;views&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt; views`&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;writeUserViewFile&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;send&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;Please log in&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&gt;&lt;/span&gt;&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:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;get&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/logout&amp;#34;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;destroy&lt;/span&gt;((&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;console&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;log&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;err&lt;/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:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;clearCookie&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;cookie_name&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;redirect&lt;/span&gt;(&lt;span style="color:#e6db74"&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&gt;&lt;/span&gt;&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:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;get&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/create/:user&amp;#34;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;params&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;views&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;user_views&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;push&lt;/span&gt;({ &lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;views&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;writeUserViewFile&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;send&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;`User &lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt; created &amp;amp; logged in`&lt;/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 style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;get&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/login/:user&amp;#34;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&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:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;findUser&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;params&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;params&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;send&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;`User &lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;params&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt; logged in`&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;redirect&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;`/create/&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;params&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&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;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;listen&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;3000&lt;/span&gt;, () =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;console&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;log&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;Server is running on port 3000&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;/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;pre tabindex="0"&gt;&lt;code&gt;app.get(&amp;#34;/create/:user/:password&amp;#34;, (req, res) =&amp;gt; { const user = req.params.user; const password = req.params.password; const views = 0; req.session.user = user; user_views.push({ user, password, views }); writeUserViewFile(); res.send(`User ${user} created &amp;amp; logged in`);});
&lt;/code&gt;&lt;/pre&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;pre tabindex="0"&gt;&lt;code&gt;app.get(&amp;#34;/create/:user/:password&amp;#34;, (req, res) =&amp;gt; { if (findUser(req.params.user)) { res.send(`User ${req.params.user} already exists`); return; } const user = req.params.user; const password = req.params.password; const views = 0; req.session.user = user; user_views.push({ user, password, views }); writeUserViewFile(); res.send(`User ${user} created &amp;amp; logged in`);});
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;and logging in:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;app.get(&amp;#34;/login/:user/:password&amp;#34;, (req, res) =&amp;gt; { const user = findUser(req.params.user); if (user &amp;amp;&amp;amp; user.password === req.params.password) { req.session.user = req.params.user; res.send(`User ${req.params.user} logged in`); return; } else { res.send(`Incorrect username or password`); }});
&lt;/code&gt;&lt;/pre&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;pre tabindex="0"&gt;&lt;code&gt;app.get(&amp;#34;/create/:user/:password&amp;#34;, (req, res) =&amp;gt; { if (findUser(req.params.user)) { res.send(`User ${req.params.user} already exists`); return; } const user = req.params.user; const hash = bcrypt.hashSync(req.params.password, saltRounds); const views = 0; req.session.user = user;  user_views.push({ user, hash, views });  writeUserViewFile();  res.send(`User ${user} created &amp;amp; logged in`); });});app.get(&amp;#34;/login/:user/:password&amp;#34;, (req, res) =&amp;gt; { const user = findUser(req.params.user); if (user &amp;amp;&amp;amp; bcrypt.compareSync(req.params.password, user.hash)) { req.session.user = req.params.user; res.send(`User ${req.params.user} logged in`); return; } else { res.send(`Incorrect username or password`); }});
&lt;/code&gt;&lt;/pre&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;pre tabindex="0"&gt;&lt;code&gt;app.post(&amp;#34;/login&amp;#34;, (req, res) =&amp;gt; { const user = findUser(req.body.username); if (user &amp;amp;&amp;amp; bcrypt.compareSync(req.body.password, user.hash)) { req.session.user = req.body.username; req.session.save((err) =&amp;gt; { if (err) { res.send(&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;); } else { res.redirect(&amp;#34;/&amp;#34;); } }); } else { res.send(`Incorrect username or password, &amp;lt;a href=&amp;#34;/login&amp;#34;&amp;gt;try again&amp;lt;/a&amp;gt;`); }});
&lt;/code&gt;&lt;/pre&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;pre tabindex="0"&gt;&lt;code&gt;app.post(&amp;#34;/register&amp;#34;, (req, res) =&amp;gt; { if (findUser(req.body.username)) { res.send(`User ${req.body.username} already exists`); return; } const user = req.body.username; const hash = bcrypt.hashSync(req.body.password, saltRounds); const views = 0; req.session.user = user; user_views.push({ user, hash, views }); writeUserViewFile(); req.session.save((err) =&amp;gt; { if (err) { res.send(&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;); } else { res.redirect(&amp;#34;/&amp;#34;); } });});
&lt;/code&gt;&lt;/pre&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;pre tabindex="0"&gt;&lt;code&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;/code&gt;&lt;/pre&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;pre tabindex="0"&gt;&lt;code&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;/code&gt;&lt;/pre&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;pre tabindex="0"&gt;&lt;code&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;/code&gt;&lt;/pre&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;pre tabindex="0"&gt;&lt;code&gt;app.use( session({ secret: sessionSecret, resave: false, saveUninitialized: true, store: new FileStore(), name: cookie_name, cookie: { secure: true, httpOnly: true, }, }));
&lt;/code&gt;&lt;/pre&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;pre tabindex="0"&gt;&lt;code&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;/code&gt;&lt;/pre&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:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;express&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;express&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;session&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;express-session&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;FileStore&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;session-file-store&amp;#34;&lt;/span&gt;)(&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;fs&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;fs&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;app&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;express&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;bcrypt&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;bcrypt&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;saltRounds&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;10&lt;/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:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user_view_file&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;./user_views.json&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;cookie_name&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;session_cookie&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:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;use&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;express&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;urlencoded&lt;/span&gt;({ &lt;span style="color:#a6e22e"&gt;extended&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;false&lt;/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:#66d9ef"&gt;let&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user_views&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; [];
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&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:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;fs&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;existsSync&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;user_view_file&lt;/span&gt;)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;user_views&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;JSON&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;parse&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;fs&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;readFileSync&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;user_view_file&lt;/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 style="color:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;writeUserViewFile&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;fs&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;writeFile&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;user_view_file&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;JSON&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;stringify&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;user_views&lt;/span&gt;), (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;console&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;log&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/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;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:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;findUser&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user_views&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;find&lt;/span&gt;((&lt;span style="color:#a6e22e"&gt;u&lt;/span&gt;) =&amp;gt; &lt;span style="color:#a6e22e"&gt;u&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt; &lt;span style="color:#f92672"&gt;===&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user&lt;/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 style="color:#75715e"&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:#75715e"&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:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;sanitise&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;str&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;str&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;replace&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;/[&amp;#34;&amp;lt;&amp;gt;]/g&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;_&amp;#34;&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;slice&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;20&lt;/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 style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;use&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;secret&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;your-secret-keyz&amp;#34;&lt;/span&gt;, &lt;span style="color:#75715e"&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; &lt;span style="color:#a6e22e"&gt;resave&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;saveUninitialized&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;store&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;FileStore&lt;/span&gt;(),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;cookie_name&lt;/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;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;get&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/&amp;#34;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user_view&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;findUser&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#f92672"&gt;!&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;user_view&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;send&lt;/span&gt;(&lt;span style="color:#e6db74"&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&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/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:#a6e22e"&gt;user_view&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;views&lt;/span&gt;&lt;span style="color:#f92672"&gt;++&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;send&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;`User &amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34; has &lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;user_view&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;views&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&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&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;writeUserViewFile&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;redirect&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/login&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&gt;&lt;/span&gt;&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:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;get&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/logout&amp;#34;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;destroy&lt;/span&gt;((&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;console&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;log&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;err&lt;/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:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;clearCookie&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;cookie_name&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;redirect&lt;/span&gt;(&lt;span style="color:#e6db74"&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&gt;&lt;/span&gt;&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:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;post&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/register&amp;#34;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;sanitise&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;body&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;username&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;findUser&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;send&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;`User &lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;body&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;username&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt; already exists`&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/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:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;hash&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;bcrypt&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;hashSync&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;body&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;password&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;saltRounds&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;views&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;user_views&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;push&lt;/span&gt;({ &lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;hash&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;views&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;writeUserViewFile&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;save&lt;/span&gt;((&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;send&lt;/span&gt;(&lt;span style="color:#e6db74"&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&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;redirect&lt;/span&gt;(&lt;span style="color:#e6db74"&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&gt;&lt;/span&gt;&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 style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;post&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/login&amp;#34;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;username&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;sanitise&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;body&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;username&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;findUser&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;username&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt; &lt;span style="color:#f92672"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;bcrypt&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;compareSync&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;body&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;password&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;hash&lt;/span&gt;)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;username&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;session&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;save&lt;/span&gt;((&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;send&lt;/span&gt;(&lt;span style="color:#e6db74"&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&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;redirect&lt;/span&gt;(&lt;span style="color:#e6db74"&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&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;send&lt;/span&gt;(&lt;span style="color:#e6db74"&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&gt;&lt;/span&gt;&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 style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;get&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/login&amp;#34;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;200&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;sendFile&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;__dirname&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;/login.html&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&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;get&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/register&amp;#34;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;200&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;sendFile&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;__dirname&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;/register.html&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&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;listen&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;3000&lt;/span&gt;, () =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;console&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;log&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;Server is running on port 3000&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;/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>React Expense Tracker App</title><link>https://blog.iankulin.com/react-expense-tracker-app/</link><pubDate>Mon, 22 Jan 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/react-expense-tracker-app/</guid><description>&lt;p&gt;I&amp;rsquo;m focused on React frontend skills these holidays, and &lt;a href="https://codewithmosh.com/p/ultimate-react-part1"&gt;working through Mosh&amp;rsquo;s React 18&lt;/a&gt; course. The exercise today (which I think I nailed, although I spent more than the recommended hour on) was a small app to track expenses. Like most of Mosh&amp;rsquo;s exercises it was great because it exercised all the understandings up to that point - so it&amp;rsquo;s a good starting React project. It used Zod for the form validation which is completely new to me, but looks great.&lt;/p&gt;
&lt;h3 id="the-specification"&gt;The Specification&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-12-31-at-6.02.08-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-12-31-at-6.02.08-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This is an app for tracking expenses. Expenses can be in one of three &lt;em&gt;categories&lt;/em&gt;, they also must have a &lt;em&gt;description&lt;/em&gt; and an &lt;em&gt;amount&lt;/em&gt;. There&amp;rsquo;s a form for entering a new expense with a submit button. The form fields are validated before the expense item is added.&lt;/p&gt;
&lt;p&gt;Below the form for adding a new expense item is a list of expenses. Each one shows its description, amount and category. There is also a button to delete this expense. At the bottom of the list is a total for the expenses shown. The expenses shown in the list can be filtered by category with a drop down.&lt;/p&gt;
&lt;h3 id="design-decisions"&gt;Design Decisions&lt;/h3&gt;
&lt;p&gt;The first decision in an React app is &amp;ldquo;What are the components going to be?&amp;rdquo;. Clearly the form at the top is a component (I called mine &lt;code&gt;AddForm&lt;/code&gt;). The bottom section could be two - the filter and the list, but my style is to start with less components then seperate them out if they are getting complex so I considered the filtered list a single component called &lt;code&gt;ExpenseList&lt;/code&gt;. In Mosh&amp;rsquo;s solution, he did have these as separate components, arguing that the filter is likely to become more complex in future which is a sound argument, but too much premature optimisation for me.&lt;/p&gt;
&lt;h3 id="code"&gt;Code&lt;/h3&gt;
&lt;p&gt;With that decision made, we have enough to write the App.tsx. I also decided to persist the array of &lt;code&gt;Expense&lt;/code&gt;s to local storage to make it a nicer demo app. So with that, the app component code looks like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;import { useState, useEffect } from &amp;#34;react&amp;#34;;import AddForm from &amp;#34;./AddForm&amp;#34;;import { Expense } from &amp;#34;./types.ts&amp;#34;;import ExpenseList from &amp;#34;./ExpenseList&amp;#34;;import { v4 as uuidv4 } from &amp;#34;uuid&amp;#34;;function App() { const [expenses, setExpenses] = useState&amp;lt;Expense[]&amp;gt;(() =&amp;gt; { // Try to load expenses from local storage const savedExpenses = localStorage.getItem(&amp;#39;mosh-expense&amp;#39;); if (savedExpenses) { return JSON.parse(savedExpenses); } else { // If there are no saved expenses, load the sample expenses return loadSampleExpenses(); } }); useEffect(() =&amp;gt; { // Save expenses to local storage whenever they change localStorage.setItem(&amp;#39;mosh-expense&amp;#39;, JSON.stringify(expenses)); }, [expenses]); return ( &amp;lt;&amp;gt; &amp;lt;AddForm expenses={expenses} setExpenses={setExpenses} /&amp;gt; &amp;lt;hr /&amp;gt; &amp;lt;ExpenseList expenses={expenses} setExpenses={setExpenses} /&amp;gt; &amp;lt;/&amp;gt; );}export default App;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Because I&amp;rsquo;ve got my big boy TypeScript pants on, there&amp;rsquo;s a types.ts file with the types defined since they are common across a number of components:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;export interface Expense { id: string; description: string; amount: number; category: string;}// needed so we can validate the form without the id then// add it laterexport interface FormExpense { description: string; amount: number; category: string;}export interface ExpenseProps { expenses: Expense[]; setExpenses: (expenses: Expense[]) =&amp;gt; void;}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The reason for the existence of &lt;code&gt;FormExpense&lt;/code&gt; without the &lt;code&gt;id&lt;/code&gt; field is that the interaction between Zod and React got very complicated if the &lt;code&gt;id&lt;/code&gt; field existed - Zod was determined to validate it. If I left it out of the Zod schema it caused problems because the type and the schema didn&amp;rsquo;t match, if I made it optional in the schema, it created type mismatch problems that were solvable with typecasting calisthenics but it was ugly and difficult to follow. In the end, I went with these two types and just added the &lt;code&gt;id&lt;/code&gt; after validation.&lt;/p&gt;
&lt;p&gt;I also made a decision in coding the two types (&lt;code&gt;FormExpense&lt;/code&gt; and &lt;code&gt;Expense&lt;/code&gt;) like this. I&amp;rsquo;ve made a small footgun for a future developer in that they might add a new Field to one of them, and neglect to add it to the other. I could have avoided that by extending one, like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;export interface FormExpense { description: string; amount: number; category: string;}export interface Expense extends FormExpense { id: string;}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That solves that problem, but to my mind is not as clear - conceptually, &lt;code&gt;Expense&lt;/code&gt; is the main thing here, and really &lt;code&gt;FormExpense&lt;/code&gt; is a derivative of it - just with the id removed. The TypeScript language designers could have helped me out here with some syntax, but I forgive you &lt;a href="https://en.wikipedia.org/wiki/Anders_Hejlsberg"&gt;Anders&lt;/a&gt; because of the good living I once made out of Delphi. In my alternative timeline TypeScript it could look like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;export interface Expense { description: string; amount: number; category: string;  id: string;}export interface FormExpense reduces Expense { id: %removed;}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So, I don&amp;rsquo;t love the duplication in the existing type definition, but it seemed like the lessor of two evils, the definitions are right next to each other, and we are working in TypeScript so the future developer will discover their mistake at the time they&amp;rsquo;ll make it.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s also a subtle design decision in the very simple &lt;code&gt;ExpenseProps&lt;/code&gt; worth thinking about. Firstly, I like having the props here with the &lt;code&gt;Expense&lt;/code&gt; definition, but mostly how simple they are. There&amp;rsquo;s no &lt;code&gt;addExpense()&lt;/code&gt;, &lt;code&gt;updateExpense()&lt;/code&gt;, or &lt;code&gt;deleteExpense()&lt;/code&gt;. That&amp;rsquo;s because, to the extent they are needed, they are dealt with inside each component.&lt;/p&gt;
&lt;p&gt;Components are already tightly bound to their data types, so we&amp;rsquo;re not making that worse by having this code with the compnent, but it would be perfectly valid to argue that all the data manipulation for expenses should be in one place. That argument would be won for me the second I needed to duplicate any of it - for example if two different components needed to delete an expense. But as the app stands now, this is neater, so that&amp;rsquo;s what I&amp;rsquo;ve gone with.&lt;/p&gt;
&lt;h3 id="expenses-list"&gt;Expenses List&lt;/h3&gt;
&lt;p&gt;The mechanism for the filter is that we have a local function that returns the filtered list based on the selected category which is a state variable of the &lt;code&gt;ExpensesList&lt;/code&gt; component. That &lt;code&gt;selectedCategory&lt;/code&gt; is updated from the onChange of the drop-down selector.&lt;/p&gt;
&lt;p&gt;The JSX just builds a table by .&lt;code&gt;map&lt;/code&gt;ping the filtered expenses. I don&amp;rsquo;t love the table code - it looks clunky. When I came back to look at it there were still some refactoring opportunities in it. Let&amp;rsquo;s have a look:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;table className=&amp;#34;table-bordered&amp;#34;&amp;gt; &amp;lt;thead&amp;gt; &amp;lt;tr&amp;gt; &amp;lt;th&amp;gt;Description&amp;lt;/th&amp;gt; &amp;lt;th&amp;gt;Amount&amp;lt;/th&amp;gt; &amp;lt;th&amp;gt;Category&amp;lt;/th&amp;gt; &amp;lt;th&amp;gt;&amp;lt;/th&amp;gt; &amp;lt;/tr&amp;gt; &amp;lt;/thead&amp;gt; &amp;lt;tbody&amp;gt; {filteredExpenses.map((expense) =&amp;gt; ( &amp;lt;tr key={expense.id}&amp;gt; &amp;lt;td className=&amp;#34;centred-td&amp;#34;&amp;gt;{expense.description}&amp;lt;/td&amp;gt; &amp;lt;td style={{ textAlign: &amp;#34;right&amp;#34; }}&amp;gt; $ {expense.amount.toLocaleString(&amp;#34;en-US&amp;#34;, { minimumFractionDigits: 2, maximumFractionDigits: 2, })} &amp;lt;/td&amp;gt; &amp;lt;td className=&amp;#34;centred-td&amp;#34;&amp;gt;{expense.category}&amp;lt;/td&amp;gt; &amp;lt;td className=&amp;#34;centred-td&amp;#34;&amp;gt; &amp;lt;button className=&amp;#34;btn btn-outline-danger&amp;#34; onClick={() =&amp;gt; handleDelete(expense.id)} &amp;gt; Delete &amp;lt;/button&amp;gt; &amp;lt;/td&amp;gt; &amp;lt;/tr&amp;gt; ))} &amp;lt;/tbody&amp;gt; &amp;lt;tfoot&amp;gt; &amp;lt;tr&amp;gt; &amp;lt;td&amp;gt;&amp;lt;/td&amp;gt; &amp;lt;td style={{ textAlign: &amp;#34;right&amp;#34;, fontWeight: &amp;#34;bold&amp;#34; }}&amp;gt; $ {filteredExpenses .reduce((total, expense) =&amp;gt; { return total + expense.amount; }, 0) .toLocaleString(&amp;#34;en-US&amp;#34;, { minimumFractionDigits: 2, maximumFractionDigits: 2, })} &amp;lt;/td&amp;gt; &amp;lt;td colSpan={2}&amp;gt;&amp;lt;/td&amp;gt; &amp;lt;/tr&amp;gt; &amp;lt;/tfoot&amp;gt;&amp;lt;/table&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The first thing I&amp;rsquo;m noticing is the code to format the amounts to US currency. That&amp;rsquo;s in there twice - once for each expense, and once for the total. We could write a function for that, but since we&amp;rsquo;re in React-land, let&amp;rsquo;s make it a component. Ideally we&amp;rsquo;d extract the user&amp;rsquo;s local currency from the browser settings somehow, but I don&amp;rsquo;t think that&amp;rsquo;s possible. In a real app, I guess we&amp;rsquo;d let them set it in settings. For the moment, they&amp;rsquo;ll just have to live in dollar land.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;interface TDCurrencyProps { children: number; fontWeight?: string; }function TDCurrency({children, fontWeight = &amp;#34;normal&amp;#34;}:TDCurrencyProps) { return ( &amp;lt;td style={{ textAlign: &amp;#34;right&amp;#34;, fontWeight: fontWeight }}&amp;gt; $ {children.toLocaleString(&amp;#34;en-US&amp;#34;, { minimumFractionDigits: 2, maximumFractionDigits: 2, })} &amp;lt;/td&amp;gt; );}export default TDCurrency;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now we&amp;rsquo;re thinking React-ivly! The table looks better already. Also, since now the alignment for the number column is in its own component, the centred styling can be applied to all the &lt;code&gt;&amp;lt;td&amp;gt;&lt;/code&gt;s so I can remove the class I had added for that:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;table className=&amp;#34;table-bordered&amp;#34;&amp;gt; &amp;lt;thead&amp;gt; &amp;lt;tr&amp;gt; &amp;lt;th&amp;gt;Description&amp;lt;/th&amp;gt; &amp;lt;th&amp;gt;Amount&amp;lt;/th&amp;gt; &amp;lt;th&amp;gt;Category&amp;lt;/th&amp;gt; &amp;lt;th&amp;gt;&amp;lt;/th&amp;gt; &amp;lt;/tr&amp;gt; &amp;lt;/thead&amp;gt; &amp;lt;tbody&amp;gt; {filteredExpenses.map((expense) =&amp;gt; ( &amp;lt;tr key={expense.id}&amp;gt; &amp;lt;td&amp;gt;{expense.description}&amp;lt;/td&amp;gt; &amp;lt;TDCurrency&amp;gt;{expense.amount}&amp;lt;/TDCurrency&amp;gt; &amp;lt;td&amp;gt;{expense.category}&amp;lt;/td&amp;gt; &amp;lt;td&amp;gt; &amp;lt;button className=&amp;#34;btn btn-outline-danger&amp;#34; onClick={() =&amp;gt; handleDelete(expense.id)} &amp;gt; Delete &amp;lt;/button&amp;gt; &amp;lt;/td&amp;gt; &amp;lt;/tr&amp;gt; ))} &amp;lt;/tbody&amp;gt; &amp;lt;tfoot&amp;gt; &amp;lt;tr&amp;gt; &amp;lt;td&amp;gt;&amp;lt;/td&amp;gt; &amp;lt;TDCurrency fontWeight=&amp;#34;bold&amp;#34;&amp;gt;{filteredTotal}&amp;lt;/TDCurrency&amp;gt; &amp;lt;td colSpan={2}&amp;gt;&amp;lt;/td&amp;gt; &amp;lt;/tr&amp;gt; &amp;lt;/tfoot&amp;gt;&amp;lt;/table&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Much nicer. Would it be crazy to component-ise that delete button as well? Probably, but while we&amp;rsquo;re on a roll, and this is starting to feel like fun.&lt;/p&gt;
&lt;p&gt;I really feel I&amp;rsquo;m starting to get the benefits of React that we&amp;rsquo;re paying for with the tooling complexity and larger bundle size.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;interface TDDeleteButtonProps { onClick: (id: string) =&amp;gt; void; id: string;}function TDDeleteButton({ onClick, id}: TDDeleteButtonProps) { return ( &amp;lt;td&amp;gt; &amp;lt;button className=&amp;#34;btn btn-outline-danger&amp;#34; onClick={() =&amp;gt; onClick(id)} &amp;gt; Delete &amp;lt;/button&amp;gt; &amp;lt;/td&amp;gt; );};export default TDDeleteButton;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I&amp;rsquo;m much happier with the table now:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;table className=&amp;#34;table-bordered&amp;#34;&amp;gt; &amp;lt;thead&amp;gt; &amp;lt;tr&amp;gt; &amp;lt;th&amp;gt;Description&amp;lt;/th&amp;gt; &amp;lt;th&amp;gt;Amount&amp;lt;/th&amp;gt; &amp;lt;th&amp;gt;Category&amp;lt;/th&amp;gt; &amp;lt;th&amp;gt;&amp;lt;/th&amp;gt; &amp;lt;/tr&amp;gt; &amp;lt;/thead&amp;gt; &amp;lt;tbody&amp;gt; {filteredExpenses.map((expense) =&amp;gt; ( &amp;lt;tr key={expense.id}&amp;gt; &amp;lt;td&amp;gt;{expense.description}&amp;lt;/td&amp;gt; &amp;lt;TDCurrency&amp;gt;{expense.amount}&amp;lt;/TDCurrency&amp;gt; &amp;lt;td&amp;gt;{expense.category}&amp;lt;/td&amp;gt; &amp;lt;TDDeleteButton onClick={handleDelete} id={expense.id}/&amp;gt; &amp;lt;/tr&amp;gt; ))} &amp;lt;/tbody&amp;gt; &amp;lt;tfoot&amp;gt; &amp;lt;tr&amp;gt; &amp;lt;td&amp;gt;&amp;lt;/td&amp;gt; &amp;lt;TDCurrency fontWeight=&amp;#34;bold&amp;#34;&amp;gt;{filteredTotal}&amp;lt;/TDCurrency&amp;gt; &amp;lt;td colSpan={2}&amp;gt;&amp;lt;/td&amp;gt; &amp;lt;/tr&amp;gt; &amp;lt;/tfoot&amp;gt;&amp;lt;/table&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The selector at the top of the expense list, is, well, okay.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;div className=&amp;#34;mb-3 category-selector&amp;#34;&amp;gt; &amp;lt;label htmlFor=&amp;#34;category&amp;#34; className=&amp;#34;form-label&amp;#34;&amp;gt; Category:{&amp;#34; &amp;#34;} &amp;lt;/label&amp;gt; &amp;lt;select id=&amp;#34;category&amp;#34; name=&amp;#34;category&amp;#34; className=&amp;#34;form-control drop-down&amp;#34; onChange={handleCategoryChange} &amp;gt; &amp;lt;option value=&amp;#34;All&amp;#34;&amp;gt;All&amp;lt;/option&amp;gt; &amp;lt;option value=&amp;#34;Groceries&amp;#34;&amp;gt;Groceries&amp;lt;/option&amp;gt; &amp;lt;option value=&amp;#34;Utilities&amp;#34;&amp;gt;Utilities&amp;lt;/option&amp;gt; &amp;lt;option value=&amp;#34;Entertainment&amp;#34;&amp;gt;Entertainment&amp;lt;/option&amp;gt; &amp;lt;/select&amp;gt;&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The specification only specified these three categories - Groceries, Utilities, and Entertainment. and I have them hard-coded here, and in the add form. In Mosh&amp;rsquo;s version he&amp;rsquo;s made them a enum which is definitely nicer and future-proof-ier for a very small increase in complexity.&lt;/p&gt;
&lt;h3 id="mosh"&gt;Mosh&lt;/h3&gt;
&lt;p&gt;I really enjoy Mosh&amp;rsquo;s videos and courses. He generally puts the first hour of his &lt;a href="https://www.youtube.com/@programmingwithmosh"&gt;courses on Youtube&lt;/a&gt;, so you can try before you buy. He has a knack for anticipating the questions that occur to you as he&amp;rsquo;s explaining something so you feel a sense of continually being slightly challenged, but never overwhelmed. Hard recommend if you are learning programming.&lt;/p&gt;</description></item><item><title>CSS for React Components</title><link>https://blog.iankulin.com/css-for-react-components/</link><pubDate>Fri, 12 Jan 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/css-for-react-components/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-12-27-at-3.30.32-pm.jpg" alt=""&gt;
&lt;em&gt;Subscribe to my UX design course 😉&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;If you think back to HTML as being a document with headings and paragraphs and other semantic bits, it made a lot of sense to have the styles (expressed as CSS) separate to the document. This allows us to change the styles without touching the document - perhaps the user wanted a dark theme, needed the text bigger for accessibility, or perhaps the document was being consumed in some other way - for example a screen reader - so the styles were superfluous.&lt;/p&gt;
&lt;p&gt;In tension to this idea, is the idea that all the code related to a single thing should be encapsulated in one place. This is why we invented object orientated programming - we are creating such huge software systems that for a human to be able to maintain them, they need broken down into chunks that can be fully held in mind while we are working on them. Also, when we are talking about a modern single page application, we&amp;rsquo;ve come a long way from thinking of a web &amp;lsquo;page&amp;rsquo; as being a document to be passively consumed.&lt;/p&gt;
&lt;p&gt;Since the point of React is to create reusable &amp;lsquo;components&amp;rsquo; where the JS and HTML are written together, it&amp;rsquo;s reasonable to wonder if perhaps the CSS shouldn&amp;rsquo;t be in there too.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s look at a few different approaches to managing styles for our components in React.&lt;/p&gt;
&lt;h3 id="old-style-global"&gt;Old Style Global&lt;/h3&gt;
&lt;p&gt;Most times when I&amp;rsquo;m writing vanilla JS, if I&amp;rsquo;m not leveraging off &lt;a href="https://picocss.com/"&gt;Pico&lt;/a&gt; or &lt;a href="https://getbootstrap.com/"&gt;Bootstrap&lt;/a&gt; I have a single site-wide &lt;code&gt;styles.css&lt;/code&gt; file. Obviously this is going to be an option for a React app too. We&amp;rsquo;d just link it from the index.html in the root of our folder. Job&amp;rsquo;s a goodun.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-12-27-at-3.27.51-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-12-27-at-3.27.51-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The most common downside of this approach, that I come across, is that I change the rest of the HTML through the various iterations, but never clean up the CSS. So I get left with bits of CSS that I&amp;rsquo;m not sure if they are being used somewhere - so I&amp;rsquo;m not brave enough to delete them because my UX testing is not good enough, so those fragments just end up sitting around forever.&lt;/p&gt;
&lt;h3 id="old-style-local"&gt;Old Style local&lt;/h3&gt;
&lt;p&gt;The first CSS we ever wrote was just stuffed in the HTML in style tags, and of course that&amp;rsquo;s still an option. We can use variables to help with the readability, and end up with something like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;function Card(props: CardProps) { const cardStyles: React.CSSProperties = { width: &amp;#39;120px&amp;#39;, boxShadow: &amp;#39;0 4px 8px 0 rgba(0, 0, 0, 0.2)&amp;#39;, textAlign: &amp;#39;center&amp;#39;, backgroundColor: &amp;#39;cornsilk&amp;#39;, margin: &amp;#39;10px&amp;#39;, padding: &amp;#39;10px&amp;#39;, transform: &amp;#39;scale(1)&amp;#39;, transition: &amp;#39;transform 0.3s&amp;#39;, }; const pictureStyles: React.CSSProperties = { width: &amp;#39;100px&amp;#39;, height: &amp;#39;150px&amp;#39;, margin: &amp;#39;auto&amp;#39;, borderRadius: &amp;#39;50%&amp;#39;, border: &amp;#39;2px solid #000&amp;#39;, }; const nameStyles: React.CSSProperties = { color: &amp;#39;black&amp;#39;, fontSize: &amp;#39;18px&amp;#39;, }; return ( &amp;lt;div style={cardStyles}&amp;gt; &amp;lt;img style={pictureStyles} src={props.image} alt={props.name} width={100} /&amp;gt; &amp;lt;p style={nameStyles}&amp;gt;{props.name}&amp;lt;/p&amp;gt; &amp;lt;/div&amp;gt; );}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Initially, this seemed like a good solution, but there&amp;rsquo;s a few bumps involved. The first is that, or course, this is not real CSS. You can see from the type that TypeScript forced me to use (React.CSSProperties) that these are React types that will get turned into CSS at some distant time in the future. Because of this, there are some oddities - my muscle memory wants to type the CSS property names like &lt;code&gt;box-shadow&lt;/code&gt;, but for this they need to be camel case.&lt;/p&gt;
&lt;p&gt;The next issue of &amp;lsquo;it&amp;rsquo;s not actually CSS&amp;rsquo; was when I wanted to do my hover effect for the cards. In CSS this is just:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;.card:hover { transform: scale(1.05); background-color: lightgoldenrodyellow;}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If I want to do that with inline styles, I need to capture the MouseEnter and MouseLeave events for the card, then swap the inline styles in and out in code. Ain&amp;rsquo;t nobody got the time for that.&lt;/p&gt;
&lt;h3 id="css--component-libraries"&gt;CSS &amp;amp; Component Libraries&lt;/h3&gt;
&lt;p&gt;A common and appealing answer to inline styles is to use CSS libraries such as BootStrap, TailWind etc. As well as allowing you to add complex styles into JSX with short memorable tags, they often enforce a design aesthetic without the developer having to put much thought into it. Since I have minimal design skills, that&amp;rsquo;s an appealing option.&lt;/p&gt;
&lt;p&gt;Taking this a step further are component are React component libraries such as &lt;a href="https://chakra-ui.com/"&gt;chakra&lt;/a&gt;, &lt;a href="https://mui.com/"&gt;Material UI&lt;/a&gt;, and &lt;a href="https://ant.design/components/overview"&gt;Ant&lt;/a&gt;. With these systems, you get styled components (that can be modified) that follow a unified design - you&amp;rsquo;re not really thinking of CSS but at a high level of abstraction.&lt;/p&gt;
&lt;h3 id="componentcss"&gt;component.css&lt;/h3&gt;
&lt;p&gt;I guess the default (since it&amp;rsquo;s generated like this in the create-app step) way of managing CSS closer to our components is to have a .CSS file for each component. This overcomes the &amp;lsquo;cleaning up&amp;rsquo; problem described above. If I&amp;rsquo;m deleting the NavBar component from this project, I can safely eliminate the NavBar.css file at the same time.&lt;/p&gt;
&lt;p&gt;The component CSS file is just imported at the top of the component file.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-12-27-at-3.59.14-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-12-27-at-3.59.14-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you build an app with the CSS in component.css files like this, all the CSS is combined into a single CSS file by the bundler. This raises the possibility of naming conflicts leading the hard to track down problems later. To avoid this I tend to use the component name as a class name and use that to tightly scope the CSS to avoid it bleeding out into other elements.&lt;/p&gt;
&lt;h3 id="modules"&gt;Modules&lt;/h3&gt;
&lt;p&gt;A bit fancier approach is to use CSS modules. This is somewhat similar to the component CSS files described above. Our CSS files have to be renamed to end in &lt;code&gt;.module.css&lt;/code&gt;, then in the code where you normally insert the class names, you use the class names from the styles library:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;import styles from &amp;#34;./Card.module.css&amp;#34;;// props for cardinterface CardProps { name: string; image: string;}function Card(props: CardProps) { return ( &amp;lt;div className={styles.card}&amp;gt; &amp;lt;img className={styles.picture} src={props.image} alt={props.name} width={100} /&amp;gt; &amp;lt;p className={styles.name}&amp;gt;{props.name}&amp;lt;/p&amp;gt; &amp;lt;/div&amp;gt; );}export default Card;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Those class names match the CSS:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;.card { width: 120px; box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); text-align: center; background-color: cornsilk; margin: 10px; padding: 10px; /* tilt and enlarge on mouseover */ transform: scale(1); transition: transform 0.3s;}.card:hover { transform: scale(1.05); background-color: lightgoldenrodyellow;}.name { font-size: 18px; color: black;}.picture { width: 100px; height: 150px; margin: auto; border-radius: 50%; border: 2px solid black;}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The advantage of using modules, is that you now do &lt;em&gt;not&lt;/em&gt; have worry about name clashes. If I build the app and have a look in the generated CSS file, you can see how that works:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;._card_j2a8o_2 { width: 120px; box-shadow: 0 4px 8px #0003; text-align: center; background-color: #fff8dc; margin: 10px; padding: 10px; transform: scale(1); transition: transform 0.3s;}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;All the generated CSS has unique class names created for it! Presumably these match the ones being used in the JSX, and this explains why you need to use the &lt;code&gt;styles.&lt;/code&gt; names in your components.&lt;/p&gt;
&lt;h3 id="styled-components"&gt;Styled Components&lt;/h3&gt;
&lt;p&gt;There are several libraries that tackle the issue of what to do about CSS in React. One of the more popular ones is &lt;code&gt;Styled Components&lt;/code&gt;. How this works is that you define a base component with some styles in it (with a weird backticky syntax), then build your own components from the styled one. It makes more sense when you see it than my written explanation.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;import styled from &amp;#34;styled-components&amp;#34;;const StyledDiv = styled.div` width: 120px; box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); text-align: center; background-color: cornsilk; margin: 10px; padding: 10px; transform: scale(1); transition: transform 0.3s; &amp;amp;:hover { transform: scale(1.05); background-color: lightgoldenrodyellow; }`;const StyledImage = styled.img` width: 100px; height: 150px; margin: auto; border-radius: 50%; border: 2px solid black;`;const StyledP = styled.p` font-size: 18px; color: black;`;// props for cardinterface CardProps { name: string; image: string;}function Card(props: CardProps) { return ( &amp;lt;StyledDiv className=&amp;#34;card&amp;#34;&amp;gt; &amp;lt;StyledImage className=&amp;#34;picture&amp;#34; src={props.image} alt={props.name} width={100} /&amp;gt; &amp;lt;StyledP className=&amp;#34;name&amp;#34;&amp;gt;{props.name}&amp;lt;/StyledP&amp;gt; &amp;lt;/StyledDiv&amp;gt; );}export default Card;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If you don&amp;rsquo;t mind more dependencies, this is a great solution - I like the clarity of the philosophy of creating styled versions of regular elements as React elements, then using them as the building blocks of our new component. All the CSS attributes you&amp;rsquo;ve learned are still there.&lt;/p&gt;
&lt;p&gt;The only thing I needed to look up was how to deal with my hover. In the CSS this had been:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;.card { width: 120px; box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); text-align: center; background-color: cornsilk; margin: 10px; padding: 10px; transform: scale(1); transition: transform 0.3s;}.card:hover { transform: scale(1.05); background-color: lightgoldenrodyellow;}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Which had to be translated into this with the &amp;amp; syntax:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const StyledDiv = styled.div` width: 120px; box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); text-align: center; background-color: cornsilk; margin: 10px; padding: 10px; transform: scale(1); transition: transform 0.3s; &amp;amp;:hover { transform: scale(1.05); background-color: lightgoldenrodyellow; }`;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So - lots of options for dealing with CSS in React. I haven&amp;rsquo;t written much, so I&amp;rsquo;m not sure what my personal preference is. styled-components is definitely the most elegant of the approaches I&amp;rsquo;ve looked at here, but I&amp;rsquo;m a dependency calorie counter so my natural inclination is look elsewhere. The CSS file for each component seems like the next best system - I like that there&amp;rsquo;s no special hooks in the JSX besides the class names I would have used in ordinary HTML, but then you have the risk of name clashes. They can be avoided with the modules - but I am sort of used to dealing with that anyway.&lt;/p&gt;</description></item><item><title>React - a To Do Example</title><link>https://blog.iankulin.com/react-a-to-do-example/</link><pubDate>Mon, 08 Jan 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/react-a-to-do-example/</guid><description>&lt;p&gt;Since I&amp;rsquo;m on a roll making different versions of the To Do app, this might be a good time to talk about &lt;a href="https://react.dev/"&gt;React&lt;/a&gt;. React is one of the giants of front end libraries. It&amp;rsquo;s based on a few big ideas - and to work effectively in React you need to wrap your head around these.&lt;/p&gt;
&lt;h3 id="overview"&gt;Overview&lt;/h3&gt;
&lt;p&gt;Components - when you are developing in React, the starting point of your build is to decompose the user interface in to logical pieces. These components (comprising a mixture of HTML and Javascript) will be the building blocks of your app. In a good composable architecture components are reusable, and that is true for React (there are several sources of components you can pull in). For example, if you created some sort of special slider for your app, it is possible to reuse that quite easily.&lt;/p&gt;
&lt;p&gt;Declarative - this was one of the big barriers when I was learning SwiftUI. The UI is just described. It&amp;rsquo;s not a big step when you&amp;rsquo;re coming from HTML - all that is is a description of the user interface. The next step of this is that React deals with using the state of your data model to update the UI. This means that these state-UI connections are made very explicit (which I like) and protected. For example if there&amp;rsquo;s a counter on a web page, you can&amp;rsquo;t change the HTML of the page to increment the counter, and in fact, you can&amp;rsquo;t directly change the counter. These things are wrapped up so React can manage them, and you have to play by React&amp;rsquo;s rules.&lt;/p&gt;
&lt;p&gt;Virtual DOM - since each component knows what state it depends upon, and changing that state causes the component to be redrawn, it quickly gets expensive with parts of the page being reloaded. To improve performance, and reduce often unneeded flashes of content loading, React keeps a copy of the DOM and makes changes to it rather than the real DOM. Since this is instrumented, React can easily see what changes &lt;em&gt;really&lt;/em&gt; need to be percolated to the browser DOM and can manage how that happens in the most efficient way. Sometimes I think I know what I want to happen for something to be efficient but in React, it&amp;rsquo;s often not in your control, and you just need to trust the system.&lt;/p&gt;
&lt;h3 id="tooling"&gt;Tooling&lt;/h3&gt;
&lt;p&gt;We&amp;rsquo;re leaving my comfort zone of straightforward development environments, along with the major benefit of working in Javascript. The complexity of the React system requires a build step to produce the production artifacts. Whether you use the standard &lt;a href="https://create-react-app.dev/"&gt;Create-React&lt;/a&gt;, or &lt;a href="https://vitejs.dev/"&gt;Vite&lt;/a&gt; (as I did for this project) you get a system for bundling the code, mapping it (since for debugging you need a way to translate the source line that&amp;rsquo;s running back to the human readable source), and a development server to run the app while you&amp;rsquo;re working on it.&lt;/p&gt;
&lt;p&gt;These things inevitably add to complexity and errors, and are the reason that big projects start to need tools like development containers to remove a pain point. Like anything, you get better with experience, but especially at the start there&amp;rsquo;s a developer cost involved. React is incredibly popular, so most people&amp;rsquo;s calculus on this is that the extra complexity in project management is made up for with improved developer experience.&lt;/p&gt;
&lt;p&gt;In any case, I got started by typing &lt;code&gt;npm create vite@latest .&lt;/code&gt; and then choosing React and Javascript. This created a starter project, and spun it up in the development server. In the &lt;code&gt;package.json&lt;/code&gt; file it gives you, there is a build command that can be run with &lt;code&gt;npm run build&lt;/code&gt; to create the output files. I had issues with the development server that serves up these frontend files - since I also needed to run a real server (the unchanged REST API server for Todo Items data from the earlier project) on a different port. Then it complained - thinking it was part of a cross-site scripting hack. To overcome it, I just built the production code for each round of testing - with such a little project this wasn&amp;rsquo;t a hardship. But when I did have an error to track, it pointed me to a line number that turned out to be about 20 pages of minified code.&lt;/p&gt;
&lt;h3 id="components"&gt;Components&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;Just a little reminder of how this app looks so we can think about how we&amp;rsquo;re going to break it down. There is a list of todo items, each one has a button to delete it (which we do when it has been completed) then at the bottom there&amp;rsquo;s a little form to add a new item (by clicking the button or hitting enter) to the bottom of the list.&lt;/p&gt;
&lt;p&gt;I ended up with three components - the App (every React app has one), the TodoList, and the AddTodoForm. Note that I could easily have had a forth component - the TodoItem. This is a bit of matter of taste - I probably would have if I wanted to do something fancier like editing in place - but for the current UX the cost of extracting out another component wasn&amp;rsquo;t worth the benefit.&lt;/p&gt;
&lt;h3 id="anatomy-of-a-component"&gt;Anatomy of a component&lt;/h3&gt;
&lt;p&gt;I claimed earlier that a component was it&amp;rsquo;s HTML and Javascript wrapped up together which, while a massive simpliciation, is a good place to start thinking about it. Every component is just a function that returns a bunch of (templated) HTML. We&amp;rsquo;ll start off by developing our AddTodoForm. At it&amp;rsquo;s simplest, it could be something like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;function AddTodoForm() { return ( &amp;lt;form&amp;gt; &amp;lt;input type=&amp;#34;text&amp;#34; name=&amp;#34;todo_item&amp;#34; id=&amp;#34;todo&amp;#34; required /&amp;gt; &amp;lt;button type=&amp;#34;submit&amp;#34;&amp;gt;Add&amp;lt;/button&amp;gt; &amp;lt;/form&amp;gt; );}export default AddTodoForm;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;But this little form component can&amp;rsquo;t really talk to the world, where as we need it to add a todo item. First, let&amp;rsquo;s track any changes to the text field.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;import { useState } from &amp;#39;react&amp;#39;;function AddTodoForm(props) { const [value, setValue] = useState(&amp;#39;&amp;#39;); return ( &amp;lt;form onSubmit={handleSubmit}&amp;gt; &amp;lt;input type=&amp;#34;text&amp;#34; name=&amp;#34;todo_item&amp;#34; id=&amp;#34;todo&amp;#34; value={value} onChange={e =&amp;gt; setValue(e.target.value)} required /&amp;gt; &amp;lt;button type=&amp;#34;submit&amp;#34;&amp;gt;Add&amp;lt;/button&amp;gt; &amp;lt;/form&amp;gt; );}export default AddTodoForm;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This is an example of the very explicit management of state I was talking about earlier.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;const [value, setValue] = useState('');&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;useState() is a React hook for managing state. This line gives us a getter (value) and setter (setValue) for this variable, and set&amp;rsquo;s its initial state to &amp;lsquo;&amp;rsquo;. If the value changes the component will be redrawn. React will know that the value has changed as this is built into the setValue() function where we never need see or worry about it. If you foolishly decided to side-step React and assign directly to &lt;code&gt;value&lt;/code&gt;, I guess there&amp;rsquo;d be a runtime error, or even worse, no error and the management of the DOM state wuld fall into some type of chaos.&lt;/p&gt;
&lt;p&gt;What we&amp;rsquo;re doing with this value (which I&amp;rsquo;m now realising is very badly named) is using it to collect the text input from out form. It&amp;rsquo;s constantly updated as the user types.&lt;/p&gt;
&lt;p&gt;onChange={e =&amp;gt; setValue(e.target.value)}&lt;/p&gt;
&lt;p&gt;So that&amp;rsquo;s our value sorted, but of course we need to add it to our data model. This model is being managed at the App level so there&amp;rsquo;s a bit of juggling needed to get it out to there. This &amp;lsquo;plumbing&amp;rsquo; cost is the downside of these types or framework, but it&amp;rsquo;s not really complex and quickly becomes routine.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;ll start at the top, here&amp;rsquo;s the relevant code from App.jsx - our App component.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; const [todos, setTodos] = useState([]); const addTodo = (newTodo) =&amp;gt; { fetch(&amp;#39;http://localhost:3000/todos&amp;#39;, { method: &amp;#39;POST&amp;#39;, headers: { &amp;#39;Content-Type&amp;#39;: &amp;#39;application/json&amp;#39;, }, body: JSON.stringify(newTodo), }) .then(response =&amp;gt; response.json()) .then(data =&amp;gt; { // Update the todos state with the new todo setTodos([...todos, data]); }); };
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This useState hook should be starting to look familiar. We read the todos from &lt;code&gt;todos&lt;/code&gt;, and write to them with &lt;code&gt;setTodos()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;addTodo&lt;/code&gt; does the actual work of saving it to the database (via our REST API) then creates a new &lt;code&gt;todos&lt;/code&gt; array by adding our new one to the end. But we need to pass this down into our AddTodoForm. Here&amp;rsquo;s the main part of the App that returns out HTML, in this case that&amp;rsquo;s the list of todos and our form:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; return ( &amp;lt;main&amp;gt; &amp;lt;h1&amp;gt;To do&amp;lt;/h1&amp;gt; &amp;lt;TodoList todos={todos} onDeleteTodo={deleteTodo}/&amp;gt; &amp;lt;AddTodoForm onAddTodo={addTodo} /&amp;gt; &amp;lt;/main&amp;gt; )
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You can see here that the todos array and a function &lt;em&gt;deleteTodo&lt;/em&gt;() are passed into the the TodoList component, and that our &lt;em&gt;addTodo()&lt;/em&gt; function is passed to the AddTodoForm.&lt;/p&gt;
&lt;p&gt;In React, things like this that are passed into a component are passed as a single object variable called &amp;lsquo;props&amp;rsquo; - short for properties. It seems crazy to me to be bundling them like this in a language in 2023 rather than passing them explicitly as separate variables. This lack or clarity about what&amp;rsquo;s being passed into a component is doubtless one of the reasons TypeScript is such a common combo with React. It&amp;rsquo;s certainly the first time I&amp;rsquo;ve felt the need of it.&lt;/p&gt;
&lt;p&gt;There is a lighter partial solution to adding types to props so that the linter can call out any issues - this is the PropTypes library - once it&amp;rsquo;s installed, we import it with &lt;code&gt;import PropTypes from 'prop-types';&lt;/code&gt; then we can add a definition for the props at the bottom of the file. For our AddTodoForm, this would look like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;AddTodoForm.propTypes = { onAddTodo: PropTypes.func.isRequired,};
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now the linter will prevent us from using anything other than props.onAddToDo, and it will flag if it&amp;rsquo;s being used as anything other than a function.&lt;/p&gt;
&lt;p&gt;Anyway, the props are passed in and we can extract the function from it to use.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;import { useState } from &amp;#39;react&amp;#39;;function AddTodoForm(props) { const [value, setValue] = useState(&amp;#39;&amp;#39;); const handleSubmit = (event) =&amp;gt; { event.preventDefault(); if (!value) return; props.onAddTodo({ todo_item: value }); setValue(&amp;#39;&amp;#39;); }; return ( &amp;lt;form onSubmit={handleSubmit}&amp;gt; &amp;lt;input type=&amp;#34;text&amp;#34; name=&amp;#34;todo_item&amp;#34; id=&amp;#34;todo&amp;#34; value={value} onChange={e =&amp;gt; setValue(e.target.value)} required /&amp;gt; &amp;lt;button type=&amp;#34;submit&amp;#34;&amp;gt;Add&amp;lt;/button&amp;gt; &amp;lt;/form&amp;gt; );}export default AddTodoForm;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The arrangement of elements in this file will start to become familiar - there&amp;rsquo;s often some state at the top with the useState() hook, then a few handler functions, then our final return of the &amp;lsquo;HTML&amp;rsquo;.&lt;/p&gt;
&lt;p&gt;Our TodoList is a bit simpler in that it doesn&amp;rsquo;t have any handlers, but a better illustration of using PropTypes since it has access to the global state of the todos.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;import PropTypes from &amp;#39;prop-types&amp;#39;;function TodoList(props) { return ( &amp;lt;ul&amp;gt; {props.todos.map(todo =&amp;gt; ( &amp;lt;li key={todo.id}&amp;gt; {todo.todo_item} &amp;lt;button onClick={() =&amp;gt; props.onDeleteTodo(todo.id)}&amp;gt; Done &amp;lt;/button&amp;gt; &amp;lt;/li&amp;gt; ))} &amp;lt;/ul&amp;gt; )}TodoList.propTypes = { todos: PropTypes.array.isRequired, onDeleteTodo: PropTypes.func.isRequired};export default TodoList;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="conclusion"&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;You would not sensibly reach for React for a project this size - the complexity of the tooling, and the fact that we&amp;rsquo;re now shipping 150K of Javascript to do something we were nicely achieving in 2K of vanilla, or 15K with htmx makes me deeply uncomfortable. Nevertheless, hundreds of thousands of developers can&amp;rsquo;t be wrong - React&amp;rsquo;s component model is a powerful one for building modern single page applications, especially when it allows you to pull in components from public or corporate collections.&lt;/p&gt;
&lt;p&gt;I plan on doing some more React - partly because its just such big part of the webdev world, and partly because I&amp;rsquo;d like to get some experience with TypeScript, so I&amp;rsquo;m fishing around for a medium size project to play with both of these technologies.&lt;/p&gt;
&lt;p&gt;(&lt;a href="https://github.com/IanKulin/todo/tree/react"&gt;source&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:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-html" data-lang="html"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&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:#75715e"&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;&amp;lt;&lt;span style="color:#f92672"&gt;html&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;lang&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;en&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;head&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;charset&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;UTF-8&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;meta&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;viewport&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;content&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;width=device-width, initial-scale=1.0&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;title&lt;/span&gt;&amp;gt;Todos&amp;lt;/&lt;span style="color:#f92672"&gt;title&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;link&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;rel&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;stylesheet&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;href&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;./styles.css&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;head&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;body&lt;/span&gt;&amp;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:#75715e"&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; &amp;lt;&lt;span style="color:#f92672"&gt;main&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;h1&lt;/span&gt;&amp;gt;To do&amp;lt;/&lt;span style="color:#f92672"&gt;h1&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;ul&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;todos_list&amp;#34;&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span style="color:#f92672"&gt;ul&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&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:#75715e"&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; &amp;lt;&lt;span style="color:#f92672"&gt;form&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;action&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;/todos&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;method&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;POST&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;input&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;type&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;text&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;todo&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;todo&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;required&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;button&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;type&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;submit&amp;#34;&lt;/span&gt;&amp;gt;Add&amp;lt;/&lt;span style="color:#f92672"&gt;button&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;form&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;main&lt;/span&gt;&amp;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;lt;&lt;span style="color:#f92672"&gt;script&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;src&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;index.js&amp;#34;&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span style="color:#f92672"&gt;script&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;body&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;html&lt;/span&gt;&amp;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:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&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:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;express&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;express&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;app&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;express&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;port&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;3000&lt;/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:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;sqlite3&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;sqlite3&amp;#39;&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;verbose&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;db&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;sqlite3&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Database&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;db/todos.sqlite&amp;#39;&lt;/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:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;use&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;express&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;json&lt;/span&gt;());
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;use&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;express&lt;/span&gt;.&lt;span style="color:#66d9ef"&gt;static&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;public&amp;#39;&lt;/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:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;get&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;/todos&amp;#39;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;db&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;all&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;SELECT * FROM todos&amp;#39;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;rows&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;500&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;json&lt;/span&gt;({ &lt;span style="color:#a6e22e"&gt;error&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;message&lt;/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:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;200&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;json&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;rows&lt;/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;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;post&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;/todos&amp;#39;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#f92672"&gt;!&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;body&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;todo_item&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;400&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;json&lt;/span&gt;({ &lt;span style="color:#a6e22e"&gt;error&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Missing todo_item field in request body&amp;#39;&lt;/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:#a6e22e"&gt;db&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;run&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;INSERT INTO todos (todo_item) VALUES (?)&amp;#39;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;body&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;todo_item&lt;/span&gt;, &lt;span style="color:#66d9ef"&gt;function&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;500&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;json&lt;/span&gt;({ &lt;span style="color:#a6e22e"&gt;error&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;message&lt;/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:#75715e"&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; &lt;span style="color:#a6e22e"&gt;db&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;get&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;SELECT * FROM todos WHERE id = ?&amp;#39;&lt;/span&gt;, &lt;span style="color:#66d9ef"&gt;this&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;lastID&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;row&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;500&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;json&lt;/span&gt;({ &lt;span style="color:#a6e22e"&gt;error&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;message&lt;/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:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;201&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;json&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;row&lt;/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;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:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#66d9ef"&gt;delete&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;/todos/:id&amp;#39;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;db&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;run&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;DELETE FROM todos WHERE id = ?&amp;#39;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;params&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;500&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;json&lt;/span&gt;({ &lt;span style="color:#a6e22e"&gt;error&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;message&lt;/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:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;204&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;end&lt;/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;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&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;&lt;span style="color:#a6e22e"&gt;db&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;run&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;CREATE TABLE IF NOT EXISTS todos (id INTEGER PRIMARY KEY AUTOINCREMENT, todo_item TEXT)&amp;#39;&lt;/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:#75715e"&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;&lt;span style="color:#a6e22e"&gt;process&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;on&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;SIGINT&amp;#39;&lt;/span&gt;, () =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;db&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;close&lt;/span&gt;((&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;console&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;error&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;message&lt;/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:#a6e22e"&gt;console&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;log&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;Database closed.&amp;#39;&lt;/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:#a6e22e"&gt;process&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;exit&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;0&lt;/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 style="color:#75715e"&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;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;listen&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;port&lt;/span&gt;, () =&amp;gt; &lt;span style="color:#a6e22e"&gt;console&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;log&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;`Todo app listening on http://localhost:&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;port&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&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:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&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:#75715e"&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:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;createTodoItem&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;todoItemText&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;li&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; document.&lt;span style="color:#a6e22e"&gt;createElement&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;li&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;button&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; document.&lt;span style="color:#a6e22e"&gt;createElement&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;button&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;button&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;innerHTML&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Done&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;button&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;addEventListener&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;click&amp;#39;&lt;/span&gt;, () =&amp;gt; &lt;span style="color:#a6e22e"&gt;handleDelete&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;li&lt;/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:#75715e"&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:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;todoTextNode&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; document.&lt;span style="color:#a6e22e"&gt;createTextNode&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;todoItemText&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;li&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;appendChild&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;todoTextNode&lt;/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:#75715e"&gt;// Then append the delete button
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;li&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;appendChild&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;button&lt;/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:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;li&lt;/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 style="color:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;handleDelete&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;li&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;fetch&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;/todos/&amp;#39;&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;, {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;method&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&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&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;then&lt;/span&gt;(() =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;li&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;remove&lt;/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;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&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;&lt;span style="color:#a6e22e"&gt;fetch&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;/todos&amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;then&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;res&lt;/span&gt; =&amp;gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;json&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;then&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;todos&lt;/span&gt; =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&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; &lt;span style="color:#a6e22e"&gt;todos&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;forEach&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;todo&lt;/span&gt; =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;li&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;createTodoItem&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;todo&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;todo_item&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;todo&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; document.&lt;span style="color:#a6e22e"&gt;querySelector&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;#todos_list&amp;#39;&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;appendChild&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;li&lt;/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;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&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;document.&lt;span style="color:#a6e22e"&gt;querySelector&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;form&amp;#39;&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;addEventListener&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;submit&amp;#39;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;e&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;e&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;preventDefault&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;todo_item&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; document.&lt;span style="color:#a6e22e"&gt;querySelector&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;#todo&amp;#39;&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;value&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;fetch&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;/todos&amp;#39;&lt;/span&gt;, {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;method&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;POST&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;headers&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Content-Type&amp;#39;&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&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&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;body&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;JSON&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;stringify&lt;/span&gt;({ &lt;span style="color:#a6e22e"&gt;todo_item&lt;/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:#a6e22e"&gt;then&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;res&lt;/span&gt; =&amp;gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;json&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;then&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;data&lt;/span&gt; =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;li&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;createTodoItem&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;todo_item&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;data&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; document.&lt;span style="color:#a6e22e"&gt;querySelector&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;#todos_list&amp;#39;&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;appendChild&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;li&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; document.&lt;span style="color:#a6e22e"&gt;querySelector&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;#todo&amp;#39;&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;value&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#39;&lt;/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;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;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;&amp;lt;html lang=&amp;#34;en&amp;#34;&amp;gt;&amp;lt;head&amp;gt; &amp;lt;meta charset=&amp;#34;UTF-8&amp;#34;&amp;gt; &amp;lt;meta name=&amp;#34;viewport&amp;#34; content=&amp;#34;width=device-width, initial-scale=1.0&amp;#34;&amp;gt; https://unpkg.com/htmx.org/dist/htmx.min.js &amp;lt;title&amp;gt;Todos&amp;lt;/title&amp;gt; &amp;lt;link rel=&amp;#34;stylesheet&amp;#34; href=&amp;#34;./styles.css&amp;#34;&amp;gt;&amp;lt;/head&amp;gt;&amp;lt;body&amp;gt;&amp;lt;!-- Main section--&amp;gt; &amp;lt;main&amp;gt; &amp;lt;h1&amp;gt;To do&amp;lt;/h1&amp;gt; &amp;lt;ul id=&amp;#34;todos_list&amp;#34; hx-get=&amp;#34;/todos&amp;#34; hx-trigger=&amp;#34;load&amp;#34;&amp;gt;&amp;lt;/ul&amp;gt; &amp;lt;!-- the list of todo items from the database gets inserted here--&amp;gt; &amp;lt;!-- form to add a todo --&amp;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; &amp;lt;input type=&amp;#34;text&amp;#34; name=&amp;#34;todo_item&amp;#34; id=&amp;#34;todo&amp;#34; required&amp;gt; &amp;lt;button type=&amp;#34;submit&amp;#34;&amp;gt;Add&amp;lt;/button&amp;gt; &amp;lt;/form&amp;gt; &amp;lt;/main&amp;gt;                  &amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&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;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;ul id=&amp;#34;todos_list&amp;#34; hx-get=&amp;#34;/todos&amp;#34; hx-trigger=&amp;#34;load&amp;#34;&amp;gt;&amp;lt;/ul&amp;gt;
&lt;/code&gt;&lt;/pre&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;pre tabindex="0"&gt;&lt;code&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;/code&gt;&lt;/pre&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:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&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:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;express&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;express&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;app&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;express&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;port&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;3000&lt;/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:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;sqlite3&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;sqlite3&amp;#39;&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;verbose&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;db&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;sqlite3&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Database&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;db/todos.sqlite&amp;#39;&lt;/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:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;use&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;express&lt;/span&gt;.&lt;span style="color:#66d9ef"&gt;static&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;public&amp;#39;&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;use&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;express&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;urlencoded&lt;/span&gt;({ &lt;span style="color:#a6e22e"&gt;extended&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;true&lt;/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:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;htmlForTodoItem&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;uid&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;item_text&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;html&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;`&amp;lt;li&amp;gt;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;item_text&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;`&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;html&lt;/span&gt; &lt;span style="color:#f92672"&gt;+=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;`&amp;lt;button hx-delete=&amp;#34;todos/&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;uid&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34; hx-target=&amp;#34;closest li&amp;#34; `&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;html&lt;/span&gt; &lt;span style="color:#f92672"&gt;+=&lt;/span&gt; &lt;span style="color:#e6db74"&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&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;html&lt;/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 style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;get&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;/todos&amp;#39;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;db&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;all&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;SELECT * FROM todos&amp;#39;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;rows&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;console&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;log&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;message&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;500&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;send&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;&amp;lt;li&amp;gt;database error&amp;lt;/li&amp;gt;&amp;#39;&lt;/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:#75715e"&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:#66d9ef"&gt;let&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;list&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;rows&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;forEach&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;row&lt;/span&gt; =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;list&lt;/span&gt; &lt;span style="color:#f92672"&gt;+=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;htmlForTodoItem&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;row&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;row&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;todo_item&lt;/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:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;200&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;send&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;list&lt;/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;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;post&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;/todos&amp;#39;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#f92672"&gt;!&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;body&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;todo_item&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;400&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;send&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;Missing todo_item field in request body&amp;#39;&lt;/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:#a6e22e"&gt;db&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;run&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;INSERT INTO todos (todo_item) VALUES (?)&amp;#39;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;body&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;todo_item&lt;/span&gt;, &lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;console&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;log&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;message&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;500&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;send&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;&amp;lt;li&amp;gt;database error&amp;lt;/li&amp;gt;&amp;#39;&lt;/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:#75715e"&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; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;200&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;send&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;htmlForTodoItem&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;this&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;lastID&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;body&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;todo_item&lt;/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;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#66d9ef"&gt;delete&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;/todos/:id&amp;#39;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;db&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;run&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;DELETE FROM todos WHERE id = ?&amp;#39;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;params&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;console&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;log&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;message&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;500&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;send&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;&amp;lt;li&amp;gt;database error&amp;lt;/li&amp;gt;&amp;#39;&lt;/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:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;200&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;end&lt;/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;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&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;&lt;span style="color:#a6e22e"&gt;db&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;run&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;CREATE TABLE IF NOT EXISTS todos (id INTEGER PRIMARY KEY AUTOINCREMENT, todo_item TEXT)&amp;#39;&lt;/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:#75715e"&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;&lt;span style="color:#a6e22e"&gt;process&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;on&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;SIGINT&amp;#39;&lt;/span&gt;, () =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;db&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;close&lt;/span&gt;((&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;console&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;error&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;message&lt;/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:#a6e22e"&gt;console&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;log&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;Database closed.&amp;#39;&lt;/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:#a6e22e"&gt;process&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;exit&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;0&lt;/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 style="color:#75715e"&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;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;listen&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;port&lt;/span&gt;, () =&amp;gt; &lt;span style="color:#a6e22e"&gt;console&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;log&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;`Todo app listening on http://localhost:&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;port&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&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;pre tabindex="0"&gt;&lt;code&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;/code&gt;&lt;/pre&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>Adding Front Matter To mdserver</title><link>https://blog.iankulin.com/adding-front-matter-to-mdserver/</link><pubDate>Fri, 24 Nov 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/adding-front-matter-to-mdserver/</guid><description>&lt;p&gt;The very first issue I opened on &lt;a href="https://blog.iankulin.com/displaying-markdown-as-html/"&gt;mdserver&lt;/a&gt; - my server project that serves HTML from markdown files - was that the title of the page (which shows in the browser tab, and is used for browser bookmarks) needed to be set &lt;em&gt;inside&lt;/em&gt; the markdown file, rather than generated from the file name. I didn&amp;rsquo;t invent this idea - I&amp;rsquo;ve seen this sort of metadata in the top of Jekyll and Hugo markdown. Here&amp;rsquo;s an example from the &lt;a href="https://jekyllrb.com/docs/front-matter/"&gt;Jekyll website&lt;/a&gt;:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;---
layout: post
title: Blogging Like a Hacker
---
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You can&amp;rsquo;t really see in this example, but the format is YAML. Although I might be interested in using it for other things (such as selecting a template) later, for now, all I need is a title. The process would be that the server would extract the title from the front matter, then inject that into the template HTML so the page had a proper title.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m using the &lt;a href="https://showdownjs.com/"&gt;Showdown&lt;/a&gt; library to do the conversion from markdown. Here&amp;rsquo;s a short demo of how that works:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const showdown = require(&amp;#39;showdown&amp;#39;);
const converter = new showdown.Converter();

const markdown = `
# heading
Some random Text
* list item
* another`

const rawHtml = converter.makeHtml(markdown);

console.log(rawHtml);
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This would output:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;h1 id=&amp;#34;heading&amp;#34;&amp;gt;heading&amp;lt;/h1&amp;gt;
&amp;lt;p&amp;gt;Some random Text&amp;lt;/p&amp;gt;
&amp;lt;ul&amp;gt;
&amp;lt;li&amp;gt;list item&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;another&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Showdown is an 827K dependency, so I figured it might already deal with front matter, or would at least have some sort of extension hooks so I could write something to scrape the title out. In fact it has both.&lt;/p&gt;
&lt;p&gt;To enable front matter, you just have to set a flag in the converter, then there&amp;rsquo;s a .getMetadata() method on the converter to get an object of all the metadata. Let&amp;rsquo;s flesh out my demo code a bit to show this, I&amp;rsquo;ll highlight the changes.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;const showdown = require(&amp;#39;showdown&amp;#39;);
const converter = new showdown.Converter({metadata: true});

const markdown = `
---
title: Test Title
---
# heading
Some random Text
* list item
* another`

const rawHtml = converter.makeHtml(markdown);

//console.log(rawHtml);
console.log(converter.getMetadata().title);
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This simply outputs &lt;code&gt;Test Title&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re wondering if that YAML pollutes the HTML output at all, it does not. The HTML from this second example is exactly the same as the first example above without the YAML.&lt;/p&gt;</description></item></channel></rss>