<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<id>https://blog.dennisokeeffe.com</id>
<title>Dennis O'Keeffe blog</title>
<updated>2026-10-24T00:00:00.000Z</updated>
<author><name>Dennis O'Keeffe</name><uri>https://dennisokeeffe.com/about</uri></author>
<link rel="alternate" href="https://dennisokeeffe.com"/>
<link rel="self" href="https://dennisokeeffe.com/rss/atom.xml"/>
<subtitle>Yet another dev blog.</subtitle>
<rights>All rights reserved 2026, Dennis O'Keeffe</rights>
<entry>
<title type="html"><![CDATA[Building the Matcha Byron Bay website]]></title>
<id>https://blog.dennisokeeffe.com/blog/2026-10-24-building-the-matcha-byron-bay-website</id>
<link href="https://dennisokeeffe.com/blog/2026-10-24-building-the-matcha-byron-bay-website"/>
<published>2026-10-24T00:00:00.000Z</published>
<updated>2026-10-24T00:00:00.000Z</updated>
<summary type="html"><![CDATA[A rundown on the design, technology and wholesale work behind the new Matcha Byron Bay website]]></summary>
<content type="html"><![CDATA[<p>Following on from <a href="https://dennisokeeffe.com/blog/2026-06-08-building-the-pocket-rice-marketing-website">the Pocket Rice marketing website</a>, the next website I have been working on is for <a href="https://matchabyronbay.com" target="_blank" rel="noopener noreferrer">Matcha Byron Bay</a>.</p>
<p>Matcha Byron Bay started as a stall at the Byron Bay Farmers Market in July 2022. The business is run by Japanese locals who source their matcha from Uji (near Kyoto), and these days they sell online Australia-wide and supply matcha and hojicha to cafes, retailers and other businesses.</p>
<p>This blog post gives an overview of the new website, some of the technology behind it (kept at a high level), the design choices we made, and where things are at with wholesale, SEO and sales so far.</p>
<h2 id="where-we-started">Where we started</h2>
<p>The previous website was built on WordPress with WooCommerce. It did the job, but it had some issues that were costing the business:</p>
<ol>
<li>Scroll-jacking on the home page that would stop you scrolling all of a sudden.</li>
<li>Adding to cart was slow, and some layouts broke at certain screen sizes.</li>
<li>The blog only ever showed 9 articles, even though there were roughly 37 of them.</li>
</ol>
<p>The guiding principle for the rebuild was that the website is the means, not the end. It exists to sell products, generate wholesale enquiries, build trust in the brand and reduce the effort it takes for someone to buy. Every decision below was weighed against those outcomes.</p>
<p>The new website launched in July 2026. As part of that, we migrated the products, collections, customers, historical orders, articles and media across to Shopify, and mapped the legacy URLs over with redirects so that existing search traffic still landed somewhere sensible.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2026-10-24-building-the-matcha-byron-bay-website/home-page-1.webp" alt="The new Matcha Byron Bay home page" loading="lazy" decoding="async"/><figcaption>The new Matcha Byron Bay home page</figcaption></figure>
<h2 id="website-at-a-high-level">Website at a high-level</h2>
<p>Shopify is the commerce backbone. It owns the catalogue, pricing, checkout, customer accounts, orders, payments, subscriptions and (more recently) wholesale companies.</p>
<p>The storefront itself is headless. It is a <a href="https://tanstack.com/start" target="_blank" rel="noopener noreferrer">TanStack Start</a> React application that talks to Shopify through the <a href="https://shopify.dev/docs/api/storefront" target="_blank" rel="noopener noreferrer">Storefront API</a>, using Shopify&#39;s new framework-agnostic <a href="https://hydrogen.shopify.dev/update/hydrogen-developer-preview" target="_blank" rel="noopener noreferrer">Hydrogen developer preview</a> (what I have been referring to as &quot;Hydrogen 2.0&quot;).</p>
<p>This version of Hydrogen describes itself as a toolkit rather than a framework. The original Hydrogen was tied to Remix (and later React Router), whereas the new core is plain JavaScript with thin bindings on top. That gives you a typed Storefront API client, cart handlers that work before any client JavaScript has loaded, and consent-aware analytics, without being locked into a particular framework. TanStack Start is not one of the officially listed frameworks in the preview, but because the core is framework-agnostic, wiring it in was straightforward.</p>
<aside class="callout callout--warning" aria-label="Warning"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><path d="m21.7 18-8-14a2 2 0 0 0-3.4 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.7-3ZM12 9v4M12 17h.01" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>  <div class="callout__body">  <p>The Hydrogen developer preview is exactly that: a preview. We pin it to an
exact version and review every upgrade manually, since the APIs are still
moving.</p> </div> </aside>
<p>The storefront is server-rendered on <a href="https://developers.cloudflare.com/workers/" target="_blank" rel="noopener noreferrer">Cloudflare Workers</a>, so pages render close to the customer and static assets are served from Cloudflare&#39;s edge. Checkout is still Shopify&#39;s hosted checkout, so payments stay with Shopify.</p>
<p>The following is a simplified look at the architecture:</p>
<figure class="diagram" data-pele="true" data-pagefind-ignore="all"><div class="diagram-canvas"><svg xmlns="http://www.w3.org/2000/svg" class="pele pele-architecture" viewBox="0 0 534 452" width="534" height="452" fill="var(--_fg)" role="graphics-document document" aria-roledescription="architecture"><g transform="translate(8,8)"><g class="pele-clusters"><g class="pele-cluster" data-id="cf"><rect x="108" y="0" width="234" height="436" fill="var(--_a)" fill-opacity="0.5" stroke="var(--_b)"></rect><svg class="pele-icon" data-icon="cloud" x="118" y="8" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 18a4 4 0 0 1-.5-7.97A6 6 0 0 1 18 9.5a4.25 4.25 0 0 1-.5 8.5z" stroke="var(--_fg)" stroke-width="1.5"></path></svg><text class="pele-cluster-label" fill="var(--_m)" x="192" y="23.6" text-anchor="middle">Cloudflare</text></g><g class="pele-cluster" data-id="shopify"><rect x="382" y="170" width="136" height="266" fill="var(--_a)" fill-opacity="0.5" stroke="var(--_b)"></rect><svg class="pele-icon" data-icon="cloud" x="392" y="178" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 18a4 4 0 0 1-.5-7.97A6 6 0 0 1 18 9.5a4.25 4.25 0 0 1-.5 8.5z" stroke="var(--_fg)" stroke-width="1.5"></path></svg><text class="pele-cluster-label" fill="var(--_m)" x="451.6" y="193.6" text-anchor="middle">Shopify</text></g></g><g class="pele-edges" fill="none" stroke="var(--_l)" stroke-linecap="round" stroke-linejoin="round"><g class="pele-edge" data-id="visitor-worker"><path d="M58,230L145,230"></path><path class="pele-marker" d="M152,230L144,233.36L144,226.64Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge" data-id="worker-assets"><path d="M176,206L176,139"></path><path class="pele-marker" d="M176,132L179.36,140L172.64,140Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge" data-id="worker-api"><path d="M200,230L419,230"></path><path class="pele-marker" d="M426,230L418,233.36L418,226.64Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge" data-id="worker-storage"><path d="M176,284L176,333"></path><path class="pele-marker" d="M176,340L172.64,332L179.36,332Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge" data-id="api-checkout"><path d="M450,302L450,333"></path><path class="pele-marker" d="M450,340L446.64,332L453.36,332Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge" data-id="checkout-jobs"><path d="M426,364L324,364"></path><path class="pele-marker" d="M317,364L325,360.64L325,367.36Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge" data-id="jobs-storage"><path d="M269,364L207,364"></path><path class="pele-marker" d="M200,364L208,360.64L208,367.36Z" fill="var(--_l)" stroke="none"></path></g></g><g class="pele-nodes"><g class="pele-node pele-service" data-id="visitor" transform="translate(34,206)"><rect x="-24" width="48" height="48" fill="var(--_s)" stroke="var(--_b)"></rect><svg class="pele-icon" data-icon="internet" x="-12" y="12" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0-18 0M3 12h18M12 3a13 13 0 0 0 0 18M12 3a13 13 0 0 1 0 18" stroke="var(--_fg)" stroke-width="1.5"></path></svg><text class="pele-label" x="0" y="69.6" text-anchor="middle">Visitor</text></g><g class="pele-node pele-service" data-id="assets" transform="translate(176,36)"><rect x="-24" width="48" height="48" fill="var(--_s)" stroke="var(--_b)"></rect><svg class="pele-icon" data-icon="disk" x="-12" y="12" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 3h14v18H5zM12 6.5a4 4 0 1 0 0 8a4 4 0 1 0 0-8M12 10.5h.01M8 18h3" stroke="var(--_fg)" stroke-width="1.5"></path></svg><text class="pele-label" text-anchor="middle" xml:space="preserve"><tspan x="0" y="68.1">Static</tspan><tspan x="0" y="89.1">assets</tspan></text></g><g class="pele-node pele-service" data-id="worker" transform="translate(176,206)"><rect x="-24" width="48" height="48" fill="var(--_s)" stroke="var(--_b)"></rect><svg class="pele-icon" data-icon="server" x="-12" y="12" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 4h18v7H3zM3 13h18v7H3zM7 7.5h.01M7 16.5h.01M11 7.5h6M11 16.5h6" stroke="var(--_fg)" stroke-width="1.5"></path></svg><text class="pele-label" x="0" y="69.6" text-anchor="middle">SSR Worker</text></g><g class="pele-node pele-service" data-id="storage" transform="translate(176,340)"><rect x="-24" width="48" height="48" fill="var(--_s)" stroke="var(--_b)"></rect><svg class="pele-icon" data-icon="database" x="-12" y="12" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6a8 3 0 1 0 16 0a8 3 0 1 0-16 0M4 6v12a8 3 0 0 0 16 0V6M4 12a8 3 0 0 0 16 0" stroke="var(--_fg)" stroke-width="1.5"></path></svg><text class="pele-label" x="0" y="69.6" text-anchor="middle">Storage</text></g><g class="pele-node pele-service" data-id="jobs" transform="translate(293,340)"><rect x="-24" width="48" height="48" fill="var(--_s)" stroke="var(--_b)"></rect><svg class="pele-icon" data-icon="server" x="-12" y="12" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 4h18v7H3zM3 13h18v7H3zM7 7.5h.01M7 16.5h.01M11 7.5h6M11 16.5h6" stroke="var(--_fg)" stroke-width="1.5"></path></svg><text class="pele-label" x="0" y="69.6" text-anchor="middle">Queues</text></g><g class="pele-node pele-service" data-id="api" transform="translate(450,206)"><rect x="-24" width="48" height="48" fill="var(--_s)" stroke="var(--_b)"></rect><svg class="pele-icon" data-icon="server" x="-12" y="12" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 4h18v7H3zM3 13h18v7H3zM7 7.5h.01M7 16.5h.01M11 7.5h6M11 16.5h6" stroke="var(--_fg)" stroke-width="1.5"></path></svg><text class="pele-label" text-anchor="middle" xml:space="preserve"><tspan x="0" y="68.1">Storefront</tspan><tspan x="0" y="89.1">API</tspan></text></g><g class="pele-node pele-service" data-id="checkout" transform="translate(450,340)"><rect x="-24" width="48" height="48" fill="var(--_s)" stroke="var(--_b)"></rect><svg class="pele-icon" data-icon="internet" x="-12" y="12" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0-18 0M3 12h18M12 3a13 13 0 0 0 0 18M12 3a13 13 0 0 1 0 18" stroke="var(--_fg)" stroke-width="1.5"></path></svg><text class="pele-label" x="0" y="69.6" text-anchor="middle">Checkout</text></g></g></g></svg></div><template>architecture-beta
group cf(cloud)[Cloudflare]
group shopify(cloud)[Shopify]

service visitor(internet)[Visitor]
service assets(disk)[Static assets] in cf
service worker(server)[SSR Worker] in cf
service storage(database)[Storage] in cf
service jobs(server)[Queues] in cf
service api(server)[Storefront API] in shopify
service checkout(internet)[Checkout] in shopify

visitor:R --&gt; L:worker
worker:T --&gt; B:assets
worker:R --&gt; L:api
worker:B --&gt; T:storage
api:B --&gt; T:checkout
checkout:L --&gt; R:jobs
jobs:L --&gt; R:storage</template></figure>
<p>The Worker renders pages using data from the Storefront API, and Shopify&#39;s checkout takes over for payment. Paid orders and other events then flow back into our background jobs, which write to storage. How the code gets deployed is covered further down.</p>
<p>A few reasons for going headless over a Shopify theme:</p>
<ol>
<li>We have full control over the design, the markup and the performance budget.</li>
<li>Features like the wholesale portal (more on that below) need real application logic, background jobs and storage, which would have been awkward to bolt onto a theme.</li>
<li>Search engine work such as structured data, canonical URLs, redirects and sitemaps all live in code that we can review and test.</li>
</ol>
<p>The trade-off is that we own more. Things Shopify themes give you for free (like predictive search and account pages) needed to be built or handed off to Shopify-hosted pages.</p>
<h2 id="under-the-hood">Under the hood</h2>
<p>Similar to Pocket Rice, I wanted the infrastructure to be reviewable and repeatable rather than clicked together in dashboards.</p>
<p>At a high level:</p>
<ol>
<li><a href="https://www.pulumi.com/" target="_blank" rel="noopener noreferrer">Pulumi</a> manages the long-lived resources: key-value storage, queues, a database, private object storage, and the GitHub deployment environments and their secrets.</li>
<li><a href="https://developers.cloudflare.com/workers/wrangler/" target="_blank" rel="noopener noreferrer">Wrangler</a> owns the Worker itself: the build, versions and bindings.</li>
<li>GitHub Actions orchestrates releases. A merge to <code>main</code> runs the tests, applies any infrastructure changes, builds the app, runs database migrations and then deploys the Worker.</li>
</ol>
<figure class="diagram" data-pele="true" data-pagefind-ignore="all"><div class="diagram-canvas"><svg xmlns="http://www.w3.org/2000/svg" class="pele pele-flowchart" viewBox="0 0 177 640" width="177" height="640" fill="var(--_fg)" role="graphics-document document" aria-roledescription="flowchart"><g class="pele-edges" fill="none" stroke="var(--_l)" stroke-linecap="round"><g class="pele-edge" data-id="L_Merge_Tests_0"><path d="M88.4,56L88.4,97"></path><path class="pele-marker" d="M88.4,104L85.04,96L91.76,96Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge" data-id="L_Tests_Infra_0"><path d="M88.4,152L88.4,193"></path><path class="pele-marker" d="M88.4,200L85.04,192L91.76,192Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge" data-id="L_Infra_Build_0"><path d="M88.4,248L88.4,289"></path><path class="pele-marker" d="M88.4,296L85.04,288L91.76,288Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge" data-id="L_Build_Migrate_0"><path d="M88.4,344L88.4,385"></path><path class="pele-marker" d="M88.4,392L85.04,384L91.76,384Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge" data-id="L_Migrate_Deploy_0"><path d="M88.4,440L88.4,481"></path><path class="pele-marker" d="M88.4,488L85.04,480L91.76,480Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge" data-id="L_Deploy_Live_0"><path d="M88.4,536L88.4,577"></path><path class="pele-marker" d="M88.4,584L85.04,576L91.76,576Z" fill="var(--_l)" stroke="none"></path></g></g><g class="pele-nodes"><g class="pele-node pele-shape-rect" data-id="Merge" transform="translate(88.4,32)"><rect x="-80.4" y="-24" width="160.8" height="48" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" x="0" y="5.6" text-anchor="middle">Merge to main</text></g><g class="pele-node pele-shape-rect" data-id="Tests" transform="translate(88.4,128)"><rect x="-42" y="-24" width="84" height="48" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" x="0" y="5.6" text-anchor="middle">Tests</text></g><g class="pele-node pele-shape-rect" data-id="Infra" transform="translate(88.4,224)"><rect x="-75.6" y="-24" width="151.2" height="48" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" x="0" y="5.6" text-anchor="middle">Pulumi apply</text></g><g class="pele-node pele-shape-rect" data-id="Build" transform="translate(88.4,320)"><rect x="-42" y="-24" width="84" height="48" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" x="0" y="5.6" text-anchor="middle">Build</text></g><g class="pele-node pele-shape-rect" data-id="Migrate" transform="translate(88.4,416)"><rect x="-66" y="-24" width="132" height="48" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" x="0" y="5.6" text-anchor="middle">Migrations</text></g><g class="pele-node pele-shape-rect" data-id="Deploy" transform="translate(88.4,512)"><rect x="-80.4" y="-24" width="160.8" height="48" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" x="0" y="5.6" text-anchor="middle">Deploy Worker</text></g><g class="pele-node pele-shape-rect" data-id="Live" transform="translate(88.4,608)"><rect x="-61.2" y="-24" width="122.4" height="48" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" x="0" y="5.6" text-anchor="middle">Live site</text></g></g></svg></div><template>flowchart LR
Merge[Merge to main] --&gt; Tests[Tests]
Tests --&gt; Infra[Pulumi apply]
Infra --&gt; Build[Build]
Build --&gt; Migrate[Migrations]
Migrate --&gt; Deploy[Deploy Worker]
Deploy --&gt; Live[Live site]</template></figure>
<p>Feature branches each get their own disposable preview Worker with a stable URL. That makes it easy to share changes with the business before anything goes live, and previews are never indexed by search engines.</p>
<p>Anything that does not need to happen during a page request is pushed into the background. Form submissions and Shopify webhooks (for events like paid orders) are recorded and handed to <a href="https://developers.cloudflare.com/queues/" target="_blank" rel="noopener noreferrer">Cloudflare Queues</a>, where consumers handle things like enquiry emails, wholesale invoices and purchase attribution. For the work that really matters (like invoices), a scheduled job picks up anything that gets stuck, so a dropped message does not mean dropped work.</p>
<p>For observability, errors go to <a href="https://sentry.io/" target="_blank" rel="noopener noreferrer">Sentry</a>, each request emits a single structured &quot;wide event&quot; to Cloudflare&#39;s logs, and product analytics go through <a href="https://posthog.com/" target="_blank" rel="noopener noreferrer">PostHog</a> once a visitor has given consent.</p>
<p>On the code side, the server is written with <a href="https://effect.website/" target="_blank" rel="noopener noreferrer">Effect</a> so that failures (Shopify being slow, a bad response, a missing permission) are typed and handled explicitly rather than silently turning into empty pages.</p>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>There is also a separate development store and environment for testing things
like wholesale checkout end-to-end. I have left that out-of-scope for this
blog post.</p> </div> </aside>
<h2 id="design-choices">Design choices</h2>
<p>The design leans on the brand&#39;s colours (deep green, olive and cream), with photos from the market stall and the tea fields in Uji doing a lot of the storytelling.</p>
<p>One small trade-off worth calling out: some of the light yellow text on green does not meet colour contrast requirements, so the site will not score a perfect accessibility audit. We decided to keep the brand colours rather than water them down.</p>
<p>The home page puts proof up front. The hero carries a Google rating badge, and directly underneath it is a &quot;Found at&quot; strip with logos of the cafes and markets where you can find the matcha, followed by a carousel of real Google reviews.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2026-10-24-building-the-matcha-byron-bay-website/home-page-2.webp" alt="Found at and loved by, right under the hero" loading="lazy" decoding="async"/><figcaption>Found at and loved by, right under the hero</figcaption></figure>
<p>From there, the focus is on helping someone pick the right tea quickly. We added a comparison between the three main teas, badges for what each one is best for, and size selection with add-to-cart directly on the card.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2026-10-24-building-the-matcha-byron-bay-website/home-page-3.webp" alt="Product cards with sizes and subscriptions" loading="lazy" decoding="async"/><figcaption>Product cards with sizes and subscriptions</figcaption></figure>
<p>The <a href="https://matchabyronbay.com/collections" target="_blank" rel="noopener noreferrer">shop</a> is broken up into collections such as ceremonial matcha, culinary matcha, hojicha, tea tools and ritual sets.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2026-10-24-building-the-matcha-byron-bay-website/shop-page-1.webp" alt="Collections page" loading="lazy" decoding="async"/><figcaption>Collections page</figcaption></figure>
<p>The product pages went through a conversion review after launch. The goal was to answer four questions as quickly as possible: which tea suits me, what will it taste like, how much tea am I getting, and what will delivery cost? That led to things like an approximate number of servings and cost per serving next to the price, delivery information next to the add to cart button, and a subscription option at 10% off.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2026-10-24-building-the-matcha-byron-bay-website/shop-page-2.webp" alt="The Nami product page" loading="lazy" decoding="async"/><figcaption>The Nami product page</figcaption></figure>
<p>Since launch we have also added moderated product reviews with verified-purchase badges, plus a free delivery tracker and a complementary add-on in the cart.</p>
<p>The flow for a retail customer looks roughly like this:</p>
<figure class="diagram" data-pele="true" data-pagefind-ignore="all"><div class="diagram-canvas"><svg xmlns="http://www.w3.org/2000/svg" class="pele pele-flowchart" viewBox="0 0 691 629" width="691" height="629" fill="var(--_fg)" role="graphics-document document" aria-roledescription="flowchart"><g class="pele-edges" fill="none" stroke="var(--_l)" stroke-linecap="round"><g class="pele-edge" data-id="L_Search_Landing_0"><path d="M88.4,56C88.4,67.39 274.2,67.61 274.2,79L274.2,85"></path><path class="pele-marker" d="M274.2,92L270.84,84L277.56,84Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge" data-id="L_Ads_Landing_0"><path d="M264.4,56C264.4,67.5 290.2,67.5 290.2,79L290.2,85"></path><path class="pele-marker" d="M290.2,92L286.84,84L293.56,84Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge" data-id="L_Social_Landing_0"><path d="M421.2,56C421.2,67.5 395.4,67.5 395.4,79L395.4,85"></path><path class="pele-marker" d="M395.4,92L392.04,84L398.76,84Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge" data-id="L_QR_Landing_0"><path d="M592.4,56C592.4,67.5 411.4,67.5 411.4,79L411.4,85"></path><path class="pele-marker" d="M411.4,92L408.04,84L414.76,84Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge" data-id="L_Landing_Choose_0"><path d="M342.8,158L342.8,187"></path><path class="pele-marker" d="M342.8,194L339.44,186L346.16,186Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge" data-id="L_Choose_Cart_0"><path d="M295.2,260L295.2,299.5C295.2,304.94 334.8,304.94 334.8,310.38L334.8,314"></path><path class="pele-marker" d="M334.8,321L331.44,313L338.16,313Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge" data-id="L_Choose_Cart_2"><path d="M390.4,260L390.4,299.5C390.4,304.94 350.8,304.94 350.8,310.38L350.8,314"></path><path class="pele-marker" d="M350.8,321L347.44,313L354.16,313Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge" data-id="L_Cart_Checkout_0"><path d="M342.8,369L342.8,398"></path><path class="pele-marker" d="M342.8,405L339.44,397L346.16,397Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge" data-id="L_Checkout_Paid_0"><path d="M342.8,453L342.8,482"></path><path class="pele-marker" d="M342.8,489L339.44,481L346.16,481Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge" data-id="L_Paid_Attribution_0"><path d="M342.8,537L342.8,566"></path><path class="pele-marker" d="M342.8,573L339.44,565L346.16,565Z" fill="var(--_l)" stroke="none"></path></g></g><g class="pele-edge-labels" font-size="14"><g class="pele-edge-label" data-id="L_Choose_Cart_0"><rect x="261.8" y="276.5" width="66.8" height="21" rx="3" fill="var(--_bg)"></rect><text x="295.2" y="291.9" text-anchor="middle">One-off</text></g><g class="pele-edge-label" data-id="L_Choose_Cart_2"><rect x="348.6" y="276.5" width="83.6" height="21" rx="3" fill="var(--_bg)"></rect><text x="390.4" y="291.9" text-anchor="middle">Subscribe</text></g></g><g class="pele-nodes"><g class="pele-node pele-shape-rect" data-id="Search" transform="translate(88.4,32)"><rect x="-80.4" y="-24" width="160.8" height="48" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" x="0" y="5.6" text-anchor="middle">Google search</text></g><g class="pele-node pele-shape-rect" data-id="Landing" transform="translate(342.8,125)"><rect x="-94.8" y="-33" width="189.6" height="66" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" text-anchor="middle" xml:space="preserve"><tspan x="0" y="-4.9">Home, product or</tspan><tspan x="0" y="16.1">guide</tspan></text></g><g class="pele-node pele-shape-rect" data-id="Ads" transform="translate(264.4,32)"><rect x="-75.6" y="-24" width="151.2" height="48" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" x="0" y="5.6" text-anchor="middle">Shopping ads</text></g><g class="pele-node pele-shape-rect" data-id="Social" transform="translate(421.2,32)"><rect x="-61.2" y="-24" width="122.4" height="48" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" x="0" y="5.6" text-anchor="middle">Instagram</text></g><g class="pele-node pele-shape-rect" data-id="QR" transform="translate(592.4,32)"><rect x="-90" y="-24" width="180" height="48" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" x="0" y="5.6" text-anchor="middle">Market QR codes</text></g><g class="pele-node pele-shape-rect" data-id="Choose" transform="translate(342.8,227)"><rect x="-94.8" y="-33" width="189.6" height="66" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" text-anchor="middle" xml:space="preserve"><tspan x="0" y="-4.9">Compare teas and</tspan><tspan x="0" y="16.1">sizes</tspan></text></g><g class="pele-node pele-shape-rect" data-id="Cart" transform="translate(342.8,345)"><rect x="-37.2" y="-24" width="74.4" height="48" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" x="0" y="5.6" text-anchor="middle">Cart</text></g><g class="pele-node pele-shape-rect" data-id="Checkout" transform="translate(342.8,429)"><rect x="-94.8" y="-24" width="189.6" height="48" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" x="0" y="5.6" text-anchor="middle">Shopify checkout</text></g><g class="pele-node pele-shape-rect" data-id="Paid" transform="translate(342.8,513)"><rect x="-66" y="-24" width="132" height="48" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" x="0" y="5.6" text-anchor="middle">Paid order</text></g><g class="pele-node pele-shape-rect" data-id="Attribution" transform="translate(342.8,597)"><rect x="-114" y="-24" width="228" height="48" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" x="0" y="5.6" text-anchor="middle">Campaign attribution</text></g></g></svg></div><template>flowchart LR
Search[Google search] --&gt; Landing[Home, product or guide]
Ads[Shopping ads] --&gt; Landing
Social[Instagram] --&gt; Landing
QR[Market QR codes] --&gt; Landing
Landing --&gt; Choose[Compare teas and sizes]
Choose --&gt;|One-off| Cart[Cart]
Choose --&gt;|Subscribe| Cart
Cart --&gt; Checkout[Shopify checkout]
Checkout --&gt; Paid[Paid order]
Paid --&gt; Attribution[Campaign attribution]</template></figure>
<p>The market QR codes are worth a mention. The stalls are still a big part of how people find Matcha Byron Bay, so for physical placements like the stall we generate QR codes with campaign tags baked in. That way offline traffic can be attributed like any other campaign.</p>
<h2 id="a-japanese-owned-business">A Japanese-owned business</h2>
<p>The owner, Ayumu, is Japanese, and the business began as a dream between Japanese friends living in Byron Bay. The brand story is very much about the connection between Uji and Byron Bay, and the <a href="https://matchabyronbay.com/about" target="_blank" rel="noopener noreferrer">about page</a> tells that story directly, including the team&#39;s names in katakana.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2026-10-24-building-the-matcha-byron-bay-website/about-page-1.webp" alt="From the mist of Uji to the rhythm of Byron Bay" loading="lazy" decoding="async"/><figcaption>From the mist of Uji to the rhythm of Byron Bay</figcaption></figure>
<p>The about page walks through the journey from that first market tent to where they are today.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2026-10-24-building-the-matcha-byron-bay-website/about-page-2.webp" alt="Our journey of a thousand mornings" loading="lazy" decoding="async"/><figcaption>Our journey of a thousand mornings</figcaption></figure>
<p>This had some practical implications for the build as well:</p>
<ol>
<li>The wholesale buyer guide and staff guide are published in both English and Japanese.</li>
<li>The wholesale walkthrough video is narrated in both languages with captions, and you can switch between them on the support page.</li>
<li>Japanese copy needs a review from a native speaker before it goes out, including Shopify&#39;s menu names, which differ between languages.</li>
<li>Some customers write in Japanese (a few of the Google reviews on the home page are in Japanese), so anything that renders customer-provided text has to handle it. The one place that does not yet is the PDF invoice renderer, which uses standard Latin fonts. Rather than silently mangling a name, anything it cannot render is flagged for review.</li>
</ol>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2026-10-24-building-the-matcha-byron-bay-website/wholesale-guide-ja.webp" alt="The Japanese version of the wholesale buyer guide" loading="lazy" decoding="async"/><figcaption>The Japanese version of the wholesale buyer guide</figcaption></figure>
<h2 id="wholesale">Wholesale</h2>
<p>Commercially, <a href="https://matchabyronbay.com/wholesale" target="_blank" rel="noopener noreferrer">the wholesale page</a> is one of the most important pages on the site. Cafes, retailers and workplaces buy in much larger quantities than retail customers, so every new wholesale partner matters.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2026-10-24-building-the-matcha-byron-bay-website/wholesale-page-1.webp" alt="The wholesale page hero" loading="lazy" decoding="async"/><figcaption>The wholesale page hero</figcaption></figure>
<p>The page went through a dedicated SEO and messaging review. The first version had a lovely brand story, but its main heading (&quot;Bring beautiful matcha to your business&quot;) did not say what the page was actually for. The heading was changed to say exactly what the page is for, and the page was restructured to answer the questions a cafe owner actually has before they enquire.</p>
<p>Social proof comes first, with logos from the wholesale community.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2026-10-24-building-the-matcha-byron-bay-website/wholesale-page-2.webp" alt="Poured, stocked and shared by our wholesale community" loading="lazy" decoding="async"/><figcaption>Poured, stocked and shared by our wholesale community</figcaption></figure>
<p>The business started in Byron Bay, but it supplies cafes Australia-wide. Rather than create a thin landing page for every city (which tends to backfire), the page talks about where we supply in one place and answers the city-specific questions in the FAQ.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2026-10-24-building-the-matcha-byron-bay-website/wholesale-page-3.webp" alt="Australia-wide wholesale, with support for key cafe markets" loading="lazy" decoding="async"/><figcaption>Australia-wide wholesale, with support for key cafe markets</figcaption></figure>
<p>One of the bigger decisions was to publish guide pricing. It is common for wholesale pages to hide pricing behind a form, but cafe owners want to compare cost per gram and cost per cup before talking to anyone. Publishing it builds trust, and it also gives search engines something concrete to rank for. The prices a buyer actually pays come from their own catalogue in Shopify.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2026-10-24-building-the-matcha-byron-bay-website/wholesale-page-4.webp" alt="Wholesale pricing, cost per gram and cost per cup" loading="lazy" decoding="async"/><figcaption>Wholesale pricing, cost per gram and cost per cup</figcaption></figure>
<p>Further down, the page explains exactly how ordering works, from first enquiry to paid invoice.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2026-10-24-building-the-matcha-byron-bay-website/wholesale-page-5.webp" alt="How the wholesale portal works" loading="lazy" decoding="async"/><figcaption>How the wholesale portal works</figcaption></figure>
<p>The enquiry form asks for enough to qualify the lead (business type, location and what they are interested in). Submitting it emails the business our wholesale pricing and partnership PDF and notifies the team, so nobody is left waiting for a reply to find out the basics.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2026-10-24-building-the-matcha-byron-bay-website/wholesale-page-6.webp" alt="The wholesale enquiry form" loading="lazy" decoding="async"/><figcaption>The wholesale enquiry form</figcaption></figure>
<p>Finally, the FAQ covers the long-tail questions cafe owners ask (and search for), such as whether we supply Sydney and Melbourne, which matcha suits lattes, and how many drinks you get out of 1kg.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2026-10-24-building-the-matcha-byron-bay-website/wholesale-page-7.webp" alt="Wholesale FAQs" loading="lazy" decoding="async"/><figcaption>Wholesale FAQs</figcaption></figure>
<p>The wholesale journey end-to-end looks like this:</p>
<figure class="diagram" data-pele="true" data-pagefind-ignore="all"><div class="diagram-canvas"><svg xmlns="http://www.w3.org/2000/svg" class="pele pele-flowchart" viewBox="0 0 510 1121" width="510" height="1121" fill="var(--_fg)" role="graphics-document document" aria-roledescription="flowchart"><g class="pele-edges" fill="none" stroke="var(--_l)" stroke-linecap="round"><g class="pele-edge" data-id="L_Find_Page_0"><path d="M327.6,56L327.6,97"></path><path class="pele-marker" d="M327.6,104L324.24,96L330.96,96Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge" data-id="L_Page_Enquiry_0"><path d="M327.6,152L327.6,193"></path><path class="pele-marker" d="M327.6,200L324.24,192L330.96,192Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge" data-id="L_Enquiry_PDF_0"><path d="M304,248C304,265.5 163.5,265.5 163.5,283L163.5,289"></path><path class="pele-marker" d="M163.5,296L160.14,288L166.86,288Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge" data-id="L_Enquiry_Team_0"><path d="M351.2,248C351.2,265.5 393.1,265.5 393.1,283L393.1,289"></path><path class="pele-marker" d="M393.1,296L389.74,288L396.46,288Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge" data-id="L_PDF_Chat_0"><path d="M163.5,344C163.5,361.5 270.4,361.5 270.4,379L270.4,385"></path><path class="pele-marker" d="M270.4,392L267.04,384L273.76,384Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge" data-id="L_Team_Chat_0"><path d="M393.1,344L393.1,385"></path><path class="pele-marker" d="M393.1,392L389.74,384L396.46,384Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge" data-id="L_Chat_Setup_0"><path d="M327.6,458L327.6,499"></path><path class="pele-marker" d="M327.6,506L324.24,498L330.96,498Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge" data-id="L_Setup_SignIn_0"><path d="M327.6,572L327.6,613"></path><path class="pele-marker" d="M327.6,620L324.24,612L330.96,612Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge" data-id="L_SignIn_Order_0"><path d="M327.6,668L327.6,709"></path><path class="pele-marker" d="M327.6,716L324.24,708L330.96,708Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge" data-id="L_Order_Checkout_0"><path d="M284.8,782L284.8,790.97C284.8,793.89 102.8,799.58 102.8,802.5L102.8,857"></path><path class="pele-marker" d="M102.8,864L99.44,856L106.16,856Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge" data-id="L_Order_BankReview_0"><path d="M327.6,782L327.6,848"></path><path class="pele-marker" d="M327.6,855L324.24,847L330.96,847Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge" data-id="L_BankReview_Unpaid_0"><path d="M327.6,921L327.6,962"></path><path class="pele-marker" d="M327.6,969L324.24,961L330.96,961Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge" data-id="L_Checkout_Invoice_0"><path d="M102.8,912L102.8,1017C102.8,1034.5 283.2,1034.5 283.2,1052L283.2,1058"></path><path class="pele-marker" d="M283.2,1065L279.84,1057L286.56,1057Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge" data-id="L_Unpaid_Invoice_0"><path d="M327.6,1017L327.6,1058"></path><path class="pele-marker" d="M327.6,1065L324.24,1057L330.96,1057Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge" data-id="L_Invoice_Order_0"><path d="M359.2,1065L359.2,1052C359.2,1034.5 466.6,1034.5 466.6,1017L466.6,808.75C466.6,801.34 370.4,801.34 370.4,793.94L370.4,789"></path><path class="pele-marker" d="M370.4,782L373.76,790L367.04,790Z" fill="var(--_l)" stroke="none"></path></g></g><g class="pele-edge-labels" font-size="14"><g class="pele-edge-label" data-id="L_Order_Checkout_0"><rect x="82" y="804.5" width="41.6" height="21" rx="3" fill="var(--_bg)"></rect><text x="102.8" y="819.9" text-anchor="middle">Card</text></g><g class="pele-edge-label" data-id="L_Order_BankReview_0"><rect x="269" y="804.5" width="117.2" height="21" rx="3" fill="var(--_bg)"></rect><text x="327.6" y="819.9" text-anchor="middle">Bank transfer</text></g><g class="pele-edge-label" data-id="L_Invoice_Order_0"><rect x="433.2" y="982.5" width="66.8" height="21" rx="3" fill="var(--_bg)"></rect><text x="466.6" y="997.9" text-anchor="middle">Reorder</text></g></g><g class="pele-nodes"><g class="pele-node pele-shape-rect" data-id="Find" transform="translate(327.6,32)"><rect x="-104.4" y="-24" width="208.8" height="48" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" x="0" y="5.6" text-anchor="middle">Search or referral</text></g><g class="pele-node pele-shape-rect" data-id="Page" transform="translate(327.6,128)"><rect x="-85.2" y="-24" width="170.4" height="48" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" x="0" y="5.6" text-anchor="middle">Wholesale page</text></g><g class="pele-node pele-shape-rect" data-id="Enquiry" transform="translate(327.6,224)"><rect x="-75.6" y="-24" width="151.2" height="48" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" x="0" y="5.6" text-anchor="middle">Enquiry form</text></g><g class="pele-node pele-shape-rect" data-id="PDF" transform="translate(163.5,320)"><rect x="-109.2" y="-24" width="218.4" height="48" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" x="0" y="5.6" text-anchor="middle">Pricing PDF emailed</text></g><g class="pele-node pele-shape-rect" data-id="Team" transform="translate(393.1,320)"><rect x="-80.4" y="-24" width="160.8" height="48" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" x="0" y="5.6" text-anchor="middle">Team notified</text></g><g class="pele-node pele-shape-rect" data-id="Chat" transform="translate(327.6,425)"><rect x="-109.2" y="-33" width="218.4" height="66" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" text-anchor="middle" xml:space="preserve"><tspan x="0" y="-4.9">Samples and product</tspan><tspan x="0" y="16.1">fit</tspan></text></g><g class="pele-node pele-shape-rect" data-id="Setup" transform="translate(327.6,539)"><rect x="-99.6" y="-33" width="199.2" height="66" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" text-anchor="middle" xml:space="preserve"><tspan x="0" y="-4.9">Account set up in</tspan><tspan x="0" y="16.1">Shopify</tspan></text></g><g class="pele-node pele-shape-rect" data-id="SignIn" transform="translate(327.6,644)"><rect x="-85.2" y="-24" width="170.4" height="48" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" x="0" y="5.6" text-anchor="middle">Portal sign in</text></g><g class="pele-node pele-shape-rect" data-id="Order" transform="translate(327.6,749)"><rect x="-94.8" y="-33" width="189.6" height="66" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" text-anchor="middle" xml:space="preserve"><tspan x="0" y="-4.9">Choose venue and</tspan><tspan x="0" y="16.1">order</tspan></text></g><g class="pele-node pele-shape-rect" data-id="Checkout" transform="translate(102.8,888)"><rect x="-94.8" y="-24" width="189.6" height="48" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" x="0" y="5.6" text-anchor="middle">Shopify checkout</text></g><g class="pele-node pele-shape-rect" data-id="BankReview" transform="translate(327.6,888)"><rect x="-90" y="-33" width="180" height="66" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" text-anchor="middle" xml:space="preserve"><tspan x="0" y="-4.9">Review total in</tspan><tspan x="0" y="16.1">portal</tspan></text></g><g class="pele-node pele-shape-rect" data-id="Unpaid" transform="translate(327.6,993)"><rect x="-75.6" y="-24" width="151.2" height="48" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" x="0" y="5.6" text-anchor="middle">Unpaid order</text></g><g class="pele-node pele-shape-rect" data-id="Invoice" transform="translate(321.2,1089)"><rect x="-90" y="-24" width="180" height="48" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" x="0" y="5.6" text-anchor="middle">Invoice emailed</text></g></g></svg></div><template>flowchart TD
Find[Search or referral] --&gt; Page[Wholesale page]
Page --&gt; Enquiry[Enquiry form]
Enquiry --&gt; PDF[Pricing PDF emailed]
Enquiry --&gt; Team[Team notified]
PDF --&gt; Chat[Samples and product fit]
Team --&gt; Chat
Chat --&gt; Setup[Account set up in Shopify]
Setup --&gt; SignIn[Portal sign in]
SignIn --&gt; Order[Choose venue and order]
Order --&gt;|Card| Checkout[Shopify checkout]
Order --&gt;|Bank transfer| BankReview[Review total in portal]
BankReview --&gt; Unpaid[Unpaid order]
Checkout --&gt; Invoice[Invoice emailed]
Unpaid --&gt; Invoice
Invoice --&gt;|Reorder| Order</template></figure>
<h2 id="the-wholesale-portal">The wholesale portal</h2>
<p>The newest piece of the website is a <a href="https://matchabyronbay.com/wholesale/portal" target="_blank" rel="noopener noreferrer">wholesale portal</a> where approved cafe and retail buyers can place orders themselves.</p>
<p>Without a portal, every wholesale order and invoice is a manual back-and-forth. That works with a handful of partners, but it does not scale.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2026-10-24-building-the-matcha-byron-bay-website/wholesale-portal-sign-in.webp" alt="Signing in to the wholesale portal" loading="lazy" decoding="async"/><figcaption>Signing in to the wholesale portal</figcaption></figure>
<p>The split of responsibilities is intentional:</p>
<ol>
<li>Shopify owns identity, company permissions, wholesale pricing, orders and payments. Buyers sign in with Shopify&#39;s hosted email sign-in and receive a one-time code, so there is no new password system to maintain.</li>
<li>Our website owns the portal experience: choosing the venue you are ordering for, the order form, order history, reordering and the invoices.</li>
</ol>
<p>Each venue is a Shopify company location with its own pricing, delivery address and order history. Buyers can pay by card through Shopify&#39;s checkout, or choose bank transfer. For bank transfers, the portal shows Shopify&#39;s calculated delivery and GST for review, places an unpaid order and emails an invoice with the payment details. Staff mark it as paid in Shopify once the money arrives.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2026-10-24-building-the-matcha-byron-bay-website/wholesale-portal-order.webp" alt="Placing a wholesale order (demo data)" loading="lazy" decoding="async"/><figcaption>Placing a wholesale order (demo data)</figcaption></figure>
<p>Invoices are generated in the background from Shopify&#39;s order events. They are stored privately, versioned when an order changes, and only served after checking that the buyer still has access to that order.</p>
<figure class="diagram" data-pele="true" data-pagefind-ignore="all"><div class="diagram-canvas"><svg xmlns="http://www.w3.org/2000/svg" class="pele pele-sequence" viewBox="0 0 680 793" width="680" height="793" fill="var(--_fg)" role="graphics-document document" aria-roledescription="sequence"><g transform="translate(40,8)"><g class="pele-lifelines" fill="none" stroke="var(--_b)"><path class="pele-lifeline" data-id="Buyer" d="M0,62V715"></path><path class="pele-lifeline" data-id="Portal" d="M164.2,62V715"></path><path class="pele-lifeline" data-id="Shopify" d="M337.2,62V715"></path><path class="pele-lifeline" data-id="Jobs" d="M468.4,62V715"></path></g><g class="pele-frames" font-size="14" stroke="var(--_b)"><g class="pele-frame pele-frame-alt"><rect x="-12" y="273" width="361.2" height="298" fill="none"></rect><path d="M-12,397H349.2" stroke-dasharray="4 4"></path><rect x="-7" y="401" width="132" height="17" rx="3" fill="var(--_bg)" stroke="none"></rect><text class="pele-frame-label" fill="var(--_m)" stroke="none" x="59" y="414.4" text-anchor="middle">[Bank transfer]</text><rect x="-12" y="273" width="35.6" height="20" fill="var(--_s)"></rect><text class="pele-frame-kind" x="5.8" y="287.2" text-anchor="middle" font-size="12" font-weight="var(--_hw)" fill="var(--_m)" stroke="none">alt</text><text class="pele-frame-label" fill="var(--_m)" stroke="none" x="56.8" y="288.4" text-anchor="middle">[Card]</text></g></g><g class="pele-edges" fill="none" stroke="var(--_l)" stroke-linecap="round"><g class="pele-edge pele-message" data-id="i0"><path d="M0,105H157.2"></path><path class="pele-marker" d="M164.2,105L156.2,108.36L156.2,101.64Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge pele-message" data-id="i1"><path d="M164.2,157H330.2"></path><path class="pele-marker" d="M337.2,157L329.2,160.36L329.2,153.64Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge pele-message" data-id="i2"><path d="M337.2,209H171.2" stroke-dasharray="4 4"></path><path class="pele-marker" d="M164.2,209L172.2,205.64L172.2,212.36Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge pele-message" data-id="i3"><path d="M0,261H157.2"></path><path class="pele-marker" d="M164.2,261L156.2,264.36L156.2,257.64Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge pele-message" data-id="i5"><path d="M164.2,348H330.2"></path><path class="pele-marker" d="M337.2,348L329.2,351.36L329.2,344.64Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge pele-message" data-id="i6"><path d="M337.2,385H7" stroke-dasharray="4 4"></path><path class="pele-marker" d="M0,385L8,381.64L8,388.36Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge pele-message" data-id="i8"><path d="M164.2,470H330.2"></path><path class="pele-marker" d="M337.2,470L329.2,473.36L329.2,466.64Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge pele-message" data-id="i9"><path d="M0,507H157.2"></path><path class="pele-marker" d="M164.2,507L156.2,510.36L156.2,503.64Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge pele-message" data-id="i10"><path d="M164.2,559H330.2"></path><path class="pele-marker" d="M337.2,559L329.2,562.36L329.2,555.64Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge pele-message" data-id="i12"><path d="M337.2,608H461.4"></path><path class="pele-marker" d="M468.4,608L460.4,611.36L460.4,604.64Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge pele-message" data-id="i13"><path d="M468.4,629H490.4A6,6 0 0 1 496.4,635V645A6,6 0 0 1 490.4,651H475.4"></path><path class="pele-marker" d="M468.4,651L476.4,647.64L476.4,654.36Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge pele-message" data-id="i14"><path d="M468.4,695H7" stroke-dasharray="4 4"></path><path class="pele-marker" d="M0,695L8,691.64L8,698.36Z" fill="var(--_l)" stroke="none"></path></g></g><g class="pele-edge-labels" font-size="14"><g class="pele-edge-label" data-id="i0"><text x="82.1" y="95.4" text-anchor="middle">Sign in</text></g><g class="pele-edge-label" data-id="i1"><text text-anchor="middle" xml:space="preserve"><tspan x="250.7" y="130.9">Hosted email sign</tspan><tspan x="250.7" y="148.9">in</tspan></text></g><g class="pele-edge-label" data-id="i2"><text text-anchor="middle" xml:space="preserve"><tspan x="250.7" y="182.9">Verified buyer</tspan><tspan x="250.7" y="200.9">and company</tspan></text></g><g class="pele-edge-label" data-id="i3"><text text-anchor="middle" xml:space="preserve"><tspan x="82.1" y="234.9">Choose venue and</tspan><tspan x="82.1" y="252.9">products</tspan></text></g><g class="pele-edge-label" data-id="i5"><text text-anchor="middle" xml:space="preserve"><tspan x="250.7" y="321.9">Cart with company</tspan><tspan x="250.7" y="339.9">pricing</tspan></text></g><g class="pele-edge-label" data-id="i6"><rect x="102.6" y="362" width="132" height="17" rx="3" fill="var(--_bg)" stroke="none"></rect><text x="168.6" y="375.4" text-anchor="middle">Hosted checkout</text></g><g class="pele-edge-label" data-id="i8"><text text-anchor="middle" xml:space="preserve"><tspan x="250.7" y="443.9">Calculate</tspan><tspan x="250.7" y="461.9">delivery and GST</tspan></text></g><g class="pele-edge-label" data-id="i9"><text x="82.1" y="497.4" text-anchor="middle">Confirm order</text></g><g class="pele-edge-label" data-id="i10"><text text-anchor="middle" xml:space="preserve"><tspan x="250.7" y="532.9">Create unpaid</tspan><tspan x="250.7" y="550.9">order</tspan></text></g><g class="pele-edge-label" data-id="i12"><text x="402.8" y="598.4" text-anchor="middle">Order webhook</text></g><g class="pele-edge-label" data-id="i13"><text text-anchor="middle" xml:space="preserve"><tspan x="563.2" y="635.9">Render invoice</tspan><tspan x="563.2" y="653.9">PDF</tspan></text></g><g class="pele-edge-label" data-id="i14"><text x="234.2" y="685.4" text-anchor="middle">Email invoice</text></g></g><g class="pele-nodes"><g class="pele-node pele-actor pele-actor-participant pele-actor-footer" data-id="Buyer"><rect x="-32" y="715" width="64" height="44" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" x="0" y="742.6" text-anchor="middle">Buyer</text></g><g class="pele-node pele-actor pele-actor-participant pele-actor-footer" data-id="Portal"><rect x="127.4" y="715" width="73.6" height="44" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" x="164.2" y="742.6" text-anchor="middle">Portal</text></g><g class="pele-node pele-actor pele-actor-participant pele-actor-footer" data-id="Shopify"><rect x="295.6" y="715" width="83.2" height="44" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" x="337.2" y="742.6" text-anchor="middle">Shopify</text></g><g class="pele-node pele-actor pele-actor-participant pele-actor-footer" data-id="Jobs"><rect x="412.4" y="715" width="112" height="62" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" text-anchor="middle" xml:space="preserve"><tspan x="468.4" y="741.1">Background</tspan><tspan x="468.4" y="762.1">jobs</tspan></text></g><g class="pele-node pele-actor pele-actor-participant" data-id="Buyer"><rect x="-32" y="18" width="64" height="44" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" x="0" y="45.6" text-anchor="middle">Buyer</text></g><g class="pele-node pele-actor pele-actor-participant" data-id="Portal"><rect x="127.4" y="18" width="73.6" height="44" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" x="164.2" y="45.6" text-anchor="middle">Portal</text></g><g class="pele-node pele-actor pele-actor-participant" data-id="Shopify"><rect x="295.6" y="18" width="83.2" height="44" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" x="337.2" y="45.6" text-anchor="middle">Shopify</text></g><g class="pele-node pele-actor pele-actor-participant" data-id="Jobs"><rect x="412.4" y="0" width="112" height="62" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" text-anchor="middle" xml:space="preserve"><tspan x="468.4" y="26.1">Background</tspan><tspan x="468.4" y="47.1">jobs</tspan></text></g></g></g></svg></div><template>sequenceDiagram
participant Buyer
participant Portal
participant Shopify
participant Jobs as Background jobs
Buyer-&gt;&gt;Portal: Sign in
Portal-&gt;&gt;Shopify: Hosted email sign in
Shopify--&gt;&gt;Portal: Verified buyer and company
Buyer-&gt;&gt;Portal: Choose venue and products
alt Card
Portal-&gt;&gt;Shopify: Cart with company pricing
Shopify--&gt;&gt;Buyer: Hosted checkout
else Bank transfer
Portal-&gt;&gt;Shopify: Calculate delivery and GST
Buyer-&gt;&gt;Portal: Confirm order
Portal-&gt;&gt;Shopify: Create unpaid order
end
Shopify-&gt;&gt;Jobs: Order webhook
Jobs-&gt;&gt;Jobs: Render invoice PDF
Jobs--&gt;&gt;Buyer: Email invoice</template></figure>
<p>Onboarding was treated as part of the feature rather than an afterthought. The <a href="https://matchabyronbay.com/wholesale/portal/support" target="_blank" rel="noopener noreferrer">support page</a> works before you sign in and has the walkthrough video with chapters for each step, plus the printable buyer guides in English and Japanese.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2026-10-24-building-the-matcha-byron-bay-website/wholesale-portal-support.webp" alt="Wholesale portal support with the bilingual walkthrough" loading="lazy" decoding="async"/><figcaption>Wholesale portal support with the bilingual walkthrough</figcaption></figure>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>Everything under the portal is marked as <code>noindex</code>, and the PDFs are never
publicly accessible. The public wholesale page is the only part of wholesale
that we want search engines to see.</p> </div> </aside>
<h2 id="other-key-pages">Other key pages</h2>
<p>The <a href="https://matchabyronbay.com/find-us" target="_blank" rel="noopener noreferrer">find us</a> page lists every market the stall attends with the day, hours, location and a link to a map. It is also marked up with local business structured data for search engines.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2026-10-24-building-the-matcha-byron-bay-website/find-us-page.webp" alt="Find Matcha Byron Bay at local markets" loading="lazy" decoding="async"/><figcaption>Find Matcha Byron Bay at local markets</figcaption></figure>
<p>The <a href="https://matchabyronbay.com/blogs" target="_blank" rel="noopener noreferrer">blog</a> holds the preparation guides, tea explainers and local guides. Like Pocket Rice, the posts now live as MDX in the repository rather than in a CMS.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2026-10-24-building-the-matcha-byron-bay-website/blog-page.webp" alt="Notes on matcha, preparation and everyday tea ritual" loading="lazy" decoding="async"/><figcaption>Notes on matcha, preparation and everyday tea ritual</figcaption></figure>
<h2 id="seo-and-backlinks">SEO and backlinks</h2>
<p>SEO for a small local business has been the hardest part of the project, and it is very much still in progress.</p>
<p>The first challenge was the migration itself. After launch, a number of old WordPress URLs were still indexed, and some of them led to pages that no longer existed. We audited the legacy URLs, added redirects for anything with value and made sure the sitemap only contains the pages we want indexed. Indexing is also locked to the production domain, so preview deployments never end up in search results.</p>
<p>Locally, things are in good shape. The Google Business Profile ranks first in the map pack for searches like &quot;byron bay matcha&quot;. One lesson here: rank trackers can be misleading for local queries. Our tracker reported a big drop for &quot;best matcha near me&quot;, but it turned out it was being served Sydney results rather than what someone in Byron Bay would see.</p>
<p>Backlinks are where it gets hard. Third-party tools show hundreds of referring domains for the site, which looks great until you look at them. Nearly all of them are automated spam and &quot;checker&quot; sites that started linking in May 2026 and carry no value. The genuine links are few and far between (and one of them is Pocket Rice, which I will admit is not exactly an independent vote).</p>
<p>What we really want are links from the cafes and businesses we supply, as well as the markets and local guides that already mention the brand. That is harder than it sounds:</p>
<ol>
<li>Most of the cafes we supply are small, busy hospitality businesses. Adding a link to their supplier is understandably not high on their list.</li>
<li>Some are more active on Instagram than on their own website, so there is not always an obvious place for a link to go.</li>
<li>Market organisers&#39; stallholder pages and local guides often already mention Matcha Byron Bay by name, but do not link to the website.</li>
</ol>
<p>So the approach is to make linking worthwhile and easy. We write content that features partners (such as <a href="https://matchabyronbay.com/blogs/news/old-maids-brunswick-heads-hojicha" target="_blank" rel="noopener noreferrer">where to find our hojicha at Old Maids in Brunswick Heads</a> and <a href="https://matchabyronbay.com/blogs/journal/best-cafes-for-matcha-byron-bay" target="_blank" rel="noopener noreferrer">the best matcha around Byron Bay and the Northern Rivers</a>), give partners something they would want to share, and ask market organisers and local guides to turn existing mentions into links. A handful of real, local links would outweigh the entire spam profile.</p>
<p>The blog has been the weakest area so far. It gets plenty of impressions but very few clicks, and some of the older articles overlap and compete with each other. The plan is to consolidate those into fewer, stronger guides with named authors and proper updated dates.</p>
<h2 id="results-so-far">Results so far</h2>
<p>It is still early, and the numbers are small enough that I want to be careful with them, but there are some encouraging signs.</p>
<p>At launch, Google&#39;s PageSpeed lab tests across 11 equivalent pages had the new site scoring 96 to 100 on mobile and 100 on desktop. For comparison, the old home page scored 83 on mobile.</p>
<p>From the September report (compared to August):</p>
<ol>
<li>Paid online order value was up roughly 79%, with average order value holding steady.</li>
<li>Google organic clicks were up about 22% and impressions up about 8%, with the average position improving from 11 to 7.8.</li>
<li>The wholesale page roughly quadrupled its organic clicks, and its average position improved from around 19 to around 10.</li>
</ol>
<aside class="callout callout--warning" aria-label="Warning"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><path d="m21.7 18-8-14a2 2 0 0 0-3.4 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.7-3ZM12 9v4M12 17h.01" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>  <div class="callout__body">  <p>These come from a small base, and one larger order made up a decent chunk of
September&#39;s increase. I am not attributing all of it to the website, but the
direction is right.</p> </div> </aside>
<h2 id="experimenting-with-google-ads">Experimenting with Google Ads</h2>
<p>In September we started running Google Shopping campaigns for the ceremonial matcha and hojicha. Getting there took more work than expected: the Merchant Center feed and the website needed clear shipping, returns and business information before Google was happy to show the products.</p>
<p>The campaigns are delivering clicks, but it is too early to say whether they are worth it. Attribution is the hard part:</p>
<ol>
<li>Analytics only run once a visitor has consented, which is the right thing to do but leaves gaps.</li>
<li>Checkout happens on Shopify, so the click and the purchase live on different systems.</li>
<li>Google&#39;s own conversion tracking has only recorded a small number of purchases so far, and some ad-assisted sales are likely going unattributed.</li>
</ol>
<p>To close that gap, we built our own purchase attribution. With consent, the campaign tags from the landing page are carried through to the cart, and when Shopify tells us an order has been paid, a background job records the purchase against the original campaign. Until that data is reliable, we are holding off on increasing spend.</p>
<h2 id="what-is-next">What is next</h2>
<ol>
<li>Onboarding the existing wholesale partners onto the portal and measuring how many orders move across.</li>
<li>Getting purchase tracking to a point where we can make real decisions about Google Ads.</li>
<li>Consolidating blog content and earning genuine local backlinks.</li>
<li>Asking customers for reviews once they have received their order.</li>
</ol>
<p>I may write a follow-up once we have a few more months of data, particularly on the wholesale side.</p>
<h2 id="links-and-further-reading">Links and further reading</h2>
<ul>
<li><a href="https://matchabyronbay.com" target="_blank" rel="noopener noreferrer">Matcha Byron Bay homepage</a></li>
<li><a href="https://matchabyronbay.com/collections" target="_blank" rel="noopener noreferrer">Matcha Byron Bay collections</a></li>
<li><a href="https://matchabyronbay.com/products/nami-ceremonial-matcha" target="_blank" rel="noopener noreferrer">Nami ceremonial matcha</a></li>
<li><a href="https://matchabyronbay.com/wholesale" target="_blank" rel="noopener noreferrer">Matcha Byron Bay wholesale</a></li>
<li><a href="https://matchabyronbay.com/wholesale/portal/support" target="_blank" rel="noopener noreferrer">Matcha Byron Bay wholesale portal support</a></li>
<li><a href="https://matchabyronbay.com/about" target="_blank" rel="noopener noreferrer">About Matcha Byron Bay</a></li>
<li><a href="https://matchabyronbay.com/find-us" target="_blank" rel="noopener noreferrer">Find Matcha Byron Bay at the markets</a></li>
<li><a href="https://matchabyronbay.com/blogs" target="_blank" rel="noopener noreferrer">Matcha Byron Bay blog</a></li>
<li><a href="https://matchabyronbay.com/blogs/news/old-maids-brunswick-heads-hojicha" target="_blank" rel="noopener noreferrer">Hojicha in Brunswick Heads at Old Maids</a></li>
<li><a href="https://matchabyronbay.com/blogs/journal/best-cafes-for-matcha-byron-bay" target="_blank" rel="noopener noreferrer">Best matcha in Byron Bay and the Northern Rivers</a></li>
<li><a href="https://dennisokeeffe.com/blog/2026-06-08-building-the-pocket-rice-marketing-website">Building the Pocket Rice marketing website</a></li>
<li><a href="https://hydrogen.shopify.dev/update/hydrogen-developer-preview" target="_blank" rel="noopener noreferrer">Hydrogen developer preview</a></li>
<li><a href="https://shopify.dev/docs/storefronts/headless" target="_blank" rel="noopener noreferrer">Shopify headless storefronts</a></li>
<li><a href="https://shopify.dev/docs/api/storefront" target="_blank" rel="noopener noreferrer">Shopify Storefront API</a></li>
<li><a href="https://shopify.dev/docs/api/customer" target="_blank" rel="noopener noreferrer">Shopify Customer Account API</a></li>
<li><a href="https://help.shopify.com/en/manual/b2b" target="_blank" rel="noopener noreferrer">Shopify B2B</a></li>
<li><a href="https://tanstack.com/start" target="_blank" rel="noopener noreferrer">TanStack Start</a></li>
<li><a href="https://developers.cloudflare.com/workers/" target="_blank" rel="noopener noreferrer">Cloudflare Workers</a></li>
<li><a href="https://developers.cloudflare.com/queues/" target="_blank" rel="noopener noreferrer">Cloudflare Queues</a></li>
<li><a href="https://developers.cloudflare.com/workers/wrangler/" target="_blank" rel="noopener noreferrer">Wrangler</a></li>
<li><a href="https://www.pulumi.com/" target="_blank" rel="noopener noreferrer">Pulumi</a></li>
<li><a href="https://effect.website/" target="_blank" rel="noopener noreferrer">Effect</a></li>
<li><a href="https://sentry.io/" target="_blank" rel="noopener noreferrer">Sentry</a></li>
<li><a href="https://posthog.com/" target="_blank" rel="noopener noreferrer">PostHog</a></li>
<li><a href="https://search.google.com/search-console/about" target="_blank" rel="noopener noreferrer">Google Search Console</a></li>
<li><a href="https://www.google.com/retail/solutions/merchant-center/" target="_blank" rel="noopener noreferrer">Google Merchant Center</a></li>
<li><a href="https://www.google.com/business/" target="_blank" rel="noopener noreferrer">Google Business Profile</a></li>
</ul>]]></content>
<category term="marketing"/><category term="architecture"/><category term="shopify"/>
</entry>
<entry>
<title type="html"><![CDATA[Building the Pocket Rice marketing website]]></title>
<id>https://blog.dennisokeeffe.com/blog/2026-06-08-building-the-pocket-rice-marketing-website</id>
<link href="https://dennisokeeffe.com/blog/2026-06-08-building-the-pocket-rice-marketing-website"/>
<published>2026-06-08T00:00:00.000Z</published>
<updated>2026-06-08T00:00:00.000Z</updated>
<summary type="html"><![CDATA[A rundown on some of the designs and working behind the Pocket Rice marketing website]]></summary>
<content type="html"><![CDATA[<p>On weekends, I pitch in my time to help my partner run her Onigiri business &quot;Pocket Rice&quot; here in Byron Bay.</p>
<p>As part of her recent push for the business, we set up <a href="https://pocketrice.com.au" target="_blank" rel="noopener noreferrer">the Pocket Rice marketing website</a> as means of generating reach within the local region and having a more professional presence.</p>
<p>This blog post gives an overview of the marketing site build and the backend with a core pillar of the build being to keep build costs low.</p>
<h2 id="website-at-a-high-level">Website at a high-level</h2>
<p>I added the marketing website and backend to a monorepo that I have already been maintaining for other apps that manage our ordering system.</p>
<p>The website itself actually started as a Next.js website that my partner &quot;vibe coded&quot; which I had to take over and optimise for certain desired outcomes (asset optimisation etc.) as well as figure out the infrastructure for.</p>
<p>Initially, the website handed over used the Next.js app router for the backend which I migrated over to a Go HTTP backend with their standard library router. After the migration, I converted the Next.js website to be a static website only.</p>
<p>For hosting, the code is (currently) hosted on GitHub and using GitHub Actions to deploy to AWS via the AWS CDK infrastructure. The repo already had this configured from the order management system application I had already configured, so all I needed to do was add a new stack.</p>
<p>This stack itself hosts everything behind <a href="https://aws.amazon.com/cloudfront/" target="_blank" rel="noopener noreferrer">AWS CloudFront</a> which has a number of benefits, but one of those was the ability to alter behaviour based on the route.</p>
<p>All key Next.js static pages are redirected to a private S3 origin to serve static assets and pages, while anything under the <code>/api/*</code> path will be directed to an <a href="https://aws.amazon.com/api-gateway/" target="_blank" rel="noopener noreferrer">AWS API Gateway</a> which has a <a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-set-up-simple-proxy.html" target="_blank" rel="noopener noreferrer">proxy integration</a> to an <a href="https://docs.aws.amazon.com/lambda/latest/dg/welcome.html" target="_blank" rel="noopener noreferrer">AWS Lambda</a> running the Golang server.</p>
<p>The following is a simplified look at the architecture:</p>
<figure class="diagram" data-pele="true" data-pagefind-ignore="all"><div class="diagram-canvas"><svg xmlns="http://www.w3.org/2000/svg" class="pele pele-architecture" viewBox="0 0 514 398" width="514" height="398" fill="var(--_fg)" role="graphics-document document" aria-roledescription="architecture"><g transform="translate(8,8)"><g class="pele-clusters"><g class="pele-cluster" data-id="aws"><rect x="108" y="0" width="282" height="266" fill="var(--_a)" fill-opacity="0.5" stroke="var(--_b)"></rect><svg class="pele-icon" data-icon="cloud" x="118" y="8" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 18a4 4 0 0 1-.5-7.97A6 6 0 0 1 18 9.5a4.25 4.25 0 0 1-.5 8.5z" stroke="var(--_fg)" stroke-width="1.5"></path></svg><text class="pele-cluster-label" fill="var(--_m)" x="158.4" y="23.6" text-anchor="middle">AWS</text></g></g><g class="pele-edges" fill="none" stroke="var(--_l)" stroke-linecap="round" stroke-linejoin="round"><g class="pele-edge" data-id="visitor-cloudfront"><path d="M58,60L150,60"></path><path class="pele-marker" d="M157,60L149,63.36L149,56.64Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge" data-id="cloudfront-s3"><path d="M205,60L291,60"></path><path class="pele-marker" d="M298,60L290,63.36L290,56.64Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge" data-id="cloudfront-gateway"><path d="M181,114L181,163"></path><path class="pele-marker" d="M181,170L177.64,162L184.36,162Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge" data-id="gateway-lambda"><path d="M205,194L291,194"></path><path class="pele-marker" d="M298,194L290,197.36L290,190.64Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge" data-id="lambda-stripe"><path d="M322,248L322,299"></path><path class="pele-marker" d="M322,306L318.64,298L325.36,298Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge" data-id="github-s3"><path d="M440,60L353,60"></path><path class="pele-marker" d="M346,60L354,56.64L354,63.36Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge" data-id="github-lambda"><path d="M464,132L464,188Q464,194 458,194L353,194"></path><path class="pele-marker" d="M346,194L354,190.64L354,197.36Z" fill="var(--_l)" stroke="none"></path></g></g><g class="pele-nodes"><g class="pele-node pele-service" data-id="visitor" transform="translate(34,36)"><rect x="-24" width="48" height="48" fill="var(--_s)" stroke="var(--_b)"></rect><svg class="pele-icon" data-icon="internet" x="-12" y="12" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0-18 0M3 12h18M12 3a13 13 0 0 0 0 18M12 3a13 13 0 0 1 0 18" stroke="var(--_fg)" stroke-width="1.5"></path></svg><text class="pele-label" x="0" y="69.6" text-anchor="middle">Visitor</text></g><g class="pele-node pele-service" data-id="cloudfront" transform="translate(181,36)"><rect x="-24" width="48" height="48" fill="var(--_s)" stroke="var(--_b)"></rect><svg class="pele-icon" data-icon="cloud" x="-12" y="12" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 18a4 4 0 0 1-.5-7.97A6 6 0 0 1 18 9.5a4.25 4.25 0 0 1-.5 8.5z" stroke="var(--_fg)" stroke-width="1.5"></path></svg><text class="pele-label" x="0" y="69.6" text-anchor="middle">CloudFront</text></g><g class="pele-node pele-service" data-id="s3" transform="translate(322,36)"><rect x="-24" width="48" height="48" fill="var(--_s)" stroke="var(--_b)"></rect><svg class="pele-icon" data-icon="disk" x="-12" y="12" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 3h14v18H5zM12 6.5a4 4 0 1 0 0 8a4 4 0 1 0 0-8M12 10.5h.01M8 18h3" stroke="var(--_fg)" stroke-width="1.5"></path></svg><text class="pele-label" text-anchor="middle" xml:space="preserve"><tspan x="0" y="68.1">Private S3</tspan><tspan x="0" y="89.1">bucket</tspan></text></g><g class="pele-node pele-service" data-id="gateway" transform="translate(181,170)"><rect x="-24" width="48" height="48" fill="var(--_s)" stroke="var(--_b)"></rect><svg class="pele-icon" data-icon="server" x="-12" y="12" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 4h18v7H3zM3 13h18v7H3zM7 7.5h.01M7 16.5h.01M11 7.5h6M11 16.5h6" stroke="var(--_fg)" stroke-width="1.5"></path></svg><text class="pele-label" x="0" y="69.6" text-anchor="middle">API Gateway</text></g><g class="pele-node pele-service" data-id="lambda" transform="translate(322,170)"><rect x="-24" width="48" height="48" fill="var(--_s)" stroke="var(--_b)"></rect><svg class="pele-icon" data-icon="server" x="-12" y="12" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 4h18v7H3zM3 13h18v7H3zM7 7.5h.01M7 16.5h.01M11 7.5h6M11 16.5h6" stroke="var(--_fg)" stroke-width="1.5"></path></svg><text class="pele-label" x="0" y="69.6" text-anchor="middle">Go Lambda</text></g><g class="pele-node pele-service" data-id="github" transform="translate(464,36)"><rect x="-24" width="48" height="48" fill="var(--_s)" stroke="var(--_b)"></rect><svg class="pele-icon" data-icon="server" x="-12" y="12" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 4h18v7H3zM3 13h18v7H3zM7 7.5h.01M7 16.5h.01M11 7.5h6M11 16.5h6" stroke="var(--_fg)" stroke-width="1.5"></path></svg><text class="pele-label" text-anchor="middle" xml:space="preserve"><tspan x="0" y="68.1">GitHub</tspan><tspan x="0" y="89.1">Actions</tspan></text></g><g class="pele-node pele-service" data-id="stripe" transform="translate(322,306)"><rect x="-24" width="48" height="48" fill="var(--_s)" stroke="var(--_b)"></rect><svg class="pele-icon" data-icon="cloud" x="-12" y="12" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 18a4 4 0 0 1-.5-7.97A6 6 0 0 1 18 9.5a4.25 4.25 0 0 1-.5 8.5z" stroke="var(--_fg)" stroke-width="1.5"></path></svg><text class="pele-label" x="0" y="69.6" text-anchor="middle">Stripe</text></g></g></g></svg></div><template>architecture-beta
group aws(cloud)[AWS]

service visitor(internet)[Visitor]
service cloudfront(cloud)[CloudFront] in aws
service s3(disk)[Private S3 bucket] in aws
service gateway(server)[API Gateway] in aws
service lambda(server)[Go Lambda] in aws
service github(server)[GitHub Actions]
service stripe(cloud)[Stripe]

visitor:R --&gt; L:cloudfront
cloudfront:R --&gt; L:s3
cloudfront:B --&gt; T:gateway
gateway:R --&gt; L:lambda
lambda:B --&gt; T:stripe
github:L --&gt; R:s3
github:B --&gt; R:lambda</template></figure>
<p>There are a number of benefits to this:</p>
<ol>
<li>The lambda at the expected volume will remain in free tier.</li>
<li>There are no cross-origin resource sharing setups that I need to configure.</li>
<li>We get the benefit of all the security rules and DDOS protections that can be applied directly with a CloudFront origin.</li>
</ol>
<p>These benefits alone have me incredibly bullish. Because we are using the Golang standard library HTTP router, if we ever outgrow lambda or costs become non-insignificant, it is easy enough to migrate over to an alternative like <a href="https://aws.amazon.com/ecs/" target="_blank" rel="noopener noreferrer">Elastic Container Service</a> (or anywhere I decide to change hosting to, but ECS can also be an API Gateway target and is easy to migrate to).</p>
<p>Once deployed via CI/CD on a merge to <code>main</code>, our <a href="https://pocketrice.com.au" target="_blank" rel="noopener noreferrer">marketing website becomes reachable</a>.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2026-06-08-building-the-pocket-rice-marketing-website/home-page-1.webp" alt="Home page view" loading="lazy" decoding="async"/><figcaption>Home page view</figcaption></figure>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>There is also a staging workflow. On branches there is a preview deployment
made but I have left that out-of-scope for this blog post.</p> </div> </aside>
<h2 id="blog-pages">Blog pages</h2>
<p>We host a <a href="https://pocketrice.com.au/blog" target="_blank" rel="noopener noreferrer">blog</a> on the marketing website as a means for helping to generate organic traffic.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2026-06-08-building-the-pocket-rice-marketing-website/blogs-page-1.webp" alt="Our blog landing page" loading="lazy" decoding="async"/><figcaption>Our blog landing page</figcaption></figure>
<p>Our goal is to have a funnel that can be used to drive awareness of our brand within Byron Bay.</p>
<p>Each blog post itself has a simple <code>MDX</code> configuration that allows us to reuse defined React.js components through the posts themselves.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2026-06-08-building-the-pocket-rice-marketing-website/blogs-page-2.webp" alt="An example blog post" loading="lazy" decoding="async"/><figcaption>An example blog post</figcaption></figure>
<p>Our goal here will be to identify some key blog posts and drive content around them. Some of our current &quot;anchor&quot; blog posts include:</p>
<ul>
<li><a href="https://pocketrice.com.au/blog/onigiri-vs-sushi" target="_blank" rel="noopener noreferrer">Education on the difference between onigiri and sushi</a>, a question we get all the time at markets.</li>
<li><a href="https://pocketrice.com.au/blog/byron-community-market-sunday-guide" target="_blank" rel="noopener noreferrer">Market guides such as the Byron Bay Community Market guide</a> in an attempt to win a share of market traffic.</li>
<li><a href="https://pocketrice.com.au/blog/best-japanese-food-byron-bay" target="_blank" rel="noopener noreferrer">Guides to Japanese good around Byron Bay</a> which we hope to begin improving our ranking for.</li>
</ul>
<p>It is still early days here, so we will be looking to drive traffic and partnerships going forward for this.</p>
<h2 id="online-store">Online store</h2>
<p>As part of our expansion online, my partner also wanted to introduce a store to sell Japanese home goods.</p>
<p>Australia is currently going through it&#39;s &quot;Japan phase&quot; where everyone is holidaying in Japan and being introduced to Japanese culture. The hope is that this can be a way to connect those positive feelings that Australians have towards Japan and to bring them into homes here.</p>
<p>The website itself has a landing page for the shop.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2026-06-08-building-the-pocket-rice-marketing-website/shop-page-1.webp" alt="Online store" loading="lazy" decoding="async"/><figcaption>Online store</figcaption></figure>
<p>As you would expect, each item and variant has its own page for adding to a cart.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2026-06-08-building-the-pocket-rice-marketing-website/shop-page-2.webp" alt="Item detail page" loading="lazy" decoding="async"/><figcaption>Item detail page</figcaption></figure>
<p>We are using <a href="https://stripe.com/au/payments/checkout" target="_blank" rel="noopener noreferrer">Stripe checkout</a> to manage our payments and manually doing shipments ourselves.</p>
<h2 id="other-key-pages">Other key pages</h2>
<p>In addition to this, we also have a few other key pages to help with organic traffic or things that we wanted to highlight.</p>
<p>Our <a href="https://pocketrice.com.au/markets" target="_blank" rel="noopener noreferrer">markets</a> page highlights where people can find us.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2026-06-08-building-the-pocket-rice-marketing-website/markets-page.webp" alt="We list all the markets we participate or have participated in" loading="lazy" decoding="async"/><figcaption>We list all the markets we participate or have participated in</figcaption></figure>
<p>We list our <a href="https://pocketrice.com.au/suppliers" target="_blank" rel="noopener noreferrer">suppliers</a> that we use for our onigiri as well to also highlight our local community members and importers.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2026-06-08-building-the-pocket-rice-marketing-website/suppliers-page.webp" alt="Our suppliers page" loading="lazy" decoding="async"/><figcaption>Our suppliers page</figcaption></figure>
<p>Finally we have a <a href="https://pocketrice.com.au/contact" target="_blank" rel="noopener noreferrer">contact page</a> to have a way for others to contact us.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2026-06-08-building-the-pocket-rice-marketing-website/contact-page.webp" alt="Contact form from the contact page" loading="lazy" decoding="async"/><figcaption>Contact form from the contact page</figcaption></figure>
<h2 id="seo-and-indexing">SEO and indexing</h2>
<p>At the moment, there is a very annoying bug where <a href="https://support.google.com/webmasters/thread/429020059/unable-to-add-service-account-email-to-google-search-console?hl=en" target="_blank" rel="noopener noreferrer">you cannot add newer Google Service accounts for the indexing API</a>, meaning that right now I am stuck doing a manual process with the <a href="https://search.google.com/search-console/about" target="_blank" rel="noopener noreferrer">Google Seach console</a> in order to update sitemaps and request page indexing.</p>
<p>Things that I am also doing at the moment include indexing for other search engines as well as working through some SEO books. I&#39;ve given myself 12 weeks from the first release of the website in order to see if I can start ranking highly across the web for our Pocket Rice brand in key categories such as markets to see and food you must eat. I may even write a follow-up post on how success is going in that area.</p>
<h2 id="links-and-further-reading">Links and further reading</h2>
<ul>
<li><a href="https://pocketrice.com.au" target="_blank" rel="noopener noreferrer">Pocket Rice homepage</a></li>
<li><a href="https://pocketrice.com.au/blog" target="_blank" rel="noopener noreferrer">Pocket Rice blog</a></li>
<li><a href="https://pocketrice.com.au/blog/onigiri-vs-sushi" target="_blank" rel="noopener noreferrer">Onigiri vs sushi</a></li>
<li><a href="https://pocketrice.com.au/blog/byron-community-market-sunday-guide" target="_blank" rel="noopener noreferrer">Byron Bay Community Market guide</a></li>
<li><a href="https://pocketrice.com.au/blog/best-japanese-food-byron-bay" target="_blank" rel="noopener noreferrer">Japanese food around Byron Bay</a></li>
<li><a href="https://pocketrice.com.au/markets" target="_blank" rel="noopener noreferrer">Pocket Rice markets</a></li>
<li><a href="https://pocketrice.com.au/suppliers" target="_blank" rel="noopener noreferrer">Pocket Rice suppliers</a></li>
<li><a href="https://pocketrice.com.au/contact" target="_blank" rel="noopener noreferrer">Pocket Rice contact page</a></li>
<li><a href="https://aws.amazon.com/cloudfront/" target="_blank" rel="noopener noreferrer">AWS CloudFront</a></li>
<li><a href="https://aws.amazon.com/api-gateway/" target="_blank" rel="noopener noreferrer">AWS API Gateway</a></li>
<li><a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-set-up-simple-proxy.html" target="_blank" rel="noopener noreferrer">API Gateway proxy integration</a></li>
<li><a href="https://docs.aws.amazon.com/lambda/latest/dg/welcome.html" target="_blank" rel="noopener noreferrer">AWS Lambda</a></li>
<li><a href="https://aws.amazon.com/ecs/" target="_blank" rel="noopener noreferrer">Amazon Elastic Container Service</a></li>
<li><a href="https://stripe.com/au/payments/checkout" target="_blank" rel="noopener noreferrer">Stripe Checkout</a></li>
<li><a href="https://support.google.com/webmasters/thread/429020059/unable-to-add-service-account-email-to-google-search-console?hl=en" target="_blank" rel="noopener noreferrer">Google Search Console service account issue</a></li>
<li><a href="https://search.google.com/search-console/about" target="_blank" rel="noopener noreferrer">Google Search Console</a></li>
</ul>]]></content>
<category term="marketing"/><category term="architecture"/>
</entry>
<entry>
<title type="html"><![CDATA[Two Pointers]]></title>
<id>https://blog.dennisokeeffe.com/blog/2026-05-07-two-pointers</id>
<link href="https://dennisokeeffe.com/blog/2026-05-07-two-pointers"/>
<published>2026-05-07T00:00:00.000Z</published>
<updated>2026-05-07T00:00:00.000Z</updated>
<summary type="html"><![CDATA[Learn how to spot two-pointer problems and walk through pair sum variants with interactive visuals]]></summary>
<content type="html"><![CDATA[<p>Two pointers is one of the highest ROI patterns for interview prep.</p>
<p>The main reason is not that the implementation is complicated. It usually is not. The value is in learning how to recognize when the problem is giving you permission to do less work.</p>
<p>If an array is sorted, or if a string can be scanned from both ends, or if you are really maintaining a shrinking window, there is a good chance a pointer-based approach is available.</p>
<p>This post starts with a walkthrough of one of the more common patterns you begin with online and then adds some other examples and visuals for other solutions.</p>
<span id="pair-sum---sorted" class="anchor-alias"><h2 id="pair-sum-sorted">Pair Sum - Sorted</h2>
<p>Let us start with a classic prompt:</p>
<blockquote>
<p>Given a sorted array of integers <code>nums</code> and a target <code>target</code>, return the indices of the two numbers that add up to the target.</p>
</blockquote>
<p>Example input:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>const nums = [1, 2, 3, 4, 6, 8, 11];
const target = 10;</code></pre>
<p>The expected answer is <code>[1, 5]</code> because <code>nums[1] + nums[5] = 2 + 8 = 10</code>.</p>
<h2 id="brute-force-intuition">Brute force intuition</h2>
<p>The most direct thought is normally: try every pair.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>export function pairSumSortedBruteForce(nums, target) {
  for (let i = 0; i &lt; nums.length; i++) {
    for (let j = i + 1; j &lt; nums.length; j++) {
      if (nums[i] + nums[j] === target) {
        return [i, j];
      }
    }
  }
  return [];
}</code></pre>
<p>This is a perfectly fine place to begin in an interview because:</p>
<ol>
<li>It is correct.</li>
<li>It gives you a baseline.</li>
<li>It makes the inefficiency obvious.</li>
</ol>
<p>The problem is the nested loops. In the worst case, you check almost every pair, which gives <code>O(n^2)</code> time.</p>
<div class="interactive"> <style>astro-island,astro-slot,astro-static-slot{display:contents}</style> </div>
<p>What the brute-force version misses is that the input is already sorted.</p>
<p>That one detail changes everything.</p>
<h2 id="the-two-pointers-idea">The two pointers idea</h2>
<p>If the array is sorted, then the leftmost value is the smallest and the rightmost value is the largest.</p>
<p>That means each comparison gives us directional information:</p>
<ol>
<li>If the sum is too small, move <code>left</code> right to increase the sum.</li>
<li>If the sum is too large, move <code>right</code> left to decrease the sum.</li>
<li>If the sum matches, we are done.</li>
</ol>
<p>Instead of restarting work for every element, we keep squeezing the search space inward.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>export function pairSumSorted(nums, target) {
  let left = 0;
  let right = nums.length - 1;

  while (left &lt; right) {
    const sum = nums[left] + nums[right];

    if (sum === target) {
      return [left, right];
    }

    if (sum &lt; target) {
      left += 1;
    } else {
      right -= 1;
    }
  }

  return [];
}</code></pre>
<p>This drops the runtime to <code>O(n)</code> and keeps space at <code>O(1)</code>.</p>
<div class="interactive">  </div>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>A useful interview sentence here is: &quot;Because the array is sorted, each
comparison lets us eliminate part of the search space.&quot;</p> </div> </aside>
<h2 id="why-the-sorted-property-matters">Why the sorted property matters</h2>
<p>Without sorting, moving a pointer left or right would not reliably tell us whether the sum should go up or down.</p>
<p>With sorting:</p>
<ol>
<li>Moving <code>left</code> right always gives a value that is the same or larger.</li>
<li>Moving <code>right</code> left always gives a value that is the same or smaller.</li>
</ol>
<p>That monotonic behavior is what makes the pointer movement safe.</p>
<h2 id="faster-alternatives-when-the-input-is-not-already-sorted">Faster alternatives when the input is not already sorted</h2>
<p>This post is centered on the sorted version of the problem, but in practice you will often be asked the unsorted variant too.</p>
<p>There are two common follow-up paths.</p>
<h2 id="sort-first-then-scan">Sort first, then scan</h2>
<p>If you want to keep the two-pointer strategy, you can sort first.</p>
<p>The catch is that if the problem asks for the original indices, you need to keep them around while sorting.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>export function pairSumWithSort(nums, target) {
  const pairs = nums.map((value, index) =&gt; ({ value, index }));
  pairs.sort((a, b) =&gt; a.value - b.value);

  let left = 0;
  let right = pairs.length - 1;

  while (left &lt; right) {
    const sum = pairs[left].value + pairs[right].value;

    if (sum === target) {
      return [pairs[left].index, pairs[right].index];
    }

    if (sum &lt; target) {
      left += 1;
    } else {
      right -= 1;
    }
  }

  return [];
}</code></pre>
<p>Complexity:</p>
<ol>
<li><code>O(n log n)</code> time because sorting dominates.</li>
<li><code>O(n)</code> space because we store value/index pairs.</li>
</ol>
<div class="interactive">  </div>
<p>I like this version when the conversation is specifically about pointer patterns, because it shows you understand how to preserve the property you need.</p>
<h2 id="hash-map-lookup">Hash map lookup</h2>
<p>If the array is unsorted and you want the fastest lookup-based solution, a hash map is usually the better fit.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>export function pairSumHashMap(nums, target) {
  const seen = new Map();

  for (let i = 0; i &lt; nums.length; i++) {
    const complement = target - nums[i];

    if (seen.has(complement)) {
      return [seen.get(complement), i];
    }

    seen.set(nums[i], i);
  }

  return [];
}</code></pre>
<p>Complexity:</p>
<ol>
<li><code>O(n)</code> time.</li>
<li><code>O(n)</code> space.</li>
</ol>
<div class="interactive">  </div>
<p>The trade-off is clean:</p>
<ol>
<li>Two pointers needs sorted order but uses constant extra space.</li>
<li>Hash map works on unsorted input but spends extra memory.</li>
</ol>
<h2 id="identifying-the-pattern">Identifying the pattern</h2>
<p>When I read a problem like this, I mentally check for these clues.</p>
<h3 id="1-is-the-input-sorted">1. Is the input sorted?</h3>
<p>This is the loudest hint.</p>
<p>If the data is sorted and the task involves pairs, ranges, or comparing extremes, two pointers should immediately be on the shortlist.</p>
<h3 id="2-am-i-asked-about-a-pair-triplet-or-contiguous-range">2. Am I asked about a pair, triplet, or contiguous range?</h3>
<p>Problems involving:</p>
<ol>
<li>two numbers,</li>
<li>three numbers,</li>
<li>a window over a string or array,</li>
</ol>
<p>often become pointer problems once ordering is useful.</p>
<h3 id="3-does-each-comparison-eliminate-possibilities">3. Does each comparison eliminate possibilities?</h3>
<p>This is the real question.</p>
<p>If one comparison tells you &quot;everything to the left is now too small&quot; or &quot;everything to the right is now too large&quot;, the problem is probably pointer-friendly.</p>
<h3 id="4-am-i-repeatedly-rescanning-work">4. Am I repeatedly rescanning work?</h3>
<p>If your brute-force version uses nested loops, ask whether a pointer can carry forward information from the last comparison instead of starting over.</p>
<h2 id="tips-and-tricks">Tips and tricks</h2>
<h3 id="say-the-invariant-out-loud">Say the invariant out loud</h3>
<p>For this problem, the invariant is that the answer, if it exists, must stay inside the current <code>[left, right]</code> range.</p>
<p>When the sum is too small, moving <code>right</code> would only make things smaller or equal, so the only useful move is <code>left += 1</code>.</p>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>Here, the &quot;invariant&quot; means a condition or fact that stays true throughout the
algorithm while the loop is running.</p> </div> </aside>
<h3 id="ask-whether-the-array-is-already-sorted">Ask whether the array is already sorted</h3>
<p>If the prompt does not say so, ask.</p>
<p>That one clarification can change the best solution from hash map to two pointers.</p>
<h3 id="watch-the-output-requirements">Watch the output requirements</h3>
<p>If the question wants original indices and you decide to sort, preserve the indices before sorting.</p>
<h3 id="distinguish-pattern-from-implementation">Distinguish pattern from implementation</h3>
<p>The pattern is not just &quot;have two variables called left and right&quot;.</p>
<p>The pattern is using structure in the input so pointer movement is meaningful.</p>
<h2 id="two-pointer-traversals">Two-pointer traversals</h2>
<p>There are three two-pointer strategies to know about:</p>
<ol>
<li>Inward traversal.</li>
<li>Unidirectional traversal.</li>
<li>Staged traversal.</li>
</ol>
<h3 id="inward-traversal">Inward traversal</h3>
<p>The two pointers start at opposing ends and work towards each other.</p>
<div class="interactive">  </div>
<p>Common examples:</p>
<ol>
<li><code>Valid Palindrome</code></li>
<li><code>Two Sum II - Input Array Is Sorted</code></li>
<li><code>Container With Most Water</code></li>
</ol>
<h3 id="unidirectional-traversal">Unidirectional traversal</h3>
<p>Both pointers start at the same end of the data structure.</p>
<div class="interactive">  </div>
<p>Common examples:</p>
<ol>
<li><code>Is Subsequence</code></li>
<li><code>Remove Duplicates From Sorted Array</code></li>
<li><code>Move Zeroes</code></li>
</ol>
<h3 id="staged-traversal">Staged traversal</h3>
<p>One pointer establishes context first, and then another pointer pair or follow-up pointer traversal takes over within that smaller search space.</p>
<div class="interactive">  </div>
<p>Common examples:</p>
<ol>
<li><code>3Sum</code></li>
<li><code>4Sum</code></li>
<li><code>Trapping Rain Water</code> when you reason in phases around boundary pointers</li>
</ol>
<h2 id="similar-problems-worth-practicing">Similar problems worth practicing</h2>
<p>These names are often used as standard interview-problem titles, but the useful part is the pattern hiding underneath each one.</p>
<h3 id="valid-palindrome-compare-characters-from-the-outside-in">Valid Palindrome: compare characters from the outside in</h3>
<p>You place one pointer at the start of the string and one at the end.</p>
<p>If the characters match, move both inward. If they do not, the string is not a palindrome. This is one of the clearest examples of two pointers on a string.</p>
<h3 id="is-subsequence-scan-one-string-while-trying-to-consume-the-other">Is Subsequence: scan one string while trying to consume the other</h3>
<p>This is another useful string variant, but the pointers do different jobs.</p>
<ol>
<li>One pointer walks through <code>s</code>, the smaller string we want to match.</li>
<li>The other pointer walks through <code>t</code>, the larger string we are scanning.</li>
<li>When the characters match, advance both pointers.</li>
<li>When they do not match, only advance the pointer in <code>t</code>.</li>
</ol>
<p>If the pointer in <code>s</code> reaches the end, then every character in <code>s</code> appeared in order inside <code>t</code>.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>function isSubsequence(s: string, t: string): boolean {
  let i = 0,
    j = 0;

  while (i &lt; s.length &amp;&amp; j &lt; t.length) {
    if (s[i] === t[j]) {
      i++;
    }

    j++;
  }

  return i === s.length;
}</code></pre>
<div class="interactive">  </div>
<h3 id="two-sum-ii-find-a-pair-in-a-sorted-array">Two Sum II: find a pair in a sorted array</h3>
<p>This is basically the same shape as the pair sum problem we just walked through.</p>
<p>The array is sorted, so each sum tells you whether to move the left pointer right or the right pointer left.</p>
<p>The main twist is that the answer must be returned as <strong>1-indexed</strong> positions.</p>
<p>One small correction to the implementation you pasted: <code>right</code> should start at <code>numbers.length - 1</code>, not <code>numbers.length</code>, otherwise the first lookup is out of bounds.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>function twoSum(numbers: number[], target: number): number[] {
  let left = 0;
  let right = numbers.length - 1;

  while (left &lt; right) {
    const sum = numbers[left] + numbers[right];

    if (sum === target) {
      return [left + 1, right + 1];
    }

    if (sum &lt; target) {
      left++;
    } else {
      right--;
    }
  }

  return [];
}</code></pre>
<p>Quick guidance:</p>
<ol>
<li><code>left</code> starts at the smallest value.</li>
<li><code>right</code> starts at the largest value.</li>
<li>If the sum is too small, move <code>left</code> right.</li>
<li>If the sum is too large, move <code>right</code> left.</li>
<li>Return <code>left + 1</code> and <code>right + 1</code> because the problem is 1-indexed.</li>
</ol>
<div class="interactive">  </div>
<span id="3sum-reduce-a-triplet-problem-into-repeated-pair-searches" class="anchor-alias"><h3 id="3-sum-reduce-a-triplet-problem-into-repeated-pair-searches">3Sum: reduce a triplet problem into repeated pair searches</h3>
<p>You sort the array, fix one number, and then use two pointers on the remaining suffix to look for the other two values.</p>
<p>This is a good example of how one pointer pattern can become a building block inside a larger solution.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>function threeSum(nums: number[]): number[][] {
  nums.sort((a, b) =&gt; a - b);
  const results: number[][] = [];
  const n = nums.length;

  for (let i = 0; i &lt; n - 2; i++) {
    if (i &gt; 0 &amp;&amp; nums[i] === nums[i - 1]) continue;

    let left = i + 1;
    let right = n - 1;

    while (left &lt; right) {
      const sum = nums[i] + nums[left] + nums[right];

      if (sum === 0) {
        results.push([nums[i], nums[left], nums[right]]);

        while (left &lt; right &amp;&amp; nums[left] === nums[left + 1]) left++;
        while (left &lt; right &amp;&amp; nums[right] === nums[right - 1]) right--;

        left++;
        right--;
      } else if (sum &lt; 0) {
        left++;
      } else {
        right--;
      }
    }
  }

  return results;
}</code></pre>
<p>Notes on the solution:</p>
<ol>
<li>Sorting is what makes the inner <code>left</code> and <code>right</code> movement meaningful.</li>
<li>The outer loop fixes one value, then the inner loop solves a pair-style search for the remaining two values.</li>
<li>You must skip duplicate fixed values, or you will produce duplicate triplets.</li>
<li>After finding a valid triplet, you also skip duplicate <code>left</code> and <code>right</code> values before continuing.</li>
<li>The runtime is <code>O(n^2)</code> because for each fixed index, the inner pointers scan the remaining suffix once.</li>
</ol>
<div class="interactive">  </div>
<h3 id="container-with-most-water-move-the-side-that-limits-the-answer">Container With Most Water: move the side that limits the answer</h3>
<p>You start with pointers at both ends and compute the area formed by the two heights.</p>
<p>The key move is to advance the shorter wall, because the shorter wall is currently limiting the area.</p>
<h3 id="remove-duplicates-from-sorted-array-use-a-slow-writer-and-a-fast-reader">Remove Duplicates From Sorted Array: use a slow writer and a fast reader</h3>
<p>This is still a two-pointers problem, but the pointers do different jobs.</p>
<p>One pointer scans forward through the array while the other marks where the next unique value should be written.</p>
<h3 id="squares-of-a-sorted-array-compare-the-extremes-and-fill-from-the-back">Squares of a Sorted Array: compare the extremes and fill from the back</h3>
<p>When negatives are involved, the largest square may come from either end of the sorted array.</p>
<p>So you compare both ends, place the larger square into the result from right to left, and move the pointer that produced it.</p>
<h2 id="a-quick-decision-guide">A quick decision guide</h2>
<p>If I had to compress the choice into a few lines:</p>
<ol>
<li>Already sorted and need a pair: start with two pointers.</li>
<li>Unsorted and need the fastest lookup: use a hash map.</li>
<li>Unsorted but want to leverage ordering: sort and then run two pointers.</li>
</ol>
<h2 id="what-comes-next">What comes next</h2>
<p>I will keep extending this post with more two-pointers examples.</p>
<p>Next up will likely be problems such as valid palindrome, where the same idea shows up in strings instead of arrays.</p>
<p>The main thing I want to reinforce is this: do not memorize the pointer movement rules in isolation. Train yourself to ask what property of the input makes pointer movement safe.</p>
<p>That is the part that transfers across problems.</p>]]></content>
<category term="interviews"/><category term="algorithms"/>
</entry>
<entry>
<title type="html"><![CDATA[Software Interview Guideline 2026 (Australian edition)]]></title>
<id>https://blog.dennisokeeffe.com/blog/2026-05-01-software-interview-guideline-2026</id>
<link href="https://dennisokeeffe.com/blog/2026-05-01-software-interview-guideline-2026"/>
<published>2026-05-04T00:00:00.000Z</published>
<updated>2026-05-04T00:00:00.000Z</updated>
<summary type="html"><![CDATA[A collection of links, ideas and approaches to interviewing for software companies in 2026.]]></summary>
<content type="html"><![CDATA[<p>For reasons outside the scope of this post, I am putting the feelers out there to explore the market.</p>
<p>My last interview was back in November 2023, which in this day and age can be appropriately measured by available LLM models: Claude 2.1, GPT 4-Turbo and Gemini 1.0 Pro. Yes, <em>that long ago</em>.</p>
<p>As Seneca says, &quot;Luck is where opportunity meets preparation&quot;. This post covers my attempt to reconcile previous approaches to software interviews alongside any tidbits of information I can find from my network and beyond.</p>
<p>I will be covering resources around the following topics:</p>
<ol>
<li>The job market.</li>
<li>What job interviews look like.</li>
<li>How to prepare for interviews.</li>
<li>Advice on your applications.</li>
</ol>
<p>Given the scope of software engineering, there is no point in pretending there is a one-size-fits-all scope.</p>
<p>Instead, this post is more of an anchor that links out where appropriate and gives my two cents (it is <em>my</em> blog after all).</p>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>Given that I am based in Australia, there is also likely a bias towards my own
job market at times.</p> </div> </aside>
<h2 id="a-guiding-principle-to-abide-by-in-the-age-of-agents">A guiding principle to abide by in the age of agents</h2>
<p>Before going any further, there is a quote from a tweet that I have been thinking about a lot:</p>
<blockquote>
<p>You can outsource thinking but you cannot outsource your understanding.</p>
</blockquote>
<p>In this day and age, this hits <strong>hard</strong>.</p>
<p>AI is tool to be used, but it can be a slippery slope. You need to best be able to determine when the use and costs are a net win and have the discipline of when to say no.</p>
<p>My current mindset: there is a place for delegating, vibe coding and multitasking, but it is your responsibility to be aware of the where and when. When it comes to guiding AI for anything mission critical, you need to be the driver. Don&#39;t slip into the habit of spinning the poker machine.</p>
<p>We will touch on AI a bit more later, but I think addressing the elephant in the room early is an appropriate starting point.</p>
<h2 id="the-job-market">The job market</h2>
<p><strong><a href="https://trueup.io" target="_blank" rel="noopener noreferrer">TrueUp</a></strong> is a great companion on our search to understand the job market. It scans the tech job market in realtime and provides a trends section.</p>
<p>Some of the positive notes (from the time of writing):</p>
<ol>
<li>The number of tech jobs has been steadily <strong>increasing</strong> since the low of 2023.</li>
<li>The remote job options have remained fairly steady, with a recent increase happening.</li>
<li>(For more own sanity) Australia sits at #10 for top countries for tech.</li>
</ol>
<p>Although it&#39;s from 2025, The Pragmatic Engineer&#39;s blog post <a href="https://newsletter.pragmaticengineer.com/p/state-of-the-tech-market-in-2025" target="_blank" rel="noopener noreferrer">State of the software engineering job market in 2025, part 1, what the data says</a> provides some insightful bonus graphs on job available by senority and the type of company.</p>
<p>Without spoiling too much, it looks positive for the entry-level and mid-tier job market.</p>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p><strong>Edit</strong>: A callout from a colleague was that the market <strong>does not</strong> look so
positive for the entry-level and mid-level job market in Australia at the
moment. I don&#39;t have anything too concrete here, but I felt it was worth
updating this to share the extra viewpoint. You may have to take these stats
with a grain of salt.</p> </div> </aside>
<p>More specifically within my market (the Australian market), a useful pulse check is the <a href="https://www.jobsandskills.gov.au/data/occupation-and-industry-profiles/occupations/261313-software-engineers" target="_blank" rel="noopener noreferrer">Government Jobs and Skills data on software engineers</a> website.</p>
<p>The latest data here from April 6th, 2026 indicates that in Australia there are 55,200 employed software engineers.</p>
<p>Based on TrueUp&#39;s data, there looks to be around 663 software engineer jobs advertised, which would indicate ~1% of the total employed work force.</p>
<aside class="callout callout--warning" aria-label="Warning"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><path d="m21.7 18-8-14a2 2 0 0 0-3.4 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.7-3ZM12 9v4M12 17h.01" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>  <div class="callout__body">  <p>I have also heard about job postings that <strong>are not</strong> looking to be fulfilled
by companies. The Australian Broadcasting Corporation posted about <a href="https://www.abc.net.au/news/2024-06-18/ghost-jobs-the-fake-listings-that-waste-employees-time/103927046" target="_blank" rel="noopener noreferrer">Ghost
jobs</a>
in 2024. I am uncertain how many roles I am looking at might be ghost job
postings.</p> </div> </aside>
<p>As a <em>not-very-science-y</em> pulse check on LinkedIn for software engineering jobs in Australia, I paginated through ~34 pages of 20 jobs each (680) and they all still had &quot;engineer&quot; in the title. Make of that what you will (I will come back with a better approach when I get a free moment in future).</p>
<p>All-in-all, if you have come here fearing doom-and-gloom then fear not my friends. Our time is coming again!</p>
<p>As for finding available jobs, here is a list of potential job search websites (in no particular order):</p>
<ul>
<li><a href="https://trueup.io" target="_blank" rel="noopener noreferrer">TrueUp</a> provides access to their list of jobs</li>
<li><a href="https://remoteok.com/" target="_blank" rel="noopener noreferrer">remote.ok</a> for remote-friendly jobs</li>
<li><a href="https://www.linkedin.com/jobs/" target="_blank" rel="noopener noreferrer">LinkedIn Jobs</a></li>
</ul>
<p>Some colleagues also shared some sites:</p>
<ul>
<li><a href="https://hatch.team/" target="_blank" rel="noopener noreferrer">hatch</a></li>
<li><a href="https://jobs.airtree.vc/companies" target="_blank" rel="noopener noreferrer">Airtree</a></li>
<li><a href="https://jobs.blackbird.vc/jobs" target="_blank" rel="noopener noreferrer">Blackbird</a></li>
<li><a href="https://squarepeg.getro.com/jobs" target="_blank" rel="noopener noreferrer">Squarepeg</a></li>
</ul>
<p>A list of Australian-specific options:</p>
<ul>
<li><a href="https://au.seek.com/" target="_blank" rel="noopener noreferrer">seek</a></li>
<li><a href="https://www.careerone.com.au/" target="_blank" rel="noopener noreferrer">careerone</a></li>
<li><a href="https://au.indeed.com/" target="_blank" rel="noopener noreferrer">indeed</a></li>
</ul>
<p>If you are interested in specific companies, then I also recommend going straight to their own advertised jobs board.</p>
<p>If you are open to any jobs and don&#39;t mind someone coming to you with options, chat to a recruiter.</p>
<h2 id="what-do-the-job-interviews-look-like">What do the job interviews look like?</h2>
<p>In my general experience, the bulk of the interviews you will do are this:</p>
<ol>
<li>First screening.</li>
<li>Technical interview(s).</li>
<li>System design interview.</li>
<li>Behavioral interview.</li>
</ol>
<p>Depending on the your field of engineering (e.g. if you are a solution&#39;s engineer), you may find some variance but these have been at the core of my experience.</p>
<p>This looks to be aligned with the <a href="https://www.techinterviewhandbook.org/software-engineering-interview-guide/" target="_blank" rel="noopener noreferrer">Tech Interview Handbook</a>, a guide that I came across which has a walkthrough on interview preparation.</p>
<p>The link also contains some really good links to courses and preparation tools, but my own personal comments on that link from my own experience:</p>
<ul>
<li>The site puts &quot;Take home assignment&quot; as rare but my experience has been to get one ~40% of the time.</li>
<li>A &quot;quiz&quot; is said to be occasional for a quick filter, but admittedly I&#39;ve never really done one.</li>
</ul>
<p>It also recommends a coding interview study plan of 11 hours a week for three months. In my honest opinion, that feels overkill (assuming this is not your first time with preparation).</p>
<p>What I would do before committing to a timeline is look for feedback on the existing interview processes of the jobs you are interested in and then narrow down what you need to be prepped for.</p>
<p>An example: In Australia, one job link shared with me was a role as a <a href="https://recruitment.macquarie.com/en_US/careers/JobDetail?jobId=21706" target="_blank" rel="noopener noreferrer">Software engineer at Macquarie Group</a>. I&#39;m not personally interested in this role, but assuming I am then I generally do a few things:</p>
<ol>
<li>Visit the company on Glassdoor and check things like <a href="https://www.glassdoor.com.au/Interview/Macquarie-Group-Interview-Questions-E12088.htm?filter.jobTitleFTS=Software+Engineer" target="_blank" rel="noopener noreferrer">the interview feedback</a>.</li>
<li>Check their <a href="https://www.glassdoor.com.au/Salary/Macquarie-Group-Software-Engineer-Salaries-E12088_D_KO16,33.htm" target="_blank" rel="noopener noreferrer">reported salaries</a> (for my own sake on expectations).</li>
<li>I also review their online <a href="https://www.macquarie.com/au/en/careers/recruitment-process.html" target="_blank" rel="noopener noreferrer">recruitment/interview process</a> if they have one.</li>
</ol>
<p>As an example (from the above three links), I can grok the follow for mid-level software engineers:</p>
<ul>
<li>They earn AUD$90K-130K per year.</li>
<li>Their interview process has a minimum of three rounds.</li>
<li>Previous interviewees indicate a technical interview with a live coding session over (at minimum) a four week process.</li>
</ul>
<p>Understanding this process goes a long way to getting a better idea about the process prior to your initial contact.</p>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>If you are looking across multiple roles, it is also worthwhile to start
collating all these links and reviews into a centralized place for you to
refer back to.</p> </div> </aside>
<h3 id="technical-interviews">Technical interviews</h3>
<p>My honest experience with technical interviews is this: if I grok the question and can quickly ascertain the correct path to go down, it&#39;s normally pretty straight forward. If I don&#39;t, it&#39;s a massive slog and sometimes a miserable failure.</p>
<p>Few love the idea of writing code under interview conditions, but it is still something best to prepare for.</p>
<p>For Leetcode-style interviews, <a href="https://algo.monster/problems/stats" target="_blank" rel="noopener noreferrer">Algomonster</a> have a &quot;return on investment&quot; style layout for tech interview preparation that reflects a lot of my own experience.</p>
<p>It outlines problem-solving approaches and your likelihood to run into those style questions:</p>
<table>
<thead>
<tr>
<th>Approach</th>
<th>Difficulty to Learn</th>
<th>Return on Investment</th>
</tr>
</thead>
<tbody>
<tr>
<td>Two Pointers</td>
<td>Easy</td>
<td>High</td>
</tr>
<tr>
<td>Sliding Window</td>
<td>Easy</td>
<td>High</td>
</tr>
<tr>
<td>Breadth-First Search</td>
<td>Easy</td>
<td>High</td>
</tr>
<tr>
<td>Depth-First Search</td>
<td>Medium</td>
<td>High</td>
</tr>
<tr>
<td>Backtracking</td>
<td>High</td>
<td>High</td>
</tr>
<tr>
<td>Heap</td>
<td>Medium</td>
<td>Medium</td>
</tr>
<tr>
<td>Binary Search</td>
<td>Easy</td>
<td>Medium</td>
</tr>
<tr>
<td>Dynamic Programming</td>
<td>High</td>
<td>Medium</td>
</tr>
<tr>
<td>Divide and Conquer</td>
<td>Medium</td>
<td>Low</td>
</tr>
<tr>
<td>Trie</td>
<td>Medium</td>
<td>Low</td>
</tr>
<tr>
<td>Union Find</td>
<td>Medium</td>
<td>Low</td>
</tr>
<tr>
<td>Greedy</td>
<td>High</td>
<td>Low</td>
</tr>
</tbody>
</table>
<p>If you are short on time, I recommend at least working through the high ROI problem solving approaches.</p>
<p>Some websites with an array of problems on those topics:</p>
<ol>
<li><a href="https://leetcode.com/problemset/" target="_blank" rel="noopener noreferrer">LeetCode</a></li>
<li><a href="https://www.hackerrank.com/" target="_blank" rel="noopener noreferrer">HackerRank</a></li>
<li><a href="https://codesignal.com/learn-app/" target="_blank" rel="noopener noreferrer">CodeSignal</a></li>
</ol>
<p>These same websites can also provide varying levels of online content for learning as well.</p>
<p>In addition to the above websites, I&#39;ve also found <a href="https://bytebytego.com/" target="_blank" rel="noopener noreferrer">ByteByteGo</a> to be a great resource for books that comes with great visual guidelines.</p>
<p>Each of these are <strong>paid solutions</strong> which come with their own cost. Over the coming weeks I will also likely introduce my own online blog resources to help with some interactive learning.</p>
<p>Most of my personal experience with technical interviews have been fairly straightforward, but you need to be able to ask the right questions and quickly identify which approach to take. There have still been times when I&#39;ve flunked straightforward questions.</p>
<p>Admittedly, I&#39;ve never had to do any interviews when I &quot;invert a binary tree&quot; or anything of the sort. Seems impractical to test on but there are horror stories out there.</p>
<h3 id="system-design-interview">System design interview</h3>
<p>For me, system design interviews feel like my bread and butter. I find them much more practical in my day-to-day work and I find them more intuitive to reason through.</p>
<p>My experience is normally to follow my own acronym <code>CASMHDR</code>:</p>
<table>
<thead>
<tr>
<th>Letter</th>
<th>Stands for</th>
<th>Meaning</th>
</tr>
</thead>
<tbody>
<tr>
<td>C</td>
<td>Clarify</td>
<td>Clarify the problem/desired outcomes</td>
</tr>
<tr>
<td>A</td>
<td>API Design</td>
<td>Think through and design API interfaces</td>
</tr>
<tr>
<td>S</td>
<td>Storage requirements</td>
<td>Consider storage systems (DBs etc) and back-of-the-envelope size estimations</td>
</tr>
<tr>
<td>M</td>
<td>Models</td>
<td>Think through the data models within the system</td>
</tr>
<tr>
<td>H</td>
<td>High-level design</td>
<td>Start with the high-level design</td>
</tr>
<tr>
<td>D</td>
<td>Detailed design</td>
<td>After agreeing on the high-level design, work through something more detailed</td>
</tr>
<tr>
<td>R</td>
<td>Review, repeat</td>
<td>Work back over the system design and look for opportunities to improve or explain trade-offs</td>
</tr>
</tbody>
</table>
<p>The acronym itself is not something that I&#39;ve taken from a course. It&#39;s just my lightweight go-to approach for something short like system design interviews.</p>
<p>I&#39;ve personally committed the analogy of &quot;crossing the chasm in high definition&quot; to memory in order to remember the acronym, and so it&#39;s something that is always in my back pocket for working through problems.</p>
<p>System design interviews are generally open-ended and are an open discussion with the interviews. Working through clarifying the problem with clear communication (to me) feels like half the battle.</p>
<p>Some resources:</p>
<ul>
<li><a href="https://bytebytego.com/courses/system-design-interview/scale-from-zero-to-millions-of-users" target="_blank" rel="noopener noreferrer">ByteByteGo</a> has a very detailed system design course and book.</li>
<li><a href="https://www.educative.io/courses/grokking-the-system-design-interview" target="_blank" rel="noopener noreferrer">Educative</a> also has a similar course.</li>
<li>As always, there is a <a href="https://github.com/ashishps1/awesome-system-design-resources" target="_blank" rel="noopener noreferrer">awesome repo</a> for System Design Resources.</li>
<li><a href="https://designgurus.substack.com/" target="_blank" rel="noopener noreferrer">System design nuggets</a> is a nice subscription to link to small subsets of info.</li>
</ul>
<p>I don&#39;t have a table to indicate any return on investment for resources here, but some key topics it is worth getting a high-level overview of at a minimum (based on my experience):</p>
<ul>
<li>Caching</li>
<li>Consistency</li>
<li>Content Delivery networks (CDNs)</li>
<li>Reliability</li>
<li>Fault tolerance</li>
<li>Idempotency</li>
<li>Rate limiting</li>
<li>Message queues</li>
<li>I/O bound vs compute bound</li>
</ul>
<p>Most of my system design interviews have focused on implementation of high-level systems. There requirements have never been as extreme as what is covered in the system design books and courses I have taken.</p>
<p>My general guideline would be this:</p>
<ol>
<li>Clarify requirements and scale estimation. Questions like &quot;how many users?&quot;, &quot;how many requests do we expect?&quot;, &quot;are loads consistent or spiky?&quot;, &quot;what are the payloads and how large are we expecting?&quot; and &quot;are there times with lower and higher traffic?&quot; are some of my gotos.</li>
<li>Jot down the key data models that the system will be working with based on the information. Clarify their properties and be prepared to re-model during review time.</li>
<li>Start as simple as possible for your first high-level overview. My first pass is almost always a standard three-layer system. On your second pass, introduce systems <em>alongside reasoning</em> as you go through the &quot;review&quot; phase and then repeat the process to add those elements in.</li>
<li>For databases, start relational until you can introduce reasoning for not doing so.</li>
<li>Look for opportunities to scale and/or reduce the load for key servers. Introduce things like rate limits, queues as needed.</li>
<li>Also spend time reviewing the solution and offering suggestions on improvements or when changes would be needed in future. Systems are fluid and different solutions optimize for different scenarios.</li>
</ol>
<p>My experience is that there is never really a &quot;right answer&quot;. It more of a conversation with the interviewers around what solutions would you suggest to look into based on X requirements.</p>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p><strong>Edit</strong>: A colleague rightfully called out that AI is also likely to get
mixed in with the system design interview process as well. We&#39;ll see if my
post ages like milk, but once others start giving some feedback on their
experiences then I will update with anything they call out. In relation to
system design interview, I would probably start by considering some pillars
such as cost optimization and reliability in AI scenarios as well as
understanding the trade-off being popular solutions such as LLMs as opposed to
your own in-house models for scenarios such as classification, predictive
modeling and sentiment abstraction.</p> </div> </aside>
<h3 id="behavioral-interviews">Behavioral interviews</h3>
<p>This one for me has always felt more vague than the others. When most companies ask &quot;why do you want to work for X company?&quot;, my honest answer has been &quot;to keep a roof over my head&quot; but that doesn&#39;t fly so well.</p>
<p>I find <a href="https://www.techinterviewhandbook.org/behavioral-interview-questions/" target="_blank" rel="noopener noreferrer">the list of questions on the tech interview handbook website</a> covers basically every question I&#39;ve ever received.</p>
<p>Some other tidbits from my experiences here:</p>
<ul>
<li>Interviewers seem more interested in my business pursuits than my side projects.</li>
<li>Prepares your stories/approaches on conflict resolution.</li>
<li>Be ready to explain why are you looking for your next role and why this role specifically (again, steer away from the bottom of <a href="https://www.simplypsychology.org/maslow.html" target="_blank" rel="noopener noreferrer">Maslow&#39;s Hierarchy of Needs</a>).</li>
<li>Prepare some notes on some of your career work that you are most proud of. If possible, pick a few moments that can speak to from complementary domains (technical, communication, management etc.).</li>
</ul>
<h2 id="what-about-ai-interviews">What about AI interviews?</h2>
<p>This is where my experience screeches to a halt. From conversations with peers, some key points that they have mentioned:</p>
<ol>
<li>Some live coding exams do not allow AI.</li>
<li>There are general conversations around how you use AI in your workflow.</li>
</ol>
<p>Another peer shared through a <a href="https://www.linkedin.com/posts/sandra-arato_interview-questions-answers-for-ai-fluency-ugcPost-7438442683485818880-GYV-" target="_blank" rel="noopener noreferrer">LinkedIn post on 20 AI fluency interview questions</a>. Some of the slides to me <em>feel</em> a little off but I think understanding some of ubiquitous language used around AI is important for expressing your own experiences with it.</p>
<p>Concepts around agents, memory, harnesses, skills, tools are all key concepts to know, and safely executing agents is another hot topic right now.</p>
<p>If you haven&#39;t spent time working with AI, dip your toes in and form your own opinion.</p>
<p><em>Assuming</em> I am to talk about my use of AI tooling in upcoming interviews, I would probably be honest and just refer to the challenges and benefits I have personally seen.</p>
<p>Some key experiences off the top of my head:</p>
<ol>
<li>It has been a force multiplier for tedious tasks. Examples like linking JIRA tickets and merge requests for security patches are now delegated and save me an hour or so each time.</li>
<li>Code outputs have been a mixed bag. I find that a &quot;clean&quot; codebase with strict standards and guidelines have been easier to work it than others and tooling I hadn&#39;t used before like open telemetry on localhost have become indispensable tools for agent verification.</li>
<li>Multi-tasking across multiple agents introduces significant brain strain for me. This leads to sleepiness and a tendency to slide in standards. I personally opt to spend a few hours a day working without AI when I need a flow state and I also look for tasks that it can significantly outpace me without burdening me with unexpected work.</li>
<li>It&#39;s very easy to &quot;do everything&quot; now that clicking &quot;continue&quot; is an option. My experience with AI is that you need to put on your product hat and understand when to say no.</li>
<li>AI is incredible when it comes to collating debugging information that you collect and actioning it.</li>
</ol>
<p>I will update this section in future as I begin the interview process and give some updates on when AI comes into the process.</p>
<h2 id="advice-on-your-applications">Advice on your applications</h2>
<p>For this section, I would take anything I say with a grain of salt. I am constantly hounded in my reviews about how my resume looks so you are better off deferring to others on this boat.</p>
<p>Online guides that I will be referencing:</p>
<ul>
<li><a href="https://bytebytego.com/courses/tech-resume/p0-c2-introduction" target="_blank" rel="noopener noreferrer">ByteByteGo has another guide on writing a good resume</a></li>
<li><a href="https://careerservices.fas.harvard.edu/resources/create-a-strong-resume/" target="_blank" rel="noopener noreferrer">Harvard have a list of tips and action verbs</a></li>
<li>My alma mater <a href="https://www.sydney.edu.au/careers/students/applying-for-jobs/how-to-write-a-resume.html" target="_blank" rel="noopener noreferrer">provides some resume tips if you prefer an Australian lens</a> as well as <a href="https://www.sydney.edu.au/careers/students/applying-for-jobs/how-to-write-a-cover-letter.html" target="_blank" rel="noopener noreferrer">tips for your cover letters</a></li>
<li><a href="https://www.reddit.com/r/jobsearchhacks/comments/1kq801b/after_10_years_of_helping_people_write_cover/" target="_blank" rel="noopener noreferrer">Reddit can also provide a nice ear-to-the-ground with tips</a></li>
</ul>
<h2 id="conclusion">Conclusion</h2>
<p>This has been <em>my own</em> guidance to software engineering interviews in 2026 built upon previous experience and my own expectations of what to expect in the upcoming process.</p>
<p>My hope is that there are some nuggets of wisdom embedded within this that can assist in your own search for work.</p>
<p>I will continue to update this page as the process goes on or as I get more feedback from peers. If you have any recommendations that you aren&#39;t pushing for your own agenda, send them through and I will update the page.</p>]]></content>
<category term="interviews"/>
</entry>
<entry>
<title type="html"><![CDATA[Using BuildKite with AWS CodeBuild]]></title>
<id>https://blog.dennisokeeffe.com/blog/2026-04-09-using-buildkite-with-aws-codebuild</id>
<link href="https://dennisokeeffe.com/blog/2026-04-09-using-buildkite-with-aws-codebuild"/>
<published>2026-04-03T00:00:00.000Z</published>
<updated>2026-04-03T00:00:00.000Z</updated>
<summary type="html"><![CDATA[A working proof-of-concept for using BuildKite with AWS CodeBuild with the AWS CDK]]></summary>
<content type="html"><![CDATA[<p>I&#39;ve been looking for alternatives to GitHub Actions for running my CI/CD pipelines. I have previously had a good experience with Buildkite (albeit circa 2018), but pre-revenue I am looking at avenues for reducing costs as much as possible without requiring any significant refactors later on.</p>
<p>Although BuildKite <a href="https://buildkite.com/platform/pipelines/templates/aws/" target="_blank" rel="noopener noreferrer">offers some recommended templates for AWS</a> that can scale CI alongside your team, I have been keen to first check out a more controlled approach with <a href="https://docs.aws.amazon.com/codebuild/latest/userguide/welcome.html" target="_blank" rel="noopener noreferrer">AWS CodeBuild</a> which I believe can enable some cost-savings early in the project.</p>
<p>This blog post is a recap on my own internal investigation into configuring this setup.</p>
<h2 id="overview">Overview</h2>
<p>The blog post covers my proof-of-concept. In it, we still host code on a private GitHub repository but creating a pull request should trigger CI to run with BuildKite + CodeBuild.</p>
<p>We will be using the AWS CDK to configure as much as possible for this, but there will still be some requirements where you will need to jump onto the AWS dashboard.</p>
<p>The desired outcome here is to run a simple build which makes use of AWS CodeBuild. Do not expect anything more advanced in this tutorial. I will cover more advanced use cases in future blog posts.</p>
<p>The following sequence diagram is an overview of our desired workflow for this post:</p>
<figure class="diagram" data-pele="true" data-pagefind-ignore="all"><div class="diagram-canvas"><svg xmlns="http://www.w3.org/2000/svg" class="pele pele-sequence" viewBox="0 0 680 658" width="680" height="658" fill="var(--_fg)" role="graphics-document document" aria-roledescription="sequence"><g transform="translate(36,8)"><g class="pele-lifelines" fill="none" stroke="var(--_b)"><path class="pele-lifeline" data-id="Local" d="M0,62V580"></path><path class="pele-lifeline" data-id="GitHub" d="M89.2,62V580"></path><path class="pele-lifeline" data-id="Buildkite" d="M212,62V580"></path><path class="pele-lifeline" data-id="CodeBuild" d="M463.6,62V580"></path></g><g class="pele-edges" fill="none" stroke="var(--_l)" stroke-linecap="round"><g class="pele-edge pele-message" data-id="i0"><path d="M0,105H82.2"></path><path class="pele-marker" d="M89.2,105L81.2,108.36L81.2,101.64Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge pele-message" data-id="i1"><path d="M89.2,142H205"></path><path class="pele-marker" d="M212,142L204,145.36L204,138.64Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge pele-message" data-id="i2"><path d="M212,163H234A6,6 0 0 1 240,169V179A6,6 0 0 1 234,185H219"></path><path class="pele-marker" d="M212,185L220,181.64L220,188.36Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge pele-message" data-id="i3"><path d="M212,206H234A6,6 0 0 1 240,212V222A6,6 0 0 1 234,228H219"></path><path class="pele-marker" d="M212,228L220,224.64L220,231.36Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge pele-message" data-id="i4"><path d="M212,280H456.6"></path><path class="pele-marker" d="M463.6,280L455.6,283.36L455.6,276.64Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge pele-message" data-id="i5"><path d="M463.6,310H485.6A6,6 0 0 1 491.6,316V326A6,6 0 0 1 485.6,332H470.6"></path><path class="pele-marker" d="M463.6,332L471.6,328.64L471.6,335.36Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge pele-message" data-id="i6"><path d="M463.6,369H485.6A6,6 0 0 1 491.6,375V385A6,6 0 0 1 485.6,391H470.6"></path><path class="pele-marker" d="M463.6,391L471.6,387.64L471.6,394.36Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge pele-message" data-id="i7"><path d="M463.6,450H219"></path><path class="pele-marker" d="M212,450L220,446.64L220,453.36Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge pele-message" data-id="i8"><path d="M212,487H456.6" stroke-dasharray="4 4"></path><path class="pele-marker" d="M463.6,487L455.6,490.36L455.6,483.64Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge pele-message" data-id="i9"><path d="M463.6,501H485.6A6,6 0 0 1 491.6,507V517A6,6 0 0 1 485.6,523H470.6"></path><path class="pele-marker" d="M463.6,523L471.6,519.64L471.6,526.36Z" fill="var(--_l)" stroke="none"></path></g><g class="pele-edge pele-message" data-id="i10"><path d="M463.6,560H219" stroke-dasharray="4 4"></path><path class="pele-marker" d="M212,560L220,556.64L220,563.36Z" fill="var(--_l)" stroke="none"></path></g></g><g class="pele-edge-labels" font-size="14"><g class="pele-edge-label" data-id="i0"><text x="44.6" y="95.4" text-anchor="middle">git push</text></g><g class="pele-edge-label" data-id="i1"><text x="150.6" y="132.4" text-anchor="middle">Push webhook</text></g><g class="pele-edge-label" data-id="i2"><text text-anchor="middle" xml:space="preserve"><tspan x="306.8" y="169.9">Trigger</tspan><tspan x="306.8" y="187.9">pipeline build</tspan></text></g><g class="pele-edge-label" data-id="i3"><text x="298.4" y="221.9" text-anchor="middle">Schedule job</text></g><g class="pele-edge-label" data-id="i4"><text text-anchor="middle" xml:space="preserve"><tspan x="337.8" y="253.9">Webhook notification for</tspan><tspan x="337.8" y="271.9">scheduled job</tspan></text></g><g class="pele-edge-label" data-id="i5"><text text-anchor="middle" xml:space="preserve"><tspan x="545.8" y="307.9">Provision</tspan><tspan x="545.8" y="325.9">runner</tspan><tspan x="545.8" y="343.9">environment</tspan></text></g><g class="pele-edge-label" data-id="i6"><text text-anchor="middle" xml:space="preserve"><tspan x="562.6" y="375.9">Start Buildkite</tspan><tspan x="562.6" y="393.9">agent</tspan></text></g><g class="pele-edge-label" data-id="i7"><text text-anchor="middle" xml:space="preserve"><tspan x="337.8" y="423.9">Agent registers / requests</tspan><tspan x="337.8" y="441.9">work</tspan></text></g><g class="pele-edge-label" data-id="i8"><text x="337.8" y="477.4" text-anchor="middle">Dispatch scheduled job</text></g><g class="pele-edge-label" data-id="i9"><text x="554.2" y="516.9" text-anchor="middle">Run job steps</text></g><g class="pele-edge-label" data-id="i10"><text x="337.8" y="550.4" text-anchor="middle">Send logs and job result</text></g></g><g class="pele-nodes"><g class="pele-node pele-actor pele-actor-actor pele-actor-footer" data-id="Local"><circle cx="0" cy="587" r="6" fill="var(--_s)" stroke="var(--_b)"></circle><path d="M0,593V604M-10,597H10M-8,614L0,604L8,614" fill="none" stroke="var(--_b)"></path><text class="pele-label" x="0" y="635.6" text-anchor="middle">Local</text></g><g class="pele-node pele-actor pele-actor-participant pele-actor-footer" data-id="GitHub"><rect x="52.4" y="580" width="73.6" height="44" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" x="89.2" y="607.6" text-anchor="middle">GitHub</text></g><g class="pele-node pele-actor pele-actor-participant pele-actor-footer" data-id="Buildkite"><rect x="160.8" y="580" width="102.4" height="44" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" x="212" y="607.6" text-anchor="middle">Buildkite</text></g><g class="pele-node pele-actor pele-actor-participant pele-actor-footer" data-id="CodeBuild"><rect x="393.2" y="580" width="140.8" height="44" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" x="463.6" y="607.6" text-anchor="middle">AWS CodeBuild</text></g><g class="pele-node pele-actor pele-actor-actor" data-id="Local"><circle cx="0" cy="7" r="6" fill="var(--_s)" stroke="var(--_b)"></circle><path d="M0,13V24M-10,17H10M-8,34L0,24L8,34" fill="none" stroke="var(--_b)"></path><text class="pele-label" x="0" y="55.6" text-anchor="middle">Local</text></g><g class="pele-node pele-actor pele-actor-participant" data-id="GitHub"><rect x="52.4" y="18" width="73.6" height="44" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" x="89.2" y="45.6" text-anchor="middle">GitHub</text></g><g class="pele-node pele-actor pele-actor-participant" data-id="Buildkite"><rect x="160.8" y="18" width="102.4" height="44" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" x="212" y="45.6" text-anchor="middle">Buildkite</text></g><g class="pele-node pele-actor pele-actor-participant" data-id="CodeBuild"><rect x="393.2" y="18" width="140.8" height="44" fill="var(--_s)" stroke="var(--_b)"></rect><text class="pele-label" x="463.6" y="45.6" text-anchor="middle">AWS CodeBuild</text></g></g></g></svg></div><template>sequenceDiagram
actor Local
participant GitHub
participant Buildkite
participant CodeBuild as AWS CodeBuild

Local-&gt;&gt;GitHub: git push
GitHub-&gt;&gt;Buildkite: Push webhook
Buildkite-&gt;&gt;Buildkite: Trigger pipeline build
Buildkite-&gt;&gt;Buildkite: Schedule job

Buildkite-&gt;&gt;CodeBuild: Webhook notification for scheduled job
CodeBuild-&gt;&gt;CodeBuild: Provision runner environment
CodeBuild-&gt;&gt;CodeBuild: Start Buildkite agent
CodeBuild-&gt;&gt;Buildkite: Agent registers / requests work
Buildkite--&gt;&gt;CodeBuild: Dispatch scheduled job
CodeBuild-&gt;&gt;CodeBuild: Run job steps
CodeBuild--&gt;&gt;Buildkite: Send logs and job result</template></figure>
<h2 id="prerequisites">Prerequisites</h2>
<p>You need the following already configured if you are following along with this tutorial:</p>
<ul>
<li>A Buildkite account.</li>
<li>A GitHub account.</li>
<li>A prepared, private GitHub repository.</li>
<li>An AWS account.</li>
</ul>
<p>The following is also recommended:</p>
<ul>
<li>Experience with the AWS CDK for declaring infrastructure-as-code.</li>
<li>Experience with managing GitHub repositories.</li>
</ul>
<span id="configuring-a-deploy-key-for-your-private-github-repository" class="anchor-alias"><h2 id="configuring-a-deploy-key-for-your-private-git-hub-repository">Configuring a deploy key for your private GitHub repository</h2>
<p>Assuming that you have already prepared a private Github repository for this tutorial, you will need to configure a <strong>deploy key</strong> so that Buildkite can access this private repository.</p>
<p>From the command line, you can run the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code># Generate the key at ~/.ssh/buildkite-codebuild-poc
$ ssh-keygen -t ed25519 -C &quot;buildkite-codebuild-poc&quot; -f ~/.ssh/buildkite-codebuild-poc</code></pre>
<p>When generating a key, do not add a passphrase as this key will be used by the Buildkite agent.</p>
<p>If created successfully, it should output both the public key <code>~/.ssh/buildkite-codebuild-poc.pub</code> and the private key <code>~/.ssh/buildkite-codebuild-poc.pub</code>.</p>
<p>You first need to add the public key to your private GitHub repository as a <strong>deploy key</strong>. You can find this under your private repository settings.</p>
<p>The private key itself will need to be added as a <strong>secret</strong> for your Buildkite agent.</p>
<h2 id="setting-the-private-deploy-key-as-a-buildkite-agent-secret">Setting the private deploy key as a Buildkite agent secret</h2>
<p>Within Buildkite, there are domain-specific terms for <a href="https://buildkite.com/docs/pipelines/security/clusters" target="_blank" rel="noopener noreferrer">clusters</a>, <a href="https://buildkite.com/docs/pipelines" target="_blank" rel="noopener noreferrer">pipelines</a>, <a href="https://buildkite.com/docs/agent" target="_blank" rel="noopener noreferrer">agents</a> and <a href="https://buildkite.com/docs/agent/queues" target="_blank" rel="noopener noreferrer">queues</a>.</p>
<p>While I won&#39;t be deep diving into these terms, please note that will be touching on all of these at some point.</p>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>If you are not familiar with these terms, it&#39;s worth having a read through the
<a href="https://buildkite.com/docs/pipelines/glossary" target="_blank" rel="noopener noreferrer">Buildkite glossary page</a> to
bring you up to speed.</p> </div> </aside>
<p>To add your private key as a <strong>secret</strong>, head to the Buildkite dashboard and then go to your selected <strong>agent cluster</strong>. If you do not have one configured, select your <strong>default cluster</strong>.</p>
<p>From within your cluster, there will be a setting option for <strong>Secrets</strong>. Add your secret under our very apt key name <code>SUPER_SECRET_SSH_KEY</code>. This will map across to our Buildkite pipeline configuration we add later.</p>
<h2 id="getting-an-agent-token-for-your-buildkite-cluster">Getting an agent token for your Buildkite cluster</h2>
<p>Next, you will need to configure an <a href="https://buildkite.com/docs/agent/self-hosted/tokens" target="_blank" rel="noopener noreferrer">agent token</a>. This token while be used by AWS components in order for communication between CodeBuild and Buildkite.</p>
<p>Within your same selected <strong>agent cluster</strong>, there will be a setting option for <strong>Agent Tokens</strong>. Create an agent token from within this UI.</p>
<p>For agent tokens, you can also lock down which inbound IP addresses are allowed. For this post, we will just allow all IP addresses for now.</p>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>If you are worried about security, go ahead and configure the <a href="https://docs.aws.amazon.com/vpc/latest/userguide/aws-ip-ranges.html" target="_blank" rel="noopener noreferrer">AWS IP
ranges</a>.</p> </div> </aside>
<h2 id="creating-a-buildkite-pipeline">Creating a Buildkite pipeline</h2>
<p>Follow the Buildkite <strong>Pipelines</strong> UI and create a new pipeline. As part of this process, you will need to select your private GitHub repository to use.</p>
<p>Be sure that you select the pipeline to run from the agent that you have already configured with the secrets.</p>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>It has been covered in this blog post, but we expect that you&#39;ve connected to
your GitHub account through OAuth via the Buildkite settings. If you have not,
go to your settings and ensure that you authorize Buildkite to connect to your
GitHub account.</p> </div> </aside>
<p>When creating, be sure to leave &quot;auto-create webhooks&quot; on so that Buildkite will create the webhooks for you within the Github repository.</p>
<p>As for the section about the &quot;script&quot; to run, add the following YAML:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="yml"><code>steps:
  - label: &quot;:aws: hello from codebuild&quot;
    command: ./scripts/hello-world.sh
    plugins:
      - git-ssh-checkout#v0.4.1:
          ssh-secret-key-name: SUPER_SECRET_SSH_KEY
          repository-url: git@github.com:okeeffed/blog-codebuild-buildkite.git
    agents:
      queue: default-queue
      project: codebuild-buildkite-codebuild-poc-runner</code></pre>
<p>Albeit we haven&#39;t wrote any code yet, the pipeline code about does the following:</p>
<ul>
<li>Runs a script from <code>./scripts/hello-world.sh</code>.</li>
<li>Makes use of a plugin <code>git-ssh-checkout</code> which makes use of our deploy key to authorize the pipeline to pull down the codebase.</li>
<li>Sets the agent to run on the <code>default-queue</code> where we expect the project name to be <code>codebuild-buildkite-codebuild-poc-runner</code>.</li>
</ul>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>The <code>default-queue</code> comes from the aptly named default queue for the agent.
It&#39;s worth sense-checking that this is the same name for your queue.</p> </div> </aside>
<p>At this point, our Buildkite configuration is ready to go, but we need to configure AWS to run.</p>
<span id="writing-the-codebuild-foundation-cdk-stack" class="anchor-alias"><h2 id="writing-the-code-build-foundation-cdk-stack">Writing the CodeBuild Foundation CDK stack</h2>
<p>For our configuration with the AWS CDK, we will be deploying two stacks:</p>
<ol>
<li>A &quot;foundation&quot; stack which sets up our <strong>AWS SecretsManager secret</strong> as well as an <strong>AWS CodeConnection</strong> which authorizes CodeBuild with GitHub.</li>
<li>A &quot;runner&quot; stack which will create our <strong>AWS CodeBuild</strong> build service.</li>
</ol>
<p>First, we need to configure the foundation stack. I will be doing so in TypeScript.</p>
<p>If you have not yet configured it, please follow the <a href="https://docs.aws.amazon.com/cdk/v2/guide/hello-world.html" target="_blank" rel="noopener noreferrer">AWS CDK guide</a> to initializing a stack.</p>
<p>Within <code>lib/buildkite-foundation-stack.ts</code>, add the following code:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import * as cdk from &quot;aws-cdk-lib&quot;;
import * as codeconnections from &quot;aws-cdk-lib/aws-codeconnections&quot;;
import * as secretsmanager from &quot;aws-cdk-lib/aws-secretsmanager&quot;;
import * as ssm from &quot;aws-cdk-lib/aws-ssm&quot;;
import { Construct } from &quot;constructs&quot;;

const buildkiteAgentTokenSecretName =
  &quot;buildkite-codebuild-poc/buildkite-agent-token&quot;;
export const codeConnectionsArnParameterName =
  &quot;/buildkite-codebuild-poc/codeconnections-arn&quot;;

export class BuildkiteFoundationStack extends cdk.Stack {
  public readonly buildkiteAgentTokenSecretName = buildkiteAgentTokenSecretName;

  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const buildkiteAgentToken = new cdk.CfnParameter(
      this,
      &quot;BuildkiteAgentToken&quot;,
      {
        type: &quot;String&quot;,
        noEcho: true,
        description: &quot;Buildkite agent token from your Buildkite cluster.&quot;,
      }
    );

    const existingCodeConnectionsArn = new cdk.CfnParameter(
      this,
      &quot;ExistingCodeConnectionsArn&quot;,
      {
        type: &quot;String&quot;,
        default: &quot;&quot;,
        description:
          &quot;Optional existing authorized AWS CodeConnections ARN. Leave empty to let this stack create the connection resource.&quot;,
      }
    );

    const createCodeConnection = new cdk.CfnParameter(
      this,
      &quot;CreateCodeConnection&quot;,
      {
        type: &quot;String&quot;,
        default: &quot;true&quot;,
        allowedValues: [&quot;true&quot;, &quot;false&quot;],
        description:
          &quot;Set to true to create the CodeConnections resource in this stack. You must still authorize it in the AWS console.&quot;,
      }
    );

    const codeConnectionName = new cdk.CfnParameter(
      this,
      &quot;CodeConnectionName&quot;,
      {
        type: &quot;String&quot;,
        default: &quot;buildkite-github&quot;,
        description: &quot;Name to use when CreateCodeConnection=true.&quot;,
      }
    );

    const useManagedCodeConnection = new cdk.CfnCondition(
      this,
      &quot;UseManagedCodeConnection&quot;,
      {
        expression: cdk.Fn.conditionEquals(
          createCodeConnection.valueAsString,
          &quot;true&quot;
        ),
      }
    );

    const agentTokenSecret = new secretsmanager.CfnSecret(
      this,
      &quot;BuildkiteAgentTokenSecret&quot;,
      {
        name: buildkiteAgentTokenSecretName,
        description:
          &quot;Buildkite agent token for the CodeBuild-hosted Buildkite runner proof of concept.&quot;,
        secretString: buildkiteAgentToken.valueAsString,
      }
    );

    const managedCodeConnection = new codeconnections.CfnConnection(
      this,
      &quot;ManagedCodeConnection&quot;,
      {
        connectionName: codeConnectionName.valueAsString,
        providerType: &quot;GitHub&quot;,
      }
    );
    managedCodeConnection.cfnOptions.condition = useManagedCodeConnection;

    // A CodeConnections resource can be provisioned by CloudFormation, but the
    // GitHub authorization handshake still needs to be completed in the console.
    const effectiveCodeConnectionsArn = cdk.Fn.conditionIf(
      useManagedCodeConnection.logicalId,
      managedCodeConnection.attrConnectionArn,
      existingCodeConnectionsArn.valueAsString
    ).toString();
    const buildkiteAgentTokenSecretArn =
      secretsmanager.CfnSecret.arnForSecret(agentTokenSecret);

    new ssm.StringParameter(this, &quot;CodeConnectionsArnParameter&quot;, {
      parameterName: codeConnectionsArnParameterName,
      stringValue: effectiveCodeConnectionsArn,
      description:
        &quot;CodeConnections ARN used by the Buildkite CodeBuild runner stack.&quot;,
    });

    new cdk.CfnOutput(this, &quot;BuildkiteAgentTokenSecretName&quot;, {
      value: this.buildkiteAgentTokenSecretName,
      description:
        &quot;Secrets Manager secret that stores the Buildkite agent token.&quot;,
    });

    new cdk.CfnOutput(this, &quot;BuildkiteAgentTokenSecretArn&quot;, {
      value: buildkiteAgentTokenSecretArn,
      description: &quot;Secrets Manager ARN for the Buildkite agent token secret.&quot;,
    });

    new cdk.CfnOutput(this, &quot;CodeConnectionsArnOutput&quot;, {
      value: effectiveCodeConnectionsArn,
      description:
        &quot;CodeConnections ARN for the repository provider used by the runner project.&quot;,
    });

    new cdk.CfnOutput(this, &quot;CodeConnectionsArnParameterName&quot;, {
      value: codeConnectionsArnParameterName,
      description:
        &quot;SSM parameter name that stores the CodeConnections ARN for the runner stack.&quot;,
    });

    new cdk.CfnOutput(this, &quot;ManagedCodeConnectionArn&quot;, {
      value: managedCodeConnection.attrConnectionArn,
      description:
        &quot;Authorize this connection in the AWS console before deploying the runner stack.&quot;,
      condition: useManagedCodeConnection,
    });

    new cdk.CfnOutput(this, &quot;ManagedCodeConnectionStatus&quot;, {
      value: managedCodeConnection.attrConnectionStatus,
      description:
        &quot;The managed CodeConnections status. It remains pending until the GitHub authorization is completed.&quot;,
      condition: useManagedCodeConnection,
    });
  }
}</code></pre>
<p>As mentioned previously, this code will set up our Buildkite agent secret within AWS SecretsManager and also prepare our CodeConnection to GitHub.</p>
<p>The latter requires a manual step, so we will need to deploy this stack and then jump into the AWS dashboard to finish off the authorization.</p>
<p>Before doing that, we will finish off our &quot;runner&quot; stack as well.</p>
<span id="writing-the-codebuild-runner-cdk-stack" class="anchor-alias"><h2 id="writing-the-code-build-runner-cdk-stack">Writing the CodeBuild Runner CDK stack</h2>
<p>Within <code>lib/codebuild-runner-stack.ts</code>, add the following code:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import * as cdk from &quot;aws-cdk-lib&quot;;
import * as codebuild from &quot;aws-cdk-lib/aws-codebuild&quot;;
import * as codeconnections from &quot;aws-cdk-lib/aws-codeconnections&quot;;
import * as cr from &quot;aws-cdk-lib/custom-resources&quot;;
import * as iam from &quot;aws-cdk-lib/aws-iam&quot;;
import * as secretsmanager from &quot;aws-cdk-lib/aws-secretsmanager&quot;;
import { Construct } from &quot;constructs&quot;;

const projectName = &quot;buildkite-codebuild-poc-runner&quot;;
const buildkiteAgentTag = `codebuild-${projectName}`;
const buildkiteAgentTokenSecretName =
  &quot;buildkite-codebuild-poc/buildkite-agent-token&quot;;

export class BuildkiteCodebuildPocStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const buildkiteAgentToken = new cdk.CfnParameter(
      this,
      &quot;BuildkiteAgentToken&quot;,
      {
        type: &quot;String&quot;,
        noEcho: true,
        description: &quot;Buildkite agent token from your Buildkite cluster.&quot;,
      }
    );

    const codeConnectionsArn = new cdk.CfnParameter(
      this,
      &quot;CodeConnectionsArn&quot;,
      {
        type: &quot;String&quot;,
        default: &quot;&quot;,
        description:
          &quot;AWS CodeConnections ARN for the repository provider used by the Buildkite runner project.&quot;,
      }
    );

    const createCodeConnection = new cdk.CfnParameter(
      this,
      &quot;CreateCodeConnection&quot;,
      {
        type: &quot;String&quot;,
        default: &quot;false&quot;,
        allowedValues: [&quot;true&quot;, &quot;false&quot;],
        description:
          &quot;Set to true to create the CodeConnections resource in this stack. You must still authorize it in the AWS console.&quot;,
      }
    );

    const codeConnectionName = new cdk.CfnParameter(
      this,
      &quot;CodeConnectionName&quot;,
      {
        type: &quot;String&quot;,
        default: &quot;buildkite-github&quot;,
        description: &quot;Name to use when CreateCodeConnection=true.&quot;,
      }
    );

    const useManagedCodeConnection = new cdk.CfnCondition(
      this,
      &quot;UseManagedCodeConnection&quot;,
      {
        expression: cdk.Fn.conditionEquals(
          createCodeConnection.valueAsString,
          &quot;true&quot;
        ),
      }
    );

    const managedCodeConnection = new codeconnections.CfnConnection(
      this,
      &quot;ManagedCodeConnection&quot;,
      {
        connectionName: codeConnectionName.valueAsString,
        providerType: &quot;GitHub&quot;,
      }
    );
    managedCodeConnection.cfnOptions.condition = useManagedCodeConnection;

    // A CodeConnections resource can be provisioned by CloudFormation, but the
    // GitHub authorization handshake still needs to be completed in the console.
    const effectiveCodeConnectionsArn = cdk.Fn.conditionIf(
      useManagedCodeConnection.logicalId,
      managedCodeConnection.attrConnectionArn,
      codeConnectionsArn.valueAsString
    ).toString();

    // The console-created runner project grants permissions against both the
    // newer codeconnections ARN form and the legacy codestar-connections ARN form.
    const connectionId = cdk.Fn.select(
      1,
      cdk.Fn.split(&quot;connection/&quot;, effectiveCodeConnectionsArn)
    );
    const codestarConnectionsArn = cdk.Stack.of(this).formatArn({
      service: &quot;codestar-connections&quot;,
      resource: &quot;connection&quot;,
      resourceName: connectionId,
    });

    const agentTokenSecret = new secretsmanager.CfnSecret(
      this,
      &quot;BuildkiteAgentTokenSecret&quot;,
      {
        name: buildkiteAgentTokenSecretName,
        description:
          &quot;Buildkite agent token for the CodeBuild-hosted Buildkite runner proof of concept.&quot;,
        secretString: buildkiteAgentToken.valueAsString,
      }
    );

    const serviceRole = new iam.Role(this, &quot;CodeBuildServiceRole&quot;, {
      assumedBy: new iam.ServicePrincipal(&quot;codebuild.amazonaws.com&quot;),
      description:
        &quot;Service role for the Buildkite runner proof of concept CodeBuild project.&quot;,
    });

    // The runner project needs the usual CodeBuild log permissions, read access
    // to the Buildkite agent token secret, and CodeConnections access so CodeBuild
    // can validate and use the linked repository connection.
    const runnerAccessPolicy = new iam.Policy(
      this,
      &quot;CodeBuildRunnerAccessPolicy&quot;,
      {
        statements: [
          new iam.PolicyStatement({
            actions: [
              &quot;logs:CreateLogGroup&quot;,
              &quot;logs:CreateLogStream&quot;,
              &quot;logs:PutLogEvents&quot;,
            ],
            resources: [&quot;*&quot;],
          }),
          new iam.PolicyStatement({
            actions: [&quot;secretsmanager:GetSecretValue&quot;],
            resources: [agentTokenSecret.attrId],
          }),
          new iam.PolicyStatement({
            // CodeBuild validates the connection during project updates and uses
            // the connection at job runtime, so it needs more than just UseConnection.
            actions: [
              &quot;codeconnections:GetConnection&quot;,
              &quot;codeconnections:GetConnectionToken&quot;,
              &quot;codeconnections:UseConnection&quot;,
              &quot;codestar-connections:GetConnection&quot;,
              &quot;codestar-connections:GetConnectionToken&quot;,
              &quot;codestar-connections:UseConnection&quot;,
            ],
            resources: [effectiveCodeConnectionsArn, codestarConnectionsArn],
          }),
        ],
      }
    );
    serviceRole.attachInlinePolicy(runnerAccessPolicy);

    const runnerProject = new codebuild.Project(
      this,
      &quot;BuildkiteRunnerProject&quot;,
      {
        projectName,
        description:
          &quot;Proof-of-concept CodeBuild project that runs Buildkite jobs in an ephemeral CodeBuild runner.&quot;,
        role: serviceRole,
        // Runner builds ignore the project buildspec unless explicitly overridden,
        // but NO_SOURCE projects still require an inline buildspec in the API model.
        buildSpec: codebuild.BuildSpec.fromObject({
          version: &quot;0.2&quot;,
          phases: {
            build: {
              commands: [
                &#39;echo &quot;Buildkite runner project uses the managed runner build flow&quot;&#39;,
              ],
            },
          },
        }),
        environment: {
          computeType: codebuild.ComputeType.SMALL,
          buildImage: codebuild.LinuxBuildImage.STANDARD_6_0,
        },
        environmentVariables: {
          CODEBUILD_CONFIG_BUILDKITE_AGENT_TOKEN: {
            type: codebuild.BuildEnvironmentVariableType.SECRETS_MANAGER,
            value: buildkiteAgentTokenSecretName,
          },
        },
        queuedTimeout: cdk.Duration.minutes(30),
        timeout: cdk.Duration.minutes(30),
        visibility: codebuild.ProjectVisibility.PRIVATE,
      }
    );

    const runnerProjectResource = runnerProject.node
      .defaultChild as codebuild.CfnProject;
    // Buildkite runner projects created in the console still use NO_SOURCE, but
    // they also carry CodeConnections-backed source auth and clone depth settings.
    // Without these source properties, CreateWebhook(RUNNER_BUILDKITE_BUILD) fails.
    runnerProjectResource.addPropertyOverride(&quot;Source.Auth&quot;, {
      Type: &quot;CODECONNECTIONS&quot;,
      Resource: effectiveCodeConnectionsArn,
    });
    runnerProjectResource.addPropertyOverride(&quot;Source.GitCloneDepth&quot;, 1);
    runnerProjectResource.addPropertyOverride(&quot;Source.BuildSpec&quot;, &quot;&quot;);

    runnerProject.node.addDependency(agentTokenSecret);
    // Updating the project to use CodeConnections can fail if CloudFormation
    // applies the project update before the runner access policy exists.
    runnerProject.node.addDependency(runnerAccessPolicy);

    const runnerWebhook = new cr.AwsCustomResource(
      this,
      &quot;BuildkiteRunnerWebhook&quot;,
      {
        installLatestAwsSdk: false,
        onCreate: {
          service: &quot;CodeBuild&quot;,
          action: &quot;createWebhook&quot;,
          parameters: {
            projectName,
            buildType: &quot;RUNNER_BUILDKITE_BUILD&quot;,
            filterGroups: [[{ type: &quot;EVENT&quot;, pattern: &quot;WORKFLOW_JOB_QUEUED&quot; }]],
            // The console includes a default PR approval policy for GitHub-backed
            // Buildkite runner webhooks. Without this field, CreateWebhook returns
            // a generic InvalidInputException for an otherwise valid project.
            pullRequestBuildPolicy: {
              requiresCommentApproval: &quot;ALL_PULL_REQUESTS&quot;,
              approverRoles: [
                &quot;GITHUB_WRITE&quot;,
                &quot;GITHUB_MAINTAIN&quot;,
                &quot;GITHUB_ADMIN&quot;,
              ],
            },
          },
          physicalResourceId: cr.PhysicalResourceId.of(
            `${projectName}-buildkite-webhook`
          ),
        },
        onUpdate: {
          service: &quot;CodeBuild&quot;,
          action: &quot;updateWebhook&quot;,
          parameters: {
            projectName,
            buildType: &quot;RUNNER_BUILDKITE_BUILD&quot;,
            filterGroups: [[{ type: &quot;EVENT&quot;, pattern: &quot;WORKFLOW_JOB_QUEUED&quot; }]],
            pullRequestBuildPolicy: {
              requiresCommentApproval: &quot;ALL_PULL_REQUESTS&quot;,
              approverRoles: [
                &quot;GITHUB_WRITE&quot;,
                &quot;GITHUB_MAINTAIN&quot;,
                &quot;GITHUB_ADMIN&quot;,
              ],
            },
          },
          physicalResourceId: cr.PhysicalResourceId.of(
            `${projectName}-buildkite-webhook`
          ),
        },
        onDelete: {
          service: &quot;CodeBuild&quot;,
          action: &quot;deleteWebhook&quot;,
          parameters: {
            projectName,
          },
          ignoreErrorCodesMatching: &quot;ResourceNotFoundException&quot;,
        },
        policy: cr.AwsCustomResourcePolicy.fromSdkCalls({
          resources: cr.AwsCustomResourcePolicy.ANY_RESOURCE,
        }),
      }
    );

    runnerWebhook.node.addDependency(runnerProject);

    new cdk.CfnOutput(this, &quot;CodeBuildProjectName&quot;, {
      value: projectName,
      description:
        &quot;The CodeBuild project name referenced from Buildkite agent tags.&quot;,
    });

    new cdk.CfnOutput(this, &quot;BuildkiteAgentTagProjectValue&quot;, {
      value: buildkiteAgentTag,
      description: &quot;Use this value in Buildkite YAML as agents.project.&quot;,
    });

    new cdk.CfnOutput(this, &quot;BuildkiteWebhookPayloadUrl&quot;, {
      value: runnerWebhook.getResponseField(&quot;webhook.payloadUrl&quot;),
      description:
        &quot;Copy this into the Buildkite organization webhook URL field.&quot;,
    });

    new cdk.CfnOutput(this, &quot;BuildkiteWebhookSecret&quot;, {
      value: runnerWebhook.getResponseField(&quot;webhook.secret&quot;),
      description:
        &quot;Copy this into the Buildkite organization webhook token field.&quot;,
    });

    new cdk.CfnOutput(this, &quot;BuildkiteAgentTokenSecretName&quot;, {
      value: buildkiteAgentTokenSecretName,
      description:
        &quot;Secrets Manager secret that stores the Buildkite agent token.&quot;,
    });

    new cdk.CfnOutput(this, &quot;CodeConnectionsArnOutput&quot;, {
      value: effectiveCodeConnectionsArn,
      description:
        &quot;CodeConnections ARN attached to the Buildkite runner project source auth.&quot;,
    });

    new cdk.CfnOutput(this, &quot;ManagedCodeConnectionArn&quot;, {
      value: managedCodeConnection.attrConnectionArn,
      description:
        &quot;Authorize this connection in the AWS console before using it for the runner project.&quot;,
      condition: useManagedCodeConnection,
    });

    new cdk.CfnOutput(this, &quot;ManagedCodeConnectionStatus&quot;, {
      value: managedCodeConnection.attrConnectionStatus,
      description:
        &quot;The managed CodeConnections status. It will remain pending until the GitHub authorization is completed.&quot;,
      condition: useManagedCodeConnection,
    });
  }
}</code></pre>
<p>There is a quite a lot going on here, so I will keep what it does as concise as possible:</p>
<ul>
<li>Provisions a CodeBuild project that runs ephemeral Buildkite agents to execute pipeline jobs</li>
<li>Stores the Buildkite agent token securely in AWS Secrets Manager and injects it at runtime</li>
<li>Supports GitHub integration via AWS CodeConnections (existing or optionally created)</li>
<li>Creates an IAM role with permissions for logs, secrets access, and CodeConnections usage</li>
<li>Configures CodeBuild to act as a Buildkite runner using CodeConnections-backed source auth</li>
<li>Sets up a custom webhook (<code>RUNNER_BUILDKITE_BUILD</code>) to trigger builds from Buildkite events</li>
<li>Ensures correct resource dependencies and deployment order for reliable setup</li>
<li>Outputs key values (webhook URL/secret, project name, agent tag) for Buildkite configuration</li>
</ul>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>In the above code, it&#39;s worth noting that I just got it working. Some things
like the conditional CodeConnections stuff can definitely be cleaned up just
to always reference the code connection we create in the foundation stack.</p> </div> </aside>
<p>Finally, we will need to update our <code>bin/buildkite-codebuild-poc.ts</code> file to have the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>#!/usr/bin/env node
import * as cdk from &quot;aws-cdk-lib&quot;;
import { BuildkiteFoundationStack } from &quot;../lib/buildkite-foundation-stack&quot;;
import { BuildkiteRunnerStack } from &quot;../lib/buildkite-runner-stack&quot;;

const app = new cdk.App();

const env = {
  account: process.env.CDK_DEFAULT_ACCOUNT,
  region: process.env.CDK_DEFAULT_REGION,
};

new BuildkiteFoundationStack(app, &quot;BuildkiteFoundationStack&quot;, {
  env,
});

new BuildkiteRunnerStack(app, &quot;BuildkiteRunnerStack&quot;, {
  env,
});</code></pre>
<p>At this point, we are ready to deploy our AWS stacks and do our final configuration touches.</p>
<span id="deploying-our-codebuild-foundation-cdk-stack" class="anchor-alias"><h2 id="deploying-our-code-build-foundation-cdk-stack">Deploying our CodeBuild Foundation CDK stack</h2>
<p>To deploy our foundation stack, first be sure to obtain your Buildkite agent token that we created earlier.</p>
<p>Next, run the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>$ npx cdk deploy BuildkiteFoundationStack --parameters BuildkiteAgentToken=&lt;replace-with-your-agent-token&gt;</code></pre>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>There is an assumption here that you have already configured your AWS
credentials locally in order to run these commands.</p> </div> </aside>
<p>If successful, this will give you outputs like the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="plaintext"><code>Outputs:
BuildkiteFoundationStack.BuildkiteAgentTokenSecretArn = REDACTED
BuildkiteFoundationStack.BuildkiteAgentTokenSecretName = buildkite-codebuild-poc/buildkite-agent-token
BuildkiteFoundationStack.CodeConnectionsArnOutput = REDACTED
BuildkiteFoundationStack.CodeConnectionsArnParameterName = REDACTED
BuildkiteFoundationStack.ManagedCodeConnectionArn = REDACTED
BuildkiteFoundationStack.ManagedCodeConnectionStatus = PENDING
Stack ARN:
arn:aws:cloudformation:REDACTED:REDACTED:stack/BuildkiteFoundationStack/REDACTED</code></pre>
<p>You will notice the <code>ManagedCodeConnectionStatus</code> is <code>PENDING</code>. In this state, you need to head to your AWS Dashboard and find the correlating code connection to authorize access.</p>
<p>I find this easiest when heading to the CloudFormation dashboard, finding the foundation stack we deployed and clicking through the resource link directly to the code connection page.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2026-04-09-using-buildkite-with-aws-codebuild/cloudformation-codeconnection.webp" alt="Cloudformation dashboard" loading="lazy" decoding="async"/><figcaption>Cloudformation dashboard</figcaption></figure>
<p>From there, you can find and click through to the pending code connection.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2026-04-09-using-buildkite-with-aws-codebuild/update-pending-connection.webp" alt="Pending code connection" loading="lazy" decoding="async"/><figcaption>Pending code connection</figcaption></figure>
<p>Select to complete the process and you will get a popup:</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2026-04-09-using-buildkite-with-aws-codebuild/codeconnection-popup.webp" alt="Pending code connection popup" loading="lazy" decoding="async"/><figcaption>Pending code connection popup</figcaption></figure>
<p>If successful, the connection should move to an available status:</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2026-04-09-using-buildkite-with-aws-codebuild/codeconnection-available.webp" alt="Available code connection" loading="lazy" decoding="async"/><figcaption>Available code connection</figcaption></figure>
<span id="deploying-our-codebuild-runner-cdk-stack" class="anchor-alias"><h2 id="deploying-our-code-build-runner-cdk-stack">Deploying our CodeBuild Runner CDK Stack</h2>
<p>With this configured, we can next deploy our CodeBuild Runner stack with <code>npx cdk deploy BuildkiteRunnerStack</code>.</p>
<p>Once successful, you will get outputs like the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="plaintext"><code>Outputs:
BuildkiteRunnerStack.BuildkiteAgentTagProjectValue = codebuild-buildkite-codebuild-poc-runner
BuildkiteRunnerStack.BuildkiteWebhookPayloadUrl = REDACTED
BuildkiteRunnerStack.BuildkiteWebhookSecret = REDACTED
BuildkiteRunnerStack.CodeBuildProjectName = buildkite-codebuild-poc-runner
BuildkiteRunnerStack.CodeConnectionsArnOutput = REDACTED
Stack ARN:
arn:aws:cloudformation:REDACTED:REDACTED:stack/BuildkiteRunnerStack/REDACTED</code></pre>
<p>You will notice that the <code>BuildkiteAgentTagProjectValue</code> of <code>codebuild-buildkite-codebuild-poc-runner</code> matches what we configured earlier in our Buildkite pipeline YAML.</p>
<span id="add-the-codebuild-webhook-into-buildkite" class="anchor-alias"><h2 id="add-the-code-build-webhook-into-buildkite">Add the CodeBuild webhook into Buildkite</h2>
<p>Now that we have deployed the resources, you will need to copy the outputs <code>BuildkiteWebhookSecret</code> and <code>BuildkiteWebhookPayloadUrl</code> from the runner stack and add them into your Buildkite <strong>Notification Services</strong> configuration.</p>
<p>This is found from within your Buildkite organization settings.</p>
<p>Once there, select to add a webhook and set the payload URL as the webhook URL and set the token as the webhook secret. Make sure that it selected for sending the token as <code>X-Buildkite-Token</code>.</p>
<p>For the webhook, you only need to configure notifications for <code>job.scheduled</code>.</p>
<p>Finally, make sure that you set it to only send for our created pipeline.</p>
<h2 id="pushing-our-code">Pushing our code</h2>
<p>Everything should now be configured to run for our Buildkite + CodeBuild set up.</p>
<p>Push your code up to the repository and then check both your Buildkite dashboard as well as your CodeBuild dashboard to confirm everything is now working.</p>
<p>On Buildkite:</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2026-04-09-using-buildkite-with-aws-codebuild/buildkite-run.webp" alt="Buildkite job" loading="lazy" decoding="async"/><figcaption>Buildkite job</figcaption></figure>
<p>On CodeBuild:</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2026-04-09-using-buildkite-with-aws-codebuild/codebuild-run.webp" alt="CodeBuild logs" loading="lazy" decoding="async"/><figcaption>CodeBuild logs</figcaption></figure>
<p>Your GitHub status checks should now also be passing:</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2026-04-09-using-buildkite-with-aws-codebuild/github-status.webp" alt="GitHub status check" loading="lazy" decoding="async"/><figcaption>GitHub status check</figcaption></figure>
<p>Success!</p>
<h2 id="conclusion">Conclusion</h2>
<p>In this post, I recapped my proof-of-concept for getting Buildkite and AWS CodeBuild working together for a private GitHub repository.</p>
<p>We also used the AWS CDK to configure the AWS portion of the infrastructure.</p>
<p>For further ideas on this project:</p>
<ul>
<li>Clean up the CDK code further.</li>
<li>Attempt a more complicated pipeline.</li>
<li>Make use of different Docker containers for running the code.</li>
<li>Use another IaC provider to configure the BuildKite portion of the code.</li>
<li>Make use of the Buildkite SDK/CLI for management of the pipeline.</li>
</ul>
<p>In future posts, I may recap some of these ideas in my own implementation.</p>
<h2 id="links-and-further-reading">Links and Further Reading</h2>
<ul>
<li><a href="https://buildkite.com/" target="_blank" rel="noopener noreferrer">Buildkite</a></li>
<li><a href="https://docs.aws.amazon.com/codebuild/latest/userguide/welcome.html" target="_blank" rel="noopener noreferrer">AWS CodeBuild</a></li>
<li><a href="https://docs.aws.amazon.com/codebuild/latest/userguide/sample-runner-buildkite.html" target="_blank" rel="noopener noreferrer">Tutorial: Configure a CodeBuild-hosted Buildkite runner</a></li>
<li><a href="https://buildkite.com/resources/plugins/buildkite-plugins/git-ssh-checkout-buildkite-plugin/" target="_blank" rel="noopener noreferrer">Buildkite Git SSH Checkout plugin</a></li>
</ul>
<p><em>Photo credit: <a href="https://unsplash.com/@johnnyb803" target="_blank" rel="noopener noreferrer"><code>johnnyb803</code></a></em></p>]]></content>
<category term="aws"/><category term="buildkite"/><category term="ci/cd"/>
</entry>
<entry>
<title type="html"><![CDATA[Using AWS Bedrock with opencode]]></title>
<id>https://blog.dennisokeeffe.com/blog/2025-11-19-using-aws-bedrock-with-opencode</id>
<link href="https://dennisokeeffe.com/blog/2025-11-19-using-aws-bedrock-with-opencode"/>
<published>2025-11-19T00:00:00.000Z</published>
<updated>2025-11-19T00:00:00.000Z</updated>
<summary type="html"><![CDATA[Setup your opencode CLI to work with AWS Bedrock foundational models]]></summary>
<content type="html"><![CDATA[<p><a href="https://github.com/sst/opencode" target="_blank" rel="noopener noreferrer">opencode</a> is a AI coding agent built for the terminal.</p>
<p>It&#39;s extensible nature means that we can configure our own models. This is where <a href="https://aws.amazon.com/bedrock/" target="_blank" rel="noopener noreferrer">AWS Bedrock</a>, an AWS offering for foundational models and agent tools, comes in.</p>
<h2 id="using-aws-bedrock-with-opencode">Using AWS Bedrock with opencode</h2>
<p>opencode (v1.0.68) will automatically enable AWS Bedrock model selection within the CLI if your terminal environment exports the expected enviornment variables (<code>AWS_ACCESS_KEY_ID</code>, <code>AWS_SECRET_ACCESS_KEY</code> and <code>AWS_REGION</code>). Personally, I make use of <a href="https://github.com/99designs/aws-vault" target="_blank" rel="noopener noreferrer">aws-vault</a> for managing this.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2025-11-19-using-aws-bedrock-with-opencode/1-opencode.webp" alt="opencode Bedrock models" loading="lazy" decoding="async"/><figcaption>opencode Bedrock models</figcaption></figure>
<h2 id="aws-bedrock-foundational-models">AWS Bedrock Foundational models</h2>
<p>For some foundational models (such as Claude models), you need to head to Bedrock on the AWS console and submit the use cases for Anthropic.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2025-11-19-using-aws-bedrock-with-opencode/2-use-case-form.webp" alt="Use case form for Anthropic" loading="lazy" decoding="async"/><figcaption>Use case form for Anthropic</figcaption></figure>
<p>This is a requirement for usage of those models in opencode.</p>
<h2 id="links-and-further-reading">Links and Further Reading</h2>
<ul>
<li><a href="https://github.com/sst/opencode" target="_blank" rel="noopener noreferrer">opencode</a></li>
<li><a href="https://aws.amazon.com/bedrock/" target="_blank" rel="noopener noreferrer">AWS Bedrock</a></li>
<li><a href="https://github.com/99designs/aws-vault" target="_blank" rel="noopener noreferrer">aws-vault</a></li>
</ul>
<p><em>Photo credit: <a href="https://unsplash.com/@johnnyb803" target="_blank" rel="noopener noreferrer"><code>johnnyb803</code></a></em></p>]]></content>
<category term="aws"/><category term="ai"/>
</entry>
<entry>
<title type="html"><![CDATA[Interface-based functional optionals in Golang]]></title>
<id>https://blog.dennisokeeffe.com/blog/2025-11-16-interface-based-functional-options-in-golang</id>
<link href="https://dennisokeeffe.com/blog/2025-11-16-interface-based-functional-options-in-golang"/>
<published>2025-11-16T00:00:00.000Z</published>
<updated>2025-11-16T00:00:00.000Z</updated>
<summary type="html"><![CDATA[A pattern to pass optional settings to build flexible constructors without a ton of parameters.]]></summary>
<content type="html"><![CDATA[<p>This blog post covers the <a href="https://100go.co/#not-using-the-functional-options-pattern-11" target="_blank" rel="noopener noreferrer">Functional Options Pattern</a> described within <a href="https://100go.co/" target="_blank" rel="noopener noreferrer">100 Go Mistakes and How To Avoid Them</a>, then compares some popular Golang repositories for how they handle flexible constructors.</p>
<h2 id="the-functional-optionals-pattern">The Functional Optionals Pattern</h2>
<p>According to the book, an example of the pattern is as follows:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="go"><code>type options struct {
    port *int
}

type Option func(options *options) error

func WithPort(port int) Option {
    return func(options *options) error {
        if port &lt; 0 {
            return errors.New(&quot;port should be positive&quot;)
        }
        options.port = &amp;port
        return nil
    }
}

// Usage example:
server, err := httplib.NewServer(
    &quot;localhost&quot;,
    httplib.WithPort(8080),
    httplib.WithTimeout(time.Second),
)</code></pre>
<p>An overview for the main idea behind the <strong>Functional Options Pattern</strong> is this (taken directly from the source):</p>
<ol>
<li>An unexported struct holds the configuration: options.</li>
<li>Each option is a function that returns the same type: <code>type Option func(options *options) error</code>. For example, <code>WithPort</code> accepts an int argument that represents the port and returns an Option type that represents how to update the options struct.</li>
</ol>
<p>This enables us to construct a <code>NewServer</code> while maintaining some flexibility and being wary of Golang <a href="https://go.dev/tour/basics/12" target="_blank" rel="noopener noreferrer">Zero values</a>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="go"><code>func NewServer(addr string, opts ...Option) (*http.Server, error) {
    var options options

    for _, opt := range opts {
        err := opt(&amp;options)
        if err != nil {
            return nil, err
        }
    }

    // At this stage, the options struct is built and contains the config.
    // Therefore, we can implement our logic related to port configuration.
    var port int

    if options.port == nil {
        port = defaultHTTPPort
    } else {
        if *options.port == 0 {
            port = randomPort()
        } else {
            port = *options.port
        }
    }

    // ...
}</code></pre>
<p>The first part of the &quot;constructor&quot; function applies the options, the second part handles an <code>nil</code> values to set defaults.</p>
<h2 id="an-interface-based-option-type">An interface-based option type</h2>
<p>Wanting to understand more about &quot;flexibility&quot; with constructors in Golang, I looked to some popular open source projects and saw an approach with <a href="google.golang.org/grpc">gRPC-Go</a> and <a href="https://github.com/uber-go/zap" target="_blank" rel="noopener noreferrer">Uber&#39;s Zap logger</a> that look to make use of an interface that has an <code>apply</code> function defined within it.</p>
<p>Interestingly enough, they both had an approach that made use of an interface with an <code>apply</code> method defined.</p>
<p>We&#39;ll show both case studies before refactoring the <code>httplib</code> example that we&#39;ve defined so far.</p>
<span id="case-study-grpc-go" class="anchor-alias"><h2 id="case-study-g-rpc-go">Case Study: gRPC-Go</h2>
<p>For example, in gRPC-Go:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="go"><code>type DialOption interface {
    apply(*dialOptions)
}
// and most options are implemented as functions that modify *dialOptions

conn, err := grpc.NewClient(
    &quot;localhost:50051&quot;,
    grpc.WithTransportCredentials(creds),
    grpc.WithBlock(),
    grpc.WithUnaryInterceptor(myInterceptor),
)</code></pre>
<p>With something like <code>grpc.NewClient</code>, it takes in a <code>...DialOption</code> as the final variable argument within the <a href="https://github.com/grpc/grpc-go/blob/v1.76.0/clientconn.go#L145" target="_blank" rel="noopener noreferrer">NewClient</a> definition:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="go"><code>func NewClient(target string, opts ...DialOption) (conn *ClientConn, err error) {
	cc := &amp;ClientConn{
		target: target,
		conns:  make(map[*addrConn]struct{}),
		dopts:  defaultDialOptions(),
	}

	cc.retryThrottler.Store((*retryThrottler)(nil))
	cc.safeConfigSelector.UpdateConfigSelector(&amp;defaultConfigSelector{nil})
	cc.ctx, cc.cancel = context.WithCancel(context.Background())

	// Apply dial options.
	disableGlobalOpts := false
	for _, opt := range opts {
		if _, ok := opt.(*disableGlobalDialOptions); ok {
			disableGlobalOpts = true
			break
		}
	}

	if !disableGlobalOpts {
		for _, opt := range globalDialOptions {
			opt.apply(&amp;cc.dopts)
		}
	}

    // NOTE: We iterate through the range of opts and call their
    // `apply` method.
	for _, opt := range opts {
		opt.apply(&amp;cc.dopts)
	}

	// ... code implementation omitted

	return cc, nil
}</code></pre>
<p>In the above implementation, we see that for each opt, the <code>apply</code> method is called.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="go"><code>// funcDialOption wraps a function that modifies dialOptions into an
// implementation of the DialOption interface.
type funcDialOption struct {
	f func(*dialOptions)
}

func (fdo *funcDialOption) apply(do *dialOptions) {
	fdo.f(do)
}

func newFuncDialOption(f func(*dialOptions)) *funcDialOption {
	return &amp;funcDialOption{
		f: f,
	}
}</code></pre>
<p>An example function that can be used for defining an option function:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="go"><code>func WithBlock() DialOption {
	return newFuncDialOption(func(o *dialOptions) {
		o.block = true
	})
}</code></pre>
<h2 id="case-study-uber-zap">Case Study: Uber Zap</h2>
<p>The same can be seen with the way Uber Zap uses it&#39;s <a href="https://github.com/uber-go/zap/blob/master/logger.go#L69" target="_blank" rel="noopener noreferrer">New constructor</a> which takes a variable <a href="https://github.com/uber-go/zap/blob/master/options.go#L31" target="_blank" rel="noopener noreferrer">Option</a> argument.</p>
<p>Within <a href="https://github.com/uber-go/zap/blob/master/options.go#L31" target="_blank" rel="noopener noreferrer">Option</a>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="go"><code>// An Option configures a Logger.
type Option interface {
	apply(*Logger)
}

// optionFunc wraps a func so it satisfies the Option interface.
type optionFunc func(*Logger)

func (f optionFunc) apply(log *Logger) {
	f(log)
}</code></pre>
<p>The <a href="https://github.com/uber-go/zap/blob/master/logger.go#L69" target="_blank" rel="noopener noreferrer">New constructor</a>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="go"><code>func New(core zapcore.Core, options ...Option) *Logger {
	if core == nil {
		return NewNop()
	}
	log := &amp;Logger{
		core:        core,
		errorOutput: zapcore.Lock(os.Stderr),
		addStack:    zapcore.FatalLevel + 1,
		clock:       zapcore.DefaultClock,
	}
	return log.WithOptions(options...)
}


// WithOptions clones the current Logger, applies the supplied Options, and
// returns the resulting Logger. It&#39;s safe to use concurrently.
func (log *Logger) WithOptions(opts ...Option) *Logger {
	c := log.clone()
	for _, opt := range opts {
		opt.apply(c)
	}
	return c
}

// optionFunc wraps a func so it satisfies the Option interface.
type optionFunc func(*Logger)

func (f optionFunc) apply(log *Logger) {
	f(log)
}

// ... an example option
// WithClock specifies the clock used by the logger to determine the current
// time for logged entries. Defaults to the system clock with time.Now.
func WithClock(clock zapcore.Clock) Option {
	return optionFunc(func(log *Logger) {
		log.clock = clock
	})
}</code></pre>
<h2 id="reworking-the-httplib-example-code">Reworking the httplib example code</h2>
<p>Taking inspiration from the above, we can rework the example code taken from <strong>100 Go Mistakes and How To Avoid Them</strong> to follow this interface-based approach.</p>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>In our case, we will still maintain the ability to return <code>error</code> to kept the
same response type as before, however this may be omitted depending on your
implementation. For example: if you handle all defaults, <code>error</code> would be
unnecessary.</p> </div> </aside>
<p>First, you define <code>options</code> struct to match what we saw before:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="go"><code>type options struct {
    port *int
    // timeout *time.Duration, etc...
}</code></pre>
<p>Next, we define the interface for <code>Option</code>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="go"><code>// Option configures how we set up the server.
type Option interface {
    apply(*options) error
}</code></pre>
<p>We then define our function-wrapper type:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="go"><code>// optionFunc wraps a func so it satisfies the Option interface.
type optionFunc func(*options) error

func (f optionFunc) apply(o *options) error {
    return f(o)
}</code></pre>
<p>Then we can declare our functional option functions:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="go"><code>func WithPort(port int) Option {
    return optionFunc(func(o *options) error {
        if port &lt; 0 {
            return errors.New(&quot;port should be positive&quot;)
        }
        o.port = &amp;port
        return nil
    })
}</code></pre>
<p>With these declarations, we can then create our new constructor:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="go"><code>func NewServer(addr string, opts ...Option) (*http.Server, error) {
    var o options

    for _, opt := range opts {
        if err := opt.apply(&amp;o); err != nil {
            return nil, err
        }
    }

    // existing port resolution logic...
}</code></pre>
<p>At this point, can still make use of the exact same signature as before:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="go"><code>// Usage example:
server, err := httplib.NewServer(
    &quot;localhost&quot;,
    httplib.WithPort(8080),
    // ... other supported options
    // httplib.WithTimeout(time.Second),
)</code></pre>
<h2 id="a-working-example">A working example</h2>
<p>If, instead of using <code>http.Server</code> we placed in our own struct <code>Server</code> for demonstration purposes, we can see a working example of this pattern with the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="go"><code>package main

import (
	&quot;errors&quot;
	&quot;fmt&quot;
)

type options struct {
	port *int
	// timeout *time.Duration, etc...
}

// Option configures how we set up the server.
type Option interface {
	apply(*options) error
}

// optionFunc wraps a func so it satisfies the Option interface.
type optionFunc func(*options) error

func (f optionFunc) apply(o *options) error {
	return f(o)
}

func WithPort(port int) Option {
	return optionFunc(func(o *options) error {
		if port &lt; 0 {
			return errors.New(&quot;port should be positive&quot;)
		}
		o.port = &amp;port
		return nil
	})
}

type Server struct {
	addr string
	port int
}

func NewServer(addr string, opts ...Option) (*Server, error) {
	var o options

	for _, opt := range opts {
		if err := opt.apply(&amp;o); err != nil {
			return nil, err
		}
	}

	srv := &amp;Server{
		addr: addr,
		port: *o.port,
	}

	return srv, nil
}

func main() {
	fmt.Println(&quot;=== Example 1: Valid usage ===&quot;)

	srv, err := NewServer(&quot;localhost&quot;,
		WithPort(8080),
	)
	if err != nil {
		fmt.Println(&quot;unexpected error:&quot;, err)
		return
	}
	fmt.Printf(&quot;Server struct: %+v\n\n&quot;, srv)

	fmt.Println(&quot;=== Example 2: Invalid usage (error) ===&quot;)

	srv2, err := NewServer(&quot;localhost&quot;,
		WithPort(-5), // invalid: negative port
	)
	if err != nil {
		fmt.Println(&quot;expected error:&quot;, err)
		return
	}
	fmt.Printf(&quot;Server struct: %+v\n&quot;, srv2)
}</code></pre>
<p>Running <code>go run</code> targeting this file prints the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>$ go run main.go
=== Example 1: Valid usage ===
Server struct: &amp;{addr:localhost port:8080}

=== Example 2: Invalid usage (error) ===
expected error: port should be positive</code></pre>
<h2 id="conclusion">Conclusion</h2>
<p>This blog post reviewed the <a href="https://100go.co/#not-using-the-functional-options-pattern-11" target="_blank" rel="noopener noreferrer">Functional Options Pattern from 100 Go Mistakes and How To Avoid Them</a>, before looking at some open-source case studies with gRPC-Go and Uber Zap.</p>
<p>We then refactored the example code to follow the interface-style approach with the <code>apply</code> function for composing options while maintaining the same capabilities to return errors for invalid option arguments.</p>
<h2 id="links-and-further-reading">Links and Further Reading</h2>
<ul>
<li><a href="https://100go.co/#not-using-the-functional-options-pattern-11" target="_blank" rel="noopener noreferrer">Functional Options Pattern — 100 Go Mistakes</a></li>
<li><a href="https://100go.co/" target="_blank" rel="noopener noreferrer">100 Go Mistakes and How To Avoid Them</a></li>
<li><a href="https://go.dev/tour/basics/12" target="_blank" rel="noopener noreferrer">Golang Zero Values</a></li>
<li><a href="https://google.golang.org/grpc" target="_blank" rel="noopener noreferrer">gRPC-Go</a></li>
<li><a href="https://github.com/grpc/grpc-go/blob/v1.76.0/clientconn.go#L145" target="_blank" rel="noopener noreferrer">gRPC-Go NewClient Implementation</a></li>
<li><a href="https://github.com/uber-go/zap" target="_blank" rel="noopener noreferrer">Uber Zap Logger</a></li>
<li><a href="https://github.com/uber-go/zap/blob/master/logger.go#L69" target="_blank" rel="noopener noreferrer">Uber Zap Logger New Constructor</a></li>
<li><a href="https://github.com/uber-go/zap/blob/master/options.go#L31" target="_blank" rel="noopener noreferrer">Uber Zap Option Interface</a></li>
</ul>
<p><em>Photo credit: <a href="https://unsplash.com/@johnnyb803" target="_blank" rel="noopener noreferrer"><code>johnnyb803</code></a></em></p>]]></content>
<category term="golang"/>
</entry>
<entry>
<title type="html"><![CDATA[Adding Swift packages in Xcode]]></title>
<id>https://blog.dennisokeeffe.com/blog/2025-10-02-adding-swift-packages-in-xcode</id>
<link href="https://dennisokeeffe.com/blog/2025-10-02-adding-swift-packages-in-xcode"/>
<published>2025-10-02T00:00:00.000Z</published>
<updated>2025-10-02T00:00:00.000Z</updated>
<summary type="html"><![CDATA[Learn how to add the popular Swift networking package Alamofire in a Swift Xcode project]]></summary>
<content type="html"><![CDATA[<p>This project walks through how to add third-party packages to your Swift projects in Xcode</p>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>Swift (this post uses Swift 6.1).</li>
</ul>
<h2 id="initializing-a-swift-executable-package">Initializing a Swift executable package</h2>
<p>You can create a new Swift executable package with the following (from the command line):</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>$ mkdir hello-swift
$ cd hello-swift
$ swift package init --type executable</code></pre>
<p>You can run <code>swift package init --help</code> to get more information around arguments like <code>--type</code>. For example:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>swift package init --help
OVERVIEW: Initialize a new package

USAGE: swift package init [--type &lt;type&gt;] [--enable-xctest] [--disable-xctest] [--enable-swift-testing] [--disable-swift-testing] [--name &lt;name&gt;]

OPTIONS:
  --type &lt;type&gt;           Package type: (default: library)
        library           - A package with a library.
        executable        - A package with an executable.
        tool              - A package with an executable that uses
                            Swift Argument Parser. Use this template if you
                            plan to have a rich set of command-line arguments.
        build-tool-plugin - A package that vends a build tool plugin.
        command-plugin    - A package that vends a command plugin.
        macro             - A package that vends a macro.
        empty             - An empty package with a Package.swift manifest.
  --enable-xctest/--disable-xctest
                          Enable support for XCTest
  --enable-swift-testing/--disable-swift-testing
                          Enable support for Swift Testing
  --name &lt;name&gt;           Provide custom package name
  --version               Show the version.
  -h, -help, --help       Show help information.</code></pre>
<p>With this new package initialized as an executable, you can navigate to <code>Sources/main.swift</code> to see the generated entry file:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="swift"><code>/ The Swift Programming Language
// https://docs.swift.org/swift-book

print(&quot;Hello, world!&quot;)</code></pre>
<h2 id="running-our-executable">Running our executable</h2>
<p>You can make use of <code>swift run</code> to run our executable:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>$ swift run
[1/1] Planning build
Building for debugging...
[1/1] Write swift-version--58304C5D6DBC2206.txt
Build of product &#39;hello-swift&#39; complete! (0.19s)
Hello, world!</code></pre>
<p>We can re-use this to run after any file changes:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>$ swift run
Building for debugging...
[7/7] Applying hello-swift
Build of product &#39;hello-swift&#39; complete! (1.01s)
Hello again, world!</code></pre>
<p>If you have a program installed such as <a href="https://github.com/watchexec/watchexec" target="_blank" rel="noopener noreferrer">watchexec</a>, you can set it to watch swift extension files with <code>watchexec --exts swift -- swift run </code>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>$ watchexec --exts swift -- swift run
[Running: swift run]
[1/1] Planning build
Building for debugging...
[1/1] Write swift-version--58304C5D6DBC2206.txt
Build of product &#39;hello-swift&#39; complete! (0.20s)
Hello again, world!
[Command was successful]
[Running: swift run]
[1/1] Planning build
Building for debugging...
[7/7] Applying hello-swift
Build of product &#39;hello-swift&#39; complete! (0.73s)
Hello again and again, world!
[Command was successful]</code></pre>
<h2 id="building-files">Building files</h2>
<p>You can run <code>swift build</code> from the project root to build out the executable.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>$ swift build
[1/1] Planning build
Building for debugging...
[1/1] Write swift-version--58304C5D6DBC2206.txt
Build complete! (0.23s)</code></pre>
<p>By default, <code>swift build</code> will build with debug configuration.</p>
<p>Outputs are placed in <code>.build</code>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>$ tree .build -L 3
.build
├── arm64-apple-macosx
│   └── debug
│       ├── description.json
│       ├── hello_swift.build
│       ├── hello-swift
│       ├── hello-swift-entitlement.plist
│       ├── hello-swift.dSYM
│       ├── hello-swift.product
│       ├── index
│       ├── ModuleCache
│       ├── Modules
│       ├── plugin-tools-description.json
│       └── swift-version--58304C5D6DBC2206.txt
├── artifacts
├── build.db
├── checkouts
├── debug -&gt; arm64-apple-macosx/debug
├── debug.yaml
├── plugin-tools.yaml
├── repositories
└── workspace-state.json</code></pre>
<p>For an optimized build, pass <code>--configuration release</code>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>$ swift build --configuration release
Building for production...
[5/5] Linking hello-swift
Build complete! (1.21s)
$ tree .build -L 3
.build
├── arm64-apple-macosx
│   ├── debug
│   │   ├── description.json
│   │   ├── hello_swift.build
│   │   ├── hello-swift
│   │   ├── hello-swift-entitlement.plist
│   │   ├── hello-swift.dSYM
│   │   ├── hello-swift.product
│   │   ├── index
│   │   ├── ModuleCache
│   │   ├── Modules
│   │   ├── plugin-tools-description.json
│   │   └── swift-version--58304C5D6DBC2206.txt
│   └── release
│       ├── description.json
│       ├── hello_swift.build
│       ├── hello-swift
│       ├── hello-swift.dSYM
│       ├── hello-swift.product
│       ├── ModuleCache
│       ├── Modules
│       ├── plugin-tools-description.json
│       └── swift-version--58304C5D6DBC2206.txt
├── artifacts
├── build.db
├── checkouts
├── debug -&gt; arm64-apple-macosx/debug
├── debug.yaml
├── plugin-tools.yaml
├── release -&gt; arm64-apple-macosx/release
├── release.yaml
├── repositories
└── workspace-state.json

20 directories, 14 files</code></pre>
<p>Once compiled, you&#39;ll have an optimized binary you can run directly (if your platform is the correct target):</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>$ ./.build/release/hello-swift
Hello again and again, world!</code></pre>
<p>Other useful flags include <code>--triple</code> to cross-compile for another platform.</p>
<h2 id="makefile">Makefile</h2>
<p>You can make use of a <code>Makefile</code> for common operations:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="makefile"><code>.PHONY: build run clean release test

build:
	swift build

release:
	swift build -c release

run:
	swift run

test:
	swift test

clean:
	swift package clean</code></pre>
<p>In use:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>$ make clean
swift package clean

$ make build
swift build
Building for debugging...
[8/8] Applying hello-swift
Build complete! (1.06s)

$ make run
swift run
[1/1] Planning build
Building for debugging...
[1/1] Write swift-version--58304C5D6DBC2206.txt
Build of product &#39;hello-swift&#39; complete! (0.21s)
Hello again and again, world!</code></pre>
<h2 id="managing-packages">Managing packages</h2>
<p>You can manage packages in <code>Package.swift</code> or via the command line.</p>
<p>The default file that was created:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="swift"><code>// Package.swift
// swift-tools-version: 6.1
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: &quot;hello-swift&quot;,
    targets: [
        // Targets are the basic building blocks of a package, defining a module or a test suite.
        // Targets can depend on other targets in this package and products from dependencies.
        .executableTarget(
            name: &quot;hello-swift&quot;),
    ]
)</code></pre>
<p>From the command line, you can install a package using <code>swift package add-dependency &lt;URL&gt;</code>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>$ swift package add-dependency https://github.com/Alamofire/Alamofire --from 5.10.2
Updating package manifest at Package.swift... done.</code></pre>
<p>In the above case, the package is installed via the GitHub repository for the popular networking package <a href="https://github.com/Alamofire/Alamofire" target="_blank" rel="noopener noreferrer">Alamofire</a>.</p>
<p>You must also provide a version.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>$ swift package add-dependency https://github.com/Alamofire/Alamofire
error: must specify one of --exact, --branch, --revision, --from, or --up-to-next-minor-from</code></pre>
<p>Once installed, your <code>Package.swift</code> file will be updated:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="swift"><code>// swift-tools-version: 6.1
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: &quot;hello-swift&quot;,
    dependencies: [
        .package(url: &quot;https://github.com/Alamofire/Alamofire&quot;, from: &quot;5.10.2&quot;),
    ],
    targets: [
        // Targets are the basic building blocks of a package, defining a module or a test suite.
        // Targets can depend on other targets in this package and products from dependencies.
        .executableTarget(
            name: &quot;hello-swift&quot;),
    ]
)</code></pre>
<p>In order to use it, you <strong>must</strong> update the relevant <code>targets</code>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="swift"><code>// swift-tools-version: 6.1
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: &quot;hello-swift&quot;,
    dependencies: [
        .package(url: &quot;https://github.com/Alamofire/Alamofire&quot;, from: &quot;5.10.2&quot;),
    ],
    targets: [
        // Targets are the basic building blocks of a package, defining a module or a test suite.
        // Targets can depend on other targets in this package and products from dependencies.
        .executableTarget(
            name: &quot;hello-swift&quot;,
            dependencies: [
                &quot;Alamofire&quot;   // 👈 add this line
            ]),
    ]
)</code></pre>
<p>We can see this in action by updating our <code>main.swift</code> file:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="swift"><code>// Sources/main.swift
// The Swift Programming Language
// https://docs.swift.org/swift-book

import Alamofire
import Foundation

@main
struct Hello {
    static func main() {
        AF.request(&quot;https://jsonplaceholder.typicode.com/todos/1&quot;)
            .responseString { response in
                print(response.value ?? &quot;no data&quot;)
                CFRunLoopStop(CFRunLoopGetMain()) // stop loop when done
            }

        // keep the program alive until CFRunLoopStop is called
        CFRunLoopRun()
    }
}</code></pre>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>In the above code, we use <code>CFRunLoopRun</code>, <code>CFRunLoopStop</code> and
<code>CFRunLoopGetMain</code> functions for writing backwards-compatible Alamofire code.
If you want to use async/await, you can update your <code>Package.swift</code> platforms
to target higher versions of MacOS (assuming you are on MacOS).</p> </div> </aside>
<p>Let&#39;s see it in action:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>$ swift run
Fetching https://github.com/Alamofire/Alamofire
Fetched https://github.com/Alamofire/Alamofire from cache (22.64s)
Computing version for https://github.com/Alamofire/Alamofire
Computed https://github.com/Alamofire/Alamofire at 5.10.2 (23.82s)
Creating working copy for https://github.com/Alamofire/Alamofire
Working copy of https://github.com/Alamofire/Alamofire resolved at 5.10.2
[1/1] Planning build
Building for debugging...
[52/52] Applying hello-swift
Build of product &#39;hello-swift&#39; complete! (6.15s)
{
  &quot;userId&quot;: 1,
  &quot;id&quot;: 1,
  &quot;title&quot;: &quot;delectus aut autem&quot;,
  &quot;completed&quot;: false
}</code></pre>
<p>Perfect, we got our response from an installed package.</p>
<h2 id="conclusion">Conclusion</h2>
<p>This post was recounting some baby steps with initializing Swift packages. I will likely be writing a few tools for MacOS with this approach for learning more about the language while diving into iOS.</p>
<h2 id="links-and-further-reading">Links and Further Reading</h2>
<ul>
<li><a href="https://github.com/Alamofire/Alamofire" target="_blank" rel="noopener noreferrer">Alamofire</a></li>
<li><a href="https://github.com/watchexec/watchexec" target="_blank" rel="noopener noreferrer">watchexec</a></li>
<li><a href="https://github.com/swiftlang/swift-package-manager" target="_blank" rel="noopener noreferrer">swiftlang/swift-package-manager</a></li>
<li><a href="https://docs.swift.org/swiftpm/documentation/packagemanagerdocs/" target="_blank" rel="noopener noreferrer">Swift Package Manager</a></li>
</ul>
<p><em>Photo credit: <a href="https://unsplash.com/@johnnyb803" target="_blank" rel="noopener noreferrer"><code>johnnyb803</code></a></em></p>]]></content>
<category term="swift"/><category term="beginner"/>
</entry>
<entry>
<title type="html"><![CDATA[Swift from the command line]]></title>
<id>https://blog.dennisokeeffe.com/blog/2025-09-10-swift-from-the-command-line</id>
<link href="https://dennisokeeffe.com/blog/2025-09-10-swift-from-the-command-line"/>
<published>2025-09-10T00:00:00.000Z</published>
<updated>2025-09-10T00:00:00.000Z</updated>
<summary type="html"><![CDATA[An introduction to using Swift without Xcode]]></summary>
<content type="html"><![CDATA[<p>The following blog post is simply a recount of me tinkering with Swift without using Xcode.</p>
<p>It explores initializing an executable, working through some simple Swift syntax and managing packages with SwiftPM (Swift Package Manager).</p>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>Swift (this post uses Swift 6.1).</li>
</ul>
<h2 id="initializing-a-swift-executable-package">Initializing a Swift executable package</h2>
<p>You can create a new Swift executable package with the following (from the command line):</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>$ mkdir hello-swift
$ cd hello-swift
$ swift package init --type executable</code></pre>
<p>You can run <code>swift package init --help</code> to get more information around arguments like <code>--type</code>. For example:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>swift package init --help
OVERVIEW: Initialize a new package

USAGE: swift package init [--type &lt;type&gt;] [--enable-xctest] [--disable-xctest] [--enable-swift-testing] [--disable-swift-testing] [--name &lt;name&gt;]

OPTIONS:
  --type &lt;type&gt;           Package type: (default: library)
        library           - A package with a library.
        executable        - A package with an executable.
        tool              - A package with an executable that uses
                            Swift Argument Parser. Use this template if you
                            plan to have a rich set of command-line arguments.
        build-tool-plugin - A package that vends a build tool plugin.
        command-plugin    - A package that vends a command plugin.
        macro             - A package that vends a macro.
        empty             - An empty package with a Package.swift manifest.
  --enable-xctest/--disable-xctest
                          Enable support for XCTest
  --enable-swift-testing/--disable-swift-testing
                          Enable support for Swift Testing
  --name &lt;name&gt;           Provide custom package name
  --version               Show the version.
  -h, -help, --help       Show help information.</code></pre>
<p>With this new package initialized as an executable, you can navigate to <code>Sources/main.swift</code> to see the generated entry file:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="swift"><code>/ The Swift Programming Language
// https://docs.swift.org/swift-book

print(&quot;Hello, world!&quot;)</code></pre>
<h2 id="running-our-executable">Running our executable</h2>
<p>You can make use of <code>swift run</code> to run our executable:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>$ swift run
[1/1] Planning build
Building for debugging...
[1/1] Write swift-version--58304C5D6DBC2206.txt
Build of product &#39;hello-swift&#39; complete! (0.19s)
Hello, world!</code></pre>
<p>We can re-use this to run after any file changes:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>$ swift run
Building for debugging...
[7/7] Applying hello-swift
Build of product &#39;hello-swift&#39; complete! (1.01s)
Hello again, world!</code></pre>
<p>If you have a program installed such as <a href="https://github.com/watchexec/watchexec" target="_blank" rel="noopener noreferrer">watchexec</a>, you can set it to watch swift extension files with <code>watchexec --exts swift -- swift run </code>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>$ watchexec --exts swift -- swift run
[Running: swift run]
[1/1] Planning build
Building for debugging...
[1/1] Write swift-version--58304C5D6DBC2206.txt
Build of product &#39;hello-swift&#39; complete! (0.20s)
Hello again, world!
[Command was successful]
[Running: swift run]
[1/1] Planning build
Building for debugging...
[7/7] Applying hello-swift
Build of product &#39;hello-swift&#39; complete! (0.73s)
Hello again and again, world!
[Command was successful]</code></pre>
<h2 id="building-files">Building files</h2>
<p>You can run <code>swift build</code> from the project root to build out the executable.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>$ swift build
[1/1] Planning build
Building for debugging...
[1/1] Write swift-version--58304C5D6DBC2206.txt
Build complete! (0.23s)</code></pre>
<p>By default, <code>swift build</code> will build with debug configuration.</p>
<p>Outputs are placed in <code>.build</code>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>$ tree .build -L 3
.build
├── arm64-apple-macosx
│   └── debug
│       ├── description.json
│       ├── hello_swift.build
│       ├── hello-swift
│       ├── hello-swift-entitlement.plist
│       ├── hello-swift.dSYM
│       ├── hello-swift.product
│       ├── index
│       ├── ModuleCache
│       ├── Modules
│       ├── plugin-tools-description.json
│       └── swift-version--58304C5D6DBC2206.txt
├── artifacts
├── build.db
├── checkouts
├── debug -&gt; arm64-apple-macosx/debug
├── debug.yaml
├── plugin-tools.yaml
├── repositories
└── workspace-state.json</code></pre>
<p>For an optimized build, pass <code>--configuration release</code>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>$ swift build --configuration release
Building for production...
[5/5] Linking hello-swift
Build complete! (1.21s)
$ tree .build -L 3
.build
├── arm64-apple-macosx
│   ├── debug
│   │   ├── description.json
│   │   ├── hello_swift.build
│   │   ├── hello-swift
│   │   ├── hello-swift-entitlement.plist
│   │   ├── hello-swift.dSYM
│   │   ├── hello-swift.product
│   │   ├── index
│   │   ├── ModuleCache
│   │   ├── Modules
│   │   ├── plugin-tools-description.json
│   │   └── swift-version--58304C5D6DBC2206.txt
│   └── release
│       ├── description.json
│       ├── hello_swift.build
│       ├── hello-swift
│       ├── hello-swift.dSYM
│       ├── hello-swift.product
│       ├── ModuleCache
│       ├── Modules
│       ├── plugin-tools-description.json
│       └── swift-version--58304C5D6DBC2206.txt
├── artifacts
├── build.db
├── checkouts
├── debug -&gt; arm64-apple-macosx/debug
├── debug.yaml
├── plugin-tools.yaml
├── release -&gt; arm64-apple-macosx/release
├── release.yaml
├── repositories
└── workspace-state.json

20 directories, 14 files</code></pre>
<p>Once compiled, you&#39;ll have an optimized binary you can run directly (if your platform is the correct target):</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>$ ./.build/release/hello-swift
Hello again and again, world!</code></pre>
<p>Other useful flags include <code>--triple</code> to cross-compile for another platform.</p>
<h2 id="makefile">Makefile</h2>
<p>You can make use of a <code>Makefile</code> for common operations:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="makefile"><code>.PHONY: build run clean release test

build:
	swift build

release:
	swift build -c release

run:
	swift run

test:
	swift test

clean:
	swift package clean</code></pre>
<p>In use:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>$ make clean
swift package clean

$ make build
swift build
Building for debugging...
[8/8] Applying hello-swift
Build complete! (1.06s)

$ make run
swift run
[1/1] Planning build
Building for debugging...
[1/1] Write swift-version--58304C5D6DBC2206.txt
Build of product &#39;hello-swift&#39; complete! (0.21s)
Hello again and again, world!</code></pre>
<h2 id="managing-packages">Managing packages</h2>
<p>You can manage packages in <code>Package.swift</code> or via the command line.</p>
<p>The default file that was created:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="swift"><code>// Package.swift
// swift-tools-version: 6.1
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: &quot;hello-swift&quot;,
    targets: [
        // Targets are the basic building blocks of a package, defining a module or a test suite.
        // Targets can depend on other targets in this package and products from dependencies.
        .executableTarget(
            name: &quot;hello-swift&quot;),
    ]
)</code></pre>
<p>From the command line, you can install a package using <code>swift package add-dependency &lt;URL&gt;</code>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>$ swift package add-dependency https://github.com/Alamofire/Alamofire --from 5.10.2
Updating package manifest at Package.swift... done.</code></pre>
<p>In the above case, the package is installed via the GitHub repository for the popular networking package <a href="https://github.com/Alamofire/Alamofire" target="_blank" rel="noopener noreferrer">Alamofire</a>.</p>
<p>You must also provide a version.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>$ swift package add-dependency https://github.com/Alamofire/Alamofire
error: must specify one of --exact, --branch, --revision, --from, or --up-to-next-minor-from</code></pre>
<p>Once installed, your <code>Package.swift</code> file will be updated:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="swift"><code>// swift-tools-version: 6.1
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: &quot;hello-swift&quot;,
    dependencies: [
        .package(url: &quot;https://github.com/Alamofire/Alamofire&quot;, from: &quot;5.10.2&quot;),
    ],
    targets: [
        // Targets are the basic building blocks of a package, defining a module or a test suite.
        // Targets can depend on other targets in this package and products from dependencies.
        .executableTarget(
            name: &quot;hello-swift&quot;),
    ]
)</code></pre>
<p>In order to use it, you <strong>must</strong> update the relevant <code>targets</code>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="swift"><code>// swift-tools-version: 6.1
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: &quot;hello-swift&quot;,
    dependencies: [
        .package(url: &quot;https://github.com/Alamofire/Alamofire&quot;, from: &quot;5.10.2&quot;),
    ],
    targets: [
        // Targets are the basic building blocks of a package, defining a module or a test suite.
        // Targets can depend on other targets in this package and products from dependencies.
        .executableTarget(
            name: &quot;hello-swift&quot;,
            dependencies: [
                &quot;Alamofire&quot;   // 👈 add this line
            ]),
    ]
)</code></pre>
<p>We can see this in action by updating our <code>main.swift</code> file:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="swift"><code>// Sources/main.swift
// The Swift Programming Language
// https://docs.swift.org/swift-book

import Alamofire
import Foundation

@main
struct Hello {
    static func main() {
        AF.request(&quot;https://jsonplaceholder.typicode.com/todos/1&quot;)
            .responseString { response in
                print(response.value ?? &quot;no data&quot;)
                CFRunLoopStop(CFRunLoopGetMain()) // stop loop when done
            }

        // keep the program alive until CFRunLoopStop is called
        CFRunLoopRun()
    }
}</code></pre>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>In the above code, we use <code>CFRunLoopRun</code>, <code>CFRunLoopStop</code> and
<code>CFRunLoopGetMain</code> functions for writing backwards-compatible Alamofire code.
If you want to use async/await, you can update your <code>Package.swift</code> platforms
to target higher versions of MacOS (assuming you are on MacOS).</p> </div> </aside>
<p>Let&#39;s see it in action:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>$ swift run
Fetching https://github.com/Alamofire/Alamofire
Fetched https://github.com/Alamofire/Alamofire from cache (22.64s)
Computing version for https://github.com/Alamofire/Alamofire
Computed https://github.com/Alamofire/Alamofire at 5.10.2 (23.82s)
Creating working copy for https://github.com/Alamofire/Alamofire
Working copy of https://github.com/Alamofire/Alamofire resolved at 5.10.2
[1/1] Planning build
Building for debugging...
[52/52] Applying hello-swift
Build of product &#39;hello-swift&#39; complete! (6.15s)
{
  &quot;userId&quot;: 1,
  &quot;id&quot;: 1,
  &quot;title&quot;: &quot;delectus aut autem&quot;,
  &quot;completed&quot;: false
}</code></pre>
<p>Perfect, we got our response from an installed package.</p>
<h2 id="conclusion">Conclusion</h2>
<p>This post was recounting some baby steps with initializing Swift packages. I will likely be writing a few tools for MacOS with this approach for learning more about the language while diving into iOS.</p>
<h2 id="links-and-further-reading">Links and Further Reading</h2>
<ul>
<li><a href="https://github.com/Alamofire/Alamofire" target="_blank" rel="noopener noreferrer">Alamofire</a></li>
<li><a href="https://github.com/watchexec/watchexec" target="_blank" rel="noopener noreferrer">watchexec</a></li>
<li><a href="https://github.com/swiftlang/swift-package-manager" target="_blank" rel="noopener noreferrer">swiftlang/swift-package-manager</a></li>
<li><a href="https://docs.swift.org/swiftpm/documentation/packagemanagerdocs/" target="_blank" rel="noopener noreferrer">Swift Package Manager</a></li>
</ul>
<p><em>Photo credit: <a href="https://unsplash.com/@johnnyb803" target="_blank" rel="noopener noreferrer"><code>johnnyb803</code></a></em></p>]]></content>
<category term="swift"/><category term="beginner"/>
</entry>
<entry>
<title type="html"><![CDATA[Building TUIs with Golang and tview]]></title>
<id>https://blog.dennisokeeffe.com/blog/2025-08-19-building-tuis-with-golang-and-tview</id>
<link href="https://dennisokeeffe.com/blog/2025-08-19-building-tuis-with-golang-and-tview"/>
<published>2025-08-19T00:00:00.000Z</published>
<updated>2025-08-19T00:00:00.000Z</updated>
<summary type="html"><![CDATA[A practical walkthrough for your first steps on building TUIs with Golang and tview]]></summary>
<content type="html"><![CDATA[<p>Long time avid TUI fan with zero contributions.</p>
<p>Given the amount of time I spend working with TUIs like <a href="https://k9scli.io/" target="_blank" rel="noopener noreferrer">k9s</a>, <a href="https://github.com/jesseduffield/lazygit" target="_blank" rel="noopener noreferrer">lazygit</a>, <a href="https://github.com/sxyazi/yazi" target="_blank" rel="noopener noreferrer">yazi</a> and <a href="https://github.com/wagoodman/dive" target="_blank" rel="noopener noreferrer">dive</a>, I decided the time is now ripe to start learning the ins and outs of TUI development.</p>
<p>This post will be a walkthrough of me testing out the waters and learning more about <a href="https://github.com/rivo/tview" target="_blank" rel="noopener noreferrer">tview</a>, a Golang package for building TUIs.</p>
<h2 id="prerequisites">Prerequisites</h2>
<p>This post expects that you&#39;ve worked with Golang before.</p>
<p>I will be using <code>go.1.24.5</code>.</p>
<h2 id="getting-started">Getting started</h2>
<p>Start by initialising a new Go module and fetching the required packages.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code># Create a project directory
mkdir blog-tview-introduction
cd blog-tview-introduction

# Initialise the module
go mod init tview.dennisokeeffe.com

# Fetch the tview package
go get github.com/rivo/tview

# Create a main.go file
touch main.go</code></pre>
<p>At this point, we are ready to get started.</p>
<h2 id="exploring-tview">Exploring tview</h2>
<p>A great place to start getting to understand the package is the <a href="https://github.com/rivo/tview/wiki" target="_blank" rel="noopener noreferrer">tview wiki</a> and the <a href="https://pkg.go.dev/github.com/rivo/tview" target="_blank" rel="noopener noreferrer">pkg.go documentation page</a>.</p>
<p>The wiki outlines a number of basic components that can be used for drawing out our TUI and we can reinforce some of what we learn by checking out the docs.</p>
<p>We can update <code>main.go</code> to start with the example <strong>Grid</strong> component:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="go"><code>package main

import (
 &quot;github.com/rivo/tview&quot;
)

func main() {
 newPrimitive := func(text string) tview.Primitive {
  return tview.NewTextView().
   SetTextAlign(tview.AlignCenter).
   SetText(text)
 }
 menu := newPrimitive(&quot;Menu&quot;)
 main := newPrimitive(&quot;Main content&quot;)
 sideBar := newPrimitive(&quot;Side Bar&quot;)

 grid := tview.NewGrid().
  SetRows(3, 0, 3).
  SetColumns(30, 0, 30).
  SetBorders(true).
  AddItem(newPrimitive(&quot;Header&quot;), 0, 0, 1, 3, 0, 0, false).
  AddItem(newPrimitive(&quot;Footer&quot;), 2, 0, 1, 3, 0, 0, false)

 // Layout for screens narrower than 100 cells (menu and side bar are hidden).
 grid.AddItem(menu, 0, 0, 0, 0, 0, 0, false).
  AddItem(main, 1, 0, 1, 3, 0, 0, false).
  AddItem(sideBar, 0, 0, 0, 0, 0, 0, false)

 // Layout for screens wider than 100 cells.
 grid.AddItem(menu, 1, 0, 1, 1, 0, 100, false).
  AddItem(main, 1, 1, 1, 1, 0, 100, false).
  AddItem(sideBar, 1, 2, 1, 1, 0, 100, false)

 if err := tview.NewApplication().SetRoot(grid, true).SetFocus(grid).Run(); err != nil {
  panic(err)
 }
}</code></pre>
<p>Most of the above code should make sense just from a read through:</p>
<ol>
<li>We built out a few primitives (more on this later) for the &quot;Menu&quot;, &quot;Main content&quot; and &quot;Side bar&quot;.</li>
<li>We built out a grid that adds a new &quot;Header&quot; and &quot;Footer&quot; primitive.</li>
<li>We add two different layouts based on available cell size.</li>
<li>We start the application with the grid as our root and focus.</li>
</ol>
<p>We can visualize this TUI by running <code>go run main.go</code>:</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2025-08-19-building-tuis-with-golang-and-tview/1-full-layout.webp" alt="Visualizing the full TUI" loading="lazy" decoding="async"/><figcaption>Visualizing the full TUI</figcaption></figure>
<p>As you may already have expected from the comments above, a smaller viewport for our terminal will change what is rendered:</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2025-08-19-building-tuis-with-golang-and-tview/2-responsive-layout.webp" alt="Responsive layout" loading="lazy" decoding="async"/><figcaption>Responsive layout</figcaption></figure>
<p>The part of the code which might not make so much sense right now are the arguments for methods such as <code>AddItem</code>.</p>
<p>Let&#39;s walk through a few of these to start getting a sense for the package.</p>
<p><code>AddItem</code> is a <a href="https://pkg.go.dev/github.com/rivo/tview#Grid.AddItem" target="_blank" rel="noopener noreferrer">Grid method</a> with the following definition:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="go"><code>func (g *tview.Grid) AddItem(p tview.Primitive, row int, column int, rowSpan int, colSpan int, minGridHeight int, minGridWidth int, focus bool) *tview.Grid</code></pre>
<p>So reading an example like <code>AddItem(newPrimitive(&quot;Header&quot;), 0, 0, 1, 3, 0, 0, false)</code> from left to right, we can understand the numerical arguments as:</p>
<ol>
<li>Row 0.</li>
<li>Column 0.</li>
<li>Row span 1.</li>
<li>Column span 3.</li>
<li>Minimum height 0.</li>
<li>Minimum width 0.</li>
</ol>
<p>We can assume that row span and column span relate to our default values, so exploring the <code>SetRows</code> and <code>SetColumns</code> method definitions can help us work backwards to understand the relationship:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="go"><code>func (g *tview.Grid) SetColumns(columns ...int) *tview.Grid
func (g *tview.Grid) SetRows(rows ...int) *tview.Grid</code></pre>
<p>Both functions follow the same principle, so it&#39;s best to read an excerpt from the <a href="https://pkg.go.dev/github.com/rivo/tview#Grid.SetColumns" target="_blank" rel="noopener noreferrer">documentation</a>:</p>
<p><em>SetColumns defines how the columns of the grid are distributed. Each value defines the size of one column, starting with the leftmost column. Values greater than 0 represent absolute column widths (gaps not included). Values less than or equal to 0 represent proportional column widths or fractions of the remaining free space, where 0 is treated the same as -1. That is, a column with a value of -3 will have three times the width of a column with a value of -1 (or 0). The minimum width set with SetMinSize() is always observed.</em></p>
<p>So in our definition of <code>SetColumns(30, 0, 30)</code> and <code>SetRows(3, 0, 3)</code>, we are basically setting the values for 3 rows and 3 columns, with the <code>0</code> value being the remaining cell space after the first and last of each row/column are set.</p>
<p>We can understand this visually from the screenshots above. Basically, we can expect our footer to double in size if we bump the row size for the footer to <code>6</code>.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2025-08-19-building-tuis-with-golang-and-tview/3-bumping-footer-size.webp" alt="Doubling the footer size" loading="lazy" decoding="async"/><figcaption>Doubling the footer size</figcaption></figure>
<p>Nice.</p>
<h2 id="making-things-interactive">Making things interactive</h2>
<p>From little things, big things grow. With that pearl of wisdom out of the way, let&#39;s add in a few more elements including a <a href="https://github.com/rivo/tview/wiki/InputField" target="_blank" rel="noopener noreferrer">InputField</a> to start to understand a bit more about interactivity.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="go"><code>package main

import (
 &quot;github.com/gdamore/tcell/v2&quot;
 &quot;github.com/rivo/tview&quot;
)

func main() {
 app := tview.NewApplication()
 newPrimitive := func(text string) tview.Primitive {
  return tview.NewTextView().
   SetTextAlign(tview.AlignCenter).
   SetText(text)
 }

 // NEW: Adding the inputField
 inputField := tview.NewInputField().
  SetLabel(&quot;Enter value: &quot;).
  SetFieldWidth(0).
  SetFieldTextColor(tcell.ColorBlack).
  SetFieldBackgroundColor(tcell.ColorWhite).
  SetDoneFunc(func(key tcell.Key) {
   app.Stop()
  })

  // NEW: Adding the flex container
 header := tview.NewFlex().
  SetDirection(tview.FlexRow).
  AddItem(newPrimitive(&quot;Header&quot;), 2, 0, false).
  AddItem(inputField, 0, 1, true)

 menu := newPrimitive(&quot;Menu&quot;)
 main := newPrimitive(&quot;Main content&quot;)
 sideBar := newPrimitive(&quot;Side Bar&quot;)

 grid := tview.NewGrid().
  SetRows(3, 0, 6).
  SetColumns(30, 0, 30).
  SetBorders(true).
  AddItem(header, 0, 0, 1, 3, 0, 0, false).
  AddItem(newPrimitive(&quot;Footer&quot;), 2, 0, 1, 3, 0, 0, false)

 // Layout for screens narrower than 100 cells (menu and side bar are hidden).
 grid.AddItem(menu, 0, 0, 0, 0, 0, 0, false).
  AddItem(main, 1, 0, 1, 3, 0, 0, false).
  AddItem(sideBar, 0, 0, 0, 0, 0, 0, false)

 // Layout for screens wider than 100 cells.
 grid.AddItem(menu, 1, 0, 1, 1, 0, 100, false).
  AddItem(main, 1, 1, 1, 1, 0, 100, false).
  AddItem(sideBar, 1, 2, 1, 1, 0, 100, false)

 if err := app.SetRoot(grid, true).SetFocus(inputField).Run(); err != nil {
  panic(err)
 }
}</code></pre>
<p>In our code above, we&#39;ve updated the grid header code we had before to add in a new <code>header</code> variable that we&#39;ve declared.</p>
<p>That <code>header</code> in itself is adding the primitive text and also the <code>inputField</code> variable which we&#39;ve declared above.</p>
<p>You can start to see a pattern occurring now with the <code>AddItem</code> method also appearing with the <code>tview.NewFlex</code> component. The method in this case has a different method signature, but I&#39;ll omit going over it as you should begin to understand that you can check the docs/intellisense and get a feel for what numeric values change.</p>
<p>The current state of the TUI looks like so:</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2025-08-19-building-tuis-with-golang-and-tview/4-input-value.webp" alt="Seeing our input value" loading="lazy" decoding="async"/><figcaption>Seeing our input value</figcaption></figure>
<p>With the <code>inputField</code> declaration, we can see the <code>SetDoneFunc</code> method that we invoke which takes an anonymous function.</p>
<p>In our case, we&#39;ve added <code>app.Stop()</code> as part of the anonymous function, so when we hit enter it will result in closing our application.</p>
<p>(From the docs) The callback function is provided with the key that was pressed, which is one of the following:</p>
<ul>
<li>KeyEnter: Done entering text.</li>
<li>KeyEscape: Abort text input.</li>
<li>KeyTab: Move to the next field.</li>
<li>KeyBacktab: Move to the previous field.</li>
</ul>
<p>We can make use of this to make our interactivity a little more interesting.</p>
<p>Let&#39;s abstract the main content into a new text view and update our <code>SetDoneFunc</code> anonymous function to update the text with the value added.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="go"><code>package main

import (
 &quot;github.com/gdamore/tcell/v2&quot;
 &quot;github.com/rivo/tview&quot;
)

func main() {
 app := tview.NewApplication()
 newPrimitive := func(text string) tview.Primitive {
  return tview.NewTextView().
   SetTextAlign(tview.AlignCenter).
   SetText(text)
 }

 // NEW: Adding the main content to be updated
 mainContent := tview.NewTextView().
  SetTextAlign(tview.AlignCenter).
  SetText(&quot;Main content - waiting for input...&quot;)

 inputField := tview.NewInputField().
  SetLabel(&quot;Enter value: &quot;).
  SetFieldWidth(0).
  SetFieldTextColor(tcell.ColorBlack).
  SetFieldBackgroundColor(tcell.ColorWhite)

 inputField.SetDoneFunc(func(key tcell.Key) {
  if key == tcell.KeyEnter {
   inputValue := inputField.GetText()
   mainContent.SetText(&quot;You entered: &quot; + inputValue)
   inputField.SetText(&quot;&quot;)
   app.SetFocus(inputField)
  }
 })

 header := tview.NewFlex().
  SetDirection(tview.FlexRow).
  AddItem(newPrimitive(&quot;Header&quot;), 2, 0, false).
  AddItem(inputField, 0, 1, true)

 menu := newPrimitive(&quot;Menu&quot;)
 sideBar := newPrimitive(&quot;Side Bar&quot;)

 grid := tview.NewGrid().
  SetRows(3, 0, 6).
  SetColumns(30, 0, 30).
  SetBorders(true).
  AddItem(header, 0, 0, 1, 3, 0, 0, false).
  AddItem(newPrimitive(&quot;Footer&quot;), 2, 0, 1, 3, 0, 0, false)

 // Layout for screens narrower than 100 cells (menu and side bar are hidden).
 grid.AddItem(menu, 0, 0, 0, 0, 0, 0, false).
  AddItem(mainContent, 1, 0, 1, 3, 0, 0, false).
  AddItem(sideBar, 0, 0, 0, 0, 0, 0, false)

 // Layout for screens wider than 100 cells.
 grid.AddItem(menu, 1, 0, 1, 1, 0, 100, false).
  AddItem(mainContent, 1, 1, 1, 1, 0, 100, false).
  AddItem(sideBar, 1, 2, 1, 1, 0, 100, false)

 if err := app.SetRoot(grid, true).SetFocus(inputField).Run(); err != nil {
  panic(err)
 }
}</code></pre>
<p>Restarting our TUI, we can see what our UI looks like with text in the input:</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2025-08-19-building-tuis-with-golang-and-tview/5-before-hitting-enter.webp" alt="Before hitting enter" loading="lazy" decoding="async"/><figcaption>Before hitting enter</figcaption></figure>
<p>And then after hitting the enter button:</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2025-08-19-building-tuis-with-golang-and-tview/6-after-enter.webp" alt="Text change after hitting enter" loading="lazy" decoding="async"/><figcaption>Text change after hitting enter</figcaption></figure>
<p>Great! Our TUI is looking a little boring, but we are making some progress.</p>
<h2 id="changing-view-focus">Changing view focus</h2>
<p>At this point, our supremely basic TUI can now dynamically update text based on it&#39;s input.</p>
<p>The <code>InputField</code> is currently our initial focus, but how can we swap between different grid items and change focus?</p>
<p>We can introduce the <code>List</code> TUI component and make some changes based on things we&#39;ve already worked with in order to get a simple list we can navigate up and down, while pressing <code>:</code> to focus on the filter bar and <code>enter</code> to re-enter the main list content.</p>
<p>We will also update the footer now to dynamically show different text help messages based on which component we are focused on.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="go"><code>package main

import (
 &quot;strings&quot;

 &quot;github.com/gdamore/tcell/v2&quot;
 &quot;github.com/rivo/tview&quot;
)

func main() {
 app := tview.NewApplication()
 newPrimitive := func(text string) tview.Primitive {
  return tview.NewTextView().
   SetTextAlign(tview.AlignCenter).
   SetText(text)
 }

 // Sample data to filter
 items := []string{&quot;Apple&quot;, &quot;Banana&quot;, &quot;Cherry&quot;, &quot;Date&quot;, &quot;Elderberry&quot;, &quot;Fig&quot;, &quot;Grape&quot;, &quot;Honeydew&quot;, &quot;Kiwi&quot;, &quot;Lemon&quot;, &quot;Mango&quot;, &quot;Orange&quot;}

 // Create the filterable list
 list := tview.NewList().
  ShowSecondaryText(false).
  SetHighlightFullLine(true)

 // Function to populate/filter the list
 updateList := func(filter string) {
  list.Clear()
  for _, item := range items {
   if strings.Contains(strings.ToLower(item), strings.ToLower(filter)) {
    list.AddItem(item, &quot;&quot;, 0, nil)
   }
  }
 }

 // Initially populate with all items
 updateList(&quot;&quot;)

 // Create dynamic footer for guidance text
 footer := tview.NewTextView().
  SetTextAlign(tview.AlignCenter).
  SetText(&quot;↑/↓ Navigate list • : Filter&quot;)

 // Function to update guidance text based on focus
 updateGuidance := func(focusedOnList bool) {
  if focusedOnList {
   footer.SetText(&quot;↑/↓ Navigate list • : Filter&quot;)
  } else {
   footer.SetText(&quot;Type to filter • Enter to focus list&quot;)
  }
 }

 inputField := tview.NewInputField().
  SetLabel(&quot;Filter: &quot;).
  SetFieldWidth(0).
  SetFieldTextColor(tcell.ColorBlack).
  SetFieldBackgroundColor(tcell.ColorWhite)

 // Add real-time filtering as user types
 inputField.SetChangedFunc(func(text string) {
  updateList(text)
 })

 // On Enter, focus the list for navigation
 inputField.SetDoneFunc(func(key tcell.Key) {
  if key == tcell.KeyEnter {
   app.SetFocus(list)
   updateGuidance(true)
  }
 })

 // Add list navigation - return to input on selection
 list.SetDoneFunc(func() {
  app.SetFocus(inputField)
  updateGuidance(false)
 })

 // Handle keys for list: escape to return to input, &#39;:&#39; to focus filter
 list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
  if event.Key() == tcell.KeyEscape {
   app.SetFocus(inputField)
   updateGuidance(false)
   return nil
  } else if event.Rune() == &#39;:&#39; {
   app.SetFocus(inputField)
   updateGuidance(false)
   return nil
  }
  return event
 })

 header := tview.NewFlex().
  SetDirection(tview.FlexRow).
  AddItem(newPrimitive(&quot;Header&quot;), 2, 0, false).
  AddItem(inputField, 0, 1, true)

 menu := newPrimitive(&quot;Menu&quot;)
 sideBar := newPrimitive(&quot;Side Bar&quot;)

 grid := tview.NewGrid().
  SetRows(3, 0, 6).
  SetColumns(30, 0, 30).
  SetBorders(true).
  AddItem(header, 0, 0, 1, 3, 0, 0, false).
  AddItem(footer, 2, 0, 1, 3, 0, 0, false)

 // Layout for screens narrower than 100 cells (menu and side bar are hidden).
 grid.AddItem(menu, 0, 0, 0, 0, 0, 0, false).
  AddItem(list, 1, 0, 1, 3, 0, 0, false).
  AddItem(sideBar, 0, 0, 0, 0, 0, 0, false)

 // Layout for screens wider than 100 cells.
 grid.AddItem(menu, 1, 0, 1, 1, 0, 100, false).
  AddItem(list, 1, 1, 1, 1, 0, 100, false).
  AddItem(sideBar, 1, 2, 1, 1, 0, 100, false)

 if err := app.SetRoot(grid, true).SetFocus(list).Run(); err != nil {
  panic(err)
 }
}</code></pre>
<p>Even though we&#39;ve added quite a bit more code, the only real changes that we&#39;ve made that look new are:</p>
<ol>
<li>The creation of the list.</li>
<li>Abstracting the anonymous function and assigning it to <code>updateList</code> where the logic filters a hardcoded list of strings.</li>
<li>Doing the same with an anonymous function <code>updateGuidance</code> for changing the <code>TextView</code> footer component text.</li>
</ol>
<p>At this point, when we run <code>go run main.go</code> we start focused now on the main content list.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2025-08-19-building-tuis-with-golang-and-tview/7-list-with-navigation-and-filter.webp" alt="Our filterable list" loading="lazy" decoding="async"/><figcaption>Our filterable list</figcaption></figure>
<p>We can toggle up and down to move through the list.</p>
<p>In order to focus on the <code>InputField</code>, we can press <code>:</code>. This focuses on the <code>InputField</code> and updates the footer helper text.</p>
<p>Typing in letters for a basic filter will narrow down the list, and we can hit enter/return in order to focus back on the list and navigate the filtered values.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2025-08-19-building-tuis-with-golang-and-tview/8-filtered-value.webp" alt="Filtered values after typing" loading="lazy" decoding="async"/><figcaption>Filtered values after typing</figcaption></figure>
<h2 id="navigation">Navigation</h2>
<p>Finally, the last thing we will walk through is this idea of &quot;simple navigation&quot; where selecting a value will navigate <em>into</em> a &quot;list detail&quot; page from the &quot;list&quot; view and then enable a way to navigate back.</p>
<p>Again, we&#39;ll hardcode the detail strings for now, but the idea will be that we augment our &quot;listeners&quot; to jump back and forward from the &quot;list&quot; and &quot;detail&quot; view with the enter/escape button respectively.</p>
<p>The code now looks like the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="go"><code>package main

import (
 &quot;strings&quot;

 &quot;github.com/gdamore/tcell/v2&quot;
 &quot;github.com/rivo/tview&quot;
)

func main() {
 app := tview.NewApplication()
 newPrimitive := func(text string) tview.Primitive {
  return tview.NewTextView().
   SetTextAlign(tview.AlignCenter).
   SetText(text)
 }

 // Sample data to filter with details
 items := []string{&quot;Apple&quot;, &quot;Banana&quot;, &quot;Cherry&quot;, &quot;Date&quot;, &quot;Elderberry&quot;, &quot;Fig&quot;, &quot;Grape&quot;, &quot;Honeydew&quot;, &quot;Kiwi&quot;, &quot;Lemon&quot;, &quot;Mango&quot;, &quot;Orange&quot;}

 // Sample detail data for each fruit
 details := map[string]string{
  &quot;Apple&quot;:      &quot;🍎 A crisp, sweet fruit that&#39;s perfect for snacking.\n\nNutritional Facts:\n• High in fiber\n• Rich in vitamin C\n• Contains antioxidants\n\nOrigin: Central Asia&quot;,
  &quot;Banana&quot;:     &quot;🍌 A tropical fruit with potassium and natural sugars.\n\nNutritional Facts:\n• High in potassium\n• Good source of vitamin B6\n• Natural sugars for energy\n\nOrigin: Southeast Asia&quot;,
  &quot;Cherry&quot;:     &quot;🍒 Small, sweet-tart stone fruits perfect for desserts.\n\nNutritional Facts:\n• Rich in antioxidants\n• Contains melatonin\n• Anti-inflammatory properties\n\nOrigin: Asia Minor&quot;,
  &quot;Date&quot;:       &quot;🫒 Sweet, chewy fruits from date palm trees.\n\nNutritional Facts:\n• High in fiber\n• Rich in potassium\n• Natural sweeteners\n\nOrigin: Middle East&quot;,
  &quot;Elderberry&quot;: &quot;🫐 Dark purple berries known for immune support.\n\nNutritional Facts:\n• High in vitamin C\n• Rich in antioxidants\n• Immune system support\n\nOrigin: Europe&quot;,
  &quot;Fig&quot;:        &quot;🟤 Sweet, soft fruits with edible seeds inside.\n\nNutritional Facts:\n• High in fiber\n• Good source of calcium\n• Contains potassium\n\nOrigin: Western Asia&quot;,
  &quot;Grape&quot;:      &quot;🍇 Juicy fruits that can be eaten fresh or made into wine.\n\nNutritional Facts:\n• Contains resveratrol\n• Rich in vitamin K\n• Natural sugars\n\nOrigin: Middle East&quot;,
  &quot;Honeydew&quot;:   &quot;🍈 Sweet, pale green melon with refreshing taste.\n\nNutritional Facts:\n• High water content\n• Rich in vitamin C\n• Low in calories\n\nOrigin: France&quot;,
  &quot;Kiwi&quot;:       &quot;🥝 Fuzzy brown fruit with bright green flesh inside.\n\nNutritional Facts:\n• Extremely high in vitamin C\n• Good source of fiber\n• Contains vitamin K\n\nOrigin: China&quot;,
  &quot;Lemon&quot;:      &quot;🍋 Sour citrus fruit used for cooking and drinks.\n\nNutritional Facts:\n• Very high in vitamin C\n• Contains citric acid\n• Low in calories\n\nOrigin: Asia&quot;,
  &quot;Mango&quot;:      &quot;🥭 Tropical stone fruit with sweet, juicy flesh.\n\nNutritional Facts:\n• High in vitamin A\n• Rich in vitamin C\n• Contains folate\n\nOrigin: India&quot;,
  &quot;Orange&quot;:     &quot;🍊 Citrus fruit packed with vitamin C and fiber.\n\nNutritional Facts:\n• Excellent source of vitamin C\n• High in fiber\n• Contains folate\n\nOrigin: China&quot;,
 }

 // Create the filterable list
 list := tview.NewList().
  ShowSecondaryText(false).
  SetHighlightFullLine(true)

 // Function to populate/filter the list
 updateList := func(filter string) {
  list.Clear()
  for _, item := range items {
   if strings.Contains(strings.ToLower(item), strings.ToLower(filter)) {
    list.AddItem(item, &quot;&quot;, 0, nil)
   }
  }
 }

 // Initially populate with all items
 updateList(&quot;&quot;)

 // Create dynamic footer for guidance text
 footer := tview.NewTextView().
  SetTextAlign(tview.AlignCenter).
  SetText(&quot;↑/↓ Navigate list • : Filter&quot;)

 // Create detail view
 detailView := tview.NewTextView().
  SetDynamicColors(true).
  SetWordWrap(true).
  SetText(&quot;Select an item to view details&quot;)

 // Create input field and header components
 inputField := tview.NewInputField().
  SetLabel(&quot;Filter: &quot;).
  SetFieldWidth(0).
  SetFieldTextColor(tcell.ColorBlack).
  SetFieldBackgroundColor(tcell.ColorWhite)

 header := tview.NewFlex().
  SetDirection(tview.FlexRow).
  AddItem(newPrimitive(&quot;Header&quot;), 2, 0, false).
  AddItem(inputField, 0, 1, true)

 menu := newPrimitive(&quot;Menu&quot;)
 sideBar := newPrimitive(&quot;Side Bar&quot;)

 // Create the grid early so it can be referenced by functions
 grid := tview.NewGrid().
  SetRows(3, 0, 6).
  SetColumns(30, 0, 30).
  SetBorders(true).
  AddItem(header, 0, 0, 1, 3, 0, 0, false).
  AddItem(footer, 2, 0, 1, 3, 0, 0, false)

 // State management for current view
 var currentView string = &quot;list&quot; // &quot;list&quot; or &quot;detail&quot;

 // Function to update guidance text based on current state
 updateGuidance := func() {
  switch currentView {
  case &quot;list&quot;:
   if app.GetFocus() == list {
    footer.SetText(&quot;↑/↓ Navigate • Enter Select • : Filter&quot;)
   } else {
    footer.SetText(&quot;: for focus on filter • Enter to focus list&quot;)
   }
  case &quot;detail&quot;:
   footer.SetText(&quot;Escape to return to list&quot;)
  }
 }

 // Function to show detail view
 showDetail := func(itemName string) {
  if detail, exists := details[itemName]; exists {
   detailView.SetText(detail)
  } else {
   detailView.SetText(&quot;No details available for &quot; + itemName)
  }
  currentView = &quot;detail&quot;
  updateGuidance()

  // Update grid to show detail view
  grid.RemoveItem(list)
  grid.AddItem(detailView, 1, 0, 1, 3, 0, 0, false)   // narrow layout
  grid.AddItem(detailView, 1, 1, 1, 1, 0, 100, false) // wide layout
  app.SetFocus(detailView)
 }

 // Function to show list view
 showList := func() {
  currentView = &quot;list&quot;
  updateGuidance()

  // Update grid to show list view
  grid.RemoveItem(detailView)
  grid.AddItem(list, 1, 0, 1, 3, 0, 0, false)   // narrow layout
  grid.AddItem(list, 1, 1, 1, 1, 0, 100, false) // wide layout
  app.SetFocus(list)
 }

 // Add real-time filtering as user types
 inputField.SetChangedFunc(func(text string) {
  updateList(text)
 })

 // On Enter, focus the list for navigation (ignore Escape)
 inputField.SetDoneFunc(func(key tcell.Key) {
  if key == tcell.KeyEnter {
   app.SetFocus(list)
   updateGuidance()
  }
  // Explicitly ignore Escape key - do nothing
 })

 // Add list navigation - show detail view on selection
 list.SetDoneFunc(func() {
  currentIndex := list.GetCurrentItem()
  itemText, _ := list.GetItemText(currentIndex)
  showDetail(itemText)
 })

 // Handle keys for list: &#39;:&#39; focuses filter, Enter shows detail when on main list screen
 list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
  // Only handle keys when we&#39;re in list view (not detail view)
  switch currentView {
  case &quot;list&quot;:
   if event.Rune() == &#39;:&#39; {
    app.SetFocus(inputField)
    updateGuidance()
    return nil
   } else if event.Key() == tcell.KeyEnter {
    // Explicitly handle Enter to show detail view
    currentIndex := list.GetCurrentItem()
    itemText, _ := list.GetItemText(currentIndex)
    showDetail(itemText)
    return nil
   } else if event.Key() == tcell.KeyEscape {
    // Do nothing
    return nil
   }
  case &quot;detail&quot;:
   if event.Key() == tcell.KeyEscape {
    showList()
    return nil
   }
  }

  // Let all other keys pass through
  return event
 })

 // Layout for screens narrower than 100 cells (menu and side bar are hidden).
 grid.AddItem(menu, 0, 0, 0, 0, 0, 0, false).
  AddItem(list, 1, 0, 1, 3, 0, 0, false).
  AddItem(sideBar, 0, 0, 0, 0, 0, 0, false)

 // Layout for screens wider than 100 cells.
 grid.AddItem(menu, 1, 0, 1, 1, 0, 100, false).
  AddItem(list, 1, 1, 1, 1, 0, 100, false).
  AddItem(sideBar, 1, 2, 1, 1, 0, 100, false)

 // Initialize guidance text
 updateGuidance()

 app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
  if currentView == &quot;detail&quot; &amp;&amp; event.Key() == tcell.KeyEscape {
   showList()
   return nil
  }

  // Let all other keys pass through
  return event
 })

 // Set up global input capture to handle Escape only in detail view
 if err := app.SetRoot(grid, true).SetFocus(list).Run(); err != nil {
  panic(err)
 }
}</code></pre>
<p>It&#39;s fairly rough code, but the <code>list.SetInputCapture</code> anonymous function here handles the filter focus and navigation to the &quot;detail&quot; view.</p>
<p>For navigating back, we use <code>app.SetInputCapture</code> to handle the escape button to invoke <code>showList()</code>.</p>
<p>Running <code>go run main.go</code> shows a familiar landing view for our TUI.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2025-08-19-building-tuis-with-golang-and-tview/9-list-view.webp" alt="Our list view" loading="lazy" decoding="async"/><figcaption>Our list view</figcaption></figure>
<p>Selecting enter onto any of our list items now will shift us into the &quot;detail view&quot; to see our hardcoded detail text that maps to the selected value:</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2025-08-19-building-tuis-with-golang-and-tview/10-detail-view.webp" alt="A detail view for a list item" loading="lazy" decoding="async"/><figcaption>A detail view for a list item</figcaption></figure>
<p>Cool beans. At this point, we can visually see how we can set different views for our TUI.</p>
<h2 id="where-to-next">Where to next?</h2>
<p>At this point, I&#39;ll call it on the blog post. This should be enough to help you on your way to playing around with components and UIs and shows some of the more common workflows you might expect.</p>
<p>As for next steps, you could either augment this simple example to refactor and improve the handlers, or add more handlers for toggling between a number of different views.</p>
<p>Afterwards, instead of having hardcoded values, you could try implementing a TUI that uses dynamic data coming from a database somewhere.</p>
<p>Referring to the <code>tview</code> GitHub wiki and online documentation will go a long way from here.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Today&#39;s post builds up a simple TUI with Golang and <code>tview</code> from first steps until we have a simple application with navigation and filtering.</p>
<p>Although trivial in nature, my hope is that this guide is enough to get you comfortable with where to go from here and how to reference the documentation.</p>
<h2 id="links-and-further-reading">Links and Further Reading</h2>
<ul>
<li><a href="https://k9scli.io/" target="_blank" rel="noopener noreferrer">k9s</a> - Kubernetes CLI for managing clusters</li>
<li><a href="https://github.com/jesseduffield/lazygit" target="_blank" rel="noopener noreferrer">lazygit</a> - Simple terminal UI for git commands</li>
<li><a href="https://github.com/sxyazi/yazi" target="_blank" rel="noopener noreferrer">yazi</a> - Blazing fast terminal file manager</li>
<li><a href="https://github.com/wagoodman/dive" target="_blank" rel="noopener noreferrer">dive</a> - Tool for exploring Docker image layers</li>
<li><a href="https://github.com/rivo/tview" target="_blank" rel="noopener noreferrer">tview</a> - Terminal UI library for Go</li>
<li><a href="https://github.com/rivo/tview/wiki" target="_blank" rel="noopener noreferrer">tview wiki</a> - Official tview documentation and examples</li>
<li><a href="https://pkg.go.dev/github.com/rivo/tview" target="_blank" rel="noopener noreferrer">tview pkg.go documentation</a> - Complete API reference</li>
</ul>
<p><em>Photo credit: <a href="https://unsplash.com/@pcbulai" target="_blank" rel="noopener noreferrer"><code>pcbulai</code></a></em></p>]]></content>
<category term="golang"/>
</entry>
<entry>
<title type="html"><![CDATA[Implementing more manageable transform streams in Node.js]]></title>
<id>https://blog.dennisokeeffe.com/blog/2025-08-03-implementing-more-manageable-transform-streams-in-nodejs</id>
<link href="https://dennisokeeffe.com/blog/2025-08-03-implementing-more-manageable-transform-streams-in-nodejs"/>
<published>2025-08-03T00:00:00.000Z</published>
<updated>2025-08-03T00:00:00.000Z</updated>
<summary type="html"><![CDATA[After working through async generators and class streams, I'm sharing my conclusion on making these streams more manageable]]></summary>
<content type="html"><![CDATA[<p>Over the past few months, I&#39;ve been tinkering with my approach to streams at work when needing to work with requirements that can be efficiently processed with Node.js streams.</p>
<p>This has led to experimentation with both Node.js stream classes and generators.</p>
<p>Although each have their pros and cons, in retrospect I&#39;ve decided that <strong>class streams</strong> are the preference for the work that I am doing.</p>
<p>Some of my personal reasons:</p>
<ol>
<li>They can be easier to reason about.</li>
<li>They can easily have dependency injected with <strong>inversion-of-control</strong> libraries like <strong>Awilix</strong> and <strong>Inversify</strong>.</li>
<li>They don&#39;t require currying or partial application for aforementioned dependency injection.</li>
</ol>
<h2 id="streams-that-i-work-with">Streams that I work with</h2>
<p>Normally you would expect this to be <strong>Strings</strong> or <strong>Buffers</strong>, but in scenarios where I am processing job records.</p>
<p>This has led me to make use of <strong>object mode</strong> and <strong>Transform streams</strong> more often than not in day-to-day work.</p>
<p>Because of this, I&#39;ve also opted to build out an abstract class for me to implement in TypeScript. The purpose of it is to contain the <code>objectMode</code> requirement and require one method that can be used for processing each chunk of a stream.</p>
<h2 id="an-abstract-class-for-object-mode-transforms">An abstract class for object-mode transforms</h2>
<p>The following is a general look at the abstracted stream.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>abstract class ChunkStream&lt;
  TInput = unknown,
  TResult = unknown,
&gt; extends Transform {
  constructor() {
    super({ objectMode: true });
  }

  abstract processChunk(chunk: TInput): TResult;

  async _transform(
    chunk: TInput,
    _encoding: BufferEncoding,
    callback: (error?: Error | null, data?: TResult) =&gt; void
  ): Promise&lt;void&gt; {
    try {
      const result = await this.processChunk(chunk);
      if (result !== undefined &amp;&amp; result !== null) {
        // Handle empty arrays - only push if there&#39;s actual data
        if (Array.isArray(result) &amp;&amp; result.length === 0) {
          callback(); // Don&#39;t push empty arrays
        } else {
          callback(null, result);
        }
      } else {
        callback();
      }
    } catch (error) {
      // Transform callbacks only expect error of type `Error | null`.
      // Handling of this is entirely up to you if you wish to alter
      // this approach.
      if (error instanceof Error) {
        callback(error);
      } else {
        console.warn(&quot;Unhandled error which is not an instanceof Error&quot;, error);
        throw error;
      }
    }
  }
}</code></pre>
<p>As mentioned earlier, the <code>processChunk</code> method is <code>abstract</code> and requires a concrete implementation of that behaviour.</p>
<p>The typing probably leaves more to be desired (it could do with some constraints), but my current implementation expects to take the incoming object chunk and then return a value that will be used as the callback for the transform stream.</p>
<h2 id="implementing-a-concrete-transform-stream">Implementing a concrete transform stream</h2>
<p>A contrived implementation may look like this:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import { err, ok } from &quot;neverthrow&quot;;

type User = {
  name: string;
  age: number;
  color: string;
};

class UserData {
  readonly _tag = &quot;UserData&quot;;

  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

// Making use of the `ChunkStream`
class SetUserDataStream extends ChunkStream {
  #logger: Logger;

  constructor(logger: Logger) {
    super();
    this.#logger = logger;
  }

  // Generator method for processing chunks
  async processChunk(chunk: User) {
    const transformed = new UserData(chunk.name, chunk.age);
    return ok(transformed);
  }

  // OPTIONAL: Override a Transform method
  _flush(callback) {
    this.#logger.info(&quot;Stream completed&quot;);
    callback();
  }
}</code></pre>
<p>The above stream expects an object that structurally matches <code>User</code> as the incoming chunk input and returns that value serialized into a <code>UserData</code> instance and wrapped inside of a <code>Ok</code> result.</p>
<p>There is also an optional <code>_flush</code> method that I have overridden. You can override <strong>any</strong> <code>Transform</code> stream method. <code>_flush</code> has been useful for logging checkpoints at the end of a stream.</p>
<p>In use, it could look something like a long-running script where you fetch batches of data remotely, run a process and then finalize it somewhere:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import { pipeline } from &quot;stream/promises&quot;;

const logger = new Logger();

// Assume this is a script that you&#39;re running to bulk run a long extract-transform-load job
async function main() {
  // Assume all streams in the middle are duplex or transform streams.
  // They could even inherit from our `ChunkStream` class.
  await pipeline(
    // Fetch data from remote
    Readable.from(fetchDataFromRemote()),
    // Serialize data
    new SetUserDataStream(logger),
    // Split into batches of 20 records to process
    new BatchStream(20),
    // Run the upload process
    new UploadToNewDataSource(),
    // Final sink to log results
    new Writable({
      write(
        chunk: User,
        _encoding: BufferEncoding,
        callback: (error?: Error | null) =&gt; void
      ) {
        // Log the output from `UploadToNewDataSource`
        logger.info(&quot;Output:&quot;, chunk);
        callback();
      },
      objectMode: true,
    })
  );
}

main();</code></pre>
<h2 id="testing-with-this-stream">Testing with this stream</h2>
<p>For unit tests on a stream, I do the following:</p>
<ol>
<li>Create a <code>Readable</code> stream for test data used as input.</li>
<li>Create an instance of my <code>Transform</code> stream to test.</li>
<li>Create a <code>Writable</code> &quot;sink&quot; where I push chunk results into an array I can assert against.</li>
</ol>
<p>An example of this:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>it(&quot;should batch items into groups of 2&quot;, async () =&gt; {
  const results: number[][] = [];
  const inputStream = [1, 2, 3, 4];
  const sink = new Writable({
    write(
      chunk: User,
      _encoding: BufferEncoding,
      callback: (error?: Error | null) =&gt; void
    ) {
      results.push(chunk);
      callback();
    },
    objectMode: true,
  });

  expect(results).toBe([
    [1, 2],
    [3, 4],
  ]);
});</code></pre>
<h2 id="caveats-to-this-approach">Caveats to this approach</h2>
<p>Node.js Streams at the time of writing have limited type support from what I could see.</p>
<p>Take my approach with a grain of salt and know that it is there to be improved upon.</p>
<h2 id="links-and-further-reading">Links and Further Reading</h2>
<ul>
<li><a href="https://nodejs.org/api/stream.html#object-mode" target="_blank" rel="noopener noreferrer">Node.js Stream Object Mode</a></li>
<li><a href="https://dennisokeeffe.com/blog/2024-07-12-transform-streams-in-nodejs">Transform Streams in Node.js</a></li>
<li><a href="https://github.com/supermacro/neverthrow" target="_blank" rel="noopener noreferrer">Neverthrow Library</a></li>
</ul>
<p><em>Photo credit: <a href="https://unsplash.com/@pcbulai" target="_blank" rel="noopener noreferrer"><code>pcbulai</code></a></em></p>]]></content>
<category term="nodejs"/><category term="streams"/>
</entry>
<entry>
<title type="html"><![CDATA[Configuring Omarchy on Beelink]]></title>
<id>https://blog.dennisokeeffe.com/blog/2025-07-17-configuring-arch-linux-omarchy-via-mac</id>
<link href="https://dennisokeeffe.com/blog/2025-07-17-configuring-arch-linux-omarchy-via-mac"/>
<published>2025-07-17T00:00:00.000Z</published>
<updated>2025-07-17T00:00:00.000Z</updated>
<summary type="html"><![CDATA[This walkthrough describes the steps taken to load Omarchy, an opinionated archlinux/Hyprland setup, onto my new Beelink SER5.]]></summary>
<content type="html"><![CDATA[<p>The following walks through my installation setup of Omarchy onto my new Beelink SER5.</p>
<p>I loaded the ISO onto a USB using my Mac, so this is a specific recount with these items in case it clarifies anything for others configuring their setup.</p>
<p>I will reference any point where it is best to refer back to DHH&#39;s video or the archlinux docs.</p>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>I operated most of this with my overbearing work laptop settings, and so
admittedly wasn&#39;t able to visit the website documentation directly due to
their strict policies. I am sure they are very good.</p> </div> </aside>
<h2 id="preparing-the-iso-on-a-bootable-usb-drive">Preparing the ISO on a bootable USB drive</h2>
<p>The following is best done alongside the archlinux installation documentation.</p>
<p>First, download the Arch Linux ISO from the official website. <a href="https://archlinux.org/download/" target="_blank" rel="noopener noreferrer">The official docs</a> recommend using a BitTorrent download, however (due to work restrictions) it was required for me to make a HTTP direct download.</p>
<p>For myself in Australia, I found a mirror website to download the ISO directly, and then made use of the <code>b2sum</code> CLI utility in order to verify the BLAKE2b checksums.</p>
<p>To do this, download the <code>b2sum.txt</code> file from the downloads section into the same folder that you downloaded the ISO and run <code>b2sum -c b2sums.txt</code> in order to validate the ISO checksum.</p>
<ul>
<li><strong>b2sum</strong> is a command-line utility for computing and verifying BLAKE2b checksums.</li>
<li><strong>BLAKE2b</strong> is a cryptographic hash function that&#39;s faster than MD5, SHA-1, and SHA-2, while providing high security. It&#39;s used to verify file integrity by comparing computed hashes against known good values.</li>
</ul>
<p>On macOS, you can use tools like <code>dd</code> or graphical utilities like Balena Etcher to create a bootable USB drive.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code># Example using dd (replace /dev/diskX with your USB device)
sudo dd if=archlinux.iso of=/dev/diskX bs=4M status=progress</code></pre>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>Always verify the USB device identifier with <code>diskutil list</code> before using dd
to avoid overwriting the wrong drive.</p> </div> </aside>
<p>This is also listed on the <a href="https://wiki.archlinux.org/title/USB_flash_installation_medium" target="_blank" rel="noopener noreferrer">archlinux USB flash installation guide</a> under <strong>1.3 In macOS</strong>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>dd if=path/to/archlinux-version-x86_64.iso of=/dev/rdiskX bs=1m</code></pre>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <strong>Take note</strong>: the <code>of</code> section prefixes the disk with <code>r</code>. </div> </aside>
<p><strong>dd</strong> (data duplicator) is a low-level copying utility that reads from an input file and writes to an output file. The options used are:</p>
<ul>
<li><code>if=</code> (input file): specifies the source file to read from</li>
<li><code>of=</code> (output file): specifies the destination to write to</li>
<li><code>bs=1m</code> (block size): sets the read/write block size to 1 megabyte for faster copying</li>
<li><code>rdisk</code> prefix: on macOS, this accesses the raw disk device for faster performance</li>
</ul>
<p>The command provided by archlinux will run silently. You can press <code>Ctrl + t</code> to get updates.</p>
<p>Once the USB is loaded, you can dismount the disk and we can plug it into our Beelink SER 5.</p>
<h2 id="entering-uefi-on-the-beelink-ser-5">Entering UEFI on the Beelink SER 5</h2>
<p>Since I pulled out my Beelink SER 5 fresh from the box, so it came preloaded with Windows 11.</p>
<p>We <em>don&#39;t care</em> about that preloaded OS, so we need to boot into <strong>UEFI</strong>.</p>
<p>You may be more familiar with <strong>BIOS</strong>. They are different technologies that do the same job.</p>
<ul>
<li><strong>UEFI</strong> (Unified Extensible Firmware Interface) is the modern successor to BIOS. It provides a more advanced interface with better security features (like Secure Boot), support for larger hard drives (over 2TB), faster boot times, and a graphical interface instead of text-only menus.</li>
<li><strong>BIOS</strong> (Basic Input/Output System) is firmware that initializes and tests hardware components during the boot process. It provides the interface between the operating system and hardware, allowing you to configure boot order, hardware settings, and system parameters before the OS loads.</li>
</ul>
<p>To enter UEFI on the Beelink SER 5:</p>
<ol>
<li>Power on the device</li>
<li>Immediately press and hold the <code>Delete</code> key (or supposedly <code>F2</code>, but I didn&#39;t try) key repeatedly during startup</li>
<li>This will bring you into the UEFI setup menu</li>
<li>Navigate to the boot options and set your USB drive as the first boot priority</li>
<li>Save and exit to boot from the Arch Linux USB</li>
</ol>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>You should be able to identity the USB drive from the description. If it&#39;s
confusing (mine was), then you can validate the drive based on your brand
online.</p> </div> </aside>
<h2 id="running-archinstall">Running archinstall</h2>
<p>At this point, the system will boot into a very basic terminal UI. For most of this section, I followed along with <a href="https://youtu.be/I5Mnni7cea8?si=gpP5_jRFNhNJovnr&amp;t=1147" target="_blank" rel="noopener noreferrer">DHH&#39;s setup tutorial</a>.</p>
<p>We want to run <code>archinstall</code> to open the Arch installer and setup our configurations.</p>
<p>I won&#39;t go too deep into this section since you can follow along with the video, but the general overview is this:</p>
<ol>
<li>Run the Arch bootscreen</li>
<li>(optional for Wifi) Configure WiFi with <code>iwctl</code></li>
<li>Open the <code>archinstall</code> TUI with <code>archinstall</code></li>
<li>Go down top-to-bottom on the left-hand sidebar</li>
<li>(optional) Configure your locales</li>
<li>Select your mirror regions to install from somewhere close to you</li>
<li>Disk configuration with the default layout and using the recommended <code>btrfs</code> file system</li>
<li>Turn on disk encryption (a must for Omarchy) -- select type <code>LUKS</code>, set the password and apply the encrytion</li>
<li>Setup <code>hostname</code></li>
<li>Set root password</li>
<li>Add user account</li>
<li>For Audio, use <code>pipewire</code></li>
<li>For network configuration, copy over what you used for install</li>
<li>For &quot;Additional packages&quot;, ensure <code>wget</code> is installed as it will be used for Omarchy</li>
<li>Save and run the installation</li>
</ol>
<p>At this point, the installer will take over and it can take a few minutes for everything to e configured.</p>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>In DHH&#39;s overview video, his load time was wickedly fast. Not sure what gear
he is working with, but I am very jealous.</p> </div> </aside>
<p>Once you&#39;ve configured <code>archinstall</code> and run the installer, it will prompt you to restart the system.</p>
<p>The following sub-headings are some details that I thought I would callout which goes a little bit deeper than the video:</p>
<h3 id="archinstall">archinstall</h3>
<p><strong>archinstall</strong> is Arch Linux&#39;s official guided installer that provides a text-based interface for installing Arch Linux. It simplifies the traditionally manual installation process by offering:</p>
<ul>
<li>Automated partitioning and filesystem setup</li>
<li>Package selection and installation</li>
<li>Bootloader configuration</li>
<li>User account creation</li>
<li>Network configuration</li>
</ul>
<h3 id="bootloader">bootloader</h3>
<p>A <strong>bootloader</strong> is a small program that loads the operating system kernel
into memory during startup. Common Linux bootloaders include GRUB and
systemd-boot. The bootloader handles the transition from firmware (BIOS/UEFI)
to the operating system, and can manage multiple OS installations on the same
machine.</p>
<h3 id="btrfs">btrfs</h3>
<p><strong>Btrfs</strong> (B-tree file system) is a modern copy-on-write filesystem for Linux that supports advanced features like snapshots, checksums, and built-in RAID. It&#39;s designed for fault tolerance, repair, and easy administration.</p>
<h3 id="zstd">zstd</h3>
<p><strong>Zstd</strong> (Zstandard) is a fast compression algorithm developed by Facebook that provides excellent compression ratios with high speed. It&#39;s commonly used for filesystem compression and package compression in Linux distributions.</p>
<h3 id="wget">wget</h3>
<p><strong>wget</strong> (web get) is a command-line utility for downloading files from web servers using HTTP, HTTPS, and FTP protocols. It supports recursive downloads, resuming interrupted transfers, and can follow redirects. The <code>-qO-</code> options make it run quietly (<code>-q</code>) and output to stdout (<code>-O-</code>) instead of saving to a file.</p>
<h2 id="installing-omarchy">Installing Omarchy</h2>
<p>Once the system reboots, you&#39;ll be prompted to unlock your encrypted disk and then log in with the configurations that you setup before.</p>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>If you&#39;re like me and normally map <code>caps</code> to <code>esc</code> and pulled an uno reverso
on yourself to configure the wrong encryption passphrase for the drive, you&#39;ll
need to make use of <code>sudo cryptsetup luksChangeKey /dev/YOUR_DRIVE_NAME</code> in
order to reset the password. You can use <code>lsblk</code> to find the <code>TYPE=crypt</code>
block device.</p> </div> </aside>
<p>In the previous section, we preemptively configured <code>wget</code>. We can now use it to install Omarchy.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>wget -qO- https://omarchy.org/install | bash</code></pre>
<p>At my time of installation, this will prompt you for your password a few times and request some configurations for Git.</p>
<p>Afterwards, it will just do it&#39;s thing and install DHH&#39;s opinionated setup.</p>
<p>Once completed, you will be prompted to restart the system to apply the changes.</p>
<p>Congratulations, you have now configured Omarchy!</p>
<blockquote>
<p>I use Arch btw</p>
</blockquote>
<h2 id="conclusion">Conclusion</h2>
<p>In this recount, we walked through how I configured my new Beelink SER5 for Omarchy with boot loading. This made use of my Mac for downloading, validating and configuring my USB drive for the Arch Linux ISO.</p>
<p>Hopefully this comes in handy for anyone else who may be keen to make the jump but wants to understand a little bit more about the configuration.</p>
<p>At this point, it&#39;s worth restarting <a href="https://youtu.be/I5Mnni7cea8?si=gpP5_jRFNhNJovnr" target="_blank" rel="noopener noreferrer">DHH&#39;s video</a> for the basics on how to work with Omarchy.</p>
<h2 id="links-and-further-reading">Links and Further Reading</h2>
<ul>
<li><a href="https://omarchy.org/" target="_blank" rel="noopener noreferrer">Omarchy Official Website</a></li>
<li><a href="https://wiki.archlinux.org/title/Installation_guide" target="_blank" rel="noopener noreferrer">Arch Linux Installation Guide</a></li>
<li><a href="https://btrfs.wiki.kernel.org/" target="_blank" rel="noopener noreferrer">Btrfs Wiki</a></li>
<li><a href="https://facebook.github.io/zstd/" target="_blank" rel="noopener noreferrer">Zstandard Compression</a></li>
<li><a href="https://hyprland.org/" target="_blank" rel="noopener noreferrer">Hyprland Window Manager</a></li>
<li><a href="https://37signals.com/" target="_blank" rel="noopener noreferrer">DHH&#39;s 37signals</a></li>
<li><a href="https://youtu.be/I5Mnni7cea8?si=gpP5_jRFNhNJovnr" target="_blank" rel="noopener noreferrer">DHH&#39;s Omarchy introduction video</a></li>
</ul>
<p><em>Photo credit: <a href="https://unsplash.com/@johnnyb803" target="_blank" rel="noopener noreferrer"><code>johnnyb803</code></a></em></p>]]></content>
<category term="linux"/>
</entry>
<entry>
<title type="html"><![CDATA[TypeScript Private Elements vs Private Modifiers]]></title>
<id>https://blog.dennisokeeffe.com/blog/2025-07-16-typescript-private-elements-vs-private-modifiers</id>
<link href="https://dennisokeeffe.com/blog/2025-07-16-typescript-private-elements-vs-private-modifiers"/>
<published>2025-07-16T00:00:00.000Z</published>
<updated>2025-07-16T00:00:00.000Z</updated>
<summary type="html"><![CDATA[We explore private elements in classes and why you should use them over modifiers]]></summary>
<content type="html"><![CDATA[<span id="private-access-modifiers-in-typescript" class="anchor-alias"><h2 id="private-access-modifiers-in-type-script">Private access modifiers in TypeScript</h2>
<p>If you&#39;ve been working with TypeScript for a while, you may be familiar with the <code>public</code>, <code>private</code> and <code>protected</code> <a href="https://www.typescriptlang.org/docs/handbook/2/classes.html#member-visibility" target="_blank" rel="noopener noreferrer">access modifiers</a> for class properties and methods:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>class Modifiers {
  public a: string;
  private b: string;
  protected c: string;

  // ... omitted
}</code></pre>
<p>These access modifiers enforce the member visibility you may be familiar with if you&#39;ve worked in other object-oriented languages before.</p>
<p>The <code>public</code> modifier makes members accessible from anywhere, <code>private</code> restricts access to only within the same class, and <code>protected</code> allows access from the class and its subclasses. By default, all class members are <code>public</code> in TypeScript, so you typically only need to explicitly declare <code>private</code> and <code>protected</code> members.</p>
<p>The problem with this approach is that these modifiers <strong>only apply in TypeScript</strong>. Once the code is transpiled, you can still access the elements on the class as if they were always public.</p>
<p>For an example of this, take the following code:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>class Target {
  private name: string;

  constructor(name: string) {
    this.name = name;
  }

  call() {
    this.log();
  }

  private log() {
    console.log(this.name);
  }
}

class A {
  private target: Target;

  constructor(target: Target) {
    this.target = target;
  }
}

const target = new Target(&quot;name&quot;);
export const a = new A(target);</code></pre>
<p>In this code, we&#39;ve set <code>target</code> as a private property on our contrived class <code>A</code>.</p>
<p>Once we transpile the code, we can see the following out:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>&quot;use strict&quot;;
Object.defineProperty(exports, &quot;__esModule&quot;, { value: true });
exports.b = exports.a = void 0;
class Target {
  name;
  constructor(name) {
    this.name = name;
  }
  call() {
    this.log();
  }
  log() {
    console.log(this.name);
  }
}
class A {
  target;
  constructor(target) {
    this.target = target;
  }
}
const target = new Target(&quot;name&quot;);
exports.a = new A(target);</code></pre>
<p>As we can see, the transpiled code strips the modifiers that were enforced <em>only</em> for type checking purposes.</p>
<p>We can confirm this by making use of this code in the Node REPL:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="s"><code>$ node
Welcome to Node.js v24.3.0.
Type &quot;.help&quot; for more information.
&gt; const {a} = require(&#39;./index.js&#39;)
undefined
&gt; a.target.name
&#39;name&#39;
&gt;</code></pre>
<p>As you can see, we were able to access both our &quot;private&quot; properties <code>target</code>, as well as that variable&#39;s private property <code>name</code>.</p>
<p>So what can we do about this?</p>
<span id="taking-some-wisdom-from-effective-typescript" class="anchor-alias"><h2 id="taking-some-wisdom-from-effective-type-script">Taking some wisdom from Effective TypeScript</h2>
<p>Let&#39;s look at some excerpts from fan-favorite book <a href="https://effectivetypescript.com/" target="_blank" rel="noopener noreferrer">Effective TypeScript</a>:</p>
<p><em>&quot;TypeScript&#39;s visibility modifiers only discourage you from accessing private data. You can even access a private property from within TypeScript using a type assertion or iteration...</em></p>
<p><em>ES2022 officially added support for private fields. Unlike TypeScript&#39;s private, ECMAScript&#39;s private is enforced both for type checking and at runtime. To use it, prefix your class property with a #...</em></p>
<p><em>What about public and protected? In JavaScript (and TypeScript), public is the default visibility so there&#39;s no need to annotate this explicitly. And while private implies encapsulation, protected implies inheritance. The general rule in object-oriented programming is to prefer composition over inheritance, so practical uses of protected are quite rare. readonly as a field modifier is a type-level construct and is fine to use... A field may be both #private and readonly.</em></p>
<p>In summary, the book recommends that we stick to the <code>#private</code> convention and highlights an important point about the <code>protected</code> modifier: object-oriented programming should prefer composition over inheritance.</p>
<span id="private-elements-in-javascript" class="anchor-alias"><h2 id="private-elements-in-java-script">Private elements in JavaScript</h2>
<p>As we saw in the previous section, ECMAScript introduced the <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_elements" target="_blank" rel="noopener noreferrer">private elements</a> with ES2022.</p>
<p>Let&#39;s rewrite our previous example to make use of this:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>class Target {
  #name: string;

  constructor(name: string) {
    this.#name = name;
  }

  call() {
    this.log();
  }

  private log() {
    console.log(this.#name);
  }
}

class A {
  #target: Target;

  constructor(target: Target) {
    this.#target = target;
  }

  call() {
    this.#target.call();
  }
}

const target = new Target(&quot;name&quot;);
export const a = new A(target);</code></pre>
<p>Our contrived classes now replace the <code>private</code> access modifier with the pound <code>#</code> prefix for our private elements.</p>
<p>Our transpiled code looks like the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>&quot;use strict&quot;;
Object.defineProperty(exports, &quot;__esModule&quot;, { value: true });
exports.a = void 0;
class Target {
  #name;
  constructor(name) {
    this.#name = name;
  }
  call() {
    this.log();
  }
  log() {
    console.log(this.#name);
  }
}
class A {
  #target;
  constructor(target) {
    this.#target = target;
  }
  call() {
    this.#target.call();
  }
}
const target = new Target(&quot;name&quot;);
exports.a = new A(target);</code></pre>
<p>Let&#39;s now see what happens in the Node REPL:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="s"><code>$ node
Welcome to Node.js v24.3.0.
Type &quot;.help&quot; for more information.
&gt; const {a} = require(&#39;./index.js&#39;)
undefined
&gt; a.#target.#name
a.#target.#name
 ^

Uncaught SyntaxError: Private field &#39;#target&#39; must be declared in an enclosing class
&gt; a.call()
name
undefined</code></pre>
<p>As we can see, we get true access control with the pound <code>#</code> prefix and must access our private elements through public methods.</p>
<h2 id="conclusion">Conclusion</h2>
<p>In general, it would be wise to opt for ECMAScript standards and make use of the pound <code>#</code> prefix introduced in ES2022 (regardless of how it feels compared to using the keyword).</p>
<p>The private elements approach provides true encapsulation at runtime, making your code more secure and predictable. As highlighted with <code>protected</code>, chances are that if you are using it, you are likely not abiding by the principle of preferring composition over inheritance.</p>
<p>Consider using private elements (<code>#</code>) when you need true privacy that persists after transpilation, and reserve TypeScript&#39;s <code>private</code> modifier for cases where you only need compile-time type checking.</p>
<h2 id="links-and-further-reading">Links and Further Reading</h2>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_elements" target="_blank" rel="noopener noreferrer">MDN | Private Elements</a></li>
<li><a href="https://www.javascripttutorial.net/javascript-private-methods/" target="_blank" rel="noopener noreferrer">JavaScript Tutorial | Private Methods</a></li>
<li><a href="https://effectivetypescript.com/" target="_blank" rel="noopener noreferrer">Effective TypeScript</a></li>
<li><a href="https://www.typescriptlang.org/docs/handbook/2/classes.html#member-visibility" target="_blank" rel="noopener noreferrer">TypeScript | Member Visibility</a></li>
<li><a href="https://www.typescripttutorial.net/typescript-tutorial/typescript-access-modifiers/" target="_blank" rel="noopener noreferrer">TypeScript Tutorial | Access Modifiers</a></li>
</ul>
<p><em>Photo credit: <a href="https://unsplash.com/@timmarshall" target="_blank" rel="noopener noreferrer"><code>timmarshall</code></a></em></p>]]></content>
<category term="typescript"/>
</entry>
<entry>
<title type="html"><![CDATA[Tail Recursion]]></title>
<id>https://blog.dennisokeeffe.com/blog/2025-07-08-tail-recursion</id>
<link href="https://dennisokeeffe.com/blog/2025-07-08-tail-recursion"/>
<published>2025-07-08T00:00:00.000Z</published>
<updated>2025-07-08T00:00:00.000Z</updated>
<summary type="html"><![CDATA[Understand what tail recursion with this visual aide and example in Zig.]]></summary>
<content type="html"><![CDATA[<h2 id="what-is-tail-recursion">What is Tail Recursion?</h2>
<p>Tail recursion is a memory-optimization technique (using tail call optimization) for the call stack.</p>
<p>Instead of piling on more data onto the stack data structure for recursive calls, tail recursion enables the optimization to deallocate the previous function call&#39;s memory prior to making the recursive call.</p>
<p>Not all languages support tail call optimization. Almost all functional languages support it, some systems languages support it, but a number of popular languages such as Python and Ruby <strong>do not</strong> support it. It&#39;s still worth knowing about.</p>
<h2 id="how-can-i-visualize-tail-recursion">How Can I Visualize Tail Recursion?</h2>
<p>Think of a stack of plates.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2025-07-08-tail-recursion/stack-of-plates.webp" alt="Stack of plates" loading="lazy" decoding="async"/><figcaption>Stack of plates</figcaption></figure>
<p>Each plate in the stack represents in a recursive call that is <strong>not</strong> tail call optimized.</p>
<p>Given enough recursive calls, the stack will build up enough, run out of space and cause a stack overflow that will crash the system.</p>
<p>In the case of tail recursion, you can think of this as one plate being added to the stack, but once it finishes it&#39;s run and makes a recursive call, the current &quot;plate&quot; is removed from the stack before the next &quot;plate&quot; is added.</p>
<p>You can think of this as a plate stack with one plate being added and removed for each recursive iteration.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2025-07-08-tail-recursion/one-plate.webp" alt="One plate" loading="lazy" decoding="async"/><figcaption>One plate</figcaption></figure>
<p>This analogy helps to visualize the lifecycle of a call stack and how that data structure works. Regardless of tail recursion, each time you are adding and removing from the stack it is like a stack of plates where you add and remove from the top.</p>
<h2 id="an-example-of-tail-recursion">An Example of Tail Recursion</h2>
<p>I will make use of Zig to compare an approach <strong>with</strong> and <strong>without</strong> tail call optimization being used.</p>
<aside class="callout callout--warning" aria-label="Warning"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><path d="m21.7 18-8-14a2 2 0 0 0-3.4 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.7-3ZM12 9v4M12 17h.01" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>  <div class="callout__body">  <p>Please note, I am no Zig expert and writing Zig requires usage of explicitly
using <code>@call</code> for tail elimination. Please forgive me, I was trying my best!</p> </div> </aside>
<pre class="astro-code night-owl" tabindex="0" data-language="zig"><code>const std = @import(&quot;std&quot;);
const print = std.debug.print;

// Get approximate stack pointer to measure stack usage
fn getStackPointer() usize {
    var dummy: u8 = 0;
    return @intFromPtr(&amp;dummy);
}

fn measureMaxStackUsage() void {
    var max_stack_used: i64 = 0;
    const initial_sp = getStackPointer();

    const result = tailRecursiveWithMeasurement(1000, 1, initial_sp, &amp;max_stack_used);

    print(&quot;=== Maximum Stack Usage Test ===\n&quot;, .{});
    print(&quot;Result: {}\n&quot;, .{result});
    print(&quot;Maximum stack used during recursion: {} bytes\n&quot;, .{max_stack_used});
}

// Separate function that tracks max usage
fn tailRecursiveWithMeasurement(n: u64, acc: u64, initial: usize, max_used: *i64) u64 {
    const current_sp = getStackPointer();
    const used = @as(i64, @intCast(initial)) - @as(i64, @intCast(current_sp));
    if (used &gt; max_used.*) {
        max_used.* = used;
    }

    // Print occasionally to show progress
    if (n % 100 == 0) {
        print(&quot;  n={}, current stack: {} bytes, max so far: {} bytes\n&quot;, .{ n, used, max_used.* });
    }

    if (n &lt;= 1) return acc;
    return @call(.always_tail, tailRecursiveWithMeasurement, .{ n - 1, acc + n, initial, max_used });
}

fn compareStackUsage() void {
    var tail_max: i64 = 0;
    var regular_max: i64 = 0;
    const initial_sp = getStackPointer();

    print(&quot;Testing tail recursion (1000 calls)...\n&quot;, .{});
    _ = tailRecursiveWithMeasurement(1000, 0, initial_sp, &amp;tail_max);

    print(&quot;\nTesting regular recursion (20 calls)...\n&quot;, .{});
    _ = regularRecursiveWithMeasurement(20, initial_sp, &amp;regular_max);

    print(&quot;\n📊 RESULTS:\n&quot;, .{});
    print(&quot;Tail recursion (1000 calls):    {} bytes max\n&quot;, .{tail_max});
    print(&quot;Regular recursion (20 calls):   {} bytes max\n&quot;, .{regular_max});
    print(&quot;Bytes per call - Tail:          {:.1} bytes\n&quot;, .{@as(f64, @floatFromInt(tail_max)) / 1000.0});
    print(&quot;Bytes per call - Regular:       {:.1} bytes\n&quot;, .{@as(f64, @floatFromInt(regular_max)) / 20.0});
}

fn regularRecursiveWithMeasurement(n: u64, initial: usize, max_used: *i64) u64 {
    const current_sp = getStackPointer();
    const used = @as(i64, @intCast(initial)) - @as(i64, @intCast(current_sp));
    if (used &gt; max_used.*) {
        max_used.* = used;
    }

    // Print occasionally to show usage
    if (n % 10 == 0) {
        print(&quot;  n={}, current stack: {} bytes, max so far: {} bytes\n&quot;, .{ n, used, max_used.* });
    }

    if (n &lt;= 1) return 1;
    return 1 + regularRecursiveWithMeasurement(n - 1, initial, max_used);
}

pub fn main() !void {
    print(&quot;\n=== Stack Usage Comparison ===\n&quot;, .{});
    compareStackUsage();
}</code></pre>
<p>Running this code results in the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="zig"><code>$ zig run -O ReleaseFast tail_recursion.zig

=== Stack Usage Comparison ===
Testing tail recursion (1000 calls)...
  n=1000, current stack: -72 bytes, max so far: 0 bytes
  n=900, current stack: -72 bytes, max so far: 0 bytes
  n=800, current stack: -72 bytes, max so far: 0 bytes
  n=700, current stack: -72 bytes, max so far: 0 bytes
  n=600, current stack: -72 bytes, max so far: 0 bytes
  n=500, current stack: -72 bytes, max so far: 0 bytes
  n=400, current stack: -72 bytes, max so far: 0 bytes
  n=300, current stack: -72 bytes, max so far: 0 bytes
  n=200, current stack: -72 bytes, max so far: 0 bytes
  n=100, current stack: -72 bytes, max so far: 0 bytes

Testing regular recursion (20 calls)...
  n=20, current stack: 57 bytes, max so far: 57 bytes
  n=10, current stack: 857 bytes, max so far: 857 bytes

📊 RESULTS:
Tail recursion (1000 calls):    0 bytes max
Regular recursion (20 calls):   1577 bytes max
Bytes per call - Tail:          0.0e0 bytes
Bytes per call - Regular:       7.9e1 bytes</code></pre>
<p>The <code>-72 bytes</code> for the tail recursion is a bit of noise, but you can see in the difference here that 20 calls with regular recursion used up around 8 bytes for invocation, while the tail recursion resulted in no memory buildup.</p>
<h2 id="links-and-further-reading">Links and Further Reading</h2>
<ul>
<li><a href="https://ziggit.dev/t/recursive-calling-strategy/6979" target="_blank" rel="noopener noreferrer">Ziggit | Recursive calling strategy</a></li>
<li><a href="https://en.wikipedia.org/wiki/Tail_call" target="_blank" rel="noopener noreferrer">Wikipedia | Tail call</a></li>
</ul>
<p><em>Photo credit: <a href="https://unsplash.com/@nervum" target="_blank" rel="noopener noreferrer"><code>nervum</code></a></em></p>]]></content>
<category term="zig"/>
</entry>
<entry>
<title type="html"><![CDATA[Data Classes in TypeScript]]></title>
<id>https://blog.dennisokeeffe.com/blog/2025-06-23-typescript-data-classes</id>
<link href="https://dennisokeeffe.com/blog/2025-06-23-typescript-data-classes"/>
<published>2025-06-23T00:00:00.000Z</published>
<updated>2025-07-02T00:00:00.000Z</updated>
<summary type="html"><![CDATA[Learn how classes focused on holding data can clean up type definitions and provide discriminants for your TypeScript code.]]></summary>
<content type="html"><![CDATA[<p>This post considers the concept of &quot;data classes&quot; as defined with <a href="https://kotlinlang.org/docs/data-classes.html" target="_blank" rel="noopener noreferrer">Kotlin</a> and focuses on some of the type benefits we might get within TypeScript with tagged/discriminant unions.</p>
<p>Please note that this is a very loose use of the term and it certainly does not plan on focuses on the methods that come with data classes in Kotlin.</p>
<h2 id="what-are-data-classes">What are data classes?</h2>
<p>As defined in the Kotlin documentation, &quot;Data classes ... are primarily used to hold data. For each data class, the compiler automatically generates additional member functions ... Data classes are marked with data&quot;.</p>
<p>So for an example definition in Kotlin, you could define a data class with <code>data class User(val name: String, val age: Int)</code>.</p>
<p>As mentioned, I&#39;m not too focused on the additional methods that come with data classes. There already is another library <a href="https://github.com/alexeyraspopov/dataclass" target="_blank" rel="noopener noreferrer">alexeyraspopov/dataclass</a> that enables something like this.</p>
<p>What I am interested in is the type properties as it relates to TypeScript that come with the idea of storing data in classes that are, by designed, <em>not meant to add any additional functionality</em>.</p>
<p>I want to explore the idea of using these data containers as classes.</p>
<span id="how-can-these-help-in-typescript" class="anchor-alias"><h2 id="how-can-these-help-in-type-script">How can these help in TypeScript?</h2>
<p>TypeScript is a structural type system as opposed to a nominal type system.</p>
<p>For an example of what I mean by structural type, take the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>interface User {
  name: string;
}

function uppercaseUserName(user: User) {
  return user.name.toUpperCase();
}

const obj = {
  name: &quot;something&quot;,
};

console.log(uppercaseUserName(obj));</code></pre>
<p>In the above code, there are no type errors <em>even though</em> our <code>obj</code> is not of type <code>User</code>. TypeScript doesn&#39;t complain because it satifies the expected structure of a <code>User</code> interface.</p>
<p>In fact, even if we add more properties to <code>obj</code>, it still won&#39;t complain:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>interface User {
  name: string;
}

function uppercaseUserName(user: User) {
  return user.name.toUpperCase();
}

const obj = {
  name: &quot;something&quot;,
  age: 22,
  address: &quot;123 fake street&quot;,
};

console.log(uppercaseUserName(obj));</code></pre>
<p>This is in contrast to a nominal type system where <strong>it would expect the input to be of type User</strong>.</p>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>There are some reasons TypeScript opted for structural typing that is
out-the-scope of this blog post.</p> </div> </aside>
<p>Let&#39;s say in fact that we <em>do</em> want to ensure that the type matches, we could do a little tinkering to refactor the above to the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>class DataUser {
  public name: string;

  constructor(name: string) {
    this.name = name;
  }
}

function uppercaseUserName(user: DataUser) {
  return user.name.toUpperCase();
}

const obj = {
  name: &quot;something&quot;,
  age: 22,
  address: &quot;123 fake street&quot;,
};

const user = new DataUser(obj.name);
console.log(uppercaseUserName(user));</code></pre>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>Note: I personally write the more verbose constructor declaration out instead
of <code>constructor(public name: string) {}</code>. Even though it is far more succint,
it is not enabled under <code>--erasableSyntaxOnly</code> in TypeScript as it transforms
that signature into the more verbose version through code generation.</p> </div> </aside>
<p>In the above code, we now explicitly create a <code>DataUser</code> instance. Note that the properties are <code>public</code> which is by design. The mental model you can use is that we are effectively encasing the object we want within a class.</p>
<p>This also looks to be similar to how Kotlin goes about this:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="kotlin"><code>val jane = User(&quot;Jane&quot;, 35)
val (name, age) = jane
println(&quot;$name, $age years of age&quot;)
// Jane, 35 years of age</code></pre>
<p>In TypeScript, some of the benefits that we get so far is that <strong>because classes themselves are a type</strong>, we don&#39;t need to declare a separate interface for these. The classes type themselves. On large-scale databases where you have many different shapes of objects being passed around, this can also be blessing for code volume reduction.</p>
<h2 id="getting-benefits-from-discriminants">Getting benefits from discriminants</h2>
<p>We can extend these benefits by introducing a discriminant into the classes:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>class DataUser {
  readonly _tag = &quot;DataUser&quot;;

  public name: string;

  constructor(name: string) {
    this.name = name;
  }
}</code></pre>
<p>In the above code, we&#39;ve now added <code>readonly _tag = &quot;DataUser&quot;</code>. But what&#39;s the value of this?</p>
<p>Adding a type &quot;discriminant&quot;, also know as a &quot;tag&quot;, is what enables the type narrowing capabilities for disciminant/tagged unions in TypeScript.</p>
<p>Take this contrived code example:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>interface User {
  name: string;
}

interface Address {
  address: string;
}

function getUserOrAddress({
  name,
  address,
}: {
  name: string;
  address: string;
}): User | Address {
  if (Math.random() &gt; 0.5) {
    return {
      name,
    };
  }

  return {
    address,
  };
}

const obj = {
  name: &quot;something&quot;,
  age: 22,
  address: &quot;123 fake&quot;,
};

const result = getUserOrAddress(obj);</code></pre>
<p>In the above code, we get no type errors due to structural typing by the return type of <code>result</code> is <code>User | Address</code>.</p>
<p>If we start a new line with <code>result.</code>, you&#39;ll notice that we don&#39;t get any type completion. The reason for this is that we need to first narrow down the type in order for TypeScript to be smart enough to know what to do.</p>
<p>In order to do this at the moment is a little shaky and specific to the output type:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>if (&quot;name&quot; in result) {
  result.name;
} else if (&quot;address&quot; in result) {
  result.address;
}</code></pre>
<p>Using this helper will assist TypeScript in knowing what types we are dealing with. Inside the <code>&#39;name&#39; in result</code> block, TypeScript now knows that the type must be a <code>User</code>, while in the other if block it knows that it must be <code>Address</code> so we get our type completion assistance back.</p>
<p>As mentioned before, this is clunky and very specific to the situation. If we add another one for age, we need the same <code>&#39;age&#39; in result</code> helper and this will change for other functions that can return different types.</p>
<p>This is where tagged unions come to the rescue:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>interface User {
  _tag: &quot;User&quot;;
  name: string;
}

interface Address {
  _tag: &quot;Address&quot;;
  address: string;
}

function getUserOrAddress({
  name,
  address,
}: {
  name: string;
  address: string;
}): User | Address {
  if (Math.random() &gt; 0.5) {
    return {
      _tag: &quot;User&quot;,
      name,
    };
  }

  return {
    _tag: &quot;Address&quot;,
    address,
  };
}

const obj = {
  name: &quot;something&quot;,
  age: 22,
  address: &quot;123 fake&quot;,
};

const result = getUserOrAddress(obj);

switch (result._tag) {
  case &quot;User&quot;:
    console.log(&quot;is user&quot;);
    break;
  case &quot;Address&quot;:
    console.log(&quot;is address&quot;);
}</code></pre>
<p>In our interfaces now, <code>_tag</code> is our disciminant. Because our union type all includes <code>_tag</code>, our <code>result</code> response knows that it has that one common property between all possible return types.</p>
<p>We can use this to our advantage. In the <code>switch</code> block, we switch on the <code>_tag</code> property and, because TypeScript is smart enough to know all possible disciminant values, is can help us autocomplete and narrow does the types that way.</p>
<p>The beauty of this approach is that if you set a common disciminant across your data, you can <strong>always</strong> rely on this approach to help you narrow types down.</p>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>For what it&#39;s worth, your disciminant property does not need to be <code>_tag</code>.
I&#39;ve taken this from Effect TS but given that they are literally called
tagged/discriminant unions, I think the naming is apt and the underscore
prefix helps to highlight it as &quot;metadata&quot;.</p> </div> </aside>
<h2 id="applying-discriminants-to-our-data-classes">Applying discriminants to our data classes</h2>
<p>Let&#39;s convert our current code example back to using &quot;data&quot; classes:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>class DataUser {
  readonly _tag = &quot;DataUser&quot;;
  public name: string;

  constructor(name: string) {
    this.name = name;
  }
}

class DataAddress {
  readonly _tag = &quot;DataAddress&quot;;
  public name: string;

  constructor(name: string) {
    this.name = name;
  }
}

function getUserOrAddress({
  name,
  address,
}: {
  name: string;
  address: string;
}): DataUser | DataAddress {
  if (Math.random() &gt; 0.5) {
    return new DataUser(name);
  }

  return new DataAddress(address);
}

const obj = {
  name: &quot;something&quot;,
  age: 22,
  address: &quot;123 fake&quot;,
};

const result = getUserOrAddress(obj);

switch (result._tag) {
  case &quot;DataUser&quot;:
    console.log(&quot;is user&quot;);
    break;
  case &quot;DataAddress&quot;:
    console.log(&quot;is address&quot;);
}</code></pre>
<p>We rewrite the classes to have a <code>readonly _tag</code> property which, assuming we keep this unique across our codebase, can now always be used as a discriminant for type narrowing.</p>
<p>This also simplies the return values to be the creation of the data class instances.</p>
<p>Of course, <code>readonly _tag</code> is something that needs to be enforced at a behavioural level. You can of course assign this to an interface/class and implement/inherit the requirement for this property, but it feels like too much overhead to me.</p>
<p>You should know pretty quickly if someone hasn&#39;t added a <code>readonly _tag</code> property if your TypeScript types widen. Correcting this also should be simple enough.</p>
<h2 id="an-opinion-on-classes-and-design-patterns">An opinion on classes and design patterns</h2>
<p>Something I am very bullish on is avoiding premature abstraction. It&#39;s one of those things I keep close to my heart <em>because</em> I&#39;ve always been the perpetrator for it and burned myself.</p>
<p>I personally believe that object-orientated design patterns can be the enemy for this if you do not stay vigilant.</p>
<p>The reason that I give this &quot;data container&quot; a pass (at least as I currently mull over them) is purely because of the lack of functionality they provide.</p>
<p>Other than defining the shape of the object I want to work with, there is no ping ponging back and forth between a business layer and data class. It feels like it can give the same level of declarativeness that you get from assigning objects inline (hopefully I am conveying what I mean on this well enough).</p>
<h2 id="usage-with-branded-types">Usage with branded types</h2>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>This section was added on July 2nd, 2025 and covers some more on using branded
types for this pattern.</p> </div> </aside>
<p>What happens when you share a common data shape but want to be able to differentiate more on the tagged union <code>_tag</code> property?</p>
<p>In a scenario like this, we can introduce the idea of <strong>branded types</strong> to our data class using generics.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>type SchemeType = &quot;Visa&quot; | &quot;MasterCard&quot;;
type SchemeTypeDataUser = `${SchemeType}DataUser`;

class DataUser&lt;T extends SchemeTypeDataUser = SchemeTypeDataUser&gt; {
  readonly _tag;
  name: string;
  age: number;

  constructor(tag: T, name: string, age: number) {
    this._tag = tag;
    this.name = name;
    this.age = age;
  }
}

const mc = new DataUser(&quot;MasterCardDataUser&quot;, &quot;Dennis&quot;, 33);
const visa = new DataUser(&quot;VisaDataUser&quot;, &quot;Dan&quot;, 21);

mc._tag; // typed as &quot;MasterCardDataUser&quot;
visa._tag; // typed as &quot;VisaDataUser&quot;</code></pre>
<p>In the above code, I make use of the <code>SchemeTypeDataUser</code> to set a &quot;branded&quot; type. This enables some re-use on a model that we may use differently based on context.</p>
<p>We use generics here in order to still enforce the <code>_tag</code> property to be a readonly literal. Without it, our <code>_tag</code> is cast to a <code>string</code>.</p>
<h2 id="branded-types-with-a-private-constructor">Branded types with a private constructor</h2>
<p>Taking things one step further, you can also enforce the use of static methods if you want to have instances built with the tagged union property baked in.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>type SchemeType = &quot;Visa&quot; | &quot;MasterCard&quot;;
type SchemeTypeDataUser = `${SchemeType}DataUser`;

class DataUser&lt;T extends SchemeTypeDataUser = SchemeTypeDataUser&gt; {
  readonly _tag;
  name: string;
  age: number;

  private constructor(tag: T, name: string, age: number) {
    this._tag = tag;
    this.name = name;
    this.age = age;
  }

  static mastercard(name: string, age: number) {
    return new DataUser(&quot;MasterCardDataUser&quot;, name, age);
  }

  static visa(name: string, age: number) {
    return new DataUser(&quot;VisaDataUser&quot;, name, age);
  }
}

const mc = DataUser.mastercard(&quot;Dennis&quot;, 33);
const visa = DataUser.visa(&quot;Dan&quot;, 21);

mc._tag; // typed as &quot;MasterCardDataUser&quot;
visa._tag; // typed as &quot;VisaDataUser&quot;

// !!! TypeScript error due to the private constructor
// const cantDo = new DataUser(&#39;MasterCardDataUser&#39;, &#39;test&#39;, 25);</code></pre>
<p>In the above code, we add two static properties which themselves call the private constructor in order to return the instances with a specific value already set for the branded type.</p>
<h2 id="conclusion">Conclusion</h2>
<p>This post speaks to the idea of containing and passing your key objects around using a &quot;data class&quot;.</p>
<p>In a recent update, we also explored the idea of using branded types and private constructors in order to get re-use out of the classes themselves.</p>
<p>It can work well with the structural nature of the TypeScript type system and using a common discriminant across the codebase can you simplify your control flow.</p>
<p>As a friendly-reminder though: These representations are not actually data classes in TypeScript. They are just inspired by the concept. The type system won&#39;t help you if you or a team member opt to start adding in instance methods. You need to find and apply another lint rule to support something like that.</p>
<h2 id="links-and-further-reading">Links and Further Reading</h2>
<ul>
<li><a href="https://kotlinlang.org/docs/data-classes.html" target="_blank" rel="noopener noreferrer">Kotlin | Data classes</a></li>
<li><a href="https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions" target="_blank" rel="noopener noreferrer">TypeScript | Discriminated unions</a></li>
</ul>
<p><em>Photo credit: <a href="https://unsplash.com/@josholalde" target="_blank" rel="noopener noreferrer"><code>josholalde</code></a></em></p>]]></content>
<category term="typescript"/><category term="kotlin"/>
</entry>
<entry>
<title type="html"><![CDATA[Isolating PostgreSQL Tests with PGLite]]></title>
<id>https://blog.dennisokeeffe.com/blog/2025-06-09-isolating-postgresql-tests-with-pglite</id>
<link href="https://dennisokeeffe.com/blog/2025-06-09-isolating-postgresql-tests-with-pglite"/>
<published>2025-06-09T00:00:00.000Z</published>
<updated>2025-06-09T00:00:00.000Z</updated>
<summary type="html"><![CDATA[Learn how to rapidly extend your test boundaries to the database without losing speed or introducing flaky tests.]]></summary>
<content type="html"><![CDATA[<p>This post introduces <a href>PGLite</a> as an in-memory way to run tests without compromising too much on speed or isolation problems.</p>
<p>We will be using <a href>Prisma</a> and <a href>Vitest</a> to demonstrate this, but please note that this approach is just as applicable to using alternative test runners and PostgresQL-compatible libraries such as Jest and Drizzle.</p>
<p>Example code used for this blog post can be found <a href="https://github.com/okeeffed/blog-pglite-vitest-prisma" target="_blank" rel="noopener noreferrer">on my GitHub</a>.</p>
<h2 id="testing-purgatory">Testing purgatory</h2>
<p>There are four things that lead to testing purgatory:</p>
<ol>
<li>Boundaries</li>
<li>Isolation</li>
<li>Mocking</li>
<li>Asserting side-effects</li>
</ol>
<p>If your test boundaries are too narrow, you lose test confidence.</p>
<p>If you fail to properly isolate your tests, it tends towards flakiness.</p>
<p>If you over-mock or mock the wrong things, you introduce unnecessary code volume to your application and present an inaccurate reflection of your application.</p>
<p>Finally, if you over-assert side-effects within a module that are separate to the desired functionality of the program, you run the risk of false negatives once refactoring occurs.</p>
<span id="how-can-pglite-help-with-isolation" class="anchor-alias"><h2 id="how-can-pg-lite-help-with-isolation">How can PGLite help with isolation?</h2>
<p><a href="https://pglite.dev/" target="_blank" rel="noopener noreferrer">PGLite</a> is full PostgreSQL database run using Web Assembly.</p>
<p>While some benefits are beyond the scope of this blog post, the main focus is on the fact that it&#39;s lightweight, supports the PostgreSQL feature set and extensions.</p>
<p>This also introduces us a perfect use-case for it: a drop-in replacement that can be used for testing.</p>
<p>The way that test runners like Vitest work is that they run tests sequentially within each file. Beyond that, the test runners attempt to parallelize the test runs whenever they can.</p>
<p>We can control this behavior by reducing the amount of parallel testing that can occur through test configuration files or the CLI in order to run tests serially.</p>
<p>In Vitest, there are a <a href="https://vitest.dev/guide/improving-performance" target="_blank" rel="noopener noreferrer">few options</a> provided around this:</p>
<ul>
<li><code>threads</code> pool runs every test file in a separate Worker.</li>
<li><code>forks</code> pool runs every test file in a separate forked child process.</li>
<li><code>vmThreads</code> pool runs every test file in a separate VM context, but it uses workers for parallelism.</li>
</ul>
<blockquote>
<p>If you are more familiar with Jest, then you may have used <code>--runInBand</code>. The <code>--runInBand</code> flag in Jest forces all tests to run serially in the same process rather than creating a worker pool of child processes.</p>
</blockquote>
<p>By default, Vitest runs a <code>forks</code> worker pool.</p>
<p>Selecting values for parallelism and worker pools is important for a number of reasons (including test speed in resource-constrained environments).</p>
<p>One of those reasons is to prevent cross-test access to shared resources such as databases. Failing to isolate this will lead to dirty reads or false assumptions about the current state of the database and, in turn, flaky tests.</p>
<p>Given the lightweight nature of PGLite and knowing that tests are parallelized per test file but serially run within, we can workaround this problem by spinning up an in-memory database for every test file.</p>
<h2 id="understanding-alternative-approaches">Understanding alternative approaches</h2>
<p>Before going too far, it&#39;s worth knowing some other approaches and what their trade-offs are.</p>
<p>In the scenario of spinning up database for tests to interact with, we already know that would be required to run tests in isolation to prevent flakiness.</p>
<p>In a previous post, <a href="https://dennisokeeffe.com/blog/2025-03-29-the-boundaries-of-typescript-testing">The Boundaries of TypeScript Testing</a>, I demonstrating writing tests using <a href="https://testcontainers.com/" target="_blank" rel="noopener noreferrer">TestContainers</a>, which can also be a great way to introduce parallelism through test sharding.</p>
<p>Test shards will split up tests in <code>N</code> shards (where <code>N</code> is the number of shards nominated by you). Doing so while also spinning up <code>N</code> test containers can re-enable parallelism to an extent, but each test shard still needs to run their own tests serially within the shard.</p>
<p>When working with databases that do not offer in-memory solutions, <a href="https://testcontainers.com/" target="_blank" rel="noopener noreferrer">TestContainers</a> can still be your best bet for enable some form of parallelism with test shards.</p>
<span id="configuring-vitest-and-prisma-for-in-memory-pglite" class="anchor-alias"><h2 id="configuring-vitest-and-prisma-for-in-memory-pg-lite">Configuring Vitest and Prisma for in-memory PGLite</h2>
<p>In my demo code, I&#39;ve followed the <a href="https://www.prisma.io/docs/getting-started" target="_blank" rel="noopener noreferrer">Getting Started</a> guide for Prisma and made use of that demonstration schema.</p>
<p>Following that, Vitest needs to be installed and configured to have following options:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>// vitest.config.ts
import { defineConfig } from &quot;vitest/config&quot;;
import { cpus } from &quot;node:os&quot;;

const maxThreads = Math.max(1, cpus().length - 1);

export default defineConfig({
  test: {
    pool: &quot;threads&quot;,
    poolOptions: {
      threads: {
        maxThreads,
        minThreads: 1,
        isolate: true,
      },
    },

    setupFiles: [&quot;./src/test/setup.ts&quot;],
    globalSetup: &quot;./src/test/global-setup.ts&quot;,

    // Add the types file to be loaded automatically
    typecheck: {
      include: [&quot;**/*.{test,spec}.{js,ts,jsx,tsx}&quot;],
    },

    testTimeout: 15000,
    hookTimeout: 10000,

    include: [&quot;**/*.{test,spec}.{js,ts,jsx,tsx}&quot;],
    exclude: [&quot;node_modules&quot;, &quot;dist&quot;, &quot;build&quot;],

    environment: &quot;node&quot;,
  },
});</code></pre>
<p>Most of it this is default configuration, but importantly we have configured a global setup file, a setup file for tests and updated our worker pool options.</p>
<p>In our case specifically, I&#39;ve opted to run isolated threads and am making use of <code>node:os</code> (since I am using Node.js in this post) to provide a limit for <code>maxThreads</code>. I&#39;ve set <code>-1</code> on the count from the max CPU cores in order not to detract compute power away from other processes. You will likely want this or <code>-2</code>.</p>
<blockquote>
<p>Please note: it&#39;s worth reviewing the configuration options and refining your configuration for what suits. Do not forget that your CI test runner environment may be different to local.</p>
</blockquote>
<p>Files added to <code>globalSetup</code> run once prior to running the tests, while the files within <code>setupFiles</code> run before each test file.</p>
<p>You want to configure it so that <code>globalSetup</code> will prepare what we need for our PGLite database while <code>setupFiles</code> will configure the in-memory database itself and allow it to be accessed for the files tests.</p>
<h2 id="configuring-our-global-setup-file">Configuring our global setup file</h2>
<p>Starting with the global setup file, we can add the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import { generateDbInitSql } from &quot;./db-setup&quot;;

export async function setup() {
  console.log(&quot;🔧 Setting up test environment...&quot;);

  try {
    // Generate the schema SQL
    await generateDbInitSql();
    console.log(&quot;✅ Database schema generated and cached&quot;);
  } catch (error) {
    console.error(&quot;❌ Failed to generate database schema:&quot;, error);
    throw error;
  }
}

export async function teardown() {
  console.log(&quot;🧹 Global teardown complete&quot;);
}</code></pre>
<p>In Vitest, the <code>setup</code> and <code>teardown</code> functions are key functions that the test runner looks for. In my case (and to keep it simple), I&#39;m running a <code>generateDbInitSql</code> function as part of global setup.</p>
<p>My <code>db-setup.ts</code> file looks like the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import { readFileSync } from &quot;node:fs&quot;;

import type { PrismaClient } from &quot;../../generated/prisma&quot;;
import type { PGlite } from &quot;@electric-sql/pglite&quot;;
import { exec } from &quot;node:child_process&quot;;
import { promisify } from &quot;node:util&quot;;
import { resolve } from &quot;node:path&quot;;
import { cwd } from &quot;node:process&quot;;
const execAsync = promisify(exec);

declare global {
  var testPrisma: PrismaClient;
  var testDb: PGlite;
}

const migrationOutputPath = resolve(cwd(), &quot;tmp/migration.sql&quot;);

/**
 * The Prisma CLI does not (yet) support initializing a PGLite database. Instead, we get Prisma to
 * dump a single migration file which can then be loaded by a PGLite instance to setup the
 * database.
 *
 * @see https://github.com/lucasthevenet/pglite-utils/issues/8#issuecomment-2147944548
 */
export async function generateDbInitSql() {
  const prismaSchemaPath = resolve(cwd(), &quot;prisma/schema.prisma&quot;);
  await execAsync(
    `prisma migrate diff --from-empty --to-schema-datamodel ${prismaSchemaPath} --script &gt; ${migrationOutputPath}`
  );
}

export function getSchemaSql() {
  return readFileSync(migrationOutputPath, &quot;utf-8&quot;);
}</code></pre>
<p>In the file, I am running a Prisma shell command in order to generate a single migration file that can be used for setting up the database for each test.</p>
<blockquote>
<p>At the time of writing, Prisma does not expose an API to do this programmatically.</p>
</blockquote>
<p>In my case, the output file will be placed locally within a <code>tmp/</code> folder. This will run everytime we re-run tests.</p>
<h2 id="configuring-our-test-setup-file">Configuring our test setup file</h2>
<p>The <code>setup</code> file adds some hooks to our before/after blocks to configure the PGLite database for Prisma and to set that to our globals:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import { beforeAll, afterAll, beforeEach } from &quot;vitest&quot;;
import { PGlite } from &quot;@electric-sql/pglite&quot;;
import { PrismaPGlite } from &quot;pglite-prisma-adapter&quot;;
import { PrismaClient } from &quot;../../generated/prisma&quot;;
import { getSchemaSql } from &quot;./db-setup&quot;;

let db: PGlite;
let prisma: PrismaClient;

beforeAll(async () =&gt; {
  console.log(&quot;🚀 Setting up database for test file...&quot;);

  db = new PGlite({
    dataDir: &quot;memory://&quot;,
  });

  await db.exec(getSchemaSql());

  prisma = new PrismaClient({
    adapter: new PrismaPGlite(db),
  });

  // Make available globally
  global.testDb = db;
  global.testPrisma = prisma;

  console.log(&quot;✅ Database ready!&quot;);
});

afterAll(async () =&gt; {
  console.log(&quot;🧹 Cleaning up database...&quot;);

  try {
    await prisma.$disconnect();
    await db.close();
  } catch (error) {
    console.warn(&quot;Cleanup warning:&quot;, error);
  }

  console.log(&quot;✅ Database cleaned up!&quot;);
});

beforeEach(async () =&gt; {
  // Get all table names excluding Prisma migration tables
  const result = await db.query&lt;{ tablename: string }&gt;(`
      SELECT tablename 
      FROM pg_tables 
      WHERE schemaname = &#39;public&#39; 
      AND tablename NOT LIKE &#39;_prisma%&#39;
      ORDER BY tablename;
    `);

  const tableNames = result.rows.map((row) =&gt; row.tablename);

  if (tableNames.length &gt; 0) {
    // Disable foreign key checks, truncate all tables, re-enable
    await db.exec(`
        SET session_replication_role = replica;
        TRUNCATE TABLE ${tableNames
          .map((name) =&gt; `&quot;${name}&quot;`)
          .join(&quot;, &quot;)} RESTART IDENTITY CASCADE;
        SET session_replication_role = DEFAULT;
      `);
  }
});</code></pre>
<p>The <code>beforeAll</code> block reads in the migration SQL file, configures the PGLite client, applies the migration and then sets up a Prisma client and assigns them to globals we can access during our tests.</p>
<p>The <code>afterAll</code> block run after all the tests runs the Prisma disconnect and closes the in-memory database.</p>
<p>Finally, our <code>beforeEach</code> below will ensure the data in the database has been truncated (removed) in order to start each test with a clean start. This is incredibly important for the testing model <code>arrange-act-assert</code>.</p>
<blockquote>
<p>You may in fact want to have seed database data for the tests. In that scenario, you should do some experimentation with seeding during <code>beforeAll</code> and working on how you will cleanup tables after tests to prevent polluting the data.</p>
</blockquote>
<p>At this point, we are ready to run some tests.</p>
<h2 id="creating-a-file-to-write-tests-against">Creating a file to write tests against</h2>
<p>I opted to write a simple repository file that had some actions we can write tests for:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import type { PrismaClient, Role } from &quot;../generated/prisma&quot;;

export interface CreateUserData {
  email: string;
  name?: string;
  role?: Role;
}

export interface UpdateUserData {
  email?: string;
  name?: string;
  role?: Role;
}

export class UserRepository {
  private prisma: PrismaClient;

  constructor(prisma: PrismaClient) {
    this.prisma = prisma;
  }

  /**
   * Naively return all users with no limit
   */
  async getAllUsers() {
    return this.prisma.user.findMany();
  }

  /**
   * Get user by ID
   */
  async getUser(id: number) {
    return this.prisma.user.findUnique({
      where: { id },
    });
  }

  /**
   * Create a new user
   */
  async createUser(data: CreateUserData) {
    return this.prisma.user.create({
      data: {
        email: data.email,
        name: data.name,
        role: data.role || &quot;USER&quot;,
      },
    });
  }

  /**
   * Update user
   */
  async updateUser(id: number, data: UpdateUserData) {
    return this.prisma.user.update({
      where: { id },
      data,
    });
  }

  /**
   * Delete user
   */
  async deleteUser(id: number) {
    try {
      await this.prisma.user.delete({
        where: { id },
      });
      return true;
    } catch (error) {
      // Handle user not found
      return false;
    }
  }
}</code></pre>
<h2 id="writing-tests-for-our-file">Writing tests for our file</h2>
<p>As for tests, I had written the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import { beforeEach, describe, expect, it } from &quot;vitest&quot;;
import { UserRepository } from &quot;./user-repository&quot;;
import { faker } from &quot;@faker-js/faker&quot;;
import { fail } from &quot;node:assert&quot;;

describe(&quot;UserRepository&quot;, () =&gt; {
  let userRepo: UserRepository;

  beforeEach(() =&gt; {
    userRepo = new UserRepository(global.testPrisma);
  });

  describe(&quot;getUser&quot;, () =&gt; {
    it(&quot;should return user by ID&quot;, async () =&gt; {
      const user = await global.testPrisma.user.create({
        data: {
          email: faker.internet.email(),
          name: faker.person.fullName(),
        },
      });

      const result = await userRepo.getUser(user.id);
      if (!result) {
        fail(&quot;Result was null&quot;);
      }

      expect(result).not.toBeNull();
      expect(result.id).toBe(user.id);
      expect(result.email).toBe(user.email);
    });

    it(&quot;should return null for non-existent user&quot;, async () =&gt; {
      const result = await userRepo.getUser(99999);
      expect(result).toBeNull();
    });
  });

  describe(&quot;createUser&quot;, () =&gt; {
    it(&quot;should create user with all fields&quot;, async () =&gt; {
      const userData = {
        email: faker.internet.email(),
        name: faker.person.fullName(),
        role: &quot;ADMIN&quot; as const,
      };

      const result = await userRepo.createUser(userData);

      expect(result.email).toBe(userData.email);
      expect(result.name).toBe(userData.name);
      expect(result.role).toBe(userData.role);
      expect(result.id).toBeDefined();
    });

    it(&quot;should create user with default role when not specified&quot;, async () =&gt; {
      const userData = {
        email: faker.internet.email(),
        name: faker.person.fullName(),
      };

      const result = await userRepo.createUser(userData);

      expect(result.email).toBe(userData.email);
      expect(result.name).toBe(userData.name);
      expect(result.role).toBe(&quot;USER&quot;);
    });

    it(&quot;should create user without name (optional field)&quot;, async () =&gt; {
      const userData = {
        email: faker.internet.email(),
      };

      const result = await userRepo.createUser(userData);

      expect(result.email).toBe(userData.email);
      expect(result.name).toBeNull();
      expect(result.role).toBe(&quot;USER&quot;);
    });

    it(&quot;should throw error for duplicate email&quot;, async () =&gt; {
      const email = faker.internet.email();

      await userRepo.createUser({
        email,
        name: faker.person.fullName(),
      });

      await expect(
        userRepo.createUser({
          email, // Same email
          name: faker.person.fullName(),
        })
      ).rejects.toThrow();
    });
  });

  describe(&quot;updateUser&quot;, () =&gt; {
    it(&quot;should update user successfully&quot;, async () =&gt; {
      const user = await global.testPrisma.user.create({
        data: {
          email: faker.internet.email(),
          name: faker.person.fullName(),
          role: &quot;USER&quot;,
        },
      });

      const updateData = {
        name: faker.person.fullName(),
        role: &quot;ADMIN&quot; as const,
      };

      const result = await userRepo.updateUser(user.id, updateData);
      if (!result) {
        fail(&quot;Update result was null&quot;);
      }

      expect(result.id).toBe(user.id);
      expect(result.name).toBe(updateData.name);
      expect(result.role).toBe(updateData.role);
      expect(result.email).toBe(user.email); // Should remain unchanged
    });

    it(&quot;should update only specified fields&quot;, async () =&gt; {
      const user = await global.testPrisma.user.create({
        data: {
          email: faker.internet.email(),
          name: faker.person.fullName(),
          role: &quot;USER&quot;,
        },
      });

      const newName = faker.person.fullName();
      const result = await userRepo.updateUser(user.id, { name: newName });
      if (!result) {
        fail(&quot;Update result was null&quot;);
      }

      expect(result.name).toBe(newName);
      expect(result.email).toBe(user.email); // Unchanged
      expect(result.role).toBe(user.role); // Unchanged
    });

    it(&quot;should return null for non-existent user&quot;, async () =&gt; {
      await expect(
        userRepo.updateUser(99999, {
          name: faker.person.fullName(),
        })
      ).rejects.toThrow();
    });

    it(&quot;should throw error when updating to duplicate email&quot;, async () =&gt; {
      const existingUser = await global.testPrisma.user.create({
        data: {
          email: faker.internet.email(),
          name: faker.person.fullName(),
        },
      });

      const userToUpdate = await global.testPrisma.user.create({
        data: {
          email: faker.internet.email(),
          name: faker.person.fullName(),
        },
      });

      await expect(
        userRepo.updateUser(userToUpdate.id, {
          email: existingUser.email, // Duplicate email
        })
      ).rejects.toThrow();
    });
  });

  describe(&quot;deleteUser&quot;, () =&gt; {
    it(&quot;should delete user successfully&quot;, async () =&gt; {
      const user = await global.testPrisma.user.create({
        data: {
          email: faker.internet.email(),
          name: faker.person.fullName(),
        },
      });

      const result = await userRepo.deleteUser(user.id);
      expect(result).toBe(true);

      // Verify user was actually deleted
      const deletedUser = await global.testPrisma.user.findUnique({
        where: { id: user.id },
      });
      expect(deletedUser).toBeNull();
    });

    it(&quot;should return false for non-existent user&quot;, async () =&gt; {
      const result = await userRepo.deleteUser(99999);
      expect(result).toBe(false);
    });
  });

  describe(&quot;getUsers&quot;, () =&gt; {
    it(&quot;should return all users&quot;, async () =&gt; {
      const data = [
        {
          email: faker.internet.email(),
          name: faker.person.fullName(),
        },
        {
          email: faker.internet.email(),
          name: faker.person.fullName(),
        },
        {
          email: faker.internet.email(),
          name: faker.person.fullName(),
        },
        {
          email: faker.internet.email(),
          name: faker.person.fullName(),
        },
        {
          email: faker.internet.email(),
          name: faker.person.fullName(),
        },
      ];
      await global.testPrisma.user.createMany({
        data,
      });

      const users = await userRepo.getAllUsers();

      // Naive test just checking database length
      expect(users.length).toBe(5);
    });
  });
});</code></pre>
<p>Each of these runs some operations against the database. I&#39;ve also opted to put a naive <code>users.length</code> check at the end of the tests to check for pollution of data and cleanup.</p>
<h2 id="running-the-tests">Running the tests</h2>
<p>Running one test itself would be fine, but we want to check the parallelism and isolation of our tests.</p>
<p>I wrote a script to create 100 copies of the tests in order to have them run in parallel and see the results.</p>
<p>The expectation here is that we will, throughout the duration of the tests, <strong>create 100 PGLite databases</strong>. This aligns with our understanding of how the setup file that we created is expected to operate.</p>
<p>Running <code>vitest run</code> enables our test runner to start. You should see it indicate that it found 100 files and the UI will indicate that it is parallelising tests in groups.</p>
<p>Ultimately, it finish with the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="txt"><code>Test Files  100 passed (100)
    Tests  1300 passed (1300)
  Start at  07:35:40
  Duration  25.00s (transform 982ms, setup 5.07s, collect 14.88s, tests 206.29s, environment 12ms, prepare 7.53s)</code></pre>
<p>Running 1300 tests in 25 seconds where each file only has 13 tests is seriously impressive when each tests needs to run access to a running PostgreSQL database!</p>
<blockquote>
<p>For what it is worth, the timing certainly depends on compute power. 25s might be arbitrary but my computer is a 2023 MacBook pro a few years old with mid-range specs!</p>
</blockquote>
<p>If you follow the logs, you will also see when tests reset the database.</p>
<h2 id="mocking-out-the-module-instead">Mocking out the module instead</h2>
<p>In our current approach that we explored, it opts to create values that are added to the global namespace. Given that my approach relies on using the global namespace to arrange the data mock data first and my tested module relies on dependency injection, this works fine for me.</p>
<p>For alternatives where you directly import the module, you may want to consider changing the setup test file to make use of module mocks instead.</p>
<p>For example:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>vi.mock(&quot;./path/to/your/module&quot;, async (importOriginal) =&gt; {
  const { PGlite } = await vi.importActual(&quot;@electric-sql/pglite&quot;);
  const { PrismaPGlite } = await vi.importActual(&quot;@prisma/adapter-pglite&quot;);
  const { PrismaClient } = await vi.importActual(&quot;@prisma/client&quot;);

  const db = new PGlite();
  const adapter = new PrismaPGlite(db);
  const prisma = new PrismaClient({ adapter });

  // Apply your generated schema
  const schemaSQL = getGeneratedSchema();
  await db.exec(schemaSQL);

  return { prisma, db };
});</code></pre>
<p>In the above, we assume that <code>./path/to/your/module</code> exports an object with <code>prisma</code> and <code>db</code> properties in this case. Adjust as required.</p>
<p><strong>Be warned though</strong>: as covered earlier, over-mocking of modules can lead to testing purgatory. Use this sparingly and when it makes the most sense to do so.</p>
<h2 id="conclusion">Conclusion</h2>
<p>This blog post explores how we can replace our PostgreSQL database for a full, in-memory version to significantly sped up our tests without compromising on test boundaries.</p>
<p>It enables use to bypass network latency and flaky limitations that can occur when running end-to-end without losing much in test confidence as we maintain a strong representation of interacting with the database.</p>
<h2 id="links-and-further-reading">Links and Further Reading</h2>
<p><em>Photo credit: <a href="https://unsplash.com/@deeezyfree" target="_blank" rel="noopener noreferrer"><code>deeezyfree</code></a></em></p>]]></content>
<category term="typescript"/><category term="postgresql"/><category term="prisma"/>
</entry>
<entry>
<title type="html"><![CDATA[PostgreSQL Performance]]></title>
<id>https://blog.dennisokeeffe.com/blog/2025-05-27-postgresql-performance</id>
<link href="https://dennisokeeffe.com/blog/2025-05-27-postgresql-performance"/>
<published>2025-05-27T00:00:00.000Z</published>
<updated>2025-05-27T00:00:00.000Z</updated>
<summary type="html"><![CDATA[Learn more about PostgreSQL indexing practices and performance measurement in this blog post.]]></summary>
<content type="html"><![CDATA[<p>This blog post works through some common PostgreSQL performance opportunities while teaching you about the common ailments and decision-making that you need to make around indexing with PostgreSQL.</p>
<p>The target audience is for TypeScript/JavaScript developers who understand the basics of interfacing with PostgreSQL, but do not yet feel confident in working directly with SQL or the impacts of certain table design decisions. For those more confident with their PostgreSQL capabilities, this post will not be anymore than a reference for you.</p>
<p>We will be using Prisma to abstract some of the table schema design work. This is purely to lower the barrier to entry for developers so we can spend more take talking about and showing improvements.</p>
<p>I&#39;ve chosen Prisma primarily as it helps for me to share this piece with work colleagues at my workplace (at the time of writing), but note that what is shown focuses more on PostgreSQL and is not ORM-bound.</p>
<h2 id="getting-started">Getting started</h2>
<h2 id="conclusion">Conclusion</h2>
<h2 id="links-and-further-reading">Links and Further Reading</h2>
<p><em>Photo credit: <a href="https://unsplash.com/@aleksdahlberg" target="_blank" rel="noopener noreferrer"><code>aleksdahlberg</code></a></em></p>]]></content>
<category term="typescript"/><category term="postgresql"/><category term="prisma"/>
</entry>
<entry>
<title type="html"><![CDATA[Multi-tenant testing]]></title>
<id>https://blog.dennisokeeffe.com/blog/2025-05-24-multitenant-testing</id>
<link href="https://dennisokeeffe.com/blog/2025-05-24-multitenant-testing"/>
<published>2025-05-24T00:00:00.000Z</published>
<updated>2025-05-24T00:00:00.000Z</updated>
<summary type="html"><![CDATA[Exploring Uber multi-tenancy paradigms and seeing how they can impact testing in TypeScript.]]></summary>
<content type="html"><![CDATA[<p>During a recent podcast on <a href="https://www.pragmaticengineer.com/" target="_blank" rel="noopener noreferrer">The Pragmatic Engineer</a>, I heard an interesting approach to testing that Uber takes with their services by leveraging multitenancy.</p>
<p>The approach sounded like an interesting alternative to managing environments with microservices, something that I&#39;ve been pondering resolutions to myself at work to cutdown lead time to feature deployment.</p>
<p>This blog post looks to built out a minimal approach to using multitenancy within TypeScript microservices, but also goes a step further to see how a unified approach to multitenancy can also be grateful support by network mocking solutions for solving those difficult-to-emulate response scenarios.</p>
<h2 id="getting-started">Getting started</h2>
<p>For this project, I&#39;ve built out a minimal Turborepo starting place to illustrate some of the different approaches and trade-offs with minimal Hono servers.</p>
<p>For the testing and servers, they are <strong>not</strong> a true reflection of my approaches to these things, but are there to focus on some key points.</p>
<p>For a better reflection on some of my thoughts behind testing in TypeScript for projects, please read my blog post <a href="https://dennisokeeffe.com/blog/2025-03-29-the-boundaries-of-typescript-testing">The Boundaries of TypeScript Testing</a>.</p>
<p>For the companion code, please see <a href="https://github.com/okeeffed/blog-multitenant-testing" target="_blank" rel="noopener noreferrer">here</a>.</p>
<h2 id="our-scenario">Our scenario</h2>
<p>For this particular scenario, we are going to work with a chain of requests across three Hono servers:</p>
<ol>
<li><code>service-a</code> will be our core server.</li>
<li><code>downstream-b</code> will be our stand-in for a service we need to call. This could be a placeholder for another micro-service within our system.</li>
<li><code>downstream-c</code> will be our stand-in for a server that <code>downstream-b</code> needs to call. This could be a placeholder for another micro-service or a third-party API we need to interact with.</li>
</ol>
<p>The scenario is straight forward: <strong>A calls B, B calls C</strong>.</p>
<p>Let&#39;s see this in action.</p>
<p>Some of the code from <strong>Service A</strong> which runs on <code>localhost:3000</code>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>app.post(&quot;/users&quot;, async (c) =&gt; {
  try {
    // ResultAsync.fromPromise comes from neverthrow
    const body = await ResultAsync.fromPromise(c.req.json(), (err) =&gt; err);
    if (body.isErr()) {
      c.status(400);
      return c.json(new NoJsonBodyError());
    }

    const userBodyResult = UsersPostBody.safeParse(body);
    if (!userBodyResult.success) {
      c.status(400);
      return c.json(new ValidationError(userBodyResult.error));
    }

    // We aren&#39;t storing this. Just illustrating some adjustments to the value
    const newUser = {
      id: v4(),
      name: userBodyResult.data.name,
    };

    const res = await axios.post(
      &quot;http://localhost:3001/notifications&quot;,
      new NewUserNotification(newUser)
    );

    switch (res.data._tag) {
      case &quot;ItemQueued&quot;:
        return c.json(new UserCreatedResponse(newUser));
      default:
        return c.json(new UnhandledResponseTagError());
    }
  } catch (err) {
    console.error(&quot;Something went wrong&quot;, err);
    return c.json(new InternalServerError());
  }
});</code></pre>
<p>The &quot;creates&quot; a new user, then forwards the request onto another service <strong>Downstream B</strong> on <code>localhost:3001</code>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>app.post(&quot;/notifications&quot;, async (c) =&gt; {
  try {
    const body = await c.req.json();
    const notificationBodyResult = NotificationPostBody.safeParse(body);
    if (!notificationBodyResult.success) {
      c.status(400);
      return c.json(new ValidationError(notificationBodyResult.error));
    }

    const res = await addToQueue(
      new NewUserNotification(notificationBodyResult.data.value)
    );

    switch (res.data._tag) {
      case &quot;SuccessfullyQueued&quot;:
        return c.json(new ItemQueuedResponse());
      // Propagate the following errors back under one
      // unified error type for the sake of it.
      case &quot;ValidationError&quot;:
      case &quot;TimeoutError&quot;:
        return c.json(new QueueError(res.data));
      default:
        return c.json(new InternalServerError());
    }
  } catch (error) {
    console.error(&quot;Error processing notification&quot;, error);
    return c.json(new InternalServerError());
  }
});</code></pre>
<p>That code attempts to add it to a &quot;queue&quot; (we are not really doing this) on another endpoint <code>localhost:3002</code> for our service <strong>Downstream C</strong>.</p>
<p>If it is successfully queued, then it will return the instance of <code>ItemQueuedResponse</code>, but our logic is handling scenarios for <code>ValidationError</code> and <code>TimeoutError</code> error as well where is returns an instance of <code>QueueError</code>.</p>
<p>For example&#39;s sake, <strong>Downstream C</strong> does not implement handling for a timeout error, however we can make the assumption that this has been implemented and that <strong>Downstream B</strong> can possibly return errors based off that.</p>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>For what it&#39;s worth, if you are feeling some confusion about the
micro-services in this example, then note that it is a contrived example but
certainly emulates real-world services that you may be working with. This
setup is to enable us to have a scenario where we can compare solutions for
testing.</p> </div> </aside>
<p>From the companion code, we can run <code>pnpm dev</code> from the root directory to start all three servers and test our code is working:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="s"><code># Start the servers
$ pnpm dev

# In another terminal window, testing the endpoint with HTTPie
$ http POST http://localhost:3000/users name=&quot;Dennis&quot;                                          HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 92
Date: Sat, 24 May 2025 05:06:39 GMT
Keep-Alive: timeout=5
content-type: application/json

{
    &quot;_tag&quot;: &quot;UserCreated&quot;,
    &quot;value&quot;: {
        &quot;id&quot;: &quot;37ba97de-df86-488f-9258-4867a655fec0&quot;,
        &quot;name&quot;: &quot;Dennis&quot;
    }
}</code></pre>
<p>At this point, we can tell that our configuration returns a response along our successful path.</p>
<p>We can also confirm that our <code>NoJsonBodyError</code> and <code>ValidationError</code> works by dropping the <code>name</code> from the request or setting it to be an invalid type:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="s"><code># NoJsonBody Error
$ http POST http://localhost:3000/users
HTTP/1.1 400 Bad Request
Connection: keep-alive
Content-Length: 68
Date: Sat, 24 May 2025 05:31:17 GMT
Keep-Alive: timeout=5
content-type: application/json

{
    &quot;_tag&quot;: &quot;NoJsonBody&quot;,
    &quot;value&quot;: {
        &quot;message&quot;: &quot;Request body is required&quot;
    }
}

# ValidationError
$ http POST http://localhost:3000/users name:=1
HTTP/1.1 400 Bad Request
Connection: keep-alive
Content-Length: 139
Date: Sat, 24 May 2025 05:33:30 GMT
Keep-Alive: timeout=5
content-type: application/json

{
    &quot;_tag&quot;: &quot;ValidationError&quot;,
    &quot;value&quot;: {
        &quot;details&quot;: &quot;✖ Invalid input: expected string, received number\n  → at name&quot;,
        &quot;message&quot;: &quot;Bad Request&quot;
    }
}</code></pre>
<p>But what happens for error scenarios that are not so easy to represent from our remote services?</p>
<p>In our code, there are potential errors to bubble up like the downstream services like <code>TimeoutError</code> which are not so easy to replicate reliably. Let&#39;s take a look at a few of our options.</p>
<h2 id="options-for-testing">Options for testing</h2>
<p>Our setup is non-trivial. Although my test setup demonstrates multiple micro-services within the same repository, we may not always have that luxury.</p>
<p>At your own workplace, you may have services distributed across multiple teams and repositories, and co-ordination for this level of testing is of utmost importance across the board.</p>
<p>Let&#39;s walkthrough some possible solutions for us now.</p>
<h2 id="an-approach-using-testing-library-mocks">An approach using testing library mocks</h2>
<p>The most basic possibility for emulating certain scenarios is to make use of library mocks.</p>
<p>Popular TypeScript testing frameworks such as Jest and Vitest have mocking capabilities out of the box.</p>
<p>Depending on you configure this, you can stub a return value for a request library (like our Axios library from the examples) or even a module that interacts with the service in order to test certain scenarios.</p>
<p>The benefits of this approach is that it is a quick-and-easy approach that comes built into our testing tools.</p>
<p>The major downside is that your test boundary ends where the mock starts. Your test can no longer give certain guarantees and confidence from the tools that are now mocked out. If we mock out our request library, then we have no confidence in the request library returning the response from the network request itself.</p>
<p>This can be a major loss from the point-of-view of testing that aims to, at a minimum, test all modules within a service until (at least) the network boundary.</p>
<h2 id="an-approach-using-mock-server-infrastructure">An approach using mock server infrastructure</h2>
<p>On the other end of the scale is to stand up infrastructure to emulate network traffic. This could be a scenario of running different environments for your service, but it can also move to more extreme cases of standing up infrastructure in an attempt to emulate third-party services and APIs that you interface with in production.</p>
<p>This approach certainly has a spectrum of complexity.</p>
<p>On the side of standing up internal service infrastructure environments, it can give you a chance to run test boundaries that give you test confidence as far as deeper data layers like your databases.</p>
<p>The trade-off with the internal services is that there is always a cost to infrastructure which is not just monetary. There is time-to-production costs, infrastructure management, complexity increases and more. In scenarios where you have multiple environments and service with multiple internal down stream services, you can also face challenges around all environments being at parity for running tests.</p>
<p>It is still standardized to run different environments for your services, but it&#39;s worth noting that the approach to this varies from company-to-company and there is still such a thing as over-kill or misprovisioning of resources.</p>
<p>In the case of third-party mock service infrastructure, you face the cost of re-implementing and maintaining an emulation of their third party service&#39;s logic and run the risk of implementation logic mismatches.</p>
<h2 id="an-approach-using-mock-service-worker">An approach using Mock Service Worker</h2>
<p>With JavaScript/TypeScript, there is the delightful <a href="https://mswjs.io/docs/getting-started/" target="_blank" rel="noopener noreferrer">Mock Service Worker (MSW)</a> library which enables you to stub out responses at the network level.</p>
<p>This differs from the module-mocking approach we spoke about earlier in that you service still operates end-to-end from within it&#39;s service boundary. As long as your mocked response matches that of the expected success/error responses, then MSW is an unbelievably useful option for emulating response types that are hard to emulate.</p>
<p>A basic example of MSW from their documentation:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>// src/mocks/handlers.js
import { http, HttpResponse } from &quot;msw&quot;;

export const handlers = [
  // Intercept &quot;GET https://example.com/user&quot; requests...
  http.get(&quot;https://example.com/user&quot;, () =&gt; {
    // ...and respond to them using this JSON response.
    return HttpResponse.json({
      id: &quot;c7b3d8e0-5e0b-4b0f-8b3a-3b9f4b3d3b3d&quot;,
      firstName: &quot;John&quot;,
      lastName: &quot;Maverick&quot;,
    });
  }),
];</code></pre>
<p>When MSW is intercepting network calls, a call to <code>https://example.com/user</code> from your request library will respond with a valid HTTP response with a JSON body.</p>
<h2 id="an-approach-using-multi-tenancy">An approach using multi-tenancy</h2>
<p><a href="https://www.uber.com/en-AU/blog/multitenancy-microservice-architecture/" target="_blank" rel="noopener noreferrer">Uber takes an approach using multi-tenancy</a>.</p>
<p>By enabling multiple tenants (such as test environments, canary releases, and product lines) to coexist within the same infrastructure, Uber achieves code isolation, flexible traffic routing, and improved integration testing, ultimately supporting rapid feature deployment and reliable service performance.</p>
<p>More relevant around the integration testing, Uber uses routing to test services without the constraints of static environment configuration.</p>
<p>Consider a scenario where Uber needs to test a new version of <strong>Service B</strong>, which interacts with <strong>Services A, C, and D</strong>. Instead of deploying this new version in a separate staging environment, Uber deploys it as a distinct tenant within the production environment, referred to as <strong>Service B&#39; (Service B Prime)</strong>. Test traffic is routed specifically to <strong>Service B&#39;</strong>, while production traffic continues to flow through the original <strong>Service B</strong>. This setup ensures that the new version can be tested in a live environment without impacting the production services.</p>
<p>Uber uses HTTP headers to implement multi-tenancy for integration testing in their microservice architecture.</p>
<p>Here&#39;s how it works:</p>
<ul>
<li><strong>Tenancy Context Propagation</strong>: When a request is made (for example, from a test client), Uber adds a special tenancy header to the HTTP request (e.g., X-Tenant-ID: integration-test-123).</li>
<li><strong>Traffic Routing</strong>: Uber&#39;s internal routing and service discovery systems use this header to direct the request to the correct tenant-specific instance of the service (e.g., a test version of Service B).</li>
<li><strong>End-to-End Isolation</strong>: This header is propagated across all service calls, ensuring that the entire chain of microservices (B → C → D) remains within the same test tenant. This allows for full end-to-end testing without touching production instances.</li>
</ul>
<p>While this approach puts a lot of emphasis on a consistent system, it also enables us to reduce some of the complexities previously spoken about around managing environments for microservices and we can leverage an approach similar using HTTP headers (or a consistent equivalent) in order to make a combination of previously explained processes in an attempt to simplify testing new versions or routing to specific HTTP mock responses.</p>
<h2 id="rethinking-our-approach-to-integration-testing">Rethinking our approach to integration testing</h2>
<p>Keeping in mind the different tooling and options that we currently have, we can start to pick-and-choose a combination of options to best reflect the trade-offs we want to make.</p>
<p>In our current setup, I&#39;m thinking this will be what works best:</p>
<ol>
<li>Making use of headers to conditionally intercept requests when MSW is enabled.</li>
<li>Configuring middleware to conditionally return certain HTTP responses from the service itself.</li>
<li>Providing route management using the headers to conditionally alter where requests are sent.</li>
</ol>
<p>With that in mind, let&#39;s work through what some of this can look like.</p>
<h2 id="taking-another-look-at-our-mock-service-worker-approach">Taking another look at our Mock Service Worker approach</h2>
<p>With the idea in mind around using HTTP headers, we can make use of MSW&#39;s <a href="https://mswjs.io/docs/api/passthrough" target="_blank" rel="noopener noreferrer">passthrough</a> helper in order to conditionally fire off the original request without mocking.</p>
<p>When running in test modes, we can enable MSW and then using those testing headers in order to run our tests. Let&#39;s look at how that could look.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import { http, passthrough, HttpResponse } from &quot;msw&quot;;

class QueueError {
  readonly _tag = &quot;QueueError&quot;;
  readonly value: unknown;

  constructor(value: unknown) {
    this.value = value;
  }
}

export const handlers = [
  http.post(&quot;http://localhost:3001/notifications&quot;, ({ request }) =&gt; {
    switch (request.headers.get(&quot;X-Tenancy-ID&quot;)) {
      case &quot;DownstreamB/ValidationError&quot;:
        return HttpResponse.json(
          new QueueError({
            _tag: &quot;ValidationError&quot;,
          })
        );
      case &quot;DownstreamB/TimeoutError&quot;:
        return HttpResponse.json(
          new QueueError({
            _tag: &quot;TimeoutError&quot;,
          })
        );
      default:
        return passthrough();
    }
  }),
];</code></pre>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>In the above, I&#39;ve chosen to use the header <code>X-Tenancy-ID</code>. This is arbitrary
and can be swapped out for whatever best suits your testing paradigm.</p> </div> </aside>
<p>The above code will conditionally switch on a <code>X-Tenancy-ID</code> value if provided <strong>and only when</strong> MSW is enabled and running. In scenarios where MSW is not enabled, it will not have an impact.</p>
<p>In the case of the development server and testing, we can conditionally enable MSW for the duration of the tests.</p>
<p>On the assumption of a Vitest test setup that has MSW configured to run using the following hooks:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>// vitest.setup.ts
import { beforeAll, afterEach, afterAll } from &quot;vitest&quot;;
import { server } from &quot;../src/mocks/node&quot;;

// Start server before all tests
beforeAll(() =&gt; {
  server.listen({
    // If you wanted to, you could set warn here in dev mode
    onUnhandledRequest: &quot;bypass&quot;,
  });
});

// Reset handlers after each test (important for test isolation)
afterEach(() =&gt; {
  server.resetHandlers();
});

// Close server after all tests
afterAll(() =&gt; {
  server.close();
});</code></pre>
<p>Then we can write a test to confirm that our configuration works with the <code>X-Tenancy-ID</code> configuration we&#39;ve made with the following test:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import axios, { isAxiosError } from &quot;axios&quot;;
import { ResultAsync } from &quot;neverthrow&quot;;
import { describe, expect, it } from &quot;vitest&quot;;

/**
 * These tests expect all the servers to be up and running.
 */
describe(&quot;app&quot;, () =&gt; {
  describe(&quot;creating a user&quot;, () =&gt; {
    describe(&quot;success states&quot;, () =&gt; {
      it(&quot;successfully creates a user&quot;, async () =&gt; {
        const response = await axios.post(
          &quot;http://localhost:3000/users&quot;,
          {
            name: &quot;Dennis&quot;,
          },
          {
            headers: {
              &quot;Content-Type&quot;: &quot;application/json&quot;,
            },
          }
        );

        expect(response.status).toBe(200);
        expect(response.data._tag).toBe(&quot;UserCreated&quot;);
      });
    });

    describe(&quot;error states&quot;, () =&gt; {
      it(&quot;requires a name property within the json body&quot;, async () =&gt; {
        const response = await ResultAsync.fromPromise(
          axios.post(
            &quot;http://localhost:3000/users&quot;,
            {},
            {
              headers: {
                &quot;Content-Type&quot;: &quot;application/json&quot;,
              },
            }
          ),
          (err) =&gt; err
        );

        // There is likely a better way to handle this, but will
        // leave it in as it&#39;s the assertions that I care most
        // about using for illustration purposes.
        if (
          !response.isErr() ||
          !isAxiosError(response.error) ||
          !response.error.response
        ) {
          throw new Error(&quot;Unexpected request success&quot;);
        }

        expect(response.error.response.status).toBe(400);
        expect(response.error.response.data._tag).toBe(&quot;ValidationError&quot;);
      });

      it(&quot;returns information on the queue service TimeoutError&quot;, async () =&gt; {
        const response = await ResultAsync.fromPromise(
          axios.post(
            &quot;http://localhost:3000/users&quot;,
            {
              name: &quot;Dennis&quot;,
            },
            {
              headers: {
                &quot;Content-Type&quot;: &quot;application/json&quot;,
                &quot;X-Tenancy-ID&quot;: &quot;DownstreamB/TimeoutError&quot;,
              },
            }
          ),
          (err) =&gt; err
        );

        // There is likely a better way to handle this, but will
        // leave it in as it&#39;s the assertions that I care most
        // about using for illustration purposes.
        if (
          !response.isErr() ||
          !isAxiosError(response.error) ||
          !response.error.response
        ) {
          throw new Error(&quot;Unexpected request success&quot;);
        }

        expect(response.error.response.status).toBe(500);
        expect(response.error.response.data._tag).toBe(&quot;QueueError&quot;);
        expect(response.error.response.data.value._tag).toBe(&quot;TimeoutError&quot;);
      });

      it(&quot;returns information on the queue service ValidationError&quot;, async () =&gt; {
        const response = await ResultAsync.fromPromise(
          axios.post(
            &quot;http://localhost:3000/users&quot;,
            {
              name: &quot;Dennis&quot;,
            },
            {
              headers: {
                &quot;Content-Type&quot;: &quot;application/json&quot;,
                &quot;X-Tenancy-ID&quot;: &quot;DownstreamB/ValidationError&quot;,
              },
            }
          ),
          (err) =&gt; err
        );

        // There is likely a better way to handle this, but will
        // leave it in as it&#39;s the assertions that I care most
        // about using for illustration purposes.
        if (
          !response.isErr() ||
          !isAxiosError(response.error) ||
          !response.error.response
        ) {
          throw new Error(&quot;Unexpected request success&quot;);
        }

        expect(response.error.response.status).toBe(500);
        expect(response.error.response.data._tag).toBe(&quot;QueueError&quot;);
        expect(response.error.response.data.value._tag).toBe(&quot;ValidationError&quot;);
      });
    });
  });
});</code></pre>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>The above tests require the servers to be running in order for the localhost
ports to resolve to the server.</p> </div> </aside>
<p>Our tests are checking the bare minimum, and I&#39;ve written them in a way that favors illustrating the MSW mock conditionally in action (it could certainly do with some TLC).</p>
<p>In our test scenarios, MSW mocks are configured to run for each test.</p>
<ul>
<li>Our successful scenario bypasses the MSW mock and calls into our microservices <strong>Downstream B</strong> and <strong>Downstream C</strong>.</li>
<li>Our failure scenarios that pass the <code>X-Tenancy-ID</code> values that match our MSW mock conditions will be intercepted at the network level and <strong>never</strong> make it to <strong>Downstream B</strong>. That being said, the mocked JSON responses are a one-to-one representation of responses from <strong>Downstream B</strong>.</li>
</ul>
<p>Of course, we can also make use of this if MSW is enabled on our development servers to check that we are getting the expected responses.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="s"><code># An example of an MSW intercepted endpoint
$ http POST http://localhost:3000/users &quot;X-Tenancy-ID:DownstreamB/TimeoutError&quot; name=&quot;Dennis&quot;
HTTP/1.1 500 Internal Server Error
Connection: keep-alive
Content-Length: 53
Date: Sun, 25 May 2025 11:42:24 GMT
Keep-Alive: timeout=5
content-type: application/json

{
    &quot;_tag&quot;: &quot;QueueError&quot;,
    &quot;value&quot;: {
        &quot;_tag&quot;: &quot;TimeoutError&quot;
    }
}</code></pre>
<p>Great! So we&#39;ve managed to use the <code>X-Tenancy-ID</code> header to incept requests for difficult-to-emulate downstream error responses in our tests to ensure that <strong>Service A</strong> can handle those as expected on its infrastructure.</p>
<p>The next step for us is make use of that header for redirecting requests.</p>
<h2 id="implementing-multi-tenancy-for-services">Implementing multi-tenancy for services</h2>
<p>On real network infrastructure, there are a number of ways to manage routing. Examples of this could be re-routing at API Gateway, load balancers, reverse proxies or within services themselves (along with more alternatives!). Each come with their own value propositions and trade-offs.</p>
<p>In our example, we&#39;re going to simplify things by conditionally routing requests based on the <code>X-Tenancy-ID</code> from within a service application. Again, it&#39;s best to do a technical spike and review your own systems in order to understand what may be most beneficial for your needs.</p>
<p>For the example, I&#39;m going to create a duplicate of the code in <strong>Downstream C</strong>. For the demonstration today, we&#39;ll name it <strong>Downstream C Prime</strong>, but you can think it of as emulating a separate, updated deployment of <strong>Downstream C</strong> that we want to test out.</p>
<p>For some contrived code in <strong>Service B</strong> that conditionally re-routes traffic based on the <code>X-Tenancy-ID</code> header, I have the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>app.post(&quot;/notifications&quot;, async (c) =&gt; {
  try {
    const body = await c.req.json();
    const notificationBodyResult = NotificationPostBody.safeParse(body);
    if (!notificationBodyResult.success) {
      c.status(400);
      return c.json(new ValidationError(notificationBodyResult.error));
    }

    // This is shoe-horned in for demonstration. In reality, you should come up
    // with a better approach to resolving this value.
    let baseUrl;
    switch (c.req.header(&quot;X-Tenancy-ID&quot;)) {
      case &quot;Feature/QueueUpdate&quot;:
        baseUrl = &quot;http://localhost:3003&quot;;
        break;
      default:
        baseUrl = &quot;http://localhost:3002&quot;;
    }

    const res = await addToQueue(
      new NewUserNotification(notificationBodyResult.data.value),
      baseUrl
    );

    switch (res.data._tag) {
      case &quot;SuccessfullyQueued&quot;:
        return c.json(new ItemQueuedResponse());
      case &quot;SuccessfullyQueuedAlt&quot;:
        return c.json(new ItemQueuedNewVersionResponse());
      // Propagate the following errors back under one
      // unified error type for the sake of it.
      case &quot;ValidationError&quot;:
      case &quot;TimeoutError&quot;:
        return c.json(new QueueError(res.data));
      default:
        return c.json(new InternalServerError());
    }
  } catch (error) {
    console.error(&quot;Error processing notification&quot;, error);
    return c.json(new InternalServerError());
  }
});</code></pre>
<p>In the above code, we have code-dependent conditional logic to resolve the base URL to use for the request with a fallback to the default service.</p>
<aside class="callout callout--warning" aria-label="Warning"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><path d="m21.7 18-8-14a2 2 0 0 0-3.4 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.7-3ZM12 9v4M12 17h.01" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>  <div class="callout__body">  <p>In real-world projects, I would spike options for remotely resolving these
URLs based on the ID or having a policy in place where the target URL has some
correlation to the tenancy ID that you are aiming to use. If you do not do
this, then you would also need a deployment to the consuming services to match
new tenancy IDs. It&#39;s out-of-scope of this blog post for me to go further into
that, but I will happily update this post if I find an approach that works
well for my work.</p> </div> </aside>
<p>The updated <strong>Downstream C Prime</strong> service also has an different response to emulate an &quot;updated&quot; version of the service.</p>
<p>We can test this works against our running servers by making a few calls:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="s"><code># Without the tenancy ID
$ http POST http://localhost:3000/users name=&quot;Dennis&quot;
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 92
Date: Sun, 25 May 2025 09:59:40 GMT
Keep-Alive: timeout=5
content-type: application/json

{
    &quot;_tag&quot;: &quot;UserCreated&quot;,
    &quot;value&quot;: {
        &quot;id&quot;: &quot;e0371a7e-56d4-4c4e-a3f3-8942caca0003&quot;,
        &quot;name&quot;: &quot;Dennis&quot;
    }
}

# With the tenancy ID
$ http POST http://localhost:3000/users X-Tenancy-ID:Feature/QueueUpdate name=&quot;Dennis&quot;
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 68
Date: Sun, 25 May 2025 09:58:48 GMT
Keep-Alive: timeout=5
content-type: application/json

{
    &quot;_message&quot;: &quot;This uses the alternative tenant&quot;,
    &quot;_tag&quot;: &quot;UserCreated&quot;
}</code></pre>
<p>As we can see, our servers are now responding with different values for our service.</p>
<p>We can also configure our tests for us to test both these alternatives by adding the alternative route as another success route:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>describe(&quot;success states&quot;, () =&gt; {
  it(&quot;successfully creates a user&quot;, async () =&gt; {
    const response = await axios.post(
      &quot;http://localhost:3000/users&quot;,
      {
        name: &quot;Dennis&quot;,
      },
      {
        headers: {
          &quot;Content-Type&quot;: &quot;application/json&quot;,
        },
      }
    );

    expect(response.status).toBe(200);
    expect(response.data._tag).toBe(&quot;UserCreated&quot;);
  });

  it(&quot;successfully creates a user taking the alternative path&quot;, async () =&gt; {
    // This test will run against Downstream C Prime thanks
    // to the X-Tenancy-ID
    const response = await axios.post(
      &quot;http://localhost:3000/users&quot;,
      {
        name: &quot;Dennis&quot;,
      },
      {
        headers: {
          &quot;Content-Type&quot;: &quot;application/json&quot;,
          &quot;X-Tenancy-ID&quot;: &quot;Feature/QueueUpdate&quot;,
        },
      }
    );

    expect(response.status).toBe(200);
    expect(response.data._tag).toBe(&quot;UserCreated&quot;);
    expect(response.data._message).toBe(&quot;This uses the alternative tenant&quot;);
  });
});</code></pre>
<p>When we run our tests with <code>pnpm test</code>, we can see that all tests pass.</p>
<aside class="callout callout--warning" aria-label="Warning"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><path d="m21.7 18-8-14a2 2 0 0 0-3.4 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.7-3ZM12 9v4M12 17h.01" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>  <div class="callout__body">  <p>Testing these two tenancy resolutions is more for the sake of showing both
alternatives can be used and are conditionally resolved with our header. When
writing integration tests for your own applications, you would need to be
considerate of your strategies based on changes from merges or dropped
tenancies. This is likely something for your release management to take
ownership of and is out-of-scope for this blog post.</p> </div> </aside>
<p>At this point, we now have a solution to optionally intercept or re-route traffic for our services based on the <code>X-Tenancy-ID</code> provided.</p>
<h2 id="conclusion">Conclusion</h2>
<p>This post explored a proof-of-concept for multi-tenant testing, as well as using networking mocking solutions to also reduce the amount of infrastructure management required.</p>
<p>These approaches adequately showed the potential that this approach has with enough buy-in and unification. I believe that refining these ideas with a technical design relevant to your situation has the potential to greatly simplify testing complicated microservice architecture systems.</p>
<h2 id="links-and-further-reading">Links and Further Reading</h2>
<ul>
<li><a href="https://www.uber.com/en-AU/blog/multitenancy-microservice-architecture/" target="_blank" rel="noopener noreferrer">Uber Blog | Why We Leverage Multi-tenancy in Uber’s Microservice Architecture</a></li>
<li><a href="https://www.uber.com/en-AU/blog/shifting-e2e-testing-left/" target="_blank" rel="noopener noreferrer">Uber Blog | Shifting E2E Left At Uber</a></li>
<li><a href="https://v1.mswjs.io/docs/" target="_blank" rel="noopener noreferrer">Mock Service Worker (MSW)</a></li>
</ul>
<p><em>Photo credit: <a href="https://unsplash.com/@aleksdahlberg" target="_blank" rel="noopener noreferrer"><code>aleksdahlberg</code></a></em></p>]]></content>
<category term="typescript"/>
</entry>
<entry>
<title type="html"><![CDATA[Retries, timeouts and spans for TS Rest clients]]></title>
<id>https://blog.dennisokeeffe.com/blog/2025-04-10-retries-spans-timeouts-for-ts-rest</id>
<link href="https://dennisokeeffe.com/blog/2025-04-10-retries-spans-timeouts-for-ts-rest"/>
<published>2025-04-10T00:00:00.000Z</published>
<updated>2025-04-10T00:00:00.000Z</updated>
<summary type="html"><![CDATA[Customize your TS Rest API calls to enable capabilities such as retries, timeouts and span information]]></summary>
<content type="html"><![CDATA[<p>Working with <a href="https://ts-rest.com/" target="_blank" rel="noopener noreferrer">TS Rest</a> is great for any workflows where your consumers are TypeScript frontend and backend applications.</p>
<p>If you work extensively with <a href="https://zod.dev/" target="_blank" rel="noopener noreferrer">Zod</a> and tools like <a href="https://tanstack.com/query/latest" target="_blank" rel="noopener noreferrer">Tanstack Query</a> (if you are using Vue/React at this point in time), it can be a natural fit and time saver to generate and distribute your clients this way.</p>
<p>In recent work, I&#39;ve been using TS Rest to pair up my smaller Hono projects with clients, but it becomes immediately apparent to me just how good Tanstack Query can be out of the box with it&#39;s out-of-the-box capabilities around retries and caching.</p>
<p>This short blog post aims to look at ways to make use of TS Rest customization in order to have some control over how we can introduce this to our other clients.</p>
<h2 id="custom-ts-rest-clients">Custom TS Rest clients</h2>
<p>TS Rest gives you the capability to create your own <a href="https://ts-rest.com/docs/core/custom" target="_blank" rel="noopener noreferrer">custom client</a> which can be used for controlling how your HTTP calls are made.</p>
<p>As a simple example:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import { initClient, tsRestFetchApi } from &quot;@ts-rest/core&quot;;
import { contract } from &quot;./contract&quot;;

const client = initClient(contract, {
  baseUrl: &quot;http://localhost:5003&quot;,
  baseHeaders: {},
  api: async (args) =&gt; {
    // Add anything you want here!

    return tsRestFetchApi(args);
  },
});</code></pre>
<p>In their example code here, they expose their default <code>tsRestFetchApi</code> and allow you to control anything you want based off the args prior to making any calls.</p>
<p>There are also guides on how to create your own custom client with <a href="https://axios-http.com/docs/intro" target="_blank" rel="noopener noreferrer">Axios</a>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import axios, { Method, AxiosError, AxiosResponse, isAxiosError } from &quot;axios&quot;;
import { initClient } from &quot;@ts-rest/core&quot;;
import { contract } from &quot;./contract&quot;;

const client = initClient(contract, {
  baseUrl: &quot;http://localhost:3333/api&quot;,
  baseHeaders: {
    &quot;Content-Type&quot;: &quot;application/json&quot;,
  },
  api: async ({ path, method, headers, body }) =&gt; {
    const baseUrl = &quot;http://localhost:3333/api&quot;; //baseUrl is not available as a param, yet
    try {
      const result = await axios.request({
        method: method as Method,
        url: `${this.baseUrl}/${path}`,
        headers,
        data: body,
      });
      return {
        status: result.status,
        body: result.data,
        headers: result.headers,
      };
    } catch (e: Error | AxiosError | any) {
      if (isAxiosError(e)) {
        const error = e as AxiosError;
        const response = error.response as AxiosResponse;
        return {
          status: response.status,
          body: response.data,
          headers: response.headers,
        };
      }
      throw e;
    }
  },
});</code></pre>
<p>In our example code today, we will be creating a custom handler for our <code>api</code> property with Axios.</p>
<h2 id="what-can-we-extend">What can we extend?</h2>
<p>We will be doing more &quot;showing&quot; than &quot;telling&quot; in this short blog post.</p>
<p>In short, the standard <code>ApiFetcherArgs</code> which are passed to the <code>api</code> handler extending the exported <code>InitClientArgs</code>. Both <code>ApiFetcherArgs</code> and <code>InitClientArgs</code> are available type exports from <code>ts-rest</code>.</p>
<p>Let&#39;s show some type definitions from my current version of TS Rest to show what we have to work with.</p>
<p>For <code>ApiFetcher</code> with is the type of <code>api</code>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>export type ApiFetcher = (args: ApiFetcherArgs) =&gt; Promise&lt;{
  status: number;
  body: unknown;
  headers: Headers;
}&gt;;</code></pre>
<p>So we know from this function signature that whatever we pass for our custom client must return a promise with <code>status</code>, <code>body</code> and <code>headers</code>.</p>
<p>As for the <code>args</code> property of type <code>ApiFetcherArgs</code>, we can actually extend this ourselves. TS Rest docs also have an <a href="https://ts-rest.com/docs/core/custom#extra-query-arguments" target="_blank" rel="noopener noreferrer">entire section demonstrating this</a>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>const client = initClient(contract, {
  baseUrl: &quot;http://localhost:5003&quot;,
  baseHeaders: {},
  api: async (args: ApiFetcherArgs &amp; { myCustomArg?: string }) =&gt; {
    if (args.myCustomArg) {
      // do something with myCustomArg ✨
    }

    return tsRestFetchApi(args);
  },
});</code></pre>
<p>As long as we follow that contractual obligation, we can customize things however we want.</p>
<h2 id="enabling-retries-timeouts-and-more">Enabling retries, timeouts and more</h2>
<p>At this point, we can show some code.</p>
<p>It&#39;s worth highlighting: this code is a <strong>rough proof-of-concept and nothing special</strong>. It will illustrate enough to get the point across, but be sure to make adjustments as your needs suit.</p>
<p>The code I will show you is a class that has a <code>handler</code> method which meets the contractual obligation to be used.</p>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>Why a class? The TS Rest docs demonstrates passing a function. In my own work,
I generally use a dependency injection library like 
<a href="https://github.com/inversify/InversifyJS">InversifyJS</a>, which you
could use to inject dependencies into our custom fetch client below.</p><p>It&#39;s purely just to demonstrate something different to the docs.</p> </div> </aside>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import type { ApiFetcherArgs } from &quot;@ts-rest/core&quot;;
import axios, { isAxiosError } from &quot;axios&quot;;
import type { AxiosError, AxiosRequestConfig } from &quot;axios&quot;;
import { merge } from &quot;es-toolkit&quot;;

const BASE_MS = 100;
const MAX_MS = 10000;
const JITTER_FACTOR = 0.5;

interface TsRestClientConfig {
  span?: { name: string; options?: Record&lt;string, unknown&gt; };
  shouldRetry?: (error: AxiosError) =&gt; boolean; // Custom function to determine retry
  retries?: number;
  timeout?: number; // in milliseconds
  retryableStatuses?: number[]; // Add specific status codes to retry
  retryDelay?: {
    baseMs?: number; // Base delay in milliseconds (default: 100)
    maxMs?: number; // Maximum delay cap in milliseconds (default: 10000)
    jitterFactor?: number; // How much jitter to add (0-1, default: 0.5)
  };
}

type ExtendedApiFetcherArgs = TsRestClientConfig &amp; ApiFetcherArgs;

const configDefaults = {
  retries: 0,
  retryDelay: {
    baseMs: BASE_MS,
    maxMs: MAX_MS,
    jitterFactor: JITTER_FACTOR,
  },
  retryableStatuses: [408, 429, 500, 502, 503, 504],
};

export class TsRestClient {
  private config: TsRestClientConfig;
  private axiosRequestConfig: AxiosRequestConfig;

  constructor(
    config: TsRestClientConfig = {},
    axiosRequestConfig: AxiosRequestConfig = {}
  ) {
    this.config = config;
    this.axiosRequestConfig = axiosRequestConfig;
  }

  public handler = async (extendedApiFetcherArgs: ExtendedApiFetcherArgs) =&gt; {
    return this.tryExecute({
      extendedApiFetcherArgs,
      attempt: 0,
    });
  };

  private async tryExecute({
    extendedApiFetcherArgs,
    attempt,
  }: {
    extendedApiFetcherArgs: ExtendedApiFetcherArgs;
    attempt: number;
  }) {
    const args = merge(this.config, extendedApiFetcherArgs);
    const {
      span,
      timeout,
      retries = configDefaults.retries,
      retryDelay = configDefaults.retryDelay,
    } = args;

    try {
      const axiosConfig: AxiosRequestConfig = {
        ...this.axiosRequestConfig,
        url: args.path,
        method: args.method,
        headers: args.headers,
        data: args.body,
        timeout,
      };

      const response = await axios(axiosConfig);

      if (span) {
        // TODO: End span
      }

      return {
        headers: response.headers,
        status: response.status,
        body: response.data,
      };
    } catch (error) {
      if (attempt &lt; retries &amp;&amp; this.isRetryable(error)) {
        const baseDelay = 2 ** (attempt + 1) * (retryDelay.baseMs ?? BASE_MS);
        const jitter =
          1 -
          (retryDelay.jitterFactor ?? JITTER_FACTOR) +
          Math.random() * 2 * (retryDelay.jitterFactor ?? JITTER_FACTOR);
        const delay = Math.min(
          Math.floor(baseDelay * jitter),
          retryDelay.maxMs ?? MAX_MS
        );

        console.log(
          `Retrying in ${delay}ms (attempt ${attempt + 1} of ${retries})...`
        );
        await new Promise((resolve) =&gt; setTimeout(resolve, delay));

        return this.tryExecute({
          extendedApiFetcherArgs,
          attempt: attempt + 1,
        });
      }

      if (span) {
        // TODO: End span
      }

      if (isAxiosError(error) &amp;&amp; error.response) {
        return {
          headers: error.response.headers,
          status: error.response.status,
          body: error.response.data,
        };
      }

      throw error;
    }
  }

  private isRetryable(error: unknown): boolean {
    if (this.config.shouldRetry &amp;&amp; isAxiosError(error))
      return this.config.shouldRetry(error);
    if (isAxiosError(error)) {
      return (
        !error.response ||
        (
          this.config.retryableStatuses ?? configDefaults.retryableStatuses
        ).includes(error.response.status)
      );
    }
    return (
      error instanceof Error &amp;&amp;
      (error.message.includes(&quot;timeout&quot;) ||
        error.message.includes(&quot;network&quot;) ||
        error.message.includes(&quot;connection&quot;))
    );
  }
}</code></pre>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>You may notice that <code>handler</code> is an arrow function. This is just to do with
binding <code>this</code> properties. You can do this another way if you wish by
explicitly binding <code>this</code>.</p> </div> </aside>
<p>In the above code, we have some helpers to determine whether a code is &quot;retry-able&quot;, we also pass a <code>timeout</code> argument property to the Axios client where Axios can control the abort signals for our calls and we leave some placeholder space for starting and stopping &quot;spans&quot;.</p>
<p>An example of implementing our client could now look like this:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import { initClient } from &quot;@ts-rest/core&quot;;
import { contract } from &quot;./contract&quot;;
import { TsRestClient } from &quot;./rpc-class&quot;;

async function main() {
  // Initialize the client with the custom API
  const client = initClient(contract, {
    baseUrl: &quot;http://localhost:3000&quot;,
    baseHeaders: {},
    api: new TsRestClient().handler,
  });

  // Example usage with helper options
  const result = await client.authors.getAuthors({
    retries: 3,
    span: { name: &quot;fetch-authors&quot;, options: { operation: &quot;read&quot; } },
    timeout: 10000,
  });
  console.log(result.status);

  switch (result.status) {
    case 200:
      console.log(result.body);
      break;
    case 404:
      console.log(result.body);
      break;
    case 500:
      console.log(result.body);
      break;
  }
}

main();</code></pre>
<p>In the above code, I pass the <code>api</code> code the <code>handler</code> method property of a new <code>TsRestClient</code> instance.</p>
<p>More notably, it is type safe for my to pass custom arguments for <code>retries</code>, <code>span</code> and <code>timeout</code>.</p>
<p>In the case of my contract, I expect I can return 200, 404 or 500s. If I run the code, I get the following for each scenario:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code># 200
npx tsx src/client.ts
Starting span: fetch-authors
attempt 0
Ending span: fetch-authors
200
[ { id: &#39;123&#39;, email: &#39;example@example.com&#39;, name: &#39;Test&#39; } ]

# 404
npx tsx src/client.ts
Starting span: fetch-authors
attempt 0
Ending span: fetch-authors
404
{ message: &#39;Not found&#39; }

# 500
$ npx tsx src/client.ts
Starting span: fetch-authors
attempt 0
Retrying in 245ms (attempt 1 of 3)...
attempt 1
Retrying in 312ms (attempt 2 of 3)...
attempt 2
Retrying in 1007ms (attempt 3 of 3)...
attempt 3
Ending span: fetch-authors
500
{ message: &#39;Internal server error&#39; }</code></pre>
<p>Other than the fact that my logging is zero-indexed (oops), you can now see that each code has different behaviour based on the responses.</p>
<p>In the case of my 200, I have a span start, span end and log the contract result from my test server.</p>
<p>The 404 behaves in a similar manner, but returns with <strong>not found</strong> after just one attempt. This follows my default configuration behind what are the retryable statuses.</p>
<p>Finally, the 500 demonstrates the retries. I makes <strong>four attempts</strong> in total, each with some exponential back-off based on my settings.</p>
<p>Our current API allows for custom overrides per invocation. So if we wanted to, we could enable 404s to be retryable.</p>
<p>An example:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>const result = await client.authors.getAuthors({
  retries: 3,
  retryableStatuses: [404],
  span: { name: &quot;fetch-authors&quot;, options: { operation: &quot;read&quot; } },
  timeout: 10000,
});</code></pre>
<p>Running this, we can see that I ended up making one retry attempt before getting the 200:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>$ npx tsx src/client.ts
Retrying in 120ms (attempt 1 of 3)...
200
[ { id: &#39;123&#39;, email: &#39;example@example.com&#39;, name: &#39;Test&#39; } ]</code></pre>
<aside class="callout callout--warning" aria-label="Warning"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><path d="m21.7 18-8-14a2 2 0 0 0-3.4 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.7-3ZM12 9v4M12 17h.01" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>  <div class="callout__body">  <p>In general, I wouldn&#39;t be retrying on 404s without good reason. Understand
your system before doing so or you might negatively impact your network
traffic.</p> </div> </aside>
<h2 id="improvements">Improvements</h2>
<p>As mentioned, we have placeholders for spans there and I haven&#39;t touched on caching.</p>
<p>It would be trivial to augment this to use an in-memory cache that you can opt into and extend our API options to include cache options.</p>
<p>As for the spans, you&#39;re more likely to be using something like Otel. In that case, you might have a configuration like the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import { NodeTracerProvider } from &quot;@opentelemetry/sdk-trace-node&quot;;
import { Resource } from &quot;@opentelemetry/resources&quot;;
import { SemanticResourceAttributes } from &quot;@opentelemetry/semantic-conventions&quot;;
import { AWSXRayIdGenerator } from &quot;@opentelemetry/id-generator-aws-xray&quot;;
import { AWSXRayPropagator } from &quot;@opentelemetry/propagator-aws-xray&quot;;
import { AWSXRayExporter } from &quot;@opentelemetry/exporter-aws-xray&quot;;
import { registerInstrumentations } from &quot;@opentelemetry/instrumentation&quot;;
import { getNodeAutoInstrumentations } from &quot;@opentelemetry/auto-instrumentations-node&quot;;

import { trace, context, propagation } from &quot;@opentelemetry/api&quot;;

// Create the tracer provider with AWS X-Ray ID generator
const provider = new NodeTracerProvider({
  idGenerator: new AWSXRayIdGenerator(),
  resource: new Resource({
    [SemanticResourceAttributes.SERVICE_NAME]: &quot;my-node-service&quot;,
  }),
});

// Set the X-Ray exporter
provider.addSpanProcessor(new SimpleSpanProcessor(new AWSXRayExporter()));

// Register the provider
provider.register();

// Set the global propagator to X-Ray format
propagation.setGlobalPropagator(new AWSXRayPropagator());

// Optional: register all known instrumentations
registerInstrumentations({
  instrumentations: [getNodeAutoInstrumentations()],
});

// Now you can use OpenTelemetry as usual
const tracer = trace.getTracer(&quot;my-node-service&quot;);</code></pre>
<p>Once you have a tracer available, you can start and stop spans like so:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>const tracer = trace.getTracer(&quot;my-node-service&quot;);

// Somewhere in your business logic
const span = tracer.startSpan(&quot;fraudCheck&quot;);

context.with(trace.setSpan(context.active(), span), () =&gt; {
  try {
    // do fraud check
    if (someRiskyCondition) {
      span.setAttribute(&quot;fraud.score&quot;, 95);
    }
    span.addEvent(&quot;fraud check complete&quot;);
  } catch (err) {
    span.recordException(err);
    span.setStatus({ code: SpanStatusCode.ERROR });
  } finally {
    span.end();
  }
});</code></pre>
<p>Instead of passing in the span arguments we made, you might pass in the span or context itself and have your client manage its lifecycle or attribute properties.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Short post today. I wanted to just show off a proof-of-concept that I had in order to illustrate that you can encapsulate some of that timeout and retry logic into your custom client logic so that others can make use it in their projects when calling your servers.</p>
<h2 id="links-and-further-reading">Links and Further Reading</h2>
<ul>
<li><a href="https://ts-rest.com/" target="_blank" rel="noopener noreferrer">TS Rest</a></li>
<li><a href="https://zod.dev/" target="_blank" rel="noopener noreferrer">Zod</a></li>
<li><a href="https://tanstack.com/query/latest" target="_blank" rel="noopener noreferrer">Tanstack Query</a></li>
<li><a href="https://ts-rest.com/docs/core/custom" target="_blank" rel="noopener noreferrer">Custom Clients in TS Rest</a></li>
<li><a href="https://ts-rest.com/docs/core/custom#extra-query-arguments" target="_blank" rel="noopener noreferrer">Extra Query Arguments in TS Rest</a></li>
<li><a href="https://axios-http.com/docs/intro" target="_blank" rel="noopener noreferrer">Axios Introduction</a></li>
<li><a href="https://github.com/inversify/InversifyJS" target="_blank" rel="noopener noreferrer">InversifyJS</a></li>
</ul>
<p><em>Photo credit: <a href="https://unsplash.com/@mickdepaola" target="_blank" rel="noopener noreferrer"><code>mickdepaola</code></a></em></p>]]></content>
<category term="typescript"/>
</entry>
<entry>
<title type="html"><![CDATA[Well Known JSON Web Key Sets (JWKS)]]></title>
<id>https://blog.dennisokeeffe.com/blog/2025-04-09-well-known-jwks</id>
<link href="https://dennisokeeffe.com/blog/2025-04-09-well-known-jwks"/>
<published>2025-04-09T00:00:00.000Z</published>
<updated>2025-04-09T00:00:00.000Z</updated>
<summary type="html"><![CDATA[Understand how JSON Web Key Sets served from .well-known endpoints enable you to dynamically verify JSON Web Tokens (JWTS).]]></summary>
<content type="html"><![CDATA[<p>When working across multiple services, being able to verify the source of a JSON Web Token (JWT) plays a vital role in proving token integrity.</p>
<p>In this blog post, we&#39;ll be working through the process of distributing JWTs and verifying them in another service by making use of a &quot;well-known&quot; public endpoint.</p>
<p>You can find the demo code examples <a href="https://github.com/okeeffed/demo-well-known-jwks" target="_blank" rel="noopener noreferrer">on my GitHub</a>.</p>
<h2 id="well-known-endpoints">Well-known endpoints</h2>
<p><a href="https://en.wikipedia.org/wiki/Well-known_URI" target="_blank" rel="noopener noreferrer">Well-known URIs</a> are public endpoints prefixed with <code>/.well-known/</code>.</p>
<p>Defined in <a href="https://datatracker.ietf.org/doc/html/rfc8615" target="_blank" rel="noopener noreferrer">RFC 8615</a>, the purpose of well known endpoints is to add a consistent resolution to web applications that require the discovery of information about an origin (also known as <strong>&quot;site-wide metadata&quot;</strong>).</p>
<p>| Well-Known Endpoint          | Purpose                                                                                                                                | Example URL                                                  |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | --- |
| <code>openid-configuration</code>       | Provides OpenID Connect provider configuration details, including endpoints and supported capabilities.                                | <code>https://example.com/.well-known/openid-configuration</code>       |
| <code>jwks.json</code>                  | Supplies the JSON Web Key Set (JWKS), containing public keys used to verify JSON Web Tokens (JWTs) issued by the authorization server. | <code>https://example.com/.well-known/jwks.json</code>                  |
| <code>webfinger</code>                  | Enables discovery of information about entities (e.g., users) identified by a URI, often used in federated systems.                    | <code>https://example.com/.well-known/webfinger</code>                  |
| <code>oauth-authorization-server</code> | Provides metadata about the OAuth 2.0 authorization server, detailing its capabilities and endpoints.                                  | <code>https://example.com/.well-known/oauth-authorization-server</code> |
| <code>security.txt</code>               | Specifies security policies and contact information for reporting vulnerabilities.                                                     | <code>https://example.com/.well-known/security.txt</code>               |    |</p>
<p>Well-known endpoints standardize the process of discovering essential service information and ease integration across various services.</p>
<p>As noted in the above table, we can use this supply the JSON Web Key Set that contains the public keys that help us with the JWT verification process.</p>
<h2 id="verifying-jwts-without-well-known-endpoints">Verifying JWTS without well-known endpoints</h2>
<p>Before demonstrating an example of a well-known endpoint assisting with JWT verification, we can take a look at how verification works.</p>
<p>In order to do this, we need to generate a key-pair to use. For the sake of mixing things up compared to my previous posts, I&#39;ll generate this key pair using <a href="https://www.npmjs.com/package/node-forge" target="_blank" rel="noopener noreferrer">node-forge</a>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import forge from &quot;node-forge&quot;;
import fs from &quot;node:fs&quot;;

const { pki } = forge;

// 1. Generate RSA keypair
const keypair = pki.rsa.generateKeyPair(2048);

// 2. Convert keys to PEM format
const privateKeyPem = pki.privateKeyToPem(keypair.privateKey);
const publicKeyPem = pki.publicKeyToPem(keypair.publicKey);

// 3. Print or save them
console.log(&quot;🔐 Private Key:\n&quot;, privateKeyPem);
console.log(&quot;🔓 Public Key:\n&quot;, publicKeyPem);

// Optional: save to files
fs.writeFileSync(&quot;keys/private.pem&quot;, privateKeyPem);
fs.writeFileSync(&quot;keys/public.pem&quot;, publicKeyPem);</code></pre>
<p>Running this script will generate my key-pair for me in a <code>keys</code> folder.</p>
<p>Once completed, we can use another script to generate a JSON Web Token (JWT) and then run verification across it.</p>
<p>Here is a simple script doing just that:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import jwt from &quot;jsonwebtoken&quot;;
import fs from &quot;node:fs&quot;;

const privateKey = fs.readFileSync(&quot;./keys/private.pem&quot;);

const issuer = &quot;your-issuer&quot;;
const audience = &quot;your-audience&quot;;
const algorithm = &quot;RS256&quot;;

const token = jwt.sign({ sub: &quot;user_123&quot;, role: &quot;admin&quot; }, privateKey, {
  algorithm,
  keyid: &quot;test-key&quot;,
  expiresIn: &quot;1h&quot;,
  issuer,
  audience,
});

const publicKey = fs.readFileSync(&quot;./keys/public.pem&quot;);

try {
  const decoded = jwt.verify(token, publicKey, {
    algorithms: [algorithm],
    issuer, // optional, but recommended
    audience, // optional
  });

  console.log(&quot;✅ Token is valid!&quot;);
  console.log(&quot;Decoded payload:&quot;, decoded);
} catch (err) {
  console.error(&quot;❌ Token verification failed:&quot;, (err as Error).message);
}</code></pre>
<p>If we run this script, we can validate that everything is verified as expected:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>npx tsx ./simple-jwt-verification.ts
✅ Token is valid!
Decoded payload: {
  sub: &#39;user_123&#39;,
  role: &#39;admin&#39;,
  iat: 1744184754,
  exp: 1744188354,
  aud: &#39;your-audience&#39;,
  iss: &#39;your-issuer&#39;
}</code></pre>
<p>As you might expect, if you update any of the verification values to expect something different i.e. a different issuer or audience, then re-running the script will fail the validation.</p>
<h2 id="verification-with-a-well-known-jwks-endpoint">Verification with a well-known JWKS endpoint</h2>
<p>Now that we understand how verification can work for JWTs, let&#39;s use a local server with a well-known endpoint to perform the verification this time.</p>
<p>For our server file, we can add the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import { Hono } from &quot;hono&quot;;
import { exportJWK, importSPKI } from &quot;jose&quot;;
import fs from &quot;node:fs&quot;;
import { serve } from &quot;@hono/node-server&quot;;

const publicKey = fs.readFileSync(&quot;./keys/public.pem&quot;, &quot;utf8&quot;);

// Convert PEM to KeyLike (SPKI = public key in PEM format)
const key = await importSPKI(publicKey, &quot;RS256&quot;);

// Convert the public key to JWK format
const jwk = await exportJWK(key);
jwk.kid = &quot;test-key&quot;; // must match JWT `kid`

const app = new Hono();

// Serve JWKS
app.get(&quot;/.well-known/jwks.json&quot;, (c) =&gt; {
  return c.json({ keys: [jwk] });
});

serve(app, (info) =&gt; {
  console.log(`Listening on http://localhost:${info.port}`);
});</code></pre>
<p>We import helper functions from the widely used <a href="https://www.npmjs.com/package/jose" target="_blank" rel="noopener noreferrer">jose</a> package for converting our public key string into a format that can be exported as a JSON Web Key.</p>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>JOSE stands for JSON Object Signing and Encryption. SPKI stands for Subject
Public Key Info.</p> </div> </aside>
<p>The SPKI format is used for representing public keys, and it&#39;s this format that we need to pass as an argument for <code>exportJWK</code>.</p>
<p>We start up a simple <a href="https://hono.dev/" target="_blank" rel="noopener noreferrer">Hono web server</a> and serve our JSON Web Key as part of an array of keys from the endpoint <code>/.well-known/jwks.json</code>.</p>
<p>As for the verification script, we can follow a similar script to before with some helper functions to derive our public key used for the verification:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import jwt from &quot;jsonwebtoken&quot;;
import axios from &quot;axios&quot;;
import jwkToPem from &quot;jwk-to-pem&quot;;
import fs from &quot;node:fs&quot;;

const issuer = &quot;http://localhost:3000&quot;; // your issuer that serves the .well-known/jwks.json
const audience = &quot;your-audience&quot;;
const algorithm = &quot;RS256&quot;;
const kid = &quot;test-key&quot;;
const jwksUrl = `${issuer}/.well-known/jwks.json`;

const privateKey = fs.readFileSync(&quot;./keys/private.pem&quot;);

const token = jwt.sign({ sub: &quot;user_123&quot;, role: &quot;admin&quot; }, privateKey, {
  algorithm,
  keyid: kid,
  expiresIn: &quot;1h&quot;,
  issuer,
  audience,
});

async function getPublicKeyFromJwks(kid: string): Promise&lt;string&gt; {
  const { data } = await axios.get(jwksUrl);
  const jwk = data.keys.find((key: any) =&gt; key.kid === kid);

  if (!jwk) {
    throw new Error(`Key with kid &quot;${kid}&quot; not found in JWKS`);
  }

  return jwkToPem(jwk);
}

async function verifyToken(token: string) {
  const decodedHeader = jwt.decode(token, { complete: true })?.header;

  if (!decodedHeader || !decodedHeader.kid) {
    throw new Error(&quot;Missing or invalid token header&quot;);
  }

  const publicKey = await getPublicKeyFromJwks(decodedHeader.kid);

  const payload = jwt.verify(token, publicKey, {
    algorithms: [algorithm],
    issuer,
    audience,
  });

  return payload;
}

verifyToken(token)
  .then((decoded) =&gt; {
    console.log(&quot;✅ Token is valid!&quot;);
    console.log(&quot;Decoded payload:&quot;, decoded);
  })
  .catch((err) =&gt; {
    console.error(&quot;❌ Token verification failed:&quot;, (err as Error).message);
  });</code></pre>
<p>In this script, we make use of the <a href="https://www.npmjs.com/package/jwk-to-pem" target="_blank" rel="noopener noreferrer">jwk-to-pem</a> library to convert the JWK that we fetch into the PEM format. We also make use of an HTTP client <a href="https://www.npmjs.com/package/axios" target="_blank" rel="noopener noreferrer">Axios</a> in order to make our GET request.</p>
<p>It&#39;s worth also calling out here that we could have used the <code>jose</code> library as well. It has some helper functions for verifying JWTs and importing JWKs. I opted not to use it in order to keep our script that previously verified with the <code>jsonwebtoken</code> library aligned to make it easier to see the differences.</p>
<p>Another potentially confusing point to clarify: I am creating the JWT within the script with the private key that we generated previously, but using the well-known endpoint to verify it with the public key information that we also generated earlier in this blog post.</p>
<p>If we run our updated script, it will give us our desired verification:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>$ npx tsx server-jwt-verification.ts
✅ Token is valid!
Decoded payload: {
  sub: &#39;user_123&#39;,
  role: &#39;admin&#39;,
  iat: 1744191716,
  exp: 1744195316,
  aud: &#39;your-audience&#39;,
  iss: &#39;http://localhost:3000&#39;
}</code></pre>
<p>Great! We successfully verified our JSON Web Token using the well-known endpoint that shared our JSON Web Keys.</p>
<h2 id="conclusion">Conclusion</h2>
<p>We explored the use of Well-Known JSON Web Key Sets (JWKS) for dynamically verifying JSON Web Tokens (JWTs) across distributed services.</p>
<p>This becomes useful in your own services if you have a centralized service delegating JWTs that you need extra verification of that are durable against key-rotations.</p>
<p>This was done using well-known endpoints, which are worth knowing as these endpoints have many use cases for distributing service metadata. You are likely to require doing this yourself with other 3rd parties at some stage.</p>
<h2 id="links-and-further-reading">Links and Further Reading</h2>
<ul>
<li><a href="https://www.npmjs.com/package/node-forge" target="_blank" rel="noopener noreferrer">node-forge (npm)</a></li>
<li><a href="https://www.npmjs.com/package/jsonwebtoken" target="_blank" rel="noopener noreferrer">jsonwebtoken (npm)</a></li>
<li><a href="https://www.npmjs.com/package/jose" target="_blank" rel="noopener noreferrer">jose (npm)</a></li>
<li><a href="https://hono.dev/" target="_blank" rel="noopener noreferrer">Hono web framework</a></li>
<li><a href="https://www.npmjs.com/package/jwk-to-pem" target="_blank" rel="noopener noreferrer">jwk-to-pem (npm)</a></li>
<li><a href="https://www.npmjs.com/package/axios" target="_blank" rel="noopener noreferrer">Axios (npm)</a></li>
<li><a href="https://datatracker.ietf.org/doc/html/rfc8615" target="_blank" rel="noopener noreferrer">RFC 8615 - Well-Known URIs</a></li>
<li><a href="https://en.wikipedia.org/wiki/Well-known_URI" target="_blank" rel="noopener noreferrer">Wikipedia - Well-known URI</a></li>
<li><a href="https://github.com/okeeffed/demo-well-known-jwks" target="_blank" rel="noopener noreferrer">GitHub demo code | okeeffed/demo-well-known-jwks</a></li>
</ul>
<p><em>Photo credit: <a href="https://unsplash.com/@alisonstardust" target="_blank" rel="noopener noreferrer"><code>alisonstardust</code></a></em></p>]]></content>
<category term="typescript"/><category term="auth"/><category term="jwts"/>
</entry>
<entry>
<title type="html"><![CDATA[Mutual Transport Layer Security (mTLS)]]></title>
<id>https://blog.dennisokeeffe.com/blog/2025-04-08-mutual-transport-layer-security-mtls</id>
<link href="https://dennisokeeffe.com/blog/2025-04-08-mutual-transport-layer-security-mtls"/>
<published>2025-04-08T00:00:00.000Z</published>
<updated>2025-04-08T00:00:00.000Z</updated>
<summary type="html"><![CDATA[Understand how mutual transport layer security works (mTLS), configure your own chain of trust and see real-world use cases.]]></summary>
<content type="html"><![CDATA[<p>Mutual Transport Layer Security (mTLS) is an extension to Transport Layer Security (TLS) that, in addition to verifying the server&#39;s, <strong>also verifies the connecting client&#39;s identity</strong>.</p>
<p>It&#39;s an extra layer of security that can be applied at the <strong>networking layer of the OSI model (layer 4)</strong> and is used extensively within architectures such as financial systems and service meshes.</p>
<p>This post will work through defining what mTLS is, work through some key components such as <strong>Certificate Authorities (CAs)</strong>, <strong>Certificate Signing Requests (CSRs)</strong> and <strong>x.509 certificates</strong>, work through building an example application with your own <strong>chain of trust</strong> that includes a <strong>root CA</strong>, <strong>intermediary CA</strong>, <strong>client</strong> and <strong>server</strong> and round off with some real-world case studies.</p>
<h2 id="mutual-transport-layer-security">Mutual Transport Layer Security</h2>
<p>To understand how mTLS extends TLS, let&#39;s recap how TLS 1.3 works at a high level.</p>
<p>The verification process of <strong>TLS 1.3</strong> introduces a more streamlined handshake compared to earlier versions.</p>
<p>It reduces the number of round trips and encrypts more of the handshake itself while maintaining strong security guarantees.</p>
<p>In <strong>standard (one-way) TLS</strong>, only the <strong>server is authenticated</strong>.</p>
<p><strong>A high-level summary of the one-way TLS 1.3 handshake</strong>:</p>
<ol>
<li><strong>Client</strong> sends a <code>ClientHello</code>, which includes supported cipher suites, extensions (like <a href="https://www.keycdn.com/support/alpn" target="_blank" rel="noopener noreferrer">ALPN: Application-Layer Protocol Negotiation</a>), and its <strong>key share</strong> (used for Diffie-Hellman key exchange).</li>
<li><strong>Server</strong> responds with a <code>ServerHello</code>, its own <strong>key share</strong>, and its <strong>certificate</strong> (usually signed by a trusted Certificate Authority).</li>
<li>The <strong>client verifies</strong> the server&#39;s certificate using its <strong>trust store</strong>.</li>
<li>Both sides use their key shares to derive a <strong>shared secret</strong> (<strong>ECDHE: Elliptic Curve Diffie-Hellman Ephemeral</strong> is common).</li>
<li>A secure, encrypted channel is established using <strong>symmetric encryption</strong> based on the <strong>shared secret</strong>.</li>
</ol>
<blockquote>
<p>The <code>ClientHello</code> and <code>ServerHello</code> messages are described in detail in the <a href="https://datatracker.ietf.org/doc/html/rfc8446#section-4.1">TLS 1.3 specification</a>.</p>
</blockquote>
<p>In this process, the server proves its identity via the certificate, but the client remains anonymous.</p>
<p>In <strong>mutual TLS (mTLS)</strong>, the handshake includes an <strong>optional client authentication step</strong>, allowing both parties to verify each other&#39;s identities.</p>
<p><strong>A high-level summary of the mutual TLS 1.3 handshake</strong>:</p>
<ol>
<li>Client initiates a connection with a <code>ClientHello</code> and its key share.</li>
<li>Server replies with <code>ServerHello</code>, its own key share, and its certificate.</li>
<li>Client verifies the server&#39;s certificate.</li>
<li><strong>Server sends a <code>CertificateRequest</code></strong> message, asking the client to authenticate.</li>
<li>Client responds with its own <strong>certificate</strong> and a <strong>CertificateVerify</strong> message (proving it holds the private key).</li>
<li>Server verifies the client&#39;s certificate and signature.</li>
<li>Both parties complete the handshake using the agreed key derived from their shared key shares.</li>
<li>A secure, mutually authenticated channel is established.</li>
</ol>
<blockquote>
<p>In mTLS, the connection is only considered secure <strong>after both certificates have been verified</strong> and the shared key is established.</p>
</blockquote>
<p>TLS 1.3&#39;s enhancements make the handshake more efficient while preserving the flexibility to support mTLS when mutual identity verification is required.</p>
<p>In order to understand more on verification with a trusted root Certificate Authority, we need to better understand how certificates work in general and their relation to this process.</p>
<h2 id="certificates-authorities-signing-requests">Certificates, Authorities, Signing Requests</h2>
<p>Let&#39;s start by defining some of the key terms:</p>
<table>
<thead>
<tr>
<th>Term</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>X.509 Certificate</strong></td>
<td>A standard digital certificate format that binds a public key to an identity (e.g. a server or user). Used in TLS to prove identity. Includes fields like Subject, Issuer, Validity Period, and the Public Key.</td>
</tr>
<tr>
<td><strong>Public/Private Key Pair</strong></td>
<td>A cryptographic pair: the private key is kept secret, and the public key is shared. If data is encrypted with one, it can only be decrypted with the other. Certificates include the public key.</td>
</tr>
<tr>
<td><strong>Certificate Authority (CA)</strong></td>
<td>A trusted entity that issues and signs certificates. Acts like a passport office verifying identities and vouching for them via digital signatures.</td>
</tr>
<tr>
<td><strong>Certificate Signing Request (CSR)</strong></td>
<td>A file generated by an entity (e.g. a server) containing its public key and identity info, which it submits to a CA to request a signed certificate.</td>
</tr>
<tr>
<td><strong>Certificate Signing and Trust Chain</strong></td>
<td>A signed certificate can be verified by following the &quot;chain of trust&quot; up to a trusted root CA. Intermediate CAs are often used between end-entity certs and root CAs.</td>
</tr>
<tr>
<td><strong>Root CA</strong></td>
<td>The top of a certificate chain. Usually self-signed and highly trusted. Kept offline for security and only used to sign intermediate CAs.</td>
</tr>
<tr>
<td><strong>Intermediate CA</strong></td>
<td>A certificate authority signed by the root CA (or another intermediate) that signs end-entity certs. Keeps the root CA isolated and adds flexibility for revocation/rotation.</td>
</tr>
<tr>
<td><strong>Self-Signed Certificate</strong></td>
<td>A certificate signed with its own private key (e.g. a root CA or test cert). Not trusted unless explicitly added to a system&#39;s trust store.</td>
</tr>
<tr>
<td><strong>CA-Signed Certificate</strong></td>
<td>A certificate signed by a trusted CA. Automatically trusted by systems and browsers if the CA is in the trusted root store.</td>
</tr>
<tr>
<td><strong>Trust Store</strong></td>
<td>A collection of root certificates that a client or system inherently trusts. Used to verify whether a certificate&#39;s signature is valid and the issuer is trusted.</td>
</tr>
<tr>
<td><strong>Key Share</strong></td>
<td>The key share is a public key generated for a temporary (ephemeral) key exchange.</td>
</tr>
</tbody>
</table>
<p>Most of these terms require some form of memorization. To help ease that process, here is an analogy with airport security that can help you better visualize how each of these concepts play a role in mTLS.</p>
<table>
<thead>
<tr>
<th>Concept</th>
<th>Analogy</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>X.509 Certificate</strong></td>
<td>Your <strong>passport</strong> – it proves your identity, shows who issued it, and includes a photo (public key) that others can verify.</td>
</tr>
<tr>
<td><strong>Public/Private Key Pair</strong></td>
<td>Like a <strong>passport photo + biometric signature</strong> – only you (private key) can match your photo in the passport (public key). The airport can scan your face/fingerprint (private key behavior) to verify the passport really belongs to you.</td>
</tr>
<tr>
<td><strong>Certificate Authority (CA)</strong></td>
<td>The <strong>passport office</strong> – a trusted organization that checks your identity and issues your passport. People trust passports <strong>because</strong> they trust the passport office.</td>
</tr>
<tr>
<td><strong>Certificate Signing Request (CSR)</strong></td>
<td>A <strong>passport application</strong> – you fill it out, include your details, and submit it to the passport office. It has your info and public key, but it&#39;s not valid yet.</td>
</tr>
<tr>
<td><strong>Certificate Signing and Trust Chain</strong></td>
<td>Imagine your passport was issued by a local passport office that was certified by the national government. Even if you don&#39;t know the local office, you trust the <strong>chain of endorsements</strong> up to the federal level. That&#39;s how a trust chain works.</td>
</tr>
<tr>
<td><strong>Root CA</strong></td>
<td>The <strong>federal government&#39;s passport authority</strong> – top of the trust chain. Everyone agrees to trust it. They don&#39;t deal with individuals but certify regional offices (intermediates).</td>
</tr>
<tr>
<td><strong>Intermediate CA</strong></td>
<td>A <strong>regional passport office</strong> – they verify people and issue passports, but they were certified by the root government authority.</td>
</tr>
<tr>
<td><strong>Self-Signed Certificate</strong></td>
<td>A <strong>DIY passport</strong> – you just printed one yourself and said, &quot;Trust me.&quot; The airport won&#39;t accept it unless you&#39;re flying private and have told them in advance to accept it.</td>
</tr>
<tr>
<td><strong>CA-Signed Certificate</strong></td>
<td>An <strong>official passport</strong> issued by a trusted passport office. Because the issuing office is on the trusted list, your passport is valid.</td>
</tr>
<tr>
<td><strong>Trust Store</strong></td>
<td>The <strong>airport&#39;s trusted list of passport authorities</strong> – if your passport was issued by someone on the list (a known CA), you&#39;re in. If not, you&#39;re denied.</td>
</tr>
<tr>
<td><strong>Key Share</strong></td>
<td>Like the first half of a secret handshake – when you arrive at the airport, you and the officer each offer your half. When combined (privately), you both create a unique shared handshake for this trip.</td>
</tr>
</tbody>
</table>
<p>For the sake of clarification in the analogy, the <strong>airport represents the server</strong> while <strong>you are the client</strong>. Where you need to get your create juices out a bit is understanding how the <strong>certificates (passports)</strong> are shown.</p>
<ul>
<li>In regular TLS, only the server shows <strong>its passport</strong> (this is where the creative thinking needs to come in, since airports obviously don&#39;t have passports). In this scenario, the client (you) checks if it is valid, but you don&#39;t need to show your ID.</li>
<li>In mutual TLS, <strong>both the server and client show their passports</strong>. The airport checks your passport and you verify theirs. No one gets in without being trusted on both sides.</li>
</ul>
<p>To best solidify these elements and the roles that they play in mTLS, we can build out our own local representation of these.</p>
<h2 id="demo-generating-certificates">Demo: Generating certificates</h2>
<p>Based on our key terms, we know there are a few things that we would like to emulate locally:</p>
<ol>
<li>The Root Certificate Authority.</li>
<li>An Intermediate Certificate Authority that acts on behalf of the Root Certificate Authority.</li>
<li>A valid Server certificate.</li>
<li>A valid Client certificate.</li>
</ol>
<p>We can make use of the <a href="https://github.com/openssl/openssl/wiki/Binaries" target="_blank" rel="noopener noreferrer">openssl</a> tool in order to create these certificates.</p>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>Most OSs come with openssl included. If not, follow the instructions on the
website linked before to install the binary.</p> </div> </aside>
<p>First, we need to create the private key that represents <strong>our Root CA&#39;s private key</strong>, then use that key to <strong>generate a x.509 certificate</strong>. Our Root CA in our case will be <strong>self-signed</strong>.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code># Root private key
openssl genrsa -out certs/rootCA.key 4096

# Root self-signed certificate
openssl req -x509 -new -nodes -key certs/rootCA.key -sha256 -days 3650 \
  -subj &quot;/CN=MyRootCA&quot; -out certs/rootCA.crt</code></pre>
<p>We can the use it to create our <strong>Intermediary Certificate Authority</strong>. This process requires us to create a <strong>Certificate Signing Request (CSR)</strong> file.</p>
<p>The <strong>CSR</strong> file that create is then used alongside our <strong>root CA</strong> certificate and private key in order to create a our <strong>intermediate CA certificate</strong>.</p>
<p>To create the Intermediary CA private key, CSR and finally x.509 certificate, we can do the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code># Intermediate CA config
cat &gt; intermediate-ext.cnf &lt;&lt;EOF
[ext]
basicConstraints = critical, CA:TRUE, pathlen:0
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
EOF

# Intermediate CA private key
openssl genrsa -out certs/intermediateCA.key 4096

# Intermediate CSR
openssl req -new -key certs/intermediateCA.key -out certs/intermediateCA.csr \
  -subj &quot;/CN=MyIntermediateCA&quot;

# Use Root CA to sign the intermediate&#39;s CSR
openssl x509 -req -in certs/intermediateCA.csr -CA certs/rootCA.crt -CAkey certs/rootCA.key \
  -CAcreateserial -out certs/intermediateCA.crt -days 1825 -sha256 \
  -extfile intermediate-ext.cnf</code></pre>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>To better understanding the configuration, see the <a href="https://docs.openssl.org/3.1/man5/x509v3_config/#subject-alternative-name" target="_blank" rel="noopener noreferrer">x509v3 config page</a>.</p> </div> </aside>
<p>We first setup a configuration file that is used during the <strong>signing process</strong>, then create the intermediary <strong>private key</strong> and <strong>certificate signing request</strong> before doing the signing to generate the <strong>certificate</strong>.</p>
<p>You might also be wondering at this point: <strong>Why use an intermediary certificate authority?</strong></p>
<p>Here&#39;s the key idea:</p>
<ul>
<li>The <strong>root certificate</strong> (public key, usually self-signed) is shared and trusted.</li>
<li>The <strong>root private key</strong> is the secret signing key and can be kept offline for safety.</li>
<li>The root only needs to be used to sign <strong>intermediates</strong>, which can then go on to sign <strong>leaf certs</strong> (like for clients or servers).</li>
</ul>
<p>So in practice:</p>
<ul>
<li>The <strong>root CA</strong> (offline) <strong>signs the intermediate CA&#39;s cert once</strong>.</li>
<li>The <strong>intermediate CA&#39;s cert</strong> (now signed by the root) is <strong>publicly distributed</strong>.</li>
<li>Clients will <strong>trust the root certificate</strong> (already in their trust store).</li>
</ul>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>There can be alternatives to this where the root certificate (but <strong>not the
key</strong>) is distributed. In the case of the <strong>intermediate cert</strong> being
distributed, the <strong>root CA</strong> is normally concatenated alongside it as the CA.</p> </div> </aside>
<p>In our case, we obviously have the representation of our root CA&#39;s certificate stored on our local system, but in practice this would be stored away securely.</p>
<p>Understanding why we don&#39;t need the root certificate private key should become clearer as we sign the client and server certificates and show them in use.</p>
<p>As a next step, we can follow a similar process for signing and creating the server certificate:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code># Server config
cat &gt; server-ext.cnf &lt;&lt;EOF
[ext]
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = DNS:localhost
EOF

# Server private key
openssl genrsa -out certs/server.key 2048

# Server CSR
openssl req -new -key certs/server.key -out certs/server.csr \
  -subj &quot;/CN=localhost&quot;

# Sign with intermediate CA
openssl x509 -req -in certs/server.csr -CA certs/intermediateCA.crt -CAkey certs/intermediateCA.key \
  -CAcreateserial -out certs/server.crt -days 825 -sha256 \
  -extfile server-ext.cnf -extensions ext</code></pre>
<p>Note that we use the <strong>Intermediate Certificate Authority&#39;s certificate and private key</strong> for signing this certificate.</p>
<p>Now, let&#39;s do the same for the client:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code># Client config
cat &gt; client-ext.cnf &lt;&lt;EOF
[ext]
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth
EOF

# Client private key
openssl genrsa -out certs/client.key 2048

# Client CSR
openssl req -new -key certs/client.key -out certs/client.csr \
  -subj &quot;/CN=client&quot;

# Sign with intermediate CA
openssl x509 -req -in certs/client.csr -CA certs/intermediateCA.crt -CAkey certs/intermediateCA.key \
  -CAcreateserial -out certs/client.crt -days 825 -sha256 \
  -extfile client-ext.cnf -extensions ext</code></pre>
<p>Crucially, the server and client certs were <strong>signed by the intermediate</strong>, but clients (and the server) <strong>only trust the root</strong>.</p>
<p>We need to send the full chain when we present a cert. In order to do so, we need to combine our output certificate files:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code># Concatenation of certificates
cat certs/server.crt certs/intermediateCA.crt &gt; certs/server-chain.crt
cat certs/client.crt certs/intermediateCA.crt &gt; certs/client-chain.crt</code></pre>
<p>Doing this will create two new certificate files that are <strong>inclusive</strong> of our intermediary certificate. This way, when the certificate requestor receives the cert, it can verify:</p>
<ol>
<li>The &quot;leaf&quot; certificate (client/server) was signed by intermediate.</li>
<li>The intermediate was signed by root.</li>
<li>The root is in the trust store.</li>
</ol>
<p>The concatenated files will be used as our certificates, while the public <code>rootCA.crt</code> file will be used as our CA cert in our mTLS configuration that you will soon see, however it&#39;s worth pointing out that you could also concatenate the intermediate and root CA certificats to be used as the CA certificate and then just use the server certificate. The verification process will have the same result.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code># Create a full CA chain file for verification as an alternative
cat certs/rootCA.crt certs/intermediateCA.crt &gt; certs/ca-chain.crt</code></pre>
<p>To see how all of this ties in together, let&#39;s see it at work with a simple Node.js application that can show when and how our certificates are used.</p>
<span id="demo-mtls-application-with-our-generated-certificates" class="anchor-alias"><h2 id="demo-m-tls-application-with-our-generated-certificates">Demo: mTLS application with our generated certificates</h2>
<p>In our demonstration, we will have both a server receiving requests and a client making the requests over a mTLS.</p>
<p>For our tiny server, we&#39;ll run the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>// server.ts
import https from &quot;node:https&quot;;
import fs from &quot;node:fs&quot;;
import type { TLSSocket } from &quot;node:tls&quot;;

try {
  // Read the files
  const serverKey = fs.readFileSync(&quot;certs/server.key&quot;);
  const serverCert = fs.readFileSync(&quot;certs/server-chain.crt&quot;); // Using individual cert
  const caCert = fs.readFileSync(&quot;certs/rootCA.crt&quot;); // Using full CA chain

  function isTlsSocket(socket: unknown): socket is TLSSocket {
    return !!socket &amp;&amp; typeof socket === &quot;object&quot; &amp;&amp; &quot;authorized&quot; in socket;
  }

  const server = https.createServer(
    {
      key: serverKey,
      cert: serverCert,
      ca: caCert,
      requestCert: true,
      rejectUnauthorized: true,
    },
    (req, res) =&gt; {
      const tlsSocket = req.socket;

      if (!isTlsSocket(tlsSocket)) {
        console.error(&quot;Invalid socket properties&quot;);
        res.writeHead(500);
        res.end(&quot;Server error&quot;);
        return;
      }

      console.log(&quot;TLS Connection Details:&quot;);
      console.log(`- Authorized: ${tlsSocket.authorized}`);
      console.log(`- Protocol: ${tlsSocket.getProtocol()}`);
      console.log(`- Cipher: ${tlsSocket.getCipher().name}`);

      if (tlsSocket.getPeerCertificate) {
        const cert = tlsSocket.getPeerCertificate(true);
        console.log(`- Client Subject: ${cert.subject?.CN || &quot;Unknown&quot;}`);
        console.log(`- Client Issuer: ${cert.issuer?.CN || &quot;Unknown&quot;}`);
      }

      if (!tlsSocket.authorized) {
        console.error(`Auth Error: ${tlsSocket.authorizationError}`);
        res.writeHead(401);
        res.end(
          `Client certificate not authorized: ${tlsSocket.authorizationError}`
        );
        return;
      }

      res.writeHead(200);
      res.end(&quot;Hello, secure world with intermediate CA!&quot;);
    }
  );

  server.on(&quot;tlsClientError&quot;, (err) =&gt; {
    console.error(&quot;TLS Client Error:&quot;, err);
  });

  server.listen(3000, () =&gt; {
    console.log(&quot;✅ HTTPS server running at https://localhost:3000&quot;);
  });
} catch (err) {
  console.error(&quot;Failed to start server:&quot;, err);
}</code></pre>
<p>Within the <code>http.createServer</code> options, can configure our mTLS options. As you can probably infer from the naming, we make use of the public root certificate <code>rootCA.crt</code> alongside the server <code>private key</code> and our <code>server-chain.crt</code>.</p>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>It&#39;s important to highlight that we&#39;re using the chained certificate. This is
important to be validated against intermediaries.</p> </div> </aside>
<p>We can demonstrate our client connecting over mTLS with the following script:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>// client.ts
import https from &quot;node:https&quot;;
import fs from &quot;node:fs&quot;;
import axios from &quot;axios&quot;;

// Read the files with better error handling
try {
  const clientKey = fs.readFileSync(&quot;certs/client.key&quot;);
  const clientCert = fs.readFileSync(&quot;certs/client-chain.crt&quot;); // Using individual cert, not chain
  const caCert = fs.readFileSync(&quot;certs/rootCA.crt&quot;); // Using full CA chain

  const agent = new https.Agent({
    key: clientKey,
    cert: clientCert,
    ca: caCert,
    rejectUnauthorized: true,
  });

  console.log(&quot;Starting mTLS client request...&quot;);

  axios
    .get(&quot;https://localhost:3000&quot;, {
      httpsAgent: agent,
      timeout: 10000, // Longer timeout for debugging
    })
    .then((res) =&gt; {
      console.log(&quot;✅ Server response status:&quot;, res.status);
      console.log(&quot;✅ Server response data:&quot;, res.data);
    })
    .catch((err) =&gt; {
      console.error(&quot;❌ Request failed:&quot;);
      if (err.response) {
        console.error(`Status: ${err.response.status}`);
        console.error(`Response: ${JSON.stringify(err.response.data)}`);
      } else if (err.request) {
        console.error(&quot;No response received from server&quot;);
      } else {
        console.error(`Error: ${err.message}`);
      }

      if (err.code) {
        console.error(`Error code: ${err.code}`);
      }

      // More detailed error info for TLS issues
      if (err.cause) {
        console.error(&quot;Error cause:&quot;, err.cause);
      }
    });
} catch (err) {
  console.error(&quot;Failed to read certificate files:&quot;, err);
}</code></pre>
<p>In our case, we&#39;re making use of the <a href="https://www.npmjs.com/package/axios" target="_blank" rel="noopener noreferrer">Axios package</a> which accepts a <code>httpsAgent</code> argument, and we configure this agent using Node&#39;s <code>https</code> agent.</p>
<p>If we start our server, we can see the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>$ npx tsx server.ts

&gt; demo-node-mtls@1.0.0 server
&gt; npx tsx server.ts

✅ HTTPS server running at https://localhost:3000
TLS Connection Details:
- Authorized: true
- Protocol: TLSv1.3
- Cipher: TLS_AES_256_GCM_SHA384
- Client Subject: client
- Client Issuer: MyIntermediateCA</code></pre>
<p>Now that the server is up and running, we can validate that our connection works:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>$ npx tsx client.ts

&gt; demo-node-mtls@1.0.0 client
&gt; npx tsx client.ts

Starting mTLS client request...
✅ Server response status: 200
✅ Server response data: Hello, secure world with intermediate CA!</code></pre>
<p>You can find <a href="https://github.com/okeeffed/demo-node-mtls" target="_blank" rel="noopener noreferrer">the example code on my GitHub</a>.</p>
<h2 id="delegating-certificates-programmatically">Delegating certificates programmatically</h2>
<p>We used the <code>openssl</code> binaries directly to work through our process of creating private keys, certificate signing requests and final signed certificates, but it&#39;s worth highlighting that you can do this programmatically.</p>
<p>Golang has a standard library package <a href="https://pkg.go.dev/crypto/x509" target="_blank" rel="noopener noreferrer">crypto/x509</a> that can generate certificates for you without external packages and is very nice.</p>
<p>I&#39;ve also included an example of doing the exact same certificate chain we did but with Golang on the <a href="https://github.com/okeeffed/demo-node-mtls/blob/main/generate_certs.go" target="_blank" rel="noopener noreferrer">GitHub repository</a>.</p>
<p>What makes this nice is that you can stand up your own private certificate authorities to manage the distribution of certicates.</p>
<p>For Node.js, packages like <a href="https://www.npmjs.com/package/node-forge" target="_blank" rel="noopener noreferrer">node-forge</a> are available which provides support for x509 certificate generation.</p>
<h2 id="case-study-aws-private-ca">Case Study: AWS Private CA</h2>
<p>Taken straight from <a href="https://docs.aws.amazon.com/privateca/latest/userguide/PcaWelcome.html" target="_blank" rel="noopener noreferrer">their documentation</a>:</p>
<blockquote>
<p>AWS Private CA enables creation of private certificate authority (CA) hierarchies, including root and subordinate CAs, without the investment and maintenance costs of operating an on-premises CA.</p>
</blockquote>
<p>It&#39;s a managed-service for your CA requirements if you wish not to manage that yourself. It offers APIs in order for you to interface with the service and can be desired if you don&#39;t wish to create a service yourself.</p>
<p>As an alternative to our approach, you could make use of AWS Private CA to create all of the certificates that we did ourselves, download them via AWS APIs and reenact the same demo that we did with our Node.js configurations.</p>
<h2 id="case-study-istio">Case Study: Istio</h2>
<p><a href="https://istio.io/" target="_blank" rel="noopener noreferrer">Istio</a> is a popular service mesh that extends capabilities for Kubernetes clusters.</p>
<p>One of its core features is to automative mTLS enforcement for inter-service and inter-node communication within microservices.</p>
<p>Within Istio service meshes running in sidecar mode, each Pod is <a href="https://istio.io/latest/docs/overview/dataplane-modes/" target="_blank" rel="noopener noreferrer">deployed with a sidecar</a> which injects an Envoy proxy.</p>
<p>The Istio daemon (istiod) distributes certificates between the sidecars as required and those proxies enforce mTLS between each other for secure, verified communication between Pods.</p>
<p>In this approach, <strong>you do not need to configure mTLS at the client-level</strong>. You make a request as you expect and the data plane will manage the mTLS verfication for you thanks to the proxies.</p>
<p>This transparency is what makes service meshes so powerful for implementing zero-trust security models <strong>without</strong> requiring changes to application code.</p>
<p>For more on its capabilities, read the <a href="https://istio.io/latest/docs/tasks/security/cert-management/plugin-ca-cert/" target="_blank" rel="noopener noreferrer">Istio documentation on certificate management</a>.</p>
<h2 id="certificate-management-and-security">Certificate management and security</h2>
<p>In our own walkthrough, we store all of the certificates locally.</p>
<p>It&#39;s worth taking a moment to highlight that well-planned and <strong>secure management of the entire certificate lifecycle</strong> is absolutely necessary.</p>
<p>Systems like Istio automate certificate issuance and rotation, and clients/servers are configured to trust the organization&#39;s CA.</p>
<p>In your own work, you&#39;ll need to ensure that you <strong>implement proper handling</strong> of keys and certificates.</p>
<p>Certificates have expirations and may require revocation, so ensure that if you are running your own certificate authority to cover these bases.</p>
<p>It&#39;s also worth clarifying that while mTLS adds extra security and authentication, it does not by itself guarantee non-repudiation of transmitted data​. You would still need to remedy this with key signatures on request data. I have an entire section on this in my blog post <a href="https://dennisokeeffe.com/blog/2025-04-07-roll-your-own-api-keys#keys-with-request-signatures">Roll Your Own API Keys</a>.</p>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>Non-repudiation is a term that&#39;s caught me out a few times early in my career. It means that someone cannot deny that they performed an action — like sending a message, approving a transaction, or signing a document.</p><p>In the context of client-server communications, non-repudiation provides proof of origin that can be independently verified and legally upheld.</p><p>Think of it like a signed receipt that can&#39;t be forged, and only you could&#39;ve created it — because it was signed using your private key, and no one else has that key.</p> </div> </aside>
<h2 id="conclusion">Conclusion</h2>
<p>Mutual TLS (mTLS) builds on the strong foundation of TLS by introducing client verification, enabling two-way trust in secure communication. Whether you&#39;re managing communications between services in a microservices architecture or securing client-server interactions in highly regulated industries, mTLS provides an effective layer of defense.</p>
<p>By understanding the roles of certificate authorities, certificate signing processes, and the chain of trust, you can confidently implement mTLS in your applications.</p>
<p>mTLS can introduce operational complexity, but in turn it offers strong identity assurance and encryption that can be foundational for zero-trust architectures and highly secure systems.</p>
<h2 id="links-and-further-reading">Links and further reading</h2>
<ul>
<li><a href="https://docs.apigee.com/api-platform/system-administration/about-ssl" target="_blank" rel="noopener noreferrer">Apigee | About SSL</a></li>
<li><a href="https://www.cloudflare.com/en-gb/learning/ssl/what-happens-in-a-tls-handshake/" target="_blank" rel="noopener noreferrer">Cloudflare | What happens in a TLS handshake</a></li>
<li><a href="https://datatracker.ietf.org/doc/html/rfc8446" target="_blank" rel="noopener noreferrer">TLS 1.3 specification (RFC 8446)</a></li>
<li><a href="https://github.com/openssl/openssl/wiki/Binaries" target="_blank" rel="noopener noreferrer">OpenSSL binaries</a></li>
<li><a href="https://pkg.go.dev/crypto/x509" target="_blank" rel="noopener noreferrer">Golang x509 Package</a></li>
<li><a href="https://istio.io/latest/docs/tasks/security/cert-management/plugin-ca-cert/" target="_blank" rel="noopener noreferrer">Istio | Certificate Management</a></li>
<li><a href="https://docs.aws.amazon.com/privateca/latest/userguide/PcaWelcome.html" target="_blank" rel="noopener noreferrer">AWS Private CA Documentation</a></li>
<li><a href="https://www.npmjs.com/package/axios" target="_blank" rel="noopener noreferrer">Axios (npm)</a></li>
<li><a href="https://www.npmjs.com/package/node-forge" target="_blank" rel="noopener noreferrer">node-forge (npm)</a></li>
<li><a href="https://github.com/okeeffed/demo-node-mtls" target="_blank" rel="noopener noreferrer">GitHub repository with demo</a></li>
<li><a href="https://dennisokeeffe.com/blog/2025-04-07-roll-your-own-api-keys">Roll You Own API Keys</a></li>
</ul>
<p><em>Photo credit: <a href="https://unsplash.com/@jrkorpa" target="_blank" rel="noopener noreferrer"><code>jrkorpa</code></a></em></p>]]></content>
<category term="typescript"/><category term="auth"/>
</entry>
<entry>
<title type="html"><![CDATA[Roll Your Own API Keys]]></title>
<id>https://blog.dennisokeeffe.com/blog/2025-04-07-roll-your-own-api-keys</id>
<link href="https://dennisokeeffe.com/blog/2025-04-07-roll-your-own-api-keys"/>
<published>2025-04-07T00:00:00.000Z</published>
<updated>2025-04-07T00:00:00.000Z</updated>
<summary type="html"><![CDATA[Ever wondered how to configure your own API keys? This blog post explores API key design options and trade-offs.]]></summary>
<content type="html"><![CDATA[<p>API keys at their most basic level offer authentication solutions for both your public-facing and internal-facing APIs. At a deeper level, your API keys can open up strategies such as authorization, rate-limiting and tamper-proofing of payloads.</p>
<p>This post will cover some of the fundamentals of rolling your own API keys in TypeScript and Node.js 22, but the knowledge is language-agnostic (and runtime agnostic for that matter). I will also close with some remarks on why TypeScript might not be the language of choice for your API keys if you expect any sort of scale.</p>
<h2 id="two-strategies-around-api-keys">Two strategies around API Keys</h2>
<p>This post will focus on two strategies around rolling your own API keys:</p>
<ol>
<li><strong>Static API keys</strong></li>
<li><strong>HMAC-signed API keys</strong></li>
</ol>
<p>Both strategies have their trade-offs, but <strong>if you are time-poor, then I would focus on the keys with request signatures</strong>.</p>
<p>We will work through some simple implementations for both that <strong>focuses on authentication</strong> and, in the case of signed keys, tamper-proofing.</p>
<aside class="callout callout--warning" aria-label="Warning"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><path d="m21.7 18-8-14a2 2 0 0 0-3.4 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.7-3ZM12 9v4M12 17h.01" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>  <div class="callout__body">  <p>Added functionality like authorization won&#39;t be the focus. The tl;dr is that
you can use a token identifier to associate things like permissions,
rate-limiting etc.</p> </div> </aside>
<h2 id="static-keys">Static keys</h2>
<p>Static API keys are simply tokens that a client can include with each request.</p>
<p>Server-side, we <strong>verify and ensure the token is valid</strong> in order to <strong>authenticate the request against our token data store</strong>.</p>
<p>You can consider this analogous in ways to <strong>providing a password with a request</strong>. The key <strong>must</strong> be sent with every request, kept secret and only transmitted over TLS to prevent eavesdropping.</p>
<p>An example implementation of static keys with a Hono router:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import { OpenAPIHono, createRoute, z } from &quot;@hono/zod-openapi&quot;;
import * as crypto from &quot;node:crypto&quot;;
import Keyv from &quot;keyv&quot;;

// Headers
const apiKeyHeader = z.object({
  Authorization: z.string().openapi({ example: &quot;Bearer pk_live_abc123&quot; }),
});

// In-memory key store
const keyStore = new Keyv({ namespace: &quot;api-key-store&quot; });

function generateApiKey(): string {
  return `pk_live_${crypto.randomBytes(16).toString(&quot;hex&quot;)}`;
}

function hashApiKey(key: string): string {
  return crypto.createHash(&quot;sha256&quot;).update(key).digest(&quot;hex&quot;);
}

export class ApiKeysController extends OpenAPIHono {
  constructor() {
    super();
    this.create();
    this.verify();
  }

  public create() {
    return this.openapi(
      createRoute({
        method: &quot;post&quot;,
        path: &quot;/keys/create&quot;,
        description: &quot;Create an API key and store its SHA-256 hash&quot;,
        responses: {
          200: {
            description: &quot;API key created&quot;,
            content: {
              &quot;application/json&quot;: {
                schema: z.object({
                  apiKey: z.string(),
                }),
              },
            },
          },
        },
      }),
      async (c) =&gt; {
        const plainKey = generateApiKey();
        const hashedKey = hashApiKey(plainKey);

        await keyStore.set(`hash:${plainKey}`, hashedKey);

        return c.json({ apiKey: plainKey });
      }
    );
  }

  public verify() {
    return this.openapi(
      createRoute({
        method: &quot;post&quot;,
        path: &quot;/keys/verify&quot;,
        description: &quot;Verify an API key using SHA-256 lookup&quot;,
        request: {
          headers: apiKeyHeader,
          body: {
            content: {
              &quot;application/json&quot;: {
                schema: z.object({}),
              },
            },
          },
        },
        responses: {
          200: {
            description: &quot;Verification result&quot;,
            content: {
              &quot;application/json&quot;: {
                schema: z.object({
                  valid: z.boolean(),
                }),
              },
            },
          },
        },
      }),
      async (c) =&gt; {
        const authHeader = c.req.header(&quot;Authorization&quot;) ?? &quot;&quot;;
        const token = authHeader.replace(&quot;Bearer &quot;, &quot;&quot;).trim();
        const expectedHash = await keyStore.get(`hash:${token}`);

        if (!expectedHash) return c.json({ valid: false });

        const isValid = hashApiKey(token) === expectedHash;
        return c.json({ valid: isValid });
      }
    );
  }
}</code></pre>
<aside class="callout callout--danger" aria-label="Danger"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><path d="m21.7 18-8-14a2 2 0 0 0-3.4 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.7-3ZM12 9v4M12 17h.01" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>  <div class="callout__body">  <p>In a real-world application, you should not store a plain text key as part of
the database key. In this proof of concept, it&#39;s more for listing out the keys
to see the data if you wish to do so.</p> </div> </aside>
<p>For the sake of understanding our example, I am using the in-memory store from <a href="https://keyv.org/docs/keyv/" target="_blank" rel="noopener noreferrer">keyv</a> as a stand-in for our data store, <a href="https://hono.dev/examples/zod-openapi" target="_blank" rel="noopener noreferrer">@hono/zod-openapi</a> as our augmented Hono server and some helpers from the <a href="https://nodejs.org/docs/latest-v22.x/api/crypto.html" target="_blank" rel="noopener noreferrer">Node.js crypto module</a> for generating random bytes for our API key and hashing that key to be stored in the database.</p>
<p>Our server has two endpoints:</p>
<ol>
<li>One for creating API keys at <code>/keys/create</code>.</li>
<li>A second for verifying those API keys at <code>/keys/verify</code>.</li>
</ol>
<p>In our <code>/keys/create</code> endpoint, we generate an API key, hash it and then store the hash as a key-value in the database under <code>hash:${plainkey}</code>.</p>
<p>We can call our endpoint like so:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>curl http://localhost:3000/v1/api-keys/keys/create \
  --request POST</code></pre>
<p>If we call our endpoint, we end up with a result like the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="json"><code>{
  &quot;apiKey&quot;: &quot;pk_live_6f6bd5c1b85c5040f4d269ae8c84ce3d&quot;
}</code></pre>
<p>If we then provide that API key under the <code>Authorization</code> header at <code>/keys/verify</code> such as the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>curl http://localhost:3000/v1/api-keys/keys/verify \
  --request POST \
  --header &#39;Authorization: Bearer pk_live_6f6bd5c1b85c5040f4d269ae8c84ce3d&#39; \
  --header &#39;Content-Type: application/json&#39; \
  --data &#39;{}&#39;</code></pre>
<p>...then we can see that our API key is valid.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="json"><code>{
  &quot;valid&quot;: true
}</code></pre>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>For the sake of completion, if you try with an invalid API key then you will
get back <code>valid</code> as <code>false</code>.</p> </div> </aside>
<p>Although static keys can be adequate for many cases, they fall down when extra security is needed. Although that can be rolled or revoked easily, simple API keys cannot verify the identity of the caller or prevent tampering of request parameters in transit, so there are weaker integrity guarantees.</p>
<p>We can work around these problems by introducing request signatures.</p>
<h2 id="keys-with-request-signatures">Keys with request signatures</h2>
<p>Request signatures introduce two values to be shared with the client at the time of API key generation:</p>
<ol>
<li>The <strong>access key ID</strong>.</li>
<li>The <strong>secret access key</strong>.</li>
</ol>
<p>Typically, the <strong>secret access key</strong> is only shared once for the user, so if they lose the <strong>secret access key</strong> then they will need to re-generate keys.</p>
<p>The client <strong>never shares the secret key back to the server</strong>. Instead, the access key ID is sent alongside a signature of the request data which is signed using the secret key.</p>
<p>Once the data is sent, the server <strong>looks up the token secret and recomputes the signature</strong>. If they match, the request is <strong>authenticated</strong> and proven <strong>integral</strong> (which is spiel for not being tampered with).</p>
<p>Let&#39;s take a look at how an implementation of this works:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import { OpenAPIHono, createRoute, z } from &quot;@hono/zod-openapi&quot;;
import * as crypto from &quot;node:crypto&quot;;
import Keyv from &quot;keyv&quot;;

// Headers
const apiKeyHeader = z.object({
  Authorization: z.string().openapi({ example: &quot;Bearer pk_live_abc123&quot; }),
});

const signedHeaders = apiKeyHeader.extend({
  &quot;X-Signature&quot;: z.string().openapi({ example: &quot;a3d4c9...&quot; }),
  &quot;X-Timestamp&quot;: z.string().openapi({ example: &quot;1712333445&quot; }),
});

// In-memory key store
const keyStore = new Keyv({ namespace: &quot;api-key-store&quot; });

function generateApiKey(): string {
  return `pk_live_${crypto.randomBytes(16).toString(&quot;hex&quot;)}`;
}

function hashApiKey(key: string): string {
  return crypto.createHash(&quot;sha256&quot;).update(key).digest(&quot;hex&quot;);
}

function generateHmac(key: string, secret: string): string {
  return crypto.createHmac(&quot;sha256&quot;, secret).update(key).digest(&quot;hex&quot;);
}

export class ApiKeysController extends OpenAPIHono {
  constructor() {
    super();

    this.verify();
    this.secureVerify();

  public secureCreate() {
    return this.openapi(
      createRoute({
        method: &quot;post&quot;,
        path: &quot;/keys/secure-create&quot;,
        description: &quot;Create an HMAC-style API key&quot;,
        responses: {
          200: {
            description: &quot;Key pair created&quot;,
            content: {
              &quot;application/json&quot;: {
                schema: z.object({
                  accessKeyId: z.string(),
                  secretAccessKey: z.string(),
                }),
              },
            },
          },
        },
      }),
      async (c) =&gt; {
        const accessKeyId = generateApiKey();
        const secretAccessKey = crypto.randomBytes(32).toString(&quot;hex&quot;);

        // WARNING: In real-world applications, encrypt this value.
        await keyStore.set(`hmac:${accessKeyId}`, { secret: secretAccessKey });

        return c.json({
          accessKeyId,
          secretAccessKey, // return only once!
        });
      },
    );
  }

  public secureVerify() {
    return this.openapi(
      createRoute({
        method: &quot;post&quot;,
        path: &quot;/keys/secure-verify&quot;,
        description: &quot;Verify HMAC-signed API request using access/secret key&quot;,
        request: {
          headers: signedHeaders,
          body: {
            content: {
              &quot;application/json&quot;: {
                schema: z.object({
                  message: z.string(),
                }),
              },
            },
          },
        },
        responses: {
          200: {
            description: &quot;Verification result&quot;,
            content: {
              &quot;application/json&quot;: {
                schema: z.object({
                  valid: z.boolean(),
                }),
              },
            },
          },
        },
      }),
      async (c) =&gt; {
        const authHeader = c.req.header(&quot;Authorization&quot;) ?? &quot;&quot;;
        const signature = c.req.header(&quot;X-Signature&quot;) ?? &quot;&quot;;
        const timestamp = c.req.header(&quot;X-Timestamp&quot;) ?? &quot;&quot;;
        const token = authHeader.replace(&quot;Bearer &quot;, &quot;&quot;).trim();

        const stored = await keyStore.get(`hmac:${token}`);
        if (!stored?.secret) return c.json({ valid: false });

        const body = await c.req.json();
        const digest = generateHmac(
          `${timestamp}:${JSON.stringify(body)}`,
          // WARNING: In real-world applications, this would be an encrypted value
          // that we should decrypt first.
          stored.secret,
        );

        const valid = crypto.timingSafeEqual(
          Buffer.from(signature),
          Buffer.from(digest),
        );

        return c.json({ valid });
      },
    );
  }
}</code></pre>
<aside class="callout callout--danger" aria-label="Danger"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><path d="m21.7 18-8-14a2 2 0 0 0-3.4 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.7-3ZM12 9v4M12 17h.01" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>  <div class="callout__body">  <p>In real-world applications, if your database is compromised as our current
implementation stands, then the attacker will have access to both the public
access key and secret access key. You should take steps to mitigate this by
storing the encrypted secret, however that sits outside the scope of the
current blog post.</p> </div> </aside>
<p>In our modified code, we again have two endpoints:</p>
<ol>
<li><code>/keys/secure-create</code></li>
<li><code>/keys/secure-verify</code></li>
</ol>
<p>In addition to this, our <code>/keys/secure-verify</code> endpoint now requires three headers:</p>
<ol>
<li><code>Authorization</code>: Our same authorization header.</li>
<li><code>X-Timestamp</code>: A timestamp to denote when the signing occurred.</li>
<li><code>X-Signature</code>: Our signed payload.</li>
</ol>
<p>We will see those headers in action in a moment, but let&#39;s park that there for now.</p>
<p>In our <code>/keys/secure-create</code> endpoint, we now return two key pieces of information. Let&#39;s see this in action.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>curl http://localhost:3000/v1/api-keys/keys/secure-create \
  --request POST</code></pre>
<p>This returns the following JSON:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="json"><code>{
  &quot;accessKeyId&quot;: &quot;pk_live_f5c4672457c78c8e019df66eeabbe33e&quot;,
  &quot;secretAccessKey&quot;: &quot;2a39d2316ef07ebb7c763f3eb06b0ef3aafbd7f0b3d0f2b3bd5acfa6e0b57fd5&quot;
}</code></pre>
<p>On the server, we store the secret for us to fetch later (please see the callout below on this) in order to be used for validating the signature. We then return both values <strong>once</strong>. Warn the user to securely store the secret, as we won&#39;t be returning that value again.</p>
<p>In order to see how signing works, here is a helper script that I used to test against our verification endpoint <code>/keys/secure-verify</code>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import crypto from &quot;node:crypto&quot;;
import axios from &quot;axios&quot;;

const { accessKeyId, secretAccessKey } = {
  accessKeyId: &quot;pk_live_f5c4672457c78c8e019df66eeabbe33e&quot;,
  secretAccessKey:
    &quot;2a39d2316ef07ebb7c763f3eb06b0ef3aafbd7f0b3d0f2b3bd5acfa6e0b57fd5&quot;,
};
const timestamp = Math.floor(Date.now() / 1000).toString();

const body = {
  message: &quot;Hello, HMAC-secured world!&quot;,
};

// Compute HMAC using timestamp and JSON body
const digest = crypto
  .createHmac(&quot;sha256&quot;, secretAccessKey)
  .update(`${timestamp}:${JSON.stringify(body, null, 0)}`)
  .digest(&quot;hex&quot;);

const start = performance.now();
const res = await axios.post(
  &quot;http://localhost:3000/v1/api-keys/keys/secure-verify&quot;,
  body,
  {
    headers: {
      Authorization: `Bearer ${accessKeyId}`,
      &quot;X-Timestamp&quot;: timestamp,
      &quot;X-Signature&quot;: digest,
      &quot;Content-Type&quot;: &quot;application/json&quot;,
    },
  }
);
const end = performance.now();
console.log(&quot;Time:&quot;, end - start);

console.log(&quot;Response:&quot;, res.data);</code></pre>
<p>In the above code we use the <code>crypto.createHmac</code> function in order to <strong>sign the request</strong>, and then we pass it along as our <code>X-Signature</code> header. The header itself uses <code>update</code> to <strong>incorporate our concatenated timestamp and body payload string into the signature</strong>.</p>
<p>The data used to verify the signature can include whatever we want, so if there are any extra headers or pieces of data that are important to validating integrity, then be sure to reflect that in the signature.</p>
<p>Once signed, <strong>we send a request that includes the required headers</strong>. Values such a <code>X-Timestamp</code> are required in order to verify the signature server-side and can also be used to <strong>reject signatures older than a certain time period</strong>.</p>
<p>In order to compare the signatures on the server-side implementation, we have the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>const valid = crypto.timingSafeEqual(
  Buffer.from(signature),
  Buffer.from(digest)
);</code></pre>
<p><a href="https://nodejs.org/docs/latest-v22.x/api/crypto.html#cryptotimingsafeequala-b" target="_blank" rel="noopener noreferrer">crypto.timingSafeEqual</a> is a function that compares the underlying bytes using a constant-time algorithm. It does not leak timing information which is an attack surface for malicious actors and is suitable for comparing HMAC digests.</p>
<p>If you are interested in reading up more about authentication practices like timing attacks, then see the <a href="https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#compare-password-hashes-using-safe-functions" target="_blank" rel="noopener noreferrer">OWASP authentication cheat sheet</a>.</p>
<p>If we run the Node.js script, we will get back the expect <code>Response: { valid: true }</code> payload.</p>
<p>Play around and attempt to tamper some of the values and see what happens. Unsurprisingly, <code>valid</code> is <code>false</code>.</p>
<span id="case-study-aws-apis" class="anchor-alias"><h2 id="case-study-aws-ap-is">Case study: AWS APIs</h2>
<p>Amazon Web Services APIs use a two-part credential: an <strong>Access Key ID</strong> (public identifier) and a <strong>Secret Access Key</strong>. Every API request must be signed using a process called <strong>Signature Version 4 (SigV4)</strong>, which is an <strong>HMAC-SHA256</strong> scheme​.</p>
<p>The client includes the <strong>Access Key ID</strong> and the <strong>computed signature</strong> in an <strong>Authorization header</strong>. AWS uses the Access Key ID to <strong>look up the secret key</strong> and then <strong>recomputes the signature from the request data</strong> on their end​. If it matches, the request is <strong>authenticated and processed</strong>. If not, it&#39;s <strong>rejected​.</strong></p>
<p>In a similar setup to our implementation, AWS requires a timestamp (<code>X-Amz-Date</code> header or an <code>HTTP Date</code> header) on each request. This is used to reject requests that are considered stale (5 minutes by default) to protect against replay attacks.</p>
<p>If you are familiar with the AWS APIs, then this process of signing the request is abstracted away from you, but there is documentation that <a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html" target="_blank" rel="noopener noreferrer">shows you how create a signed AWS API request</a> that is an important reference if you want to cross-reference your implementations against theirs.</p>
<p>An example of a SigV4 Authorization header:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="txt"><code>Authorization: AWS4-HMAC-SHA256
Credential=AKIAIOSFODNN7EXAMPLE/20220830/us-east-1/ec2/aws4_request,
SignedHeaders=host;x-amz-date,
Signature=calculated-signature</code></pre>
<p>An example of authentication parameters in the query string:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="txt"><code>https://ec2.amazonaws.com/?
Action=DescribeInstances&amp;
Version=2016-11-15&amp;
X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;
X-Amz-Credential=AKIAIOSFODNN7EXAMPLE/20220830/us-east-1/ec2/aws4_request&amp;
X-Amz-Date=20220830T123600Z&amp;
X-Amz-SignedHeaders=host;x-amz-date&amp;
X-Amz-Signature=calculated-signature</code></pre>
<span id="why-slow-hashing-isnt-required" class="anchor-alias"><h2 id="why-slow-hashing-isn-t-required">Why slow hashing isn&#39;t required</h2>
<p>Unlike user passwords, which are usually hashed with slow algorithms like <strong>bcrypt</strong>, API secrets are often hashed with a standard cryptographic hash (e.g. SHA-256) or stored encrypted.</p>
<p>The reason comes down to <strong>entropy and usage patterns</strong>. API keys are <strong>high-entropy</strong>, machine-generated secrets (often 128 bits or more of randomness), whereas <strong>user passwords are typically low-entropy</strong> (words or common patterns)​.</p>
<p>Because API keys are essentially unguessable by brute force (I wrote this pre-quantum), it&#39;s acceptable to use a fast hash for storage.</p>
<p>By contrast, passwords require slow hashing (bcrypt, Argon2, PBKDF2) are used specifically to thwart offline guessing if hashes are stolen​.</p>
<h2 id="cryptography-at-scale">Cryptography at scale</h2>
<p>Finally, I wanted to touch on your choice for tooling. Node.js can be incredible for <strong>I/O bound tasks</strong> like HTTP requests, but some of the trade-offs with Node.js is that <strong>compute bound tasks</strong> such a cryptography operations can be <strong>significantly slower</strong> for computation time when compared to other runtimes and languages.</p>
<p>If you are building a real-world application, consider taking this into account as it can have real implications on compute costs and auto-scaling nodes. The same concepts apply agnostically across languages, so find that happy point between your estimated scale and what works best for your developers and budgets.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Whether you&#39;re shipping a side project or laying the groundwork for production-scale auth, rolling your own API keys is a great exercise in balancing simplicity, security, and developer control.</p>
<p>Start small, sign what matters, and revisit your setup as your needs evolve. Be sure to cross-reference this post with other public APIs when designing your own implementations and <strong>ensure</strong> that your storage of the API keys is secure and resistant to issues around stolen database data.</p>
<p><em>Photo credit: <a href="https://unsplash.com/@mickdepaola" target="_blank" rel="noopener noreferrer"><code>mickdepaola</code></a></em></p>]]></content>
<category term="typescript"/><category term="auth"/>
</entry>
<entry>
<title type="html"><![CDATA[The Boundaries of TypeScript Testing]]></title>
<id>https://blog.dennisokeeffe.com/blog/2025-03-29-the-boundaries-of-typescript-testing</id>
<link href="https://dennisokeeffe.com/blog/2025-03-29-the-boundaries-of-typescript-testing"/>
<published>2025-03-29T00:00:00.000Z</published>
<updated>2025-03-29T00:00:00.000Z</updated>
<summary type="html"><![CDATA[Learn how to increase your testing confidence vector with principles and tools like MSW and testcontainers.]]></summary>
<content type="html"><![CDATA[<p>Over my years of software engineering, there is hill that I stumbled upon that I am willing to die on: <strong>Test coverage does not equate to your test confidence</strong>.</p>
<p>Increasing your <strong>test confidence vector</strong> is a hot topic. Some of my greatest mistakes and, as far as I am concerned, most useless code has written done around testing.</p>
<p>In this post, we&#39;ll focus on principles for <strong>static analysis</strong>, <strong>unit testing</strong> and <strong>integration testing</strong> that can increase your confidence vector as much as possible while limiting some of the pain points. Some of these points are certain to be contentious and are unashamedly biased from the parts of testing that have consumed <em>way-too-many-hours-to-admit</em> from my short tenure on this planet.</p>
<h2 id="companion-code">Companion code</h2>
<p>Unlike my other blog posts, this one won&#39;t be building anything from scratch. There is a <a href="https://github.com/okeeffed/blog-typescript-unit-tests" target="_blank" rel="noopener noreferrer">companion project to this blog post</a> that I will often link to and share code from throughout this post.</p>
<p>The principles I will talk through today are <em>mostly</em> agnostic to language, tool and runtime, although whether it&#39;s idiomatic or frowned up depends on your community and how well-equipped your chosen ecosystem is.</p>
<p>We&#39;ll be walking through these principles in TypeScript to satiate some of my recent thoughts.</p>
<h2 id="issues-to-acknowledge-with-test-coverage">Issues to acknowledge with test coverage</h2>
<p>Software teams use a <strong>minimum coverage percentage</strong> as a quality gate, but what that number is varies from source-to-source. <a href="https://testing.googleblog.com/2020/08/code-coverage-best-practices.html" target="_blank" rel="noopener noreferrer">Google&#39;s testing blog</a> has testing coverage values where 60% is acceptable, 75% is commendable and 90% is exemplary. <a href="https://www.atlassian.com/continuous-delivery/software-testing/code-coverage" target="_blank" rel="noopener noreferrer">Atlassian&#39;s page on code coverage</a> mentions that 80% coverage is good to aim for, but higher coverage might yield dimishing returns.</p>
<p>I personally have two main gripes around test coverage:</p>
<ol>
<li>The reported coverage can be misleading.</li>
<li>Strict quality gates can lead to <strong>writing tests for the sake of writing tests</strong>, which leads to bad tests.</li>
</ol>
<p>To illustrate some of the problems with code coverage, let&#39;s start with the basics. Take the following pure function <code>add</code>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>export function add(a: number, b: number) {
  return a + b;
}</code></pre>
<p>We can write a simple test to validate this:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import { expect, test } from &quot;vitest&quot;;
import { add } from &quot;./math&quot;;

test(&quot;should add 1 + 2&quot;, () =&gt; {
  expect(add(1, 2)).toEqual(3);
});</code></pre>
<p>With <code>vitest</code>, if I test with coverage e.g. <code>vitest --coverage</code>, I get the following output:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="txt"><code>--------------------------|---------|----------|---------|---------|-------------------
File                      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------------------|---------|----------|---------|---------|-------------------
All files                 |     100 |      100 |     100 |     100 |
 utils                    |     100 |      100 |     100 |     100 |
  math.ts                 |     100 |      100 |     100 |     100 |
--------------------------|---------|----------|---------|---------|-------------------</code></pre>
<p>Great! We have 100% coverage! No need for tests to try to trip up the boundaries, right? Well, writing a test to check 1 + 2 isn&#39;t exactly adventurous. We hardly pushed our function and it looks as if we&#39;re at the finish line.</p>
<p>Other than pushing the limits of our function, what happens if someone has come in and adds some sweet nothings to our function?</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>interface Loggable {
  log: (msg: string) =&gt; void;
}

function sideEffect(logger: Loggable) {
  logger.log(&quot;yo&quot;);
}

export function add(a: number, b: number, logger: Loggable) {
  sideEffect(logger);
  return a + b;
}</code></pre>
<p>We have a new side effect introduced, but what happens if we update our test to pass through a <code>console</code> argument?</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import { expect, test, vi } from &quot;vitest&quot;;
import { add } from &quot;./math&quot;;

test(&quot;should add 1 + 2&quot;, () =&gt; {
  const result = add(1, 2, console);
  expect(result).toEqual(3);
});</code></pre>
<pre class="astro-code night-owl" tabindex="0" data-language="txt"><code>--------------------------|---------|----------|---------|---------|-------------------
File                      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------------------|---------|----------|---------|---------|-------------------
All files                 |     100 |      100 |     100 |     100 |
 utils                    |     100 |      100 |     100 |     100 |
  math.ts                 |     100 |      100 |     100 |     100 |
--------------------------|---------|----------|---------|---------|-------------------</code></pre>
<p>Still 100%, what gives!? In a world where <code>console.log</code> is injected a <code>logger</code> object that throws an error e.g. <code>add(1, 2, { log: () =&gt; { throw new Error(&#39;KABOOM&#39;) }})</code>, we&#39;re still at 100% coverage.</p>
<p>Worst of all, what happens if they changed our test to validate only the logging?</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import { expect, test, vi } from &quot;vitest&quot;;
import { add } from &quot;./math&quot;;

test(&quot;should add 1 + 2&quot;, () =&gt; {
  const spy = vi.spyOn(console, &quot;log&quot;);
  add(1, 2, console);
  expect(spy).toHaveBeenCalledOnce();
});</code></pre>
<p>If we check out test coverage again...</p>
<pre class="astro-code night-owl" tabindex="0" data-language="txt"><code>--------------------------|---------|----------|---------|---------|-------------------
File                      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------------------|---------|----------|---------|---------|-------------------
All files                 |     100 |      100 |     100 |     100 |
 utils                    |     100 |      100 |     100 |     100 |
  math.ts                 |     100 |      100 |     100 |     100 |
--------------------------|---------|----------|---------|---------|-------------------</code></pre>
<p>We didn&#39;t even test our return value!</p>
<p>Code coverage, at least in the forms that I&#39;ve seen it, is a guideline. In reality, it&#39;s unlikely for me to come across tests that don&#39;t verify something as important as the return value. What has been problematic for me though have been <strong>high test coverage thresholds that force your hand</strong>.</p>
<p>This has happened to me plenty of times where I have ended up writing tests that <em>do nothing</em> other than get me passed the numbers required to pass CI:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>// code
function someService(id: string): { id: string; title: string } {
  return someRepo(id);
}

// test
test(&quot;someService&quot;, () =&gt; {
  const spy = vi.spyOn(someRepo);
  spy.mockImplementationOnce((id: string) =&gt; ({ id: &quot;1&quot;, title: &quot;title&quot; }));
  const res = someService(&quot;123&quot;);
  expect(res).toBe({ id: &quot;1&quot;, title: &quot;title&quot; });
});</code></pre>
<p>In testing environments that require you to mock out responses with hazy boundaries, you can end up writing tests like this simply to make coverage requirements.</p>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>If you <em>are</em> a previous employer reading this, I apologize for the sins that I
have committed.</p> </div> </aside>
<p><strong>To summarize:</strong> committing to code coverage <em>can</em> be hazardous. If you&#39;re going to do it, <strong>pick a safe threshold</strong> and embed it into the engineering culture at your company to prioritize <strong>high-quality tests that are not easily fallible</strong>.</p>
<p>As we continue into the age of AI assistance, configure your review agents to help scrutinize your choice of edge cases and make sure that any iffy boundaries are covered by tests.</p>
<h2 id="issues-to-acknowledge-with-mocking-spies-and-test-boundaries">Issues to acknowledge with mocking, spies and test boundaries</h2>
<p>Mocks are commonly used within testing to help with isolation. This can be helpful, but can become a scapegoat with setups that feel non-trivial to configure correctly to better represent your system.</p>
<p>In the most recent code example, we hinted towards test boundaries that can <strong>negatively impact our test confidence</strong>.</p>
<p>But <em>what</em> is a <strong>&quot;test boundary&quot;</strong>? My definition of the <strong>test boundary</strong> is the scope of what we can <strong>accurately</strong> test against. Accuracy being the crucial point here.</p>
<p>Let&#39;s look out test again, make some adjustments and add some more context on what the <code>someRepo</code> function does:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>// code
async function someRepo(id: string): { id: string; title: string } {
  console.log(&quot;Attempting fetch from cache&quot;);
  const cacheResult = await checkCache(id);
  if (cacheResult) {
    return cacheResult;
  }

  console.log(&quot;Attemping fetch from database&quot;);
  const dbResult = await fetchFromDb(id);
  return dbResult;
}

async function someService(id: string): { id: string; title: string } {
  return someRepo(id);
}

// test
test(&quot;someService&quot;, async () =&gt; {
  const spy = vi.spyOn(someRepo);
  spy.mockImplementationOnce((id: string) =&gt; ({ id: &quot;1&quot;, title: &quot;title&quot; }));
  const res = await someService(&quot;123&quot;);
  expect(res).toBe({ id: &quot;1&quot;, title: &quot;title&quot; });
});</code></pre>
<p>In our code now, the <code>someRepo</code> function checks a remote cache for a result, returns it if there is a hit or returns the result from the database. The test hasn&#39;t changed from what it was previously.</p>
<p>Ask yourself the question: <strong>what boundaries of this functionality are tested with the current implementation?</strong> The entire <em>raison d&#39;être</em> for our <code>someService</code> test is to evaluate it&#39;s functionality, but here <strong>we do not step in</strong> into our code beyond the current <code>someService</code> context due to our mock.</p>
<p>In practice, <strong>increasing our test confidence requires</strong> us to <strong>increase how far we can &quot;step in&quot; our code</strong> in order to reliably test as close to our system as possible.</p>
<p>As a first pass at our test refactoring, what happens if we update our mocks to move to the boundary where they mock our <code>checkCache</code> and <code>fetchFromDatabase</code> instead?</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>describe(&quot;someService&quot;, () =&gt; {
  test(&quot;returns expected value from database&quot;, async () =&gt; {
    const consoleSpy = vi.spyOn(console);
    const checkCacheSpy = vi.spyOn(checkCache);
    const fetchFromDbSpy = vi.spyOn(fetchFromDb);
    checkCacheSpy.mockImplementationOnce((id: string) =&gt; null);
    fetchFromDbSpy.mockImplementationOnce((id: string) =&gt; ({
      id: &quot;1&quot;,
      title: &quot;title&quot;,
    }));

    const res = await someService(&quot;123&quot;);
    expect(res).toBe({ id: &quot;1&quot;, title: &quot;title&quot; });
    expect(checkCacheSpy).toHaveBeenCalledOnce();
    expect(fetchFromDb).toHaveBeenCalledOnce();
    expect(console).toHaveBeenCalledTimes(2);
  });

  test(&quot;returns expected value from cache&quot;, async () =&gt; {
    const consoleSpy = vi.spyOn(console);
    const checkCacheSpy = vi.spyOn(checkCache);
    const fetchFromDbSpy = vi.spyOn(fetchFromDb);
    checkCacheSpy.mockImplementationOnce((id: string) =&gt; ({
      id: &quot;1&quot;,
      title: &quot;title&quot;,
    }));

    const res = await someService(&quot;123&quot;);
    expect(res).toBe({ id: &quot;1&quot;, title: &quot;title&quot; });
    expect(checkCacheSpy).toHaveBeenCalledOnce();
    expect(fetchFromDb).not.toHaveBeenCalledOnce();
    expect(console).toHaveBeenCalledOnce();
  });
});</code></pre>
<p>In our tests now, we have shifted the test boundaries a little deeper to where <code>checkCache</code> and <code>fetchFromDb</code> begin. We can improve on this more, but first let&#39;s take a moment to consider our spies and <code>.toHaveBeenCalledOnce</code> calls.</p>
<p>Consider the following refactor:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>async function someRepo(id: string): { id: string; title: string } {
  console.log(&quot;Running someRepo&quot;);
  const cacheResult = await checkCache(id);
  if (cacheResult) {
    return cacheResult;
  }

  // CONSOLE LOG REMOVED
  const dbResult = await fetchFromDb(id);
  return dbResult;
}</code></pre>
<p>In our <code>someRepo</code> code, we&#39;ve refactored the logs but the core functionality has <strong>not changed</strong>. If we run our tests though... <strong>they fail</strong>.</p>
<p>You can probably know why: one of our tests expect <code>console</code> to have been called twice. This is where we fall into &quot;false negative&quot; territory. As far as desired output behavior goes for our <code>someRepo</code> function, it still operates the same as before, but our tests were written as a false representation of that expected behavior.</p>
<p>Unless it is absolutely necessary to ensure a function is called as part of another, my ethos is that <strong>you should omit it entirely</strong>. We&#39;ll justify the reasons in an upcoming section, but for now my starting guideline would be to test the inputs and outputs.</p>
<p>For now, let&#39;s refactor the tests back to the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>describe(&quot;someService&quot;, () =&gt; {
  test(&quot;returns expected value from database&quot;, async () =&gt; {
    const checkCacheSpy = vi.spyOn(checkCache);
    const fetchFromDbSpy = vi.spyOn(fetchFromDb);
    checkCacheSpy.mockImplementationOnce((id: string) =&gt; null);
    fetchFromDbSpy.mockImplementationOnce((id: string) =&gt; ({
      id: &quot;1&quot;,
      title: &quot;title&quot;,
    }));

    const res = await someService(&quot;123&quot;);
    expect(res).toBe({ id: &quot;1&quot;, title: &quot;title&quot; });
  });

  test(&quot;returns expected value from cache&quot;, async () =&gt; {
    const checkCacheSpy = vi.spyOn(checkCache);
    checkCacheSpy.mockImplementationOnce((id: string) =&gt; ({
      id: &quot;1&quot;,
      title: &quot;title&quot;,
    }));

    const res = await someService(&quot;123&quot;);
    expect(res).toBe({ id: &quot;1&quot;, title: &quot;title&quot; });
  });
});</code></pre>
<p>Now we can freely refactor non-critical side-effects like <code>console.log</code> without returning <strong>false negatives</strong> about the desired function&#39;s behavior.</p>
<p>An example where validating calls made would be for functions where their <strong>entire purpose</strong> <em>is</em> to emit expected, measurable side-effects that the system <strong>should prevent from unintended changes</strong>.</p>
<p>In the accompanying repository, see these tests for a <a href="https://github.com/okeeffed/blog-typescript-unit-tests/blob/c9a1a3ed99624cce44611cf431766f1e19c49fbc/src/proxies/add-trace.test.ts" target="_blank" rel="noopener noreferrer">Proxy where the entire purpose is to emit logs at the beginning and end of a function call</a>.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import { beforeEach, describe, expect, test, vi } from &quot;vitest&quot;;
import { addTrace, addTraceCurried } from &quot;./add-trace&quot;;
import { container } from &quot;../config/ioc-test&quot;;
import { ILoggerService } from &quot;@/services/logger-service&quot;;
import { IocKeys } from &quot;@/config/ioc-keys&quot;;

class Target {
  needle() {
    return null;
  }

  async needleAsync() {
    return Promise.resolve(null);
  }
}

describe(&quot;trace proxies&quot;, () =&gt; {
  let logger: ILoggerService;

  beforeEach(() =&gt; {
    logger = container.get&lt;ILoggerService&gt;(IocKeys.LoggerService);
  });

  describe(&quot;addTrace&quot;, () =&gt; {
    test(&quot;invokes two trace logs at the beginning and end of a function&quot;, () =&gt; {
      const spy = vi.spyOn(logger, &quot;trace&quot;);
      const target = addTrace(new Target(), logger);

      expect(target.needle()).toBeNull();
      expect(spy).toHaveBeenCalledTimes(2);
      expect(spy.mock.calls).toEqual([
        [{ args: [], className: &quot;Target&quot; }, `Target.needle() called`],
        [
          { className: &quot;Target&quot;, duration: expect.any(String) },
          `Target.needle() returned`,
        ],
      ]);
    });

    test(&quot;invokes two trace logs at the beginning and end of an async function&quot;, async () =&gt; {
      const spy = vi.spyOn(logger, &quot;trace&quot;);
      const target = addTrace(new Target(), logger);

      expect(await target.needleAsync()).toBeNull();
      expect(spy).toHaveBeenCalledTimes(2);
      expect(spy.mock.calls).toEqual([
        [{ args: [], className: &quot;Target&quot; }, `Target.needleAsync() called`],
        [
          { className: &quot;Target&quot;, duration: expect.any(String) },
          `Target.needleAsync() returned`,
        ],
      ]);
    });
  });

  describe(&quot;addTraceCurried&quot;, () =&gt; {
    test(&quot;full application invokes two trace logs at the beginning and end of a function&quot;, () =&gt; {
      const spy = vi.spyOn(logger, &quot;trace&quot;);
      const partiallyAppliedFn = addTraceCurried(logger);
      const target = partiallyAppliedFn(new Target());

      expect(target.needle()).toBeNull();
      expect(spy).toHaveBeenCalledTimes(2);
      expect(spy.mock.calls).toEqual([
        [{ args: [], className: &quot;Target&quot; }, `Target.needle() called`],
        [
          { className: &quot;Target&quot;, duration: expect.any(String) },
          `Target.needle() returned`,
        ],
      ]);
    });

    test(&quot;invokes two trace logs at the beginning and end of an async function&quot;, async () =&gt; {
      const spy = vi.spyOn(logger, &quot;trace&quot;);
      const partiallyAppliedFn = addTraceCurried(logger);
      const target = partiallyAppliedFn(new Target());

      expect(await target.needleAsync()).toBeNull();
      expect(spy).toHaveBeenCalledTimes(2);
      expect(spy.mock.calls).toEqual([
        [{ args: [], className: &quot;Target&quot; }, `Target.needleAsync() called`],
        [
          { className: &quot;Target&quot;, duration: expect.any(String) },
          `Target.needleAsync() returned`,
        ],
      ]);
    });
  });
});</code></pre>
<p>In these tests, I explicitly spy on the instance of the <code>ILoggerService</code> to ensure that <code>logger.trace</code> is called with the expected arguments as that is the purpose of the proxy.</p>
<p>For this particular test, asserting that the dependency calls is as much confidence as I want from these tests.</p>
<p>For the astute, you will have notice that <strong>spies don&#39;t inherently mock out functionality unless explicitly stated</strong>. This means that I can assert the desired functionality while testing against a real implementation.</p>
<p>Just to reiterate: <strong>I do not use spies like this for every function call</strong>.</p>
<p>Remember what we covered about false negatives. I will only ever do this in scenarios where a side effect is the <strong>sole desired outcome</strong> for the function existing.</p>
<p>In these scenarios, there is normally <strong>no response</strong> or <strong>the side effect has higher priority</strong>. In my example, the proxy return type plays second fiddle to the trace logging.</p>
<p>For impure functions that return values that you care about (from databases etc), <strong>assert against output</strong> and <strong>do not get caught up on the implementation details</strong>.</p>
<p>In the accompanying codebase, you can see some examples of <a href="https://github.com/okeeffed/blog-typescript-unit-tests/blob/c9a1a3ed99624cce44611cf431766f1e19c49fbc/src/repositories/blog-repository.test.ts?plain=1#L42-L83" target="_blank" rel="noopener noreferrer">testing a repository layer that cares more about the output than the implementation details</a>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>describe(&quot;getBlog&quot;, () =&gt; {
  let post: Post;

  beforeEach(async () =&gt; {
    post = postFactory.build();

    await prisma.post.create({
      data: post,
    });
  });

  test(&quot;can get a blog&quot;, async () =&gt; {
    const result = await blogRepository.getBlog({ param: { blogId: post.id } });
    expect(result.isOk()).toBe(true);
    result.map((value) =&gt; {
      expect(value.data?.title).toBe(post.title);
      expect(value.data?.content).toBe(post.content);
    });
  });

  test(&quot;can retrieve a blog from cache&quot;, async () =&gt; {
    const noCacheResult = await blogRepository.getBlog({
      param: { blogId: post.id },
    });
    expect(noCacheResult.isOk()).toBe(true);
    noCacheResult.map((value) =&gt; {
      expect(value._cacheHit).toBe(false);
    });

    const result = await blogRepository.getBlog({ param: { blogId: post.id } });
    expect(result.isOk()).toBe(true);
    result.map((value) =&gt; {
      expect(value._cacheHit).toBe(true);
    });
  });

  test(&quot;returns BlogNotFoundError when there is no matching blog found&quot;, async () =&gt; {
    const result = await blogRepository.getBlog({
      param: { blogId: faker.string.uuid() },
    });
    expect(result.isErr()).toBe(true);
    result.mapErr((e) =&gt; {
      expect(e._tag).toBe(&quot;BlogNotFoundError&quot;);
    });
  });
});</code></pre>
<p>Notice that it <strong>does not check</strong> that certain wrapper library functionality is called. Is purely cares on whether or not the functionality returns the expected data and information about hitting the cache. If I swapped out cache implementation or ORM but kept the contract, then <strong>this test will not need to change</strong> and it will be the best indicator that I&#39;ve maintained the expected functionality.</p>
<p>Finally, I wanted to finish by acknowledging an area where I find mocking appropriate at the cost of shortening the test boundary: <strong>error cases that are not easily replicated and are explicitly handled for unhappy paths</strong>.</p>
<p>Applications are fallible to the most bizarre things, so if you&#39;re working with a system that handles edge cases that are not easily replicated but are actively handled within your application, then mocking will come to the rescue.</p>
<p>For example (albeit contrived), what happens if you&#39;re writing code that specifically handles cases where a disk is full?</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>// file-service.ts
import { promises as fs } from &quot;fs&quot;;
import path from &quot;path&quot;;
import { err, ok } from &quot;neverthrow&quot;;

/**
 * Saves data to a file, with retry logic for edge cases
 * @param filePath Path to save the file
 * @param data Content to write
 */
async function saveFile(filePath: string, data: string): Promise&lt;void&gt; {
  try {
    // Create directory if it doesn&#39;t exist
    const directory = path.dirname(filePath);
    await fs.mkdir(directory, { recursive: true });

    // Write the file
    await fs.writeFile(filePath, data, { encoding: &quot;utf8&quot; });
    return ok(true);
  } catch (error) {
    if (err.code === &quot;ENOSPC&quot;) {
      // No space left on device
      // Very difficult to emulate without actually filling disk
      return err({
        _tag: &quot;DiskFull&quot;,
        somethingImportant: true,
      });
    }

    return err({
      _tag: &quot;ContrivedCatchAll&quot;,
    });
  }
}</code></pre>
<p>In this example, replicating the disk full scenario is not easily done. Mocking comes to our rescue here:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>// file-service.test.ts
import { describe, beforeEach, it, expect, vi } from &quot;vitest&quot;;
import { promises as fs } from &quot;fs&quot;;
import path from &quot;path&quot;;
import { saveFile } from &quot;./file-service&quot;;

// Mock the fs module
vi.mock(&quot;fs&quot;, () =&gt; ({
  promises: {
    writeFile: vi.fn(),
    mkdir: vi.fn().mockResolvedValue(undefined),
  },
}));

describe(&quot;saveFile&quot;, () =&gt; {
  beforeEach(() =&gt; {
    vi.resetAllMocks();
    vi.mocked(fs.mkdir).mockResolvedValue(undefined);
  });

  it(&quot;should handle disk full error correctly&quot;, async () =&gt; {
    // Mock writeFile to throw an ENOSPC error
    const mockError = new Error(&quot;No space left on device&quot;);
    // Add the code property to the error
    (mockError as any).code = &quot;ENOSPC&quot;;

    // Setup the mock to reject with our custom error
    vi.mocked(fs.writeFile).mockRejectedValue(mockError);

    const result = await saveFile(&quot;/path/to/file.txt&quot;, &quot;test data&quot;);

    // Verify the error was handled correctly
    expect(result.isErr()).toBeTruthy();
    result.mapErr((e) =&gt; {
      expect(e._tag).toBe(&quot;DiskFull&quot;);
      expect(e.somethingImportant).toBe(true);
    });
  });
});</code></pre>
<h2 id="guidelines-for-test-boundaries">Guidelines for test boundaries</h2>
<p>With our test boundaries, how far <em>can</em> and <em>should</em> we push? I believe this strongly depends on preferences and configuration, but my guidelines:</p>
<ol>
<li>For tests that compose of functionality that makes network requests, <strong>test out until the network boundary</strong> with request interceptors like <a href="https://mswjs.io/" target="_blank" rel="noopener noreferrer">MSW</a> where possible.</li>
<li>For tests that compose of functionality that interface directly with databases, <strong>attempt to run tests against a local copy of the expected database</strong> using tools like <a href="https://testcontainers.com/" target="_blank" rel="noopener noreferrer">
testcontainers</a>.</li>
<li>For tests that interface with third party library utilities, test <strong>without mocking</strong> out the utilities.</li>
<li>For other side effects types like interfacing with file systems, <strong>if it&#39;s undesirable to write to the file-system and clean up, then attempt to mock in-memory</strong>. For something like Node.js, there exists utilities like <a href="https://www.npmjs.com/package/mock-fs" target="_blank" rel="noopener noreferrer">mock-fs</a>.</li>
</ol>
<p>These guidelines begin to <strong>blur the definitions between unit testing and integration testing</strong>. If you&#39;re familiar with the <a href="https://circleci.com/blog/testing-pyramid/" target="_blank" rel="noopener noreferrer">testing pyramid</a>, then you may favor unit tests as your baseline. Over time, I&#39;ve personally grown to become more in favor of model closer to the <a href="https://kentcdodds.com/blog/the-testing-trophy-and-testing-classifications" target="_blank" rel="noopener noreferrer">testing trophy</a> which places emphasis on <strong>static analysis as your baseline</strong> and <strong>integration testing as the testing layer to focus on most</strong>.</p>
<p>Most of the time, blurring these lines is inconsequential for projects. For projects that require more setup and most importantly are actively becoming an acknowledged problem and time consuming to run, then consider updating your testing configuration to separate these.</p>
<p>Finally, one common bottleneck that I haven&#39;t called out here is that to <strong>prevent state management race conditions</strong> for dependencies like databases, you generally need to <strong>forego any parallelization</strong> for tests running on the same machine. I should reiterate that <strong>until this becomes a problem</strong>, I wouldn&#39;t be too concerned about this. Locally, tests runners allow running a subset of tests. On CI, there are also strategies to split tests across test runners with isolated database that might be worth considering first. Seek out alternatives first and proactively evaluate any changes to your approach.</p>
<p><a href="https://github.com/okeeffed/blog-typescript-unit-tests/blob/c9a1a3ed99624cce44611cf431766f1e19c49fbc/vitest.config.ts?plain=1#L1" target="_blank" rel="noopener noreferrer">An example configuration for enforcing single-threaded, sequential testing with Vitest</a>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import { defineConfig } from &quot;vite&quot;;

export default defineConfig({
  test: {
    // ... some data omitted
    pool: &quot;forks&quot;,
    poolOptions: {
      forks: {
        singleFork: true,
      },
    },
    globalSetup: [&quot;./src/tests/vitest-global-setup.ts&quot;],
    setupFiles: [&quot;./src/tests/vitest-setup.ts&quot;, &quot;./src/tests/matchers.ts&quot;],
  },
});</code></pre>
<p>In my example code, I&#39;m spinning up both a <a href="https://valkey.io/" target="_blank" rel="noopener noreferrer">Valkey</a> and <a href="https://www.postgresql.org/" target="_blank" rel="noopener noreferrer">Postgres</a> test container, along with using <a href="https://mswjs.io/" target="_blank" rel="noopener noreferrer">MSW</a> to intercept my HTTP requests to a make-believe service, so I use the global setup and setup files to prepare this.</p>
<p>My global setup will stand up the databases and tear them down at the end of the tests:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import { execSync } from &quot;child_process&quot;;
import { psqlContainer, valkeyContainer } from &quot;../lib/testcontainers&quot;;

export async function setup() {
  // Start the PostgreSQL container
  const [psql, valkey] = await Promise.all([psqlContainer, valkeyContainer]);
  const databaseUrl = psql.getConnectionUri();
  process.env.DATABASE_URL = databaseUrl;

  // Start the Valkey container
  const valkeyUrl = valkey.getConnectionUrl();
  process.env.VALKEY_URL = valkeyUrl;

  // Run migrations and seed your database
  execSync(&quot;npx prisma migrate reset --force --skip-seed&quot;, {
    stdio: &quot;inherit&quot;,
    env: { ...process.env, DATABASE_URL: databaseUrl },
  });
}

export async function teardown() {
  const psql = await psqlContainer;
  const valkey = await valkeyContainer;

  await Promise.all([psql.stop(), valkey.stop()]);
}</code></pre>
<p>My setup files apply Vitest hooks in order to ensure that my databases and MSW mocks are cleared between runs to prevent race conditions:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import { server } from &quot;../mocks/server&quot;;
import { PrismaClient } from &quot;@prisma/client&quot;;
import { container } from &quot;../config/ioc-test&quot;;
import { beforeAll, afterEach, afterAll, beforeEach } from &quot;vitest&quot;;
import { IocKeys } from &quot;../config/ioc-keys&quot;;

const keyv = container.get&lt;any&gt;(IocKeys.KeyvClient);
const prisma = container.get&lt;PrismaClient&gt;(IocKeys.PrismaClient);

/**
 * Clean all tables in the database
 */
async function cleanDatabase() {
  // Get all tables
  const tables = await prisma.$queryRaw&lt;Array&lt;{ tablename: string }&gt;&gt;`
    SELECT tablename FROM pg_tables 
    WHERE schemaname=&#39;public&#39; 
    AND tablename NOT IN (&#39;_prisma_migrations&#39;)
  `;

  // Truncate each table
  for (const { tablename } of tables) {
    try {
      await prisma.$executeRawUnsafe(`TRUNCATE &quot;${tablename}&quot; CASCADE;`);
    } catch (error) {
      console.log(`Error truncating ${tablename}`);
    }
  }
}

beforeAll(async () =&gt; {
  server.listen({ onUnhandledRequest: &quot;bypass&quot; });
}, 30000);

beforeEach(async () =&gt; {
  // Clean the database before each test to start fresh
  await cleanDatabase();
});

afterEach(async () =&gt; {
  await keyv.clear();
  server.resetHandlers();
});

afterAll(async () =&gt; {
  server.close();

  await prisma.$disconnect();
  await keyv.disconnect();
});</code></pre>
<p>In the example code, I&#39;ve chosen to use table truncation for clearing the relational database between runs as it feels the simplest for my use case.</p>
<p>The time is takes for me to run the tests from a fresh start with cached containers is about 7 seconds for the full suite, which includes starting up the Docker containers (albeit there are only ~30 tests).</p>
<p>Remotely, the <a href="https://github.com/okeeffed/blog-typescript-unit-tests/actions/runs/14186010256/job/39741350669" target="_blank" rel="noopener noreferrer">CI job takes ~16 seconds</a>.</p>
<aside class="callout callout--danger" aria-label="Danger"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><path d="m21.7 18-8-14a2 2 0 0 0-3.4 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.7-3ZM12 9v4M12 17h.01" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>  <div class="callout__body">  <p>Using real databases comes with more trade-offs that we haven&#39;t covered in
this post. If your setup flakes more often than not, then blurring those lines
is going to lead to a bad time. If your perspective is that you are not time
poor, then you should address those configuration issues rather than
separating the tests and isolating the problem to one set of tests.</p> </div> </aside>
<h2 id="test-data-guidelines">Test data guidelines</h2>
<p>The previous section covered database integration into our testing, so let&#39;s recap some guidelines that around this:</p>
<ol>
<li><strong>Serialize tests</strong> if tests share the same database.</li>
<li><strong>Write global setup and teardown functionality</strong> to ensure safe setup and teardown of your database(s).</li>
<li><strong>Enforce a clean slate before each test</strong>.</li>
<li><strong>Colocate the test data within the test file</strong>. It should be easy to track down where data was initialized from.</li>
</ol>
<p>There may be instances where some of these guidelines don&#39;t apply. For example. some possible options for database isolation that have different trade-offs for cost:</p>
<ol>
<li>Database isolation per test.</li>
<li>Transactions per test that are rolled back.</li>
<li>Truncating tables after each tests.</li>
</ol>
<p>In addition to that, testing in CI may open up avenues to splits tests up across shards that may allow you to configure different databases per shard.</p>
<h2 id="the-problems-with-emulation">The problems with emulation</h2>
<p>I&#39;ll keep this section short, but as a general rule of thumb: <strong>if you are going to test your application beyond the application boundary, then beware emulation</strong>. If you can&#39;t use the <em>actual</em> technology locally, then question it&#39;s use at all.</p>
<p>This is more a warning than a &quot;never do it&quot; scenario, because there are times where things like drop-in replacements for providing in-memory APIs can be incredibly useful.</p>
<p>In general though, if you&#39;re using something 3rd party to emulate something in your environment that belongs to closed-source work, <strong>know that any breaking changes or upgrades to the original product will cause you grief</strong> (on top of any bugs or lack-of-support from the 3rd party software). Weigh up the trade-offs before implementing something like this.</p>
<p>Using <strong>cloud technologies</strong> is an example of something <strong>I will never bother to emulate for testing or development</strong> in my personal work. If something is not officially provided to run locally, I don&#39;t bother. I learned this the hard way and spent way too many hours debugging 3rd party emulators when <em>running on the actual platform</em> worked as expected.</p>
<p>You&#39;re better off testing on cloud hardware if you ever end up in a scenario like this. Once again, evaluate whether or not <strong>the cost and complexity</strong> of doing so is worth it.</p>
<h2 id="pure-functions-and-bounded-domains-and-ranges">Pure functions and bounded domains and ranges</h2>
<p><a href="https://en.wikipedia.org/wiki/Pure_function" target="_blank" rel="noopener noreferrer">Pure functions</a> have two properties:</p>
<ol>
<li><strong>No side-effects</strong>: Things like logging, interfacing with networks or file systems constitutes a side-effect.</li>
<li><strong>Same input == same output</strong>: When passing the exact same arguments in, we expect the same result every time.</li>
</ol>
<p>Let&#39;s go back to a previous example of implementing <code>add</code>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>interface Loggable {
  log: (msg: string) =&gt; void;
}

function sideEffect(logger: Loggable) {
  logger.log(&quot;yo&quot;);
}

export function add(a: number, b: number, logger: Loggable) {
  sideEffect(logger);
  return a + b;
}</code></pre>
<p>We know this violates rule (1) of pure functions due to it&#39;s side-effect, so let&#39;s refactor a little more and add some constraints around our <code>add</code> function:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>function constrainedAdd(a: number, b: number) {
  if (isNaN(a) || isNaN(b)) {
    return 0;
  }

  a = a &lt; 0 ? 0 : a &gt; 2 ? 2 : a;
  b = b &lt; 0 ? 0 : b &gt; 2 ? 2 : b;

  return Math.floor(a) + Math.floor(b);
}</code></pre>
<p>This completely useless function will add two integers together, with any argument smaller than 0 being set as 0 and any argument larger than 2 being set to 2. It also enforces the numbers added to be integers.</p>
<p>What happens if we were to put it to use?</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>constrainedAdd(1, 2); // 3
constrainedAdd(-1, -1); // 0
constrainedAdd(-1, 3); // 2
constrainedAdd(3, 3); // 4
constrainedAdd(1.9, 2.1); // 3
constrainedAdd(NaN, 2); // 0</code></pre>
<p>This function may appear useless, but it is a pure function.</p>
<ol>
<li>There are no side effects.</li>
<li>Each valid input will return the same output.</li>
</ol>
<p>Here is another useful tidbit about pure functions: <strong>functions that are only composed of other pure functions are themselves pure</strong>. Assuming <code>Math.floor</code> and <code>isNaN</code> themselves are pure functions, our <code>constrainedAdd</code> does not violate any rules on pure functions. This has some useful properties that make our lives with testing much easier.</p>
<p>The next important lesson we can take away from <code>constrainedAdd</code> is how we define the <strong>domain</strong> and <strong>range</strong> of the function.</p>
<p>The <strong>domain</strong> describes the bounds of valid input, and any given <strong>domain</strong> input itself for a pure function can map directly to the <strong>range</strong> which is a set of all possible outputs.</p>
<p>For our function, our <strong>domain</strong> for the arguments can be described as any valid number, but with this implementation it&#39;s our <strong>range</strong> that is is most interesting. Our output is locked to be any valid integer within the set <code>{0,1,2,3,4}</code>.</p>
<p>The beauty of pure functions it makes life easier to test against edge cases if you can understand the range.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>it.each([
  // [a, b, expected]
  [1, 2, 3], // Normal case within constraints
  [-1, -1, 0], // Both negative values constrained to 0
  [-1, 3, 2], // Negative and above constraint combination
  [3, 3, 4], // Both values above constraints
  [1.9, 2.1, 3], // Decimal values with flooring and constraints
  [NaN, 2, 0], // NaN constrains the output to 0
])(&quot;adds constrained %f and %f to equal %i&quot;, (a, b, expected) =&gt; {
  expect(constrainedAdd(a, b)).toBe(expected);
});</code></pre>
<p>In a test like the one above, <strong>we increase our test confidence vector by increasing our assertion coverage for as many edge cases as we can infer</strong> from the domain and range.</p>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>The domain and range may not always be feasible to calculate and test all
possible options for. It&#39;s worth trying to calculate these parameters bounds
as you write functions though, as it can help you understand the complexity of
what you are writing from a practical standpoint.</p> </div> </aside>
<p>Although math and numbers are the most relatable when it comes to domain and range (for anyone stuck doing Calculus anyway), this also applies to any argument where the output is consistent for the same input.</p>
<p>The following is also a pure function:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>function getSecret(code: &quot;alpha&quot; | &quot;bravo&quot; | &quot;delta&quot;) {
  switch (code) {
    case &quot;alpha&quot;:
      return &quot;hello&quot;;
    case &quot;bravo&quot;:
      return &quot;from&quot;;
    case &quot;delta&quot;:
      return &quot;planet earth&quot;;
  }
}</code></pre>
<p>In the above, our domain is one of three values <code>alpha</code>, <code>bravo</code> or <code>delta</code> while the range is <code>hello</code>, <code>from</code> and <code>planet earth</code>.</p>
<blockquote>
<p>In hindsight after writing this, explaining <strong>domain</strong> and <strong>range</strong> with <code>getSecret</code> seems easier to understand than my <code>constrainedAdd</code> function...</p>
</blockquote>
<h2 id="functional-core-imperative-shell">Functional core, imperative shell</h2>
<p>This pattern is something I first came across at Ruby conferences five or so years ago and it is a pattern that I&#39;ve written about before.</p>
<p>The heart of this concepts lies around what we&#39;ve learned so far related to pure functions and composition. <strong>Pure functions</strong> are <strong>easier to reason about</strong>, and so if you start with your &quot;deepest&quot; code as being composed of pure functions (a.k.a. a <strong>functional core</strong>), then testing those functions can yield more consistency.</p>
<p><a href="https://www.destroyallsoftware.com/screencasts/catalog/functional-core-imperative-shell" target="_blank" rel="noopener noreferrer">destroyallsoftware</a> has a great screencast outlining this pattern.</p>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>Not to mislead anyone with this analogy, but when I visualize this pattern
then I simply think of a planet where the &quot;core&quot; is the deepest part.</p> </div> </aside>
<p>In practical terms, if we consider our <strong>controller-service-repository layer application</strong>, we already know that <strong>we can&#39;t avoid side-effects with our repository layers</strong>. That&#39;s okay, the idea is to <strong>invert as much of our impure logic upward</strong> until we hit our &quot;imperative shell&quot;. The boundary of the shell and core can be thought of as fluid.</p>
<p>For example, take the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>function functionB() {
  console.log(&quot;core started&quot;);
  somePureFunction();
  console.log(&quot;core ended&quot;);
}

function functionA() {
  functionB();
}

functionA();</code></pre>
<p>At our current state, assuming that <code>somePureFunction</code> is in fact pure, then our functional core boundary starts at the <code>somePureFunction</code> invocation (and our imperative shell boundary ends), because <code>functionB</code> is impure thanks to the logs.</p>
<p>Let&#39;s refactor again:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>function functionB() {
  somePureFunction();
}

function functionA() {
  console.log(&quot;core started&quot;);
  functionB();
  console.log(&quot;core ended&quot;);
}

functionA();</code></pre>
<p>If we &quot;push up&quot; the logs to <code>functionA</code>, then <code>functionB</code> through composition has become a pure function itself, so now the functional core starts at the invocation of <code>functionB</code>.</p>
<p>In my mind, the ideology of &quot;functional core, imperative shell&quot; isn&#39;t explicit that certain layers are entirely pure or impure. I think of it as &quot;dipping into&quot; functional cores at those boundaries. You can had methods within your service layer that are entire pure, or they&#39;re impure because they call into functionality that interfaces with something like our databases.</p>
<p>Something I think is worthwhile highlighting is that <a href="https://github.com/okeeffed/blog-typescript-unit-tests/blob/6c361d2b8c4135c727659757533492acf3de0e0d/src/config/ioc.ts?plain=1#L40-L59" target="_blank" rel="noopener noreferrer">my &quot;production&quot; Inversify container injects a proxy for logging into the layers</a> with <code>addTrace</code>, whereas my <a href="https://github.com/okeeffed/blog-typescript-unit-tests/blob/6c361d2b8c4135c727659757533492acf3de0e0d/src/config/ioc-test.ts?plain=1#L30-L35" target="_blank" rel="noopener noreferrer">Inversify test container does not</a>. This means that, when testing, <strong>none of the composed classes are automatically made impure from the proxy</strong>. This <strong>is</strong> a purposeful decision.</p>
<p>In practice, I find that service layers methods are the point where you normally <em>&quot;dip in and out&quot;</em> of the line between the <strong>functional core</strong> and <strong>imperative shell</strong>, but that&#39;s not a hard rule. Business layer logic that operates on concrete instances can normally be implemented as functional while calls out to the database can only ever be impure.</p>
<h2 id="dependency-injection">Dependency injection</h2>
<p>Dependency injection has many benefits, but it also enables a level of control that can be second-to-none around testing. Inverting control of what is constructed for a class instance can be a powerful tool.</p>
<p>Take the following code:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>interface IUser {
  id: string;
  name: string;
}

interface IUserRepository {
  findById(id: string) =&gt; IUser;
}

class UserRepository extends IUserRepository {
  async findById(id: string) {
    return someWayToGetUser(id);
  }
}

class Service {
  private userRepo: IUserRepository

  constructor(userRepo: IUserRepository) {
    this.userRepo = userRepo;
  }

  async findUser(id: string) {
    const user = await findUser(id);
    return user;
  }
}</code></pre>
<p>In our contrived code, we&#39;ve inverted control over the <code>userRepo</code> to be anything that adheres to the <code>IUserRepository</code>.</p>
<p>When it comes to testing, we can inject anything to adheres to the <code>IUserRepository</code> contract, be that an instance of <code>UserRepository</code> or any mock that adheres to the contract.</p>
<aside class="callout callout--warning" aria-label="Warning"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><path d="m21.7 18-8-14a2 2 0 0 0-3.4 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.7-3ZM12 9v4M12 17h.01" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>  <div class="callout__body">  <p>That all being said, please don&#39;t forget the guidelines. Unless given an
explicit reason to implement a mock, stick as close to the real thing as you
can.</p> </div> </aside>
<p>In cases where you use tools like <a href="https://effect.website/" target="_blank" rel="noopener noreferrer">Effect</a>, <a href="https://github.com/microsoft/tsyringe" target="_blank" rel="noopener noreferrer">tsyringe</a> or <a href="https://github.com/inversify/InversifyJS" target="_blank" rel="noopener noreferrer">Inversify</a>, this can make it trivial to provide test-specific dependencies where required.</p>
<p>Within the accompanying repository, I&#39;ve configured <a href="https://github.com/inversify/InversifyJS" target="_blank" rel="noopener noreferrer">Inversify</a> with a test-specific container. This testing container can be used to supply the mock implementations or remove unnecessary behaviour (like I have with removing higher-order proxies).</p>
<h2 id="conclusion">Conclusion</h2>
<p>Today&#39;s post covered a lot of testing methodology and guidelines that enable you to reason more about what effective testing can look like for you.</p>
<p>A recap of the most important talking points:</p>
<ul>
<li>The problem with code coverage</li>
<li>The impacts mocking can have on your test confidence vector</li>
</ul>
<p><em>Photo credit: <a href="https://unsplash.com/@zhangkaiyv" target="_blank" rel="noopener noreferrer"><code>zhangkaiyv</code></a></em></p>]]></content>
<category term="typescript"/>
</entry>
<entry>
<title type="html"><![CDATA[Effective TypeScript Principles in 2025]]></title>
<id>https://blog.dennisokeeffe.com/blog/2025-03-16-effective-typescript-principles-in-2025</id>
<link href="https://dennisokeeffe.com/blog/2025-03-16-effective-typescript-principles-in-2025"/>
<published>2025-03-16T00:00:00.000Z</published>
<updated>2025-03-16T00:00:00.000Z</updated>
<summary type="html"><![CDATA[A set of guidelines to reduce code management and improve developer experience in 2025]]></summary>
<content type="html"><![CDATA[<p>Some guidelines for how I <em>want</em> to write TypeScript in 2025. Feel free to take it or leave it. Always remember that opinions have trade-offs and come from experiences that may have blinded me to better alternatives.</p>
<blockquote>
<p>&quot;First, your <strong>refactoring</strong> was not part of our negotiations nor our agreement so I must do nothing. And secondly, you must be a <strong>lead developer</strong> for changes to the developer&#39;s code to apply and you&#39;re not. And thirdly, the code is more what you&#39;d call &#39;rules&#39; than actual guidelines. Welcome aboard <strong>Corporate Development</strong>, Miss Turner&quot; -- <em>Barbossa, Project Lead, 2003.</em></p>
</blockquote>
<p>We will be covering a few topics where each is ramping up into the next:</p>
<ol>
<li>The prerequisites
<ul>
<li>Code volume</li>
<li>Control flow</li>
<li>State management</li>
</ul>
</li>
<li>The principles
<ul>
<li>Composition over inheritance</li>
<li>Parse, don&#39;t validate</li>
<li>Never throw errors</li>
<li>Metadata</li>
<li>Define your source of truth</li>
<li>Let controllers tell you everything</li>
<li>Don&#39;t emulate network infrastructure</li>
<li>Don&#39;t let AI take the driver&#39;s seat</li>
<li>Generate as much code as possible</li>
<li>Write to refactor programmatically</li>
<li>Don&#39;t go overkill on abstraction layers</li>
</ul>
</li>
</ol>
<p>I won&#39;t be covering teamwork and only briefly touching upon inter-service communication at points.</p>
<h2 id="the-prerequisites">The prerequisites</h2>
<p>This section will briefly cover three elements of the development process that come from an older <a href="https://frontendmasters.com/courses/enterprise-patterns/" target="_blank" rel="noopener noreferrer">Enterprise Architecture Patterns course</a> that I took back in 2021, but have found to been of constant importance.</p>
<p>These cornerstones have a direct correlation to complexity and brain overload. They are:</p>
<ol>
<li>Code volume.</li>
<li>Control flow.</li>
<li>State management.</li>
</ol>
<p>Any mishandling of the following topics will lead development towards &quot;developer purgatory&quot; and, as a bonus side-effect, make future devs die inside.</p>
<aside class="callout callout--warning" aria-label="Warning"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><path d="m21.7 18-8-14a2 2 0 0 0-3.4 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.7-3ZM12 9v4M12 17h.01" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>  <div class="callout__body">  <p>Note that there won&#39;t be code examples for this topic.</p> </div> </aside>
<h3 id="code-volume">Code volume</h3>
<p>Code volume relates to <strong>the amount of code that you have to manage</strong>. In layperson spiel: less lines of code to manage is better.</p>
<p>I always find this concept to be at odds with <a href="https://www.oreilly.com/library/view/clean-code-a/9780136083238/" target="_blank" rel="noopener noreferrer">Clean Code</a> and <a href="https://www.digitalocean.com/community/tutorials/gangs-of-four-gof-design-patterns" target="_blank" rel="noopener noreferrer">Gang of Four design patterns</a>. My reason for this is that Clean Code and GO4 patterns become the scapegoat to justify over-bloat and unnecessary abstractions, or premature abstractions that doesn&#39;t keep the necessary separation of concerns for the domain.</p>
<p>Over-bloat and unnecessary abstractions may be easier to imagine. If you&#39;re working in the codebase where you need to make jumps to ten different definitions in order to understand the inheritance chain or follow the path of the business logic, then you have probably over-engineered the shit out of it. Principles like &quot;composition over inheritance&quot; and &quot;parse, don&#39;t validate&quot; can help mitigate volume creep (which I touch on in their own section), but there are some general guiding principles that I recommend to get around this:</p>
<ol>
<li>Service layers that handle the business logic should be <strong>as &quot;shallow&quot; as possible</strong>. What I mean by this is that service layers should not compose other service layers, and layers that are required here should themselves aim to stay as shallow as possible.</li>
<li>Functional core, imperative shell. The idea here is that the &quot;deepest&quot; layers invoked should be as pure as possible. Side-effects like logging should be the responsibility the the imperative shell (in our case, we aim for that to be the service layer or injected at the service layer). This also becomes a God-send for a controlled domain and range of tests for pure functions.</li>
</ol>
<p>As for the premature abstractions, this one might be harder to understand at first. <a href="https://overreacted.io/goodbye-clean-code/" target="_blank" rel="noopener noreferrer">Dan Abramov&#39;s &quot;Goodbye Clean Code&quot;</a> does a good job of outlining the core idea that combining similar interfaces to reduce the code volume is not always the best idea.</p>
<p>But what gives? Isn&#39;t the idea that less code is better? You need to understand when contradictions to this guideline are necessary. I&#39;ll touch later on approaches to mitigate this problem with code generation.</p>
<p>The important thing to remember here is that <strong>the code volume cornerstone applies to code that you have to manage as a developer, not the size of the codebase itself</strong>.</p>
<h3 id="control-flow">Control flow</h3>
<p>Control flow speaks to the branches in logic. Choices in how you apply control flow, how much of it there is and where logic &quot;branches&quot; can make or break your day.</p>
<p>We&#39;ll be touching on this topic with the following sections:</p>
<ul>
<li>Let your controllers tell your everything.</li>
<li>Don&#39;t go overkill on abstraction layers.</li>
<li>Never throw errors.</li>
<li>Metadata.</li>
</ul>
<h3 id="state-management">State management</h3>
<p>State management refers to how applications track and maintain data that changes over time. This &quot;state&quot; includes things like user inputs, API responses, UI configurations, and application settings.</p>
<p>Mismanagement here can create some deeply-knotted problems:</p>
<ol>
<li>Unpredictable Behavior: When state can be modified from multiple places without clear patterns, applications become unpredictable. Developers can&#39;t easily reason about what will happen when code executes.</li>
<li>Debugging Nightmares: Without clear state flows, finding the root cause of bugs becomes extremely difficult. A bug might manifest in one component but originate from state modifications elsewhere.</li>
<li>Technical Debt Accumulation: Poor state management compounds over time through things like duplicated state, stale state and side-effects.</li>
<li>Readability and Maintainability Issues: New developers struggle to understand applications.</li>
<li>Performance Problems: Unnecessary re-renders, Memory leaks, Network request redundancy.</li>
</ol>
<p>Although this post won&#39;t spend too much time on state management, it is also partly related to these topics:</p>
<ul>
<li>Parse, don&#39;t validate.</li>
<li>Never throw errors.</li>
<li>Metadata.</li>
<li>Define your source of truth.</li>
</ul>
<h2 id="the-principles">The principles</h2>
<p>With the foundation prerequisites out of the way, the rest of the section will speak to some examples scenarios. You should keep those three cornerstones in mind while reading through each guideline.</p>
<h3 id="composition-over-inheritance">Composition over inheritance</h3>
<p>Composition over inheritance is a design principle that suggests you should prefer building complex functionality by combining simpler objects (composition) rather than through class inheritance hierarchies.
Key Benefits of Composition</p>
<p>Some of the benefits:</p>
<ul>
<li>Easier to change behavior at runtime</li>
<li>Looser coupling between components</li>
<li>Simpler to test isolated components</li>
</ul>
<p><strong>Don&#39;t</strong>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>abstract class Pizza {
  prepare() {
    console.log(&quot;Preparing something hidden away from concrete classes&quot;);
  }

  // This method needs to be implemented by subclasses
  abstract addToppings(): void;
}

// Concrete subclasses
class PineapplePizza extends Pizza {
  addToppings(): void {
    console.log(&quot;Adding pineapple chunks&quot;);
  }
}

const p = new PineapplePizza();
p.prepare();
p.addToppings();</code></pre>
<p><strong>Do</strong>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>interface Pizza {
  prepare(): void;
  addToppings(): void;
}

class PineapplePizza implements Pizza {
  prepare() {
    console.log(&quot;Preparing pineable pizza&quot;);
  }

  addToppings(): void {
    console.log(&quot;Adding pineapple chunks&quot;);
  }
}

const p = new PineapplePizza();
p.prepare();
p.addToppings();</code></pre>
<p>One thing about this approach: use composition over inheritance for <strong>enforcing behaviors</strong>.</p>
<p>When it comes to properties on the class, <strong>don&#39;t rely on implementation an interface to enforce that property</strong>. Implementing interfaces comes with the caveat that you cannot implement non-public properties. In those scenarios, use dependency injection.</p>
<p><strong>Don&#39;t</strong>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>interface Pizza {
  id: string;
  name: string;
  prepare(): void;
  addToppings(): void;
}

class PineapplePizza implements Pizza {
  // Can&#39;t be private
  public id: string;
  public name: string;

  constructor(id: string, name: string) {
    this.id = id;
    this.name = name;
  }

  prepare() {
    console.log(&quot;Preparing pineable pizza&quot;);
  }

  addToppings(): void {
    console.log(&quot;Adding pineapple chunks&quot;);
  }
}

const p = new PineapplePizza(&quot;1&quot;, &quot;pineapple&quot;);</code></pre>
<p><strong>Do</strong>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>interface Pizza {
  prepare(): void;
  addToppings(): void;
}

interface PizzaProps {
  id: string;
  name: string;
}

class PineapplePizza implements Pizza {
  private id: string;
  private name: string;

  constructor(props: PizzaProps) {
    this.id = props.id;
    this.name = props.name;
  }

  prepare() {
    console.log(&quot;Preparing pineable pizza&quot;);
  }

  addToppings(): void {
    console.log(&quot;Adding pineapple chunks&quot;);
  }
}

const p = new PineapplePizza({ id: &quot;1&quot;, name: &quot;pineapple&quot; });</code></pre>
<p>As a natural extension to this when it come to interfaces, <strong>avoid extending interfaces with other interfaces</strong>.</p>
<span id="parse-dont-validate" class="anchor-alias"><h3 id="parse-don-t-validate">Parse, don&#39;t validate</h3>
<p>Popularized by <a href="https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/" target="_blank" rel="noopener noreferrer">this article by the same name</a> by Alexis King.</p>
<p>The core idea behind &quot;parse, don&#39;t validate&quot; is to transform untyped or less-typed data into well-typed data early in your program flow, rather than checking validity throughout your code. With this approach:</p>
<ol>
<li>You parse input data once, at the boundary of your system.</li>
<li>This parsing step both validates and transforms the data.</li>
<li>After parsing, you work with fully-typed, validated data throughout the rest of your code.</li>
</ol>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p><strong>Boundary</strong> refers to both entry points and exit points for the system. It&#39;s
at the &quot;edge&quot; of how data enters the system (think of things like from
requests to your server and responses from your server&#39;s requests).</p> </div> </aside>
<p>You rely on the parser to take unknown data into known data, and in regards to TypeScript, that known type then improves our confidence vector for the rest of the code as we can give the remaining heavy lifting to static analysis with the type checker.</p>
<p>Keeping the validation at the boundary also improves develop experience ensuring that validation logic isn&#39;t littered throughout our control flow, which helps to keep our control flow cornerstone healthy.</p>
<p><strong>Don&#39;t:</strong></p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>interface Request {
  body: unknown;
}

class Service {
  action(body: unknown) {
    // First check if body is a non-null object
    if (body !== null &amp;&amp; typeof body === &quot;object&quot;) {
      // Now TypeScript knows body is an object, so we can use &#39;in&#39; operator
      if (&quot;name&quot; in body) {
        // Do something with body.name
        // Note: At this point, TypeScript knows body has a &#39;name&#39; property,
        // but doesn&#39;t know its type. If needed, we could add further type narrowing:
        const typedBody = body as { name: string };
        // Now we can use typedBody.name as a string
      }
    }
  }
}

class Controller {
  private service: Service;

  constructor(service: Service) {
    this.service = service;
  }

  get(req: Request) {
    this.service.action(req.body);

    // ... omitted
  }
}</code></pre>
<p>Here, validation happens away from the boundary and we are passing <code>unknown</code> data types around.</p>
<p><strong>Do:</strong></p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import { z } from &quot;zod&quot;;

interface Request {
  body: unknown;
}

// Define the schema for our request body
const UserSchema = z.object({
  name: z.string(),
  email: z.string().email(),
  age: z.number().int().positive().optional(),
});

// Derive TypeScript type from the schema
type User = z.infer&lt;typeof UserSchema&gt;;

class Service {
  action(user: User) {
    // No need to validate
  }
}

class Controller {
  private service: Service;

  constructor(service: Service) {
    this.service = service;
  }

  get(req: Request) {
    // Parse and validate at the boundary
    const userResult = UserSchema.safeParse(req.body);

    if (!userResult.success) {
      // Handle error case
      return;
    }

    // Pass the parsed and typed data to the service
    this.service.action(userResult.data);

    // ... omitted
  }
}</code></pre>
<p>In the example above, we are using Zod to demonstrate an example of parsing into a well-known type.</p>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>We also could have done this in the &quot;Service&quot; class for the <strong>don&#39;t</strong> example,
but that still would have violated this guideline by happening away from the
boundary.</p> </div> </aside>
<h3 id="never-throw-errors">Never throw errors</h3>
<p>Errors fall into two categories: <strong>expected</strong> and <strong>unexpected</strong>.</p>
<p>In the scenario of an <strong>expected error</strong>, we want to take control of how we respond to that error and keep our system fault tolerant. In a scenario where a downstream server may be unavailable, a particular response like a 504 Gateway Timeout may knowingly want to be handled with retries, while for 400 errors we knowingly want to respond to our own request straight away with actionable details instead of retries. All of these scenarios fall under the domain that I considered <strong>expected errors</strong>.</p>
<p>In contrast, errors that are <strong>unexpected as far as the application is concerned</strong> fall into the <strong>unexpected errors</strong>, also known as <strong>defects</strong>. In these cases, an exception is generally raised and managed outside of the scope of the controller (for example, this could be catch-all middleware).</p>
<p>To elaborate a bit more, &quot;unexpected as far as the application is concerned&quot; does imply that it&#39;s a possible error scenario that you may expect to happen, but haven&#39;t implemented anything to handle it and don&#39;t mind that it&#39;s caught and tracked by a catch-all implementation. As for the &quot;catch-all&quot; middleware example, I will elaborate more on that in the** &quot;Let controllers tell you everything&quot;** section.</p>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>For more on this, I find it best summarized by <a href="https://effect.website/docs/error-management/two-error-types" target="_blank" rel="noopener noreferrer">the EffectTS article &quot;Two
Types of
Errors&quot;</a>.</p> </div> </aside>
<p>An example of this in action with the <a href="https://github.com/supermacro/neverthrow" target="_blank" rel="noopener noreferrer">neverthrow library</a>.</p>
<p><strong>Don&#39;t</strong>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>class Data {
  readonly _tag = &quot;Data&quot;;
  private myData: string;

  constructor(myData: string) {
    this.myData = myData;
  }
}

class Service {
  create() {
    // stand-in
    const isErr = false;

    if (isErr) {
      throw new Error(&quot;ErrorOne&quot;);
    }

    if (isErr) {
      throw new Error(&quot;ErrorTwo&quot;);
    }

    // Stand-in for applying actual work
    return new Data(&quot;Yay&quot;);
  }
}

class Controller {
  private service: Service;

  constructor(service: Service) {
    this.service = service;
  }

  create() {
    try {
      const createResult = this.service.create();

      return {
        status: 200,
        message: &quot;Success&quot;,
      };
    } catch (err) {
      // Some catch error handling
      // ... omitted

      // If we can&#39;t match the expected cases here, throw to the catch-all handler.
      throw err;
    }
  }
}</code></pre>
<p>In the above case, we are throwing errors as stand-ins for what could be handled as expected errors. A non-exhaustive list of problems with this:</p>
<ol>
<li>If we want to catch and run business logic on this, we need to do so at the controller. In cases that don&#39;t match, if we don&#39;t apply our observability tools here, then we need to re-throw. This creates some havoc on our control flow cornerstone and may also bloat our error logs and tools with known errors that we didn&#39;t handle because static analysis couldn&#39;t help us.</li>
<li>Throwing errors aren&#39;t visible in the types. If we use our Intellisense on the <code>Service.create</code> method, all we see if that the return type is <code>Data</code>. A developer cannot grok from our types what can go wrong in an expected way.</li>
</ol>
<p>In my experience as well, this approach also doesn&#39;t really happen in practice. Not all thrown errors are caught and managed correctly, so you end up with hard-to-follow try-catch behavior littered throughout implementation.</p>
<p><strong>Do</strong>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import { ok, err } from &quot;neverthrow&quot;;

class ExpectedErrorOne {
  readonly _tag = &quot;ExpectedErrorOne&quot;;
}

class ExpectedErrorTwo {
  readonly _tag = &quot;ExpectedErrorTwo&quot;;
}

class Data {
  readonly _tag = &quot;Data&quot;;
  private myData: string;

  constructor(myData: string) {
    this.myData = myData;
  }
}

class Service {
  create() {
    // Stand-in
    const isErr = false;

    if (isErr) {
      return err(new ExpectedErrorOne());
    }

    if (isErr) {
      return err(new ExpectedErrorTwo());
    }

    // Stand-in for applying actual work
    return ok(new Data(&quot;Yay&quot;));
  }
}

class Controller {
  private service: Service;

  constructor(service: Service) {
    this.service = service;
  }

  create() {
    const createResult = this.service.create();

    return createResult.match(
      // Success case
      (data) =&gt; ({
        status: 200,
        message: &quot;Success&quot;,
      }),

      // Error case - pattern match on _tag
      (error) =&gt; {
        switch (error._tag) {
          case &quot;ExpectedErrorOne&quot;:
            return {
              status: 400,
              message: &quot;Bad request&quot;,
            };
          case &quot;ExpectedErrorTwo&quot;:
            return {
              status: 422,
              message: &quot;Unprocessable entity&quot;,
            };
          default:
            // Optional Exhaustiveness check - will error if we add a new response type
            // and forget to handle it.
            const _exhaustiveCheck: never = error;
        }
      }
    );
  }
}</code></pre>
<p>In the above, I&#39;ve made use of the <a href="https://github.com/supermacro/neverthrow" target="_blank" rel="noopener noreferrer">neverthrow library</a> to demonstrate an approach of returning known errors.</p>
<p>Some of the benefits of this:</p>
<ol>
<li>The return type is known typed to know what can go wrong <code>const createResult: Err&lt;never, ExpectedErrorOne&gt; | Err&lt;never, ExpectedErrorTwo&gt; | Ok&lt;Data, never&gt;</code>.</li>
<li>There are no try-catch clauses. In the case where an error is thrown from something <strong>unexpected</strong>, we consider this a <strong>defect</strong> and should have systems in place to capture that error and inform the developers (not shown here).</li>
<li>Our controller can have an easier time managing responses at the boundary, while our developers working on this can learn a lot about this endpoint and possible responses without diving into the business logic.</li>
</ol>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>If you look at the <code>Data</code> and expected error classes, you&#39;ll known the <code>_tag</code>
property (which I&#39;ve adopted from EffectTS). I&#39;ll talk more to this on the
metadata section.</p> </div> </aside>
<p>I should finish here by saying that &quot;never&quot; here is a bit strong. I&#39;ve recently heard an engineering manager use the quote &quot;use exceptions for exceptional circumstances&quot;, and I find that to be a useful quote around throwing errors in TypeScript. Do so sparingly and with good reason.</p>
<h3 id="metadata">Metadata</h3>
<p>There should be an easy way to delineate data which is important for internal affairs.</p>
<p>As mentioned in the previous section, I use the underscore prefix <code>_</code> as an easy for way for me to delineate between properties important for internal logic, or represent data that has only has importance to internal debugging and observability tools.</p>
<p>I opted for this style of delineation after spending some time working with <a href="https://effect.website/" target="_blank" rel="noopener noreferrer">EffectTS</a>, but you certainly do not have to follow my conventions. The important part is that there are conventions and those are not easily confused with other data.</p>
<p><strong>Don&#39;t</strong>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>class InvalidDatabaseObject {
  readonly tag = &quot;InvalidDatabaseObject&quot;;
  readonly errorId = 123;
  readonly interalMessage = &quot;To resolve, check a...b...c...&quot;
  readonly statusCode = 422;
  readonly message = &quot;We are unable to process your request at this time&quot;;
}

class Controller() {
  create() {
    // Assume this created object was the response
    const result = new InvalidDatabaseObject();

    if (result.tag === &quot;InvalidDatabaseObject&quot;) {
      // Implement any observability if we need based on error properties
      logger.error(&quot;Failed to do something&quot;, {
        tag: result.tag,
        errorId: result.errorId,
        internalMessage: result.interalMessage,
      })
    }

    return {
      tag: result.tag,
      status: result.statusCode,
      message: result.message
    }
  }
}</code></pre>
<p>In the above, it&#39;s hard for future developers to understand which properties are used for internal affairs.</p>
<p><strong>Do</strong>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>class InvalidDatabaseObject {
  readonly _tag = &quot;InvalidDatabaseObject&quot;;
  readonly _errorId = 123;
  readonly _interalMessage = &quot;To resolve, check a...b...c...&quot;
  readonly statusCode = 422;
  readonly message = &quot;We are unable to process your request at this time&quot;;
}

class Controller() {
  create() {
    // Assume this created object was the response
    const result = new InvalidDatabaseObject();

    if (result._tag === &quot;InvalidDatabaseObject&quot;) {
      // Implement any observability if we need based on error properties
      logger.error(&quot;Failed to do something&quot;, {
        _tag: result._tag,
        _errorId: result._errorId,
        _internalMessage: result._interalMessage,
      })
    }

    return {
      _tag: result._tag,
      status: result.statusCode,
      message: result.message
    }
  }
}</code></pre>
<p>In the above code, we can note visually delineate what is useful for internal development and which belongs to things like response objects. Note that we also return metadata with the response with <code>_tag</code>. This enables other systems to make use of the same unified control flow. For client applications, this enables the ability to be able to customize behavior and messages beyond simple status codes.</p>
<p>I normally take this approach in conjunction with the control flow helpers we&#39;ve explored for the two types of errors, but let me reiterate that the important part here is that you have consistent delineation defined within the engineering cohort. It may be more approach to separate responses and objects into something like <code>metadata</code> and <code>data</code> properties on the object (similar to how AWS does this) or something else completely different.</p>
<p>Please note that if you nail this stuff down, it also becomes a miracle for micro-services. Whenever you write decouple, asynchronously processed code that has an internal, well-defined rule set, you&#39;ll find that handling these responses across services, clients and communication lines like websockets becomes increasingly nicer to reason about and manage.</p>
<h3 id="define-your-source-of-truth">Define your source of truth</h3>
<p>If your types come from remote sources, don&#39;t define TypeScript types and interfaces and set this as your source of truth. It can and will fall out of sync.</p>
<p><strong>Don&#39;t</strong>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>interface User {
  name: string;
}

function getUser(): User {
  const response: { data: unknown } = await getUserFromRemote();

  return response.data as unknown as User;
}</code></pre>
<p><strong>Do</strong>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import { z } from &quot;zod&quot;;

const UserSchema = z.object({
  name: z.string(),
});

type User = z.infer&lt;typeof UserSchema&gt;;

function getUser(): User {
  const response: { data: unknown } = await getUserFromRemote();
  const userResult = UserSchema.parse(response.data);

  return userResult;
}</code></pre>
<p>Secondly, don&#39;t mix up representations for different concerns. For example, if you are writing schemas to represent the query params, have one for the stringified version and another to represent the coercion. This is one of those times were code volume will increase, but also opens you up the better generate that code and not have to manage it in the first place.</p>
<p><strong>Don&#39;t</strong>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>const getUsersQueryParams = z
  .object({
    name: z.string(),
    age: z.coerce.number(),
  })
  .partial();</code></pre>
<p><strong>Do</strong>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>const getUserQueryParams = z
  .object({
    name: z.string(),
    age: z.string(),
  })
  .partial();

const getUserCoercedQueryParams = z
  .object({
    name: z.string(),
    age: z.coerce.number(),
  })
  .partial();

// In use
const result = getUserQueryParams
  .pipe(getUserCoercedQueryParams)
  .parse({ name: &quot;Daniel&quot;, age: &quot;22&quot; });</code></pre>
<p>This certainly violates the code volume principle, but it is surprising I&#39;ve required these values to have separation for different purposes. This do/don&#39;t falls under the category of &quot;code generation&quot; and well-defined schemas will make generating this separation very easy.</p>
<h3 id="let-controllers-tell-you-everything">Let controllers tell you everything</h3>
<p>I&#39;ve already touched on this principle with &quot;never throw errors&quot;, but I will just re-iterate the guidelines for this one:</p>
<ol>
<li>Controllers should control all the request received/response flows.</li>
<li>As a developer, from the controller code, you should be able to see and understand route-specific middleware applied, expected incoming data (params, body, headers) and <strong>all possible responses</strong> (with the possible exception of defects if you handling that at a catch-all middleware handler).</li>
</ol>
<p><strong>Don&#39;t</strong>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>class Data {
  readonly _tag = &quot;Data&quot;;
  private myData: string;

  constructor(myData: string) {
    this.myData = myData;
  }
}

class Service {
  create() {
    // stand-in
    const isErr = false;

    if (isErr) {
      throw new Error(&quot;ErrorOne&quot;);
    }

    if (isErr) {
      throw new Error(&quot;ErrorTwo&quot;);
    }

    // Stand-in for applying actual work
    return new Data(&quot;Yay&quot;);
  }
}

class Controller {
  private service: Service;

  constructor(service: Service) {
    this.service = service;
  }

  create() {
    const createResult = this.service.create();

    return {
      status: 200,
      message: &quot;Success&quot;,
    };

    // NO ERROR HANDLING, all sent to the catch-all. BAD.
  }
}</code></pre>
<p><strong>Do</strong>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import { ok, err } from &quot;neverthrow&quot;;

class ExpectedErrorOne {
  readonly _tag = &quot;ExpectedErrorOne&quot;;
}

class ExpectedErrorTwo {
  readonly _tag = &quot;ExpectedErrorTwo&quot;;
}

class Data {
  readonly _tag = &quot;Data&quot;;
  private myData: string;

  constructor(myData: string) {
    this.myData = myData;
  }
}

class Service {
  create() {
    // Stand-in
    const isErr = false;

    if (isErr) {
      return err(new ExpectedErrorOne());
    }

    if (isErr) {
      return err(new ExpectedErrorTwo());
    }

    // Stand-in for applying actual work
    return ok(new Data(&quot;Yay&quot;));
  }
}

class Controller {
  private service: Service;

  constructor(service: Service) {
    this.service = service;
  }

  create() {
    const createResult = this.service.create();

    return createResult.match(
      // Success case
      (data) =&gt; ({
        status: 200,
        message: &quot;Success&quot;,
      }),

      // Error case - pattern match on _tag
      (error) =&gt; {
        switch (error._tag) {
          case &quot;ExpectedErrorOne&quot;:
            return {
              status: 400,
              message: &quot;Bad request&quot;,
            };
          case &quot;ExpectedErrorTwo&quot;:
            return {
              status: 422,
              message: &quot;Unprocessable entity&quot;,
            };
          default:
            // Optional Exhaustiveness check - will error if we add a new response type
            // and forget to handle it.
            const _exhaustiveCheck: never = error;
        }
      }
    );
  }
}</code></pre>
<aside class="callout callout--danger" aria-label="Danger"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><path d="m21.7 18-8-14a2 2 0 0 0-3.4 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.7-3ZM12 9v4M12 17h.01" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>  <div class="callout__body">  <p>I actually haven&#39;t included the example to properly demonstrate this. It&#39;s not
inclusive of handling incoming request information. I will update this when I
get the chance with something from one of my actual projects.</p> </div> </aside>
<p>In regards to the catch-all middleware handler, <strong>do not</strong> use that as a point to add control flow for different error handling responses outside of the controller.</p>
<p><strong>Don&#39;t</strong>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import { Hono } from &quot;hono&quot;;
import { HTTPException } from &quot;hono/http-exception&quot;;

const app = new Hono();

// Error handling middleware
app.use(&quot;*&quot;, async (c, next) =&gt; {
  try {
    await next();
  } catch (error) {
    console.error(&quot;Error caught:&quot;, error);

    // Handle Hono&#39;s built-in HTTPException
    if (error instanceof HTTPException) {
      return error.getResponse();
    }

    // Custom error with status code
    if (&quot;status&quot; in error &amp;&amp; typeof error.status === &quot;number&quot;) {
      return c.json(
        {
          message: error.message || &quot;An error occurred&quot;,
          code: error.code || &quot;UNKNOWN_ERROR&quot;,
        },
        error.status
      );
    }

    // Authentication errors
    if (
      error.name === &quot;AuthenticationError&quot; ||
      error.code === &quot;UNAUTHENTICATED&quot;
    ) {
      return c.json(
        {
          message: &quot;Authentication failed&quot;,
          details: error.message,
        },
        401
      );
    }

    // Validation errors with details
    if (error.name === &quot;ValidationError&quot; || &quot;validationErrors&quot; in error) {
      return c.json(
        {
          message: &quot;Validation failed&quot;,
          errors: error.validationErrors || error.details || [error.message],
        },
        400
      );
    }

    // Database errors
    if (&quot;code&quot; in error &amp;&amp; error.code?.startsWith(&quot;DB_&quot;)) {
      // Log database errors but don&#39;t expose details to client
      console.error(&quot;Database error:&quot;, error);
      return c.json({ message: &quot;Database operation failed&quot; }, 500);
    }

    // Default case - generic 500 error
    return c.json({ message: &quot;Internal server error&quot; }, 500);
  }
});

export default app;</code></pre>
<p><strong>Do</strong>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import { Hono } from &quot;hono&quot;;
import { HTTPException } from &quot;hono/http-exception&quot;;

const app = new Hono();

// Error handling middleware
app.use(&quot;*&quot;, async (c, next) =&gt; {
  try {
    await next();
  } catch (error) {
    // Handle observability here. Log errors, capture with third-party tools like Sentry etc.

    // Default case - generic 500 error
    return c.json({ message: &quot;Internal server error&quot; }, 500);
  }
});

export default app;</code></pre>
<p>In this example, our catch-all is purely for handling defects and recovering with valid responses to the user. Use the catch-all with no control flow and only as a mechanism to notify the developers.</p>
<h3 id="conversations-with-ai">Conversations with AI</h3>
<p>Don&#39;t rely on AI to give you the best developer experience. While these tools are getting better, without guidelines, they have overcomplicated and over-bloated their answers for queries that I&#39;ve made before.</p>
<p>AI is great to chat shop with and pass some ideas by, but don&#39;t let it take control of your codebase (at least not as it is at the time of writing). For any suggestions that you adopt, scrutinize them and do your own investigations and spikes into it.</p>
<h3 id="generate-as-much-code-as-possible">Generate as much code as possible.</h3>
<p>Such an underrated time-saver. If there are explicit patterns that are repetitive that you can identity, spend the time to write a script to run the generation. Most of the time for myself, I end up git ignoring these files and generating them for local development and as part of the CI pipeline.</p>
<p>An example of this normally comes with generating SDKs for code bases with OpenAPI specs. Thanks to the spec definition, you can relying write code to parse this information and whatever you need.</p>
<p>This is also a place where AI with guardrails can be super useful, but do take care with this. I wouldn&#39;t make use of AI generation as part of a pipeline, so I would be committing that code (and likely not running the AI script much).</p>
<p>I also like to use template libraries here personally. It acts as both a way to easily generate code and a guideline for anyone joining the codebase. Whenever I change repetitive standards, it&#39;s normally the templates that I am confirming my ideas against.</p>
<h3 id="write-to-refactor-programmatically">Write to refactor programmatically</h3>
<p>The more you contain the complexity in your managed code, the easier it is to write codemods for when standards change.</p>
<p>Codemods enable you to programmatically change the code within your files. Personally, I find <a href="https://github.com/dsherret/ts-morph" target="_blank" rel="noopener noreferrer">ts-morph</a> the most accessible option, but there are plenty of great AST parsers and codemod tools out there.</p>
<p>As a bonus, if you like &quot;code-as-documentation&quot; in regards to generating diagrams and scripts, then these same tools can be your friend. Just a fair warning that the more complicated your code is to programmatically traverse, the less these things become an option.</p>
<span id="dont-go-overkill-on-abstraction-layers" class="anchor-alias"><h3 id="don-t-go-overkill-on-abstraction-layers">Don&#39;t go overkill on abstraction layers</h3>
<blockquote>
<p>&quot;All problems in computer science can be solved by another level of indirection&quot; -- The &quot;fundamental theorem of software engineering&quot;.</p>
</blockquote>
<p>Some guidelines:</p>
<ul>
<li>Sparingly use abstract classes and inheritance.</li>
<li>Sparingly extend interfaces. The same rules can apply here for composition over inheritance.</li>
<li>When using generics, do whatever it takes to have generics infer the types instead of supplying them where possible.</li>
<li>Invert control back to the invocation layer where possible.</li>
<li>Use dependency injection.</li>
<li>Keep your internal codebase &quot;shallow&quot;.</li>
</ul>
<p>For the example around interfaces, we can demonstrate what I mean with this very contrived useless code:</p>
<p><strong>Don&#39;t</strong>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>interface Retryable {
  redrive: () =&gt; void;
}

interface Queue extends Retryable {
  process: () =&gt; Promise&lt;void&gt;;
}

class DataQueue implements Queue {
  private dlq: string[] = [];
  private q: string[] = [];

  redrive() {
    this.q = [...this.q, ...this.dlq];
    this.dlq = [];
  }

  async process() {
    await Promise.all(this.q.map((el) =&gt; console.log));
  }

  // Rest omitted
}</code></pre>
<p>In this example, we have <code>Queue</code> now extending for <code>Retryable</code>, instead of composing them together.</p>
<p><strong>Do</strong>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>interface Retryable {
  redrive: () =&gt; void;
}

interface Queue {
  process: () =&gt; Promise&lt;void&gt;;
}

class DataQueue implements Retryable, Queue {
  private dlq: string[] = [];
  private q: string[] = [];

  redrive() {
    this.q = [...this.q, ...this.dlq];
    this.dlq = [];
  }

  async process() {
    await Promise.all(this.q.map((el) =&gt; console.log));
  }

  // Rest omitted
}</code></pre>
<p>In the above, there is a separation of concerns for the interfaces, and understanding the definition of <code>redrive</code> doesn&#39;t require us to jump through to inheritance.</p>
<blockquote>
<p>&quot;All problems in computer science can be solved by another level of indirection. Except for the problem of indirection.&quot;</p>
</blockquote>
<h2 id="conclusion">Conclusion</h2>
<p>This wraps up my opinionated list of things. At least this first iteration. I&#39;ve been sitting here typing away for close to four hours now, so the quality may be diminishing returns.</p>
<p>I want to finish with a couple of &quot;don&#39;t forgets&quot;:</p>
<ol>
<li>These are guidelines. They serve me well currently, but may be out-of-date by the end of the year. If you don&#39;t agree or have a strong opinion on any of them, I would welcome your critiques.</li>
<li>Even the best developers have moments where they commit cardinal sins with their code. It&#39;s a team effort for this to work, so ensure that your design patterns and ideas are reviewed by your team members as they will always have better feedback on your own code than you do.</li>
<li>This is purely about my more recent experiences with TypeScript codebases. What I am speaking to does not apply to other languages that make trade-offs that actively want you to contradict these guidelines.</li>
</ol>
<p>If I get another moment where I feel like furiously typing away on the keyboard, I would like to talk about team processes, microservices and/or release management.</p>
<p><em>Photo credit: <a href="https://unsplash.com/@martinsanchez" target="_blank" rel="noopener noreferrer"><code>martinsanchez</code></a></em></p>]]></content>
<category term="typescript"/>
</entry>
<entry>
<title type="html"><![CDATA[Better .NET Controllers with OneOf]]></title>
<id>https://blog.dennisokeeffe.com/blog/2025-02-23-better-dotnet-controllers-with-oneof</id>
<link href="https://dennisokeeffe.com/blog/2025-02-23-better-dotnet-controllers-with-oneof"/>
<published>2025-02-23T00:00:00.000Z</published>
<updated>2025-02-23T00:00:00.000Z</updated>
<summary type="html"><![CDATA[A reflection on a recent codebase change where the choice came between handling exceptions from middleware or controlling responses from the controller.]]></summary>
<content type="html"><![CDATA[<p>In this blog post, we will be looking at <a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling?view=aspnetcore-9.0" target="_blank" rel="noopener noreferrer">error handling for .NET applications</a> and looking at how we can help write our services to invert control back to our controllers for when it comes to making request responses using the <a href="https://github.com/mcintyre321/OneOf" target="_blank" rel="noopener noreferrer">OneOf library</a>, a library that provides F# style unions.</p>
<h2 id="context-setting">Context setting</h2>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>For anyone not interested in the backstory, you can skip along.</p> </div> </aside>
<p>For anyone that follows my content, you will know that I am not a C# nor .NET developer. My experience with the language itself is limited to time spent working with C# at university or through hobby projects (game development, Arduino).</p>
<p>That being said, work has recently asked me to make some changes to our old .NET 6 server. Some of the challenges with this legacy code base has been tracking error management and understanding potential return types where the current style of relying on raising exceptions for control flow lacks intellisense for developers working on the codebase.</p>
<p>It should be disclosed that as an unseasoned C# developer, this can make adjudicating good practice and idiomatic C# a little more difficult. On the flip-side of this, one could see it as me looking at C# with a fresh set of eyes.</p>
<h2 id="reviewing-the-net-error-handling-page">Reviewing the .NET error handling page</h2>
<p>One of the challenges I was facing while reading through the legacy codebase was understanding how our non-2xx error code responses were happening. A lot of responses were returning as 2xx series responses with <code>null</code> values, even in scenarios where the request body itself was invalid.</p>
<p>I opted to first read through the recommendations for <a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling?view=aspnetcore-9.0" target="_blank" rel="noopener noreferrer">error handling that the .NET documentation presents</a> (here I am sharing the latest version of the docs, although I was looking through .NET 6 documentation).</p>
<p>Something that stood out to me was the <a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling?view=aspnetcore-9.0#exception-handler-lambda" target="_blank" rel="noopener noreferrer">lambda exception handler</a> where we could pass a C# handler to the <code>app.UseExceptionHandler</code> method:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="csharp"><code>var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler(exceptionHandlerApp =&gt;
    {
        exceptionHandlerApp.Run(async context =&gt;
        {
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;

            // using static System.Net.Mime.MediaTypeNames;
            context.Response.ContentType = Text.Plain;

            await context.Response.WriteAsync(&quot;An exception was thrown.&quot;);

            var exceptionHandlerPathFeature =
                context.Features.Get&lt;IExceptionHandlerPathFeature&gt;();

            if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
            {
                await context.Response.WriteAsync(&quot; The file was not found.&quot;);
            }

            if (exceptionHandlerPathFeature?.Path == &quot;/&quot;)
            {
                await context.Response.WriteAsync(&quot; Page: Home.&quot;);
            }
        });
    });

    app.UseHsts();
}</code></pre>
<p>In the example above, the code could be configured within a .NET application to capture raised exceptions and use control flow to act upon them.</p>
<p>In particular, the example code sets the 500 status code and conditionally sends responses from the server based on business logic.</p>
<h2 id="the-challenges-that-raising-exceptions-presents">The challenges that raising exceptions presents</h2>
<p>The current .NET configuration for the application that I was working with had a version of this configured, but this is where abstract boolean logic itself can take a nasty turn. The file itself is a minefield of nested if/else statements where the final response is &quot;funneled&quot; through that logic based on the type of exception it is (similar to <code>exceptionHandlerPathFeature?.Error is FileNotFoundException</code> above), as well as some business-specific control flow qualifiers (statements not based on the error type).</p>
<p>Contributing to the file itself would be adding to the debt that had built up, but in some cases for legacy code that may be considered fine (and may actually end up being what I am requested to do), but there are two reasons why I struggle with this style of controller management:</p>
<ol>
<li>Type signatures do not inform the developer of potential raised exceptions.</li>
<li>Controllers should be the entry point and exit point for all expected HTTP responses.</li>
</ol>
<p>It is of my personal belief that controllers should be the &quot;air traffic controllers&quot; - they should handle the delegating of business logic to the services, while be in control of returning all expected responses.</p>
<p>What I mean by this is that, as a developer, if I am wanting to look at how an endpoint is handled, I should be able to start at the controller in order to grok how data is handled coming in while also understanding all expected responses going out -- not just the &quot;ok&quot; response.</p>
<p>When I say &quot;expected responses&quot;, that includes management for all non-200 responses for percievable issues such as request validation failure, unprocessible issues due to dependencies, etc. There is always the possibility for unexpected errors (which I normally refer to as defects), and in my opinion <strong>only these errors</strong> should fall outside of the domain of controller management.</p>
<p>The code shared above for the recommended <code>app.UseExceptionHandler</code> I find to be appropriate for <strong>defects</strong>. I consider defects to be the <strong>unexpected errors of the application</strong>.</p>
<p>I consider the <code>app.UseExceptionHandler</code> to be appropriate for these unforeseen circumstances. Within the lambda, I would configure the 500 internal error response and have any <strong>3rd party error tracking</strong> occur in this lambda only. The <code>UseExceptionHandler</code> for me should only contain the business logic around handling these unexpected scenarios. Everything else should be managed by the controller as far as I am concerned.</p>
<h2 id="return-types-in-net">Return types in .NET</h2>
<p>The first of the two reasons that I presented for why raising exceptions is challenging is that the type signatures do not reflect the return type.</p>
<p>Take the following code example:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="csharp"><code>public async Task&lt;string&gt; DecryptCipherText(string cipherText)
{
    if (string.IsNullOrEmpty(cipherText))
    {
        throw new ArgumentNullException(nameof(cipherText), &quot;Cipher text cannot be null or empty&quot;);
    }

    // ... pretend to process things
    return &quot;Processed cipherText&quot;;
}</code></pre>
<p>In this code block, we have a contrived example of a function that expects to decrypt some cipher text. In the scenario that the <code>cipherText</code> passed in is null or empty, we raise an <code>ArgumentNullException</code>.</p>
<p>However, if we look at the type signature, all we see is <code>(awaitable) Task&lt;string&gt; CardDataEncryption.DecryptCipherText(string cipherText)</code>. There is no indication to anyone from the call site of the function (be it another service or whatever) that an <code>ArgumentNullException</code> can be thrown.</p>
<p>While this is not unique to C# itself, there are alternatives that offer up solutions for us. This is where I want to introduce <a href="https://github.com/mcintyre321/OneOf" target="_blank" rel="noopener noreferrer">OneOf</a>.</p>
<p>As mentioned as the beginning of the post, <code>OneOf</code> is a library that provides F# style unions for C#.</p>
<p>If we look at our function again with the return signature <code>Task&lt;string&gt;</code>, then we note that out-of-the-box, there is no &quot;union&quot; style returns where we could indicate different return types. This is where <code>OneOf</code> steps in.</p>
<p>It enables us to be able to alter signatures to indicate multiple return types and provides tools to help us match on those return types.</p>
<p>For example, we could alter our code to the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="csharp"><code>public async Task&lt;OneOf&lt;string, ArgumentNullException&gt;&gt; DecryptCipherText(string cipherText)
{
    if (string.IsNullOrEmpty(cipherText))
    {
        // Note that we are now returning this value
        return new ArgumentNullException(nameof(cipherText), &quot;Cipher text cannot be null or empty&quot;);
    }

    // ... pretend to process things
    return &quot;Processed cipherText&quot;;
}</code></pre>
<p>In the above, instead of raising an exception, we can now <em>return</em> the exception.</p>
<p>Looking at the type signature now, we see it&#39;s adjusted to <code>(awaitable) Task&lt;OneOf&lt;string, ArgumentNullException&gt;&gt; CardDataEncryption.DecryptCipherText(string cipherText)</code>.</p>
<p>From a developer point-of-view, Intellisense providing more information on potential errors that can go wrong is a powerful to invert control of how to manage results from a function from the caller. This is particularly powerful for developers like myself who stumble into a codebase for the first time.</p>
<p>The library itself provides a number of ways to fetch values from this response type, but the two that I use the most are <code>Match</code>, <code>TryPickT...</code> or directly accessing the value itself with the value property <code>AsT...</code> where the <code>...</code> in both cases represents a number for the argument order passed to the type signature (i.e. <code>AsT0</code> for our example would be <code>string</code> while <code>AsT1</code> would be <code>ArgumentNullException</code>).</p>
<p>For example, with our function defined above we could access the results like so:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="csharp"><code>const result = await DecryptCipherText(&#39;someCipherTextValue&#39;);

// Using match to determine to send a 200 or 400
return result.Match(
    result =&gt; Ok(result),
    ArgumentNullException =&gt; new BadRequest(&quot;...&quot;)
);

// Using TryPick - where you might want to pick out the error to return early and bubble up the error
if (thingOrNotFoundOrError.TryPickT1(out ArgumentNullException error, out var remainder)) {
  return error;
}

// Accessing the property directly
Console.WriteLine(result.AsT0); // Possibly the string!</code></pre>
<p>The above demonstrates the few ways that I&#39;ve used the library.</p>
<ol>
<li>The first example with <code>Match</code> is how I managed the &quot;air traffic controller&quot; for responding from the controller. The <code>Match</code> type is exhaustive and requires you to handle all expected responses.</li>
<li>The second example allows you to &quot;extract&quot; a value to use as required. Normally for myself, this might be early propagation of the error up to where it can be handled (normally at the controller).</li>
<li>Demonstrates access the value directly. I normally use this in tandem with (2) to ensure that the value exists before applying more logic to it (or you could do the opposite and pick out the valid response and return all others!).</li>
</ol>
<p>The library itself extends possible return values as you have more applied. For a very contrived example of this, the following would have <code>AsTN</code> where <code>N</code> could be 0 to 4:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="csharp"><code>(awaitable) Task&lt;OneOf&lt;string, ArgumentNullException, ArithmeticException, ArgumentException, PaymentException&gt;&gt; CardDataEncryption.DecryptCipherText(string cipherText)</code></pre>
<p>To nail the point home - the value of having a separation of &quot;expected&quot; and &quot;unexpected&quot; errors is that the former allows you to self-document and manage all expected responses in a matter-of-fact way.</p>
<p>Keeping that separation can even enable things such as more effective testing given that the domain and range of a function makes the testing strongly in favour of &quot;input-output testing&quot;. It means functions will no longer contain the side-effects of thrown functions that do not return, which is heavensent when it comes to testing functions orientated around business logic.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Today&#39;s blog post introduces the idea of using the library <a href="https://github.com/mcintyre321/OneOf" target="_blank" rel="noopener noreferrer">OneOf</a> in order to better manage union response types, enabling the control flow and development of C# to focus on handling expected outcomes.</p>
<p>This was demonstrated by following along an example that I had personally come across where reliance on control flow with throwing exceptions leads to a more difficult developer experience and merging of error state management across different concerns.</p>
<h2 id="further-links-and-resources">Further links and resources</h2>
<ul>
<li><a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling?view=aspnetcore-9.0~" target="_blank" rel="noopener noreferrer">Handling errors in ASP.NET Core</a></li>
<li><a href="https://github.com/mcintyre321/OneOf" target="_blank" rel="noopener noreferrer">mcintyre321/OneOf</a></li>
</ul>
<p><em>Photo credit: <a href="https://unsplash.com/@wolfgang_hasselmann" target="_blank" rel="noopener noreferrer"><code>wolfgang_hasselmann</code></a></em></p>]]></content>
<category term="dotnet"/><category term="csharp"/>
</entry>
<entry>
<title type="html"><![CDATA[AWS Aurora DSQL in Action]]></title>
<id>https://blog.dennisokeeffe.com/blog/2024-12-05-aws-aurora-dsql-in-action</id>
<link href="https://dennisokeeffe.com/blog/2024-12-05-aws-aurora-dsql-in-action"/>
<published>2024-12-05T00:00:00.000Z</published>
<updated>2024-12-05T00:00:00.000Z</updated>
<summary type="html"><![CDATA[Take a look into AWS' recently announced distributed Aurora SQL solution, now in preview!]]></summary>
<content type="html"><![CDATA[<p>On December 3rd, 2024, <a href="https://aws.amazon.com/blogs/database/introducing-amazon-aurora-dsql/" target="_blank" rel="noopener noreferrer">Amazon announced the preview of Amazon Aurora DSQL</a>, which is introduced as <em>&quot;the fastest serverless distributed SQL database for always available applications&quot;</em>.</p>
<p>From all the sources I have read so far, it looks to have been something that the team has had in the works for a long time. It comes in a few different flavours, but this blog post will focus more so on playing around with DSQL and seeing it in action.</p>
<p>To keep things simple, we will be using the AWS console to setup a new cluster and then connecting to it with Bun, TypeScript and Drizzle.</p>
<h2 id="getting-started">Getting started</h2>
<p>First, let&#39;s configure the project with Bun and Drizzle.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code># Configure git, initialise the project
$ mkdir demo-dsql-drizzle
$ cd demo-dsql-drizzle
$ bun init
$ bun add drizzle-orm pg dotenv
$ bun add -D drizzle-kit @types/pg
$ mkdir -p src/db drizzle
$ touch src/index.ts src/db/schema.ts drizzle.config.ts</code></pre>
<p>We&#39;re going to follow a slightly modified version of <a href="https://orm.drizzle.team/docs/get-started/postgresql-new" target="_blank" rel="noopener noreferrer">Drizzle&#39;s getting started guide for Postgres</a>. Please note that DSQL is only Postgres-compatible at the time of the announcement.</p>
<p>First, we need to configure our basic schema. Add the following to <code>src/db/schema.ts</code>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import { integer, pgTable, varchar } from &quot;drizzle-orm/pg-core&quot;;

export const usersTable = pgTable(&quot;users&quot;, {
  id: integer().primaryKey().unique(),
  name: varchar({ length: 255 }).notNull(),
  age: integer().notNull(),
  email: varchar({ length: 255 }).notNull().unique(),
});</code></pre>
<p>DSQL has limitations around constraints that I won&#39;t dive too deep into here, just note that you <strong>cannot</strong> use the <code>generatedAlwaysAsIdentity()</code> constraint as expected. AWS will respond with an error during the migration. You can <a href="https://docs.aws.amazon.com/aurora-dsql/latest/userguide/working-with-postgresql-compatibility-unsupported-features.html" target="_blank" rel="noopener noreferrer">read more about the unsupported features here</a>.</p>
<p>At this point, we can pivot to the AWS console and create a new Aurora DSQL cluster. This will be required for filling out the remaining files as we will need some configuration setup.</p>
<h2 id="creating-a-new-aurora-dsql-cluster">Creating a new Aurora DSQL cluster</h2>
<p>Through the AWS console, you can search and navigate to Aurora DSQL.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2024-12-05-aws-aurora-dsql-in-action/create-cluster.webp" alt="Creating a cluster via the console" loading="lazy" decoding="async"/><figcaption>Creating a cluster via the console</figcaption></figure>
<p>If you are not currently in a supported region, AWS will prompt you to change regions.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2024-12-05-aws-aurora-dsql-in-action/unsupported-regions.webp" alt="Change region request" loading="lazy" decoding="async"/><figcaption>Change region request</figcaption></figure>
<p>Once you are in a supported region, you can select to create a cluster to move forward. Once it&#39;s created, go into the details for the cluster and you will see a &quot;connect&quot; button that will give you the endpoint and other details required to connect to the cluster.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2024-12-05-aws-aurora-dsql-in-action/connect.webp" alt="Highlighted connect button within the UI" loading="lazy" decoding="async"/><figcaption>Highlighted connect button within the UI</figcaption></figure>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2024-12-05-aws-aurora-dsql-in-action/auth.webp" alt="Auth details modal" loading="lazy" decoding="async"/><figcaption>Auth details modal</figcaption></figure>
<p>At this point, we&#39;ll have a cluster running with some details about the endpoint and cluster ID, etc. Collect the details for the endpoint and we can move forward.</p>
<h2 id="connecting-to-the-aurora-dsql-cluster">Connecting to the Aurora DSQL cluster</h2>
<p>Back in our project, we can configure out connection. AWS has <a href="https://docs.aws.amazon.com/aurora-dsql/latest/userguide/accessing-postgresql-endpoints.html" target="_blank" rel="noopener noreferrer">some more details about using the PostgreSQL protocol</a> that you can read, but in our case I will walk use through what we need.</p>
<p>We&#39;ll need to configure our <code>drizzle.config.ts</code> file in the root of the project in order to run our migration. Given that this is just a walkthrough, I won&#39;t be using any <code>.env</code> configuration files, but the security is back on you to manage. We&#39;ll be using the admin configuration in our case, so the default password limit is 15 minutes to add some sense of security. In practice, you&#39;ll either need to manage IAM or make use of APIs to generate the configuration required to connect.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import &quot;dotenv/config&quot;;
import { defineConfig } from &quot;drizzle-kit&quot;;

export default defineConfig({
  out: &quot;./drizzle&quot;,
  schema: &quot;./src/db/schema.ts&quot;,
  dialect: &quot;postgresql&quot;,
  dbCredentials: {
    host: &quot;&lt;add in the endpoint from the console here&gt;&quot;,
    port: 5432,
    user: &quot;admin&quot;,
    password: &quot;&lt;add in the entire password string from the console here&gt;&quot;,
    database: &quot;postgres&quot;,
    ssl: true,
  },
});</code></pre>
<p>By default, the <strong>user</strong> will be <code>admin</code> and the <strong>database</strong> will be <code>postgres</code>. The <strong>password</strong> will be the one you set when creating the cluster.</p>
<p>The password itself includes the host as well. It&#39;s a very long string but just note that you need the entire thing in the password field.</p>
<p>Please note that <code>ssl</code> is required for the connection to work.</p>
<h2 id="running-a-simple-migration">Running a simple migration</h2>
<p>Once this is all configured, we can run the migration:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>$ bunx drizzle-kit push</code></pre>
<p>You can read up more on Drizzle kit migrations on the <a href="https://orm.drizzle.team/docs/migrations" target="_blank" rel="noopener noreferrer">Drizzle migrations documentation</a>.</p>
<p>If everything is successful, you should the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>$ bunx drizzle-kit push
No config path provided, using default &#39;drizzle.config.ts&#39;
Reading config file &#39;/Users/dennis.okeeffe/code/okeeffed/demo-dsql-drizzle/drizzle.config.ts&#39;
Using &#39;pg&#39; driver for database querying
[✓] Pulling schema from database...
[✓] Changes applied</code></pre>
<p>Let&#39;s finish by seeding some data and applying some basics calls against the database.</p>
<h2 id="running-some-code-against-the-database">Running some code against the database</h2>
<p>We&#39;ll modify an example script from Drizzle to run against the database slightly as we will need to provide the integer IDs ourselves. Not having certain key constraints means that we will need to enforce it at the application layer.</p>
<p>In <code>src/index.ts</code>, add the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import &quot;dotenv/config&quot;;
import { drizzle } from &quot;drizzle-orm/node-postgres&quot;;
import { eq } from &quot;drizzle-orm&quot;;
import { usersTable } from &quot;./db/schema&quot;;

const db = drizzle({
  connection: {
    host: &quot;&lt;add in the endpoint from the console here&gt;&quot;,
    port: 5432,
    user: &quot;admin&quot;,
    password: &quot;&lt;add in the entire password string from the console here&gt;&quot;,
    database: &quot;postgres&quot;,
    ssl: true,
  },,
});

async function main() {
  const user: typeof usersTable.$inferInsert = {
    id: 1234,
    name: &quot;John&quot;,
    age: 30,
    email: &quot;john@example.com&quot;,
  };
  const user2: typeof usersTable.$inferInsert = {
    id: 5678,
    name: &quot;Tracy&quot;,
    age: 48,
    email: &quot;tracy@example.com&quot;,
  };

  await db.insert(usersTable).values([user, user2]);
  console.log(&quot;New user created!&quot;);

  const users = await db.select().from(usersTable);
  console.log(&quot;Getting all users from the database: &quot;, users);
  /*
  const users: {
    id: number;
    name: string;
    age: number;
    email: string;
  }[]
  */

  await db
    .update(usersTable)
    .set({
      age: 31,
    })
    .where(eq(usersTable.email, user.email));
  console.log(&quot;User info updated!&quot;);

  await db.delete(usersTable).where(eq(usersTable.email, user.email));
  console.log(&quot;User deleted!&quot;);
}

main();</code></pre>
<p>Similar to before, I&#39;m just dumping in the credentials here for the sake of the walkthrough, but the <code>connection</code> object will look just like what we configured previously in the <code>drizzle.config.ts</code> file.</p>
<p>Running <code>bun src/index.ts</code>, we can finally test our code against the DSQL cluster:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>$ bun src/index.ts
New user created!
Getting all users from the database:  [
  {
    id: 1234,
    name: &quot;John&quot;,
    age: 30,
    email: &quot;john@example.com&quot;,
  }, {
    id: 5678,
    name: &quot;Tracy&quot;,
    age: 48,
    email: &quot;tracy@example.com&quot;,
  }
]
User info updated!
User deleted!</code></pre>
<h2 id="conclusion">Conclusion</h2>
<p>This blog post was more &quot;show&quot; than &quot;tell&quot;. I have plenty more about Aurora DSQL that I need to dive into and understand, but hopefully this will give you some insight into how you can setup and connect to your own DSQL cluster.</p>
<p>Don&#39;t forget to tear down your cluster once you are done to avoid any unnecessary costs. As far as I can tell, costs are free during this preview period, but it&#39;s always good to be cautious.</p>
<h2 id="further-links-and-resources">Further links and resources</h2>
<ul>
<li><a href="https://orm.drizzle.team/docs/get-started/postgresql-new" target="_blank" rel="noopener noreferrer">Drizzle&#39;s getting started guide for Postgres</a></li>
<li><a href="https://aws.amazon.com/blogs/database/introducing-amazon-aurora-dsql/" target="_blank" rel="noopener noreferrer">Amazon announced the preview of Amazon Aurora DSQL</a></li>
<li><a href="https://orm.drizzle.team/docs/migrations" target="_blank" rel="noopener noreferrer">Drizzle migrations documentation</a></li>
<li><a href="https://docs.aws.amazon.com/aurora-dsql/latest/userguide/accessing-postgresql-endpoints.html" target="_blank" rel="noopener noreferrer">Aurora DSQL details about using the PostgreSQL protocol</a></li>
<li><a href="https://docs.aws.amazon.com/aurora-dsql/latest/userguide/what-is-aurora-dsql.html" target="_blank" rel="noopener noreferrer">Aurora DSQL user guide</a></li>
</ul>
<p><em>Photo credit: <a href="https://unsplash.com/@joelfilip" target="_blank" rel="noopener noreferrer"><code>joelfilip</code></a></em></p>]]></content>
<category term="database"/><category term="aws"/><category term="sql"/>
</entry>
<entry>
<title type="html"><![CDATA[Exploring Git Worktree]]></title>
<id>https://blog.dennisokeeffe.com/blog/2024-08-14-exploring-git-worktree</id>
<link href="https://dennisokeeffe.com/blog/2024-08-14-exploring-git-worktree"/>
<published>2024-08-14T00:00:00.000Z</published>
<updated>2024-08-14T00:00:00.000Z</updated>
<summary type="html"><![CDATA[An exploration on how Git Worktree makes stash redundant and saves you headaches across large changes.]]></summary>
<content type="html"><![CDATA[<p>Git worktree is a feature that allows you to have multiple working directories associated with a single Git repository. This means you can check out different branches or commits into separate directories on your filesystem, all linked to the same repository.</p>
<p>This demo walks through using Git worktree with an example using different npm dependencies to demonstrate a use case for multiple worktrees.</p>
<p>Our demonstration will emulate (albeit contrived and small) a &quot;major&quot; change within the repository. We&#39;re going to dumb it down to something contrived like migrating from <a href="https://lodash.com/" target="_blank" rel="noopener noreferrer">Lodash</a> to <a href="https://es-toolkit.slash.page/" target="_blank" rel="noopener noreferrer">es-toolkit</a>, but you&#39;ll need to use your imagination and pretend like our work may take something like a week or so to complete, and that you may need to ship other features while doing the migration.</p>
<p>It really doesn&#39;t matter which language you&#39;re working with, but I&#39;m using JavaScript to demonstrate here because you have likely found yourself jump between trees and needing to re-install different dependencies at some stage as you switch back-and-forth.</p>
<p>The process will look like this:</p>
<ol>
<li>Have a base worktree with Lodash.</li>
<li>Create a separate worktree for the migration.</li>
<li>Re-enact the process where we need to need to add a new feature on the base tree.</li>
<li>Continue on with the migration.</li>
</ol>
<h2 id="getting-started">Getting started</h2>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code># Configure git, initialise the project
$ mkdir demo-git-worktree
$ cd demo-git-worktree
$ npm init -y
$ npm install lodash
$ touch index.js README.md
$ git init
$ echo node_modules &gt; .gitignore</code></pre>
<p>In <code>index.js</code>, add the following to represent our large repository code:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const _ = require(&quot;lodash&quot;);

function main() {
  console.log(_.camelCase(&quot;Our crazy big home project&quot;));
}

main();</code></pre>
<p>You can now run this code to see what the project is doing:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="s"><code>$ node index.js
ourCrazyBigHomeProject</code></pre>
<p>Let&#39;s commit what&#39;s we&#39;ve got:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="s"><code># Add our commits
$ git add --all
$ git commit -m &quot;feat: first commit&quot;
[main (root-commit) 3fd5292] feat: first commit
 5 files changed, 86 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 README.md
 create mode 100644 index.js
 create mode 100644 package-lock.json
 create mode 100644 package.json</code></pre>
<p>Perfect! At this stage, you git log should show something like this:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="s"><code># Show git log
$ git log
commit 3fd5292b0a49ed8a5a6c4a5e2b9ee7394453e290 (HEAD -&gt; main)
Author: Dennis O&#39;Keeffe &lt;hello@dennisokeeffe.com&gt;
Date:   Wed Aug 14 08:20:12 2024 +1000

    feat: first commit</code></pre>
<p>Now let&#39;s emulate our project to migrate over.</p>
<h2 id="using-git-worktree">Using Git worktree</h2>
<p>We&#39;re creating a new worktree for this.</p>
<p>Run the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="s"><code># List our worktree
$ git worktree list
/path/to/okeeffed/demo-git-worktree  3fd5292 [main]

# Create a new branch for our migration
$ git branch feat/estoolkit-migration

# Create a new worktree
$ git worktree add ../demo-git-worktree-estoolkit-migration feat/estoolkit-migration
Preparing worktree (checking out &#39;feat/estoolkit-migration&#39;)
HEAD is now at 3fd5292 feat: first commit


# List our trees again
$ git worktree list
git worktree list
/path/to/okeeffed/demo-git-worktree                      3fd5292 [main]
/path/to/okeeffed/demo-git-worktree-estoolkit-migration  3fd5292 [feat/estoolkit-migration]</code></pre>
<p>Great! At this point, we now have another directory at <code>../demo-git-worktree-estoolkit-migration</code> which represents our estoolkit-migration worktree. It is checked out onto <code>feat/estoolkit-migration</code>.</p>
<p>Git operates differently between the main worktree and a linked worktree. It&#39;s best to reference the docs, but here are some quick pointers.</p>
<p>The main differences in created linked work trees are:</p>
<ol>
<li><strong>Separate working directories</strong>: Each worktree has its own working directory, allowing you to work on different branches simultaneously without switching.</li>
<li><strong>Shared repository</strong>: All worktrees share the same Git repository (objects and refs).</li>
<li><strong>Independent HEAD</strong>: Each worktree has its own HEAD, index, and config file.</li>
<li><strong>Branch isolation</strong>: You can&#39;t check out the same branch in multiple worktrees simultaneously (except for bare repositories).</li>
</ol>
<p>At this point, we can work separately between the two folders for our changes.</p>
<h2 id="making-our-updates-for-the-toolkit-migration">Making our updates for the toolkit migration</h2>
<p>Heading into <code>../demo-git-worktree-estoolkit-migration</code> and make the following changes:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="s"><code># Change into our linked worktree directory
$ cd ../demo-git-worktree-estoolkit-migration

# Add es-toolkit and remove Lodash
$ npm install es-toolkit

# Remove Lodash
$ npm uninstall lodash</code></pre>
<p>Now let&#39;s update <code>index.js</code>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const { camelCase } = require(&quot;es-toolkit&quot;);

function main() {
  console.log(camelCase(&quot;Our crazy big home project&quot;));
}

main();</code></pre>
<p>If we run <code>node index.js</code> again, we&#39;ll notice that nothing has changed:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="s"><code>$ node index.js
ourCrazyBigHomeProject</code></pre>
<p>Great! Let&#39;s commit.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="s"><code># Our commit
$ git add --all
$ git commit -m &quot;feat: migrated lodash camelCase&quot;</code></pre>
<h2 id="going-back-for-our-new-feature">Going back for our &quot;new feature&quot;</h2>
<p>Let&#39;s say now that we need to pivot midway through our migration for something else.</p>
<p>Back in the main worktree, let&#39;s go through a flow of adding another feature. Again, this will be contrived, but work with me.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="s"><code># Back to our main worktree
$ cd ../demo-git-worktree</code></pre>
<p>Even though Lodash is used in the repo, notice that if we run <code>index.js</code>, everything still works even though we remove that dependency in the linked worktree:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="s"><code># Running on our main worktree without re-installations
$ node index.js
ourCrazyBigHomeProject</code></pre>
<p>Great! Let&#39;s branch off, create a new feature and then merge back in.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="s"><code># Create a new branch
$ git branch feat/snakecase
$ git checkout feat/snakecase</code></pre>
<p>Let&#39;s update our <code>index.js</code> file:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const _ = require(&quot;lodash&quot;);

function main() {
  console.log(_.camelCase(&quot;Our crazy big home project&quot;));
  console.log(_.snakeCase(&quot;Our crazy big feature update&quot;));
}

main();</code></pre>
<p>Running this will log out the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="s"><code>$ node index.js
ourCrazyBigHomeProject
our_crazy_big_feature_update</code></pre>
<p>Let&#39;s commit and merge to main:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="s"><code># Commit
$ git add index.js
$ git commit -m &quot;feat: added snakecase&quot;
[feat/snakecase ab1e6d5] feat: added snakecase
 2 files changed, 173 insertions(+), 1 deletion(-)

# Head back to the target branch
$ git checkout main
# Merge into main
$ git merge feat/snakecase
Updating 3fd5292..37ed0e0
Fast-forward
 index.js  |   1 +
 2 files changed, 174 insertions(+), 1 deletion(-)</code></pre>
<blockquote>
<p>Normally in the work setting we would have our pull request made for the merge.</p>
</blockquote>
<p>At this point, our target main branch has also differed from our worktree. Let&#39;s fix that.</p>
<h2 id="rebasing-from-another-worktree">Rebasing from another worktree</h2>
<p>We can rebase from our worktree just like we normally would.</p>
<p>Let&#39;s head to our worktree and rebase:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="s"><code># Go back to the linked tree
$ cd ../demo-git-worktree-estoolkit-migration

# Run the rebase
$ git rebase main
Auto-merging index.js
CONFLICT (content): Merge conflict in index.js
error: could not apply 8325647... feat: migrated lodash camelCase
hint: Resolve all conflicts manually, mark them as resolved with
hint: &quot;git add/rm &lt;conflicted_files&gt;&quot;, then run &quot;git rebase --continue&quot;.
hint: You can instead skip this commit: run &quot;git rebase --skip&quot;.
hint: To abort and get back to the state before &quot;git rebase&quot;, run &quot;git rebase --abort&quot;.
hint: Disable this message with &quot;git config advice.mergeConflict false&quot;
Could not apply 8325647... feat: migrated lodash camelCase</code></pre>
<p>At this point, we have our conflict that we expected from the change.</p>
<p>Let&#39;s fix our conflict by using <code>snakeCase</code> from <code>es-toolkit</code> as well. Our code should look like this:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const { camelCase, snakeCase } = require(&quot;es-toolkit&quot;);

function main() {
  console.log(camelCase(&quot;Our crazy big home project&quot;));
  console.log(snakeCase(&quot;Our crazy big feature update&quot;));
}

main();</code></pre>
<p>Again, we can check it works without the need to re-install anything:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="s"><code># Running the changes
$ node index.js
ourCrazyBigHomeProject
our_crazy_big_feature_update</code></pre>
<p>Perfect! Let&#39;s finish the rebase by continuing.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="s"><code># Continue the rebase
$ git add index.js
$ git rebase --continue</code></pre>
<p>Let&#39;s bring this project to a close after merging a our linked tree code to our main tree.</p>
<h2 id="merging-back-to-main">Merging back to main</h2>
<p>Head back to the main tree and merge in the branch.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="s"><code># Make sure we commit from within our merge tree
$ cd ../demo-git-worktree
$ git merge feat/estoolkit-migration
Merge made by the &#39;ort&#39; strategy.
 index.js          |  6 +++---
 package-lock.json | 10 +++++-----
 package.json      |  2 +-
 3 files changed, 9 insertions(+), 9 deletions(-)</code></pre>
<blockquote>
<p>Again, we merged locally but you&#39;d normally have a pull request for this.</p>
</blockquote>
<p>At this point, you would notice that if you tried to run <code>index.js</code>, then finally we would run into an issue:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="s"><code># Demoing the issue
$ node index.js
node:internal/modules/cjs/loader:1147
  throw err;
  ^

Error: Cannot find module &#39;es-toolkit&#39;
Require stack:
- /path/to/okeeffed/okeeffed/demo-git-worktree/index.js
    at Module._resolveFilename (node:internal/modules/cjs/loader:1144:15)
    at Module._load (node:internal/modules/cjs/loader:985:27)
    at Module.require (node:internal/modules/cjs/loader:1235:19)
    at require (node:internal/modules/helpers:176:18)
    at Object.&lt;anonymous&gt; (/path/to/okeeffed/okeeffed/demo-git-worktree/index.js:1:34)
    at Module._compile (node:internal/modules/cjs/loader:1376:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1435:10)
    at Module.load (node:internal/modules/cjs/loader:1207:32)
    at Module._load (node:internal/modules/cjs/loader:1023:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:135:12) {
  code: &#39;MODULE_NOT_FOUND&#39;,
  requireStack: [ &#39;/path/to/okeeffed/demo-git-worktree/index.js&#39; ]
}

Node.js v20.10.0</code></pre>
<p>Naturally, both of our worktrees maintained a different <code>node_modules</code> folder. That&#39;s how we were able to jump between both seamlessly.</p>
<p>All we need to do is update our main worktree to install the latest <code>package.json</code> configuration.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="s"><code># Install current configuration
$ npm install

added 1 package, removed 1 package, and audited 2 packages in 1s

found 0 vulnerabilities

# Attempt run again
$ node index.js
ourCrazyBigHomeProject
our_crazy_big_feature_update</code></pre>
<p>Happy days! All we have left to do is cleanup.</p>
<h2 id="cleaning-up-our-linked-tree">Cleaning up our linked tree</h2>
<p>We can do so with the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="s"><code># Remove the worktree
$ git worktree remove ../demo-git-worktree-estoolkit-migration
$ git worktree list
/Users/dennis.okeeffe/code/okeeffed/demo-git-worktree  8a9f3a1 [main]</code></pre>
<p>At this point, our linked tree has been cleaned up.</p>
<p>If you run <code>git branch --list</code>, you&#39;ll notice that although the worktree is gone, our branch remains.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="s"><code># List our branches
$ git branch --list
  feat/estoolkit-migration
  feat/snakecase
* main</code></pre>
<p>Given the rules of worktrees, now that our other worktree is gone, we&#39;re free to checkout <code>feat/estoolkit-migration</code> from our main worktree now.</p>
<h2 id="further-links-and-resources">Further links and resources</h2>
<ul>
<li><a href="https://git-scm.com/docs/git-worktree" target="_blank" rel="noopener noreferrer">Git worktree documentation</a></li>
<li><a href="https://www.gitkraken.com/learn/git/git-worktree" target="_blank" rel="noopener noreferrer">GitKraken | worktree</a></li>
</ul>
<p><em>Photo credit: <a href="https://unsplash.com/@roadtripwithraj" target="_blank" rel="noopener noreferrer"><code>roadtripwithraj</code></a></em></p>]]></content>
<category term="git"/>
</entry>
<entry>
<title type="html"><![CDATA[Expressing Your Thoughts in Software]]></title>
<id>https://blog.dennisokeeffe.com/blog/2024-07-31-expressing-your-thoughts-in-software</id>
<link href="https://dennisokeeffe.com/blog/2024-07-31-expressing-your-thoughts-in-software"/>
<published>2024-07-31T00:00:00.000Z</published>
<updated>2024-07-31T00:00:00.000Z</updated>
<summary type="html"><![CDATA[Learn to express your thoughts and opinions better with this guide.]]></summary>
<content type="html"><![CDATA[<p>Do you consider expressing your ideas in software a difficult concept? You are not alone. Expression is hard, but sprinkled in with the many layers of software development, it can be even more challenging.</p>
<p>I personally waiver back and forward between how confident I feel about my ability to express my thoughts. It&#39;s something that I&#39;ve been thinking a lot about more recently as some long-term projects come to a close at my company. Today&#39;s post aims to share some of thoughts.</p>
<p>We&#39;ll cover why I believe expression to be important, why it feels so hard a software engineer and a strategy that I&#39;ve come up with to help me improve.</p>
<h2 id="the-importance-of-expression">The importance of expression</h2>
<p>How often in your daily role in software have you come across a situation where you want to make your opinion known but felt unable to find the words to do so? Worry not. You&#39;re not alone.</p>
<p>The ability to either express yourself well or not to have a wide-ranging set of implications.</p>
<p>Some of the positives:</p>
<ul>
<li>Feeling heard.</li>
<li>Constructive conversations and feedback.</li>
<li>An exponential speedup in learning.</li>
</ul>
<p>In contrast, the <em>inability</em> to express yourself does not leave one in a neutral state. I find it likely to have the opposite effect:</p>
<ul>
<li>Feeling like you have no say.</li>
<li>Internal conflict.</li>
<li>A undesirable state of dogmatism.</li>
</ul>
<p>This could be the point to slam you, <em>my revered reader</em>, with some statistics to back up my claims. However, given that <em>I&#39;ve never actually looked into this</em>, we&#39;re going off a data point of one: me.</p>
<p>And for me, <em>our honorable data point</em>, I find how well I express myself at any given point over my career <strong>directly impacts employee happiness and satisfaction</strong>.</p>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>Of course I also have colleagues and friends that I have spoken to about this,
but it feels remiss to refer to my experiences with colleagues as
all-encompassing. As far as I know, every workplace in Australia is an echo
chamber and not representative of how my readers experience.</p> </div> </aside>
<p><em>My expression couldn&#39;t lead to constructive conversation?</em> I feel my opinion is not valued and I find myself devaluing theirs. <em>I couldn&#39;t adequately disagree with our approach?</em> It festers inside and it can lead to self-imposed stress or irritation. <em>My approach was not understood well and dismissed?</em> Sometimes I could lean harder into my idea without taking a step back to think more holistically about alternatives.</p>
<p>Why is it that we find ourselves going through these motions? It&#39;s human nature. Not everyone will have the same responses I feel, but I believe the majority of us to share some common cornerstones:</p>
<ol>
<li>We are social beings.</li>
<li>We value achievement.</li>
<li>We value contribution.</li>
</ol>
<p>These three points are a natural extension of communicating effectively. If you ever end up in a situation where you fundamentally disagree with a concept or approach, express it well and followup with an engaging, constructive conversation, the outcome is likely to be positive irrespective of the approach taken. If you&#39;ve expressed your idea clearly followed up by a well-thought discussion, you are likely to come to an understanding of the other party&#39;s perspective while still having the satisfaction of feeling you have made your contribution towards the final result.</p>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>When I say, &quot;followup with an engaging, constructive conversation&quot;, it is
important to state the obvious: it takes two to tango. You need multiple
parties will to come to the table with the mindset of engaging in constructive
conversation. That&#39;s a discussion for another post.</p> </div> </aside>
<p>All of this is a key part of evaluating our own self-worth. But why does it feel so hard for us to do as software engineers?</p>
<h2 id="why-is-expression-hard-for-software-engineers">Why is expression hard for software engineers?</h2>
<p>The obvious answer to my last dramatic question: it is not hard for all software engineers. The key to great communication is the adage of <strong>practice makes perfect</strong>.</p>
<p>Software engineers who are following their career path often end up in a situation where they often choose between a line towards management/technical lead, or a line towards honing their expertise as an individual contributor.</p>
<p>In my opinion, the former <strong>requires</strong> you to express yourself more often. The nature of the role is to engage with others and make decisions. The latter, however, naturally tends towards more hours working diligently and less time expressing yourself to others.</p>
<p>Working as an individual contributor in software can have some fundamentally unhealthy side effects:</p>
<ol>
<li>We spend a lot time at the desk.</li>
<li>We spend a lot of time in our heads.</li>
</ol>
<p>Unless we find ways to counter this, you&#39;ll may come to find (as our have) that if you are not &quot;practicing&quot; good communication, then it becomes a use-it-or-lose-it situation.</p>
<p>So how do we counter this? Without going into a specific approach just yet, I want to share some of the ways that I feel great individual contributors keep up their communication skills: the teach and/or share often.</p>
<p>The method of teaching is also important. For any Duolingo fanatics out there, you may have found yourself surprised to learn that <em>even after 1000 consecutive days of learning Spanish</em>, you still can&#39;t speak it. You might be able to read and write incredibly well if you were diligent, but speaking is another dimension to it.</p>
<p>What I&#39;m trying to illustrate here is that although outlets like writing documentation and verbally shouting opinion pieces (like this one) into the void are in themselves very constructive for certain forms of communication, they are not going to result in the same level of improvement for your capability to convey opinions with speech.</p>
<p>This is a fundamental theme behind this post: it&#39;s not that as individuals we don&#39;t have opinions or thoughts, it&#39;s that <strong>we fall out of practice with expressing them</strong>.</p>
<p>If you&#39;ve ever watched developers on YouTube or on stream that you feel do an incredibly job of adhoc-explaining their thoughts or processes off-script, then you&#39;ll be pleased to know that is a result of practice.</p>
<p>Some things that great individual contributors do that helps them become better at expressing themselves:</p>
<ol>
<li>Pair programming.</li>
<li>Streaming.</li>
<li>Public speaking.</li>
<li>Video content.</li>
</ol>
<p>If the chasm towards this still feels untenable or too far a chasm to cross, then even begin to express your thoughts to yourself in the mirror. Refine your thoughts and opinions. Practice makes perfect.</p>
<h2 id="an-approach-to-clear-expression">An approach to clear expression</h2>
<p>The approach I have working on recent is what I call the <strong>CLEAR framework</strong> (<strong>ba dum tss</strong>). It&#39;s a simple, easily remember acronym that touches on the key points:</p>
<ul>
<li><strong>C</strong>: Clarity</li>
<li><strong>L</strong>: Lens</li>
<li><strong>E</strong>: Ease</li>
<li><strong>A</strong>: Align</li>
<li><strong>R</strong>: Rehearse</li>
</ul>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>The power of analogies, mnemonics and metaphors is not to be underestimated.
That in itself requires a blog post of it&#39;s own as it can go a long way to
containing complexity in your ideas and making them more memorable.</p> </div> </aside>
<h3 id="how-to-visualize-the-clear-framework">How to visualize the CLEAR framework</h3>
<p>Imagine you have a glass prism. <strong>Light enters the prism</strong> and is refracted into a <strong>spectrum of colors</strong>. For a more vivid version of this, think of <strong>The Great Pyramids of Gaza</strong> as our <strong>glass prism</strong>, with the <strong>sunlight</strong> coming through being refracted out the other side of the pyramid as a <strong>gigantic rainbow</strong>.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2024-07-31-expression-your-thoughts-in-software/light-prism.webp" alt="Our light prism, as seen though the lens of an AI prompt." loading="lazy" decoding="async"/><figcaption>Our light prism, as seen though the lens of an AI prompt.</figcaption></figure>
<ul>
<li>The <strong>light coming into the prism</strong> as the <strong>input we want to comment on</strong>.</li>
<li>The <strong>glass prism</strong> itself is representational of <strong>our opinion forming process</strong>.</li>
<li>The <strong>spectrum of colors</strong> represents our <strong>expression of that opinion</strong>.</li>
</ul>
<p>The glass prism is where our <strong>CLEAR</strong> process comes in. It guides our approach to taking that input, making sense of where we stand on it and how we want to convey it.</p>
<p>Let&#39;s walk through each step.</p>
<h3 id="clarity">Clarity</h3>
<p>Seek clarify on your opinion by asking yourself questions. If open questions are too broad to answer, try to ask yourself closed questions (yes/no). This will help you to refine your stance on the matter.</p>
<h3 id="lens">Lens</h3>
<p>You can think about this point in a number of ways:</p>
<ol>
<li>What is the lens that <strong>you are looking through</strong>?</li>
<li>What is the lens that <strong>your audience is looking through</strong>?</li>
<li>What is a &quot;lens&quot; that <strong>you could use to simplify what you want to say</strong>?</li>
</ol>
<h3 id="ease">Ease</h3>
<p>This in a natural extension of the &quot;<strong>lens</strong>&quot; property. Simplify the lens that you and others can look through.</p>
<p>I think of this point as being <strong>the most integral</strong>. For difficult concepts, <strong>close that chasm</strong> that others need to cross in order to <strong>understand your thought process</strong>.</p>
<p>In regards to technical terms, concepts like <strong>Domain Name Server</strong> (<strong>DNS</strong>) can be simplified to &quot;the phone book of the internet&quot;. Examples like this can go a long way to making your opinion more accessible.</p>
<p>This is also true for <strong>non-technical terms</strong>. If you wish to convey why you dislike certain processes or approaches around project management or team dynamics, seek a way to simplify your opinion to a more relatable viewpoint.</p>
<h3 id="align">Align</h3>
<p>For this point, you can ask yourself these questions:</p>
<ol>
<li>How will your opinion align with the <strong>desired outcome</strong>?</li>
<li>How does it align with <strong>your values</strong> or <strong>the values or the greater team</strong>?</li>
</ol>
<p>Keeping the end in mind for your opinion will guide how you speak.</p>
<h3 id="rehearse">Rehearse</h3>
<p>This point speaks to the practice that we mentioned. Of course, this is not always possible in the moment, but you&#39;ll often find that the values that drive your opinion are often the same values that drive your current state as a software engineer.</p>
<p>If you have common gripes or event some post-trauma about something that went wrong, then you likely already have strong opinions on those things. Rehearse those opinions. Refine them. Make them more accessible. That preparation will help you in the moment.</p>
<h3 id="some-final-points-on-clear">Some final points on CLEAR</h3>
<p>Here is the reality of forming an opinion: you won&#39;t always nail the delivery.</p>
<p>Don&#39;t sweat it. Even the best communicators have moments where their delivery doesn&#39;t nail the landing.</p>
<p>It&#39;s all a process. The more you practice, the better your average delivery will become.</p>
<p>Finally, if CLEAR doesn&#39;t work for you or the visual I provided doesn&#39;t resonate, then this is a great chance to put this into practice for yourself. Make your own approach that helps you to express your thoughts better.</p>
<h2 id="conclusion">Conclusion</h2>
<p>In conclusion, <strong>expressing ideas in software development</strong> can indeed be challenging, but it&#39;s an <strong>essential skill</strong> that significantly impacts both personal satisfaction and professional growth. As software engineers, the balance between technical expertise and effective communication is crucial. By recognizing the importance of expression and actively working to improve it, we can enhance our contributions, foster constructive conversations, and ultimately achieve better outcomes in our projects.</p>
<p>The <strong>CLEAR framework</strong> provides a structured approach to refining our communication skills. By focusing on <strong>CLEAR</strong>, we can systematically enhance how we articulate our thoughts and opinions. Remember, <em>practice is key</em>. Whether through pair programming, public speaking, or simply rehearsing your ideas, the more you practice, the more proficient you will become at expressing yourself.</p>
<p>Effective communication is not just about being heard but about creating a dialogue that leads to understanding and collaboration. By continuously working on our ability to express our ideas, we not only improve our own satisfaction but also contribute to a more inclusive and innovative working environment. So, take the time to practice, refine your approach, and don&#39;t be afraid to express your thoughts. <em>It&#39;s a journey worth taking.</em></p>
<p><strong>Disclaimer</strong>: This blog post used AI to generate the images used for the analogy. You may have figured that out when you saw the light not entering the prism as a straight line but shining at the tip. I left it in because it felt cool.</p>
<p><em>Photo credit: <a href="https://unsplash.com/@acharki95" target="_blank" rel="noopener noreferrer"><code>acharki95</code></a></em></p>]]></content>
<category term="soft-skills"/>
</entry>
<entry>
<title type="html"><![CDATA[Resource Management in TypeScript]]></title>
<id>https://blog.dennisokeeffe.com/blog/2024-07-15-resource-management-in-typescript</id>
<link href="https://dennisokeeffe.com/blog/2024-07-15-resource-management-in-typescript"/>
<published>2024-07-15T00:00:00.000Z</published>
<updated>2024-07-15T00:00:00.000Z</updated>
<summary type="html"><![CDATA[We look into implementing the Acquire and Release Pattern in TypeScript for creating a sequence of operations with compensating actions on failure.]]></summary>
<content type="html"><![CDATA[<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p><strong>Update July 15th 2024:</strong> Based on some changes that I made to
the <code>Result</code> type in a previous post, I&#39;ve implemented changes here as well.
The updated version will be it&#39;s own section at the.</p> </div> </aside>
<h2 id="overview">Overview</h2>
<p>In a previous post, we looked into how we could implement a <strong>Result</strong> type in TypeScript. This type was designed to represent either a <strong>success</strong> or a <strong>failure</strong> in a type-safe manner.</p>
<p>Building on top of that, we are next going to look at how we could use that <strong>Result</strong> type to implement the <strong>Acquire and Release Pattern</strong> in TypeScript.</p>
<p>Often, you may need to <strong>execute a series of linked operations</strong> where the <strong>success of each step</strong> relies on the <strong>success of the previous one</strong>. If <strong>any step fails</strong>, you will want to <strong>undo the effects of all the preceding successful steps</strong>. The <strong>Acquire and Release Pattern</strong> is a pattern that we can use to handle this situation.</p>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p>This approach is heavily inspired by <a href="https://effect.website/docs/guides/resource-management/patterns/sequence-of-operations-with-compensating-actions-on-failure" target="_blank" rel="noopener noreferrer">the approach that <strong>EffectTS</strong> takes for their approach to resource management</a>.</p> </div> </aside>
<h2 id="implementing-the-acquire-and-release-pattern">Implementing the Acquire and Release Pattern</h2>
<p>We can implement a version of the acquire and release pattern with the following helper function:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import { Result, ErrorType } from &quot;./alt-result&quot;;

type AcquireRelease&lt;A, E extends ErrorType, R = void&gt; = {
  resource: A;
  release: () =&gt; Promise&lt;Result&lt;R, E&gt;&gt;;
};

export class AcquireReleaseError {
  readonly _tag = &quot;AcquireReleaseError&quot;;
  readonly error: Error;

  constructor(error: Error) {
    this.error = error;
  }
}

/**
 * Acquires a resource and provides a function to release it.
 *
 * @template T The type of the resource to be acquired.
 * @template E The type of error that might occur during acquire or release (default: Error).
 * @template R The type of the result returned by the release function (default: unknown).
 *
 * @param acquire A function that returns a Promise resolving to a Result containing the resource or an error.
 * @param release A function that takes the acquired resource and returns a Promise resolving to a Result of the release operation.
 *
 * @returns A Promise resolving to a Result containing either an AcquireRelease object or an error.
 *
 * @remarks
 * The R type parameter represents the result type of the release operation.
 * It&#39;s set to &#39;unknown&#39; by default, allowing the release operation to potentially return any type.
 * This can be useful if the release operation needs to return some specific information or status.
 *
 * Rejected promises are treated as defects and not handled in this function.
 * They are thrown as exceptions.
 */
export async function acquireRelease&lt;T, E extends ErrorType, R = unknown&gt;(
  acquire: () =&gt; Promise&lt;Result&lt;T, E&gt;&gt;,
  release: (resource: T) =&gt; Promise&lt;Result&lt;R, E&gt;&gt;
): Promise&lt;Result&lt;AcquireRelease&lt;T, E, R&gt;, E | AcquireReleaseError&gt;&gt; {
  try {
    const acquireResult = await acquire();

    if (acquireResult.isFailure()) {
      return Result.fail(acquireResult.error);
    }

    const resource = acquireResult.data;

    return Result.succeed({
      resource,
      release: () =&gt; release(resource),
    });
  } catch (err) {
    return Result.fail(
      new AcquireReleaseError(
        err instanceof Error ? err : new Error(String(err))
      )
    );
  }
}</code></pre>
<p>This function first attempts to &quot;acquire&quot; a resource by calling the <code>acquire</code> function.</p>
<p>If the <strong>acquisition is successful</strong>, it then <strong>returns an object containing the acquired resource and a <code>release</code> function</strong> that can be used to release the resource.</p>
<p>In the situation where the <strong>acquisition fails</strong>, the function <strong>returns the error</strong> that occurred during the acquisition.</p>
<p>Both <strong>success</strong> and <strong>failure</strong> situations makes use of <a href="https://dennisokeeffe.com/blog/2024-07-14-creating-a-result-type-in-typescript">our previously implement Result type</a>.</p>
<h2 id="surfacing-what-we-need-to-rollback">Surfacing what we need to rollback</h2>
<p>In the current approach I&#39;ve taken, it returns the helper function to rollback, but it doesn&#39;t explicitly define how we rollback.</p>
<p>I&#39;ve inverted that control to the caller to define how they want to rollback. We can see what I mean by looking at the following example that aims to create a few AWS resources and then gives us a way to rollback if any of them fail:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import { acquireRelease } from &quot;./alt-acq&quot;;
import { ErrorType, Failure, Result } from &quot;./alt-result&quot;;

// ... assume we have imported all the helper create and delete functions used below, as well as the custom errors

// Step 1: Create user record
const recordCreateResult = await acquireRelease(
  () =&gt; createUserRecord(userData),
  async (user) =&gt; {
    console.log(`Releasing user record`);
    return await deleteUserRecord(user.userId);
  }
);
if (recordCreateResult.isFailure()) {
  return Result.fail(new RecordCreateError(recordCreateResult));
}

const { resource: user, release: releaseUser } = recordCreateResult.data;
console.log(&quot;User record created:&quot;, user);

// Step 2: Create S3 bucket
const s3BucketCreateResult = await acquireRelease(
  () =&gt; createS3Bucket(`user-${user.userId}-bucket`),
  (bucket) =&gt; {
    console.log(`Releasing S3 bucket`);
    return deleteS3Bucket(bucket.bucketName);
  }
);
if (s3BucketCreateResult.isFailure()) {
  return Result.fail(new S3Error(s3BucketCreateResult, [releaseUser]));
}

const { resource: bucket, release: releaseBucket } = s3BucketCreateResult.data;
console.log(&quot;S3 bucket created:&quot;, bucket);

// Step 3: Upload record to S3
const uploadToS3Result = await acquireRelease(
  () =&gt;
    uploadRecordToS3(bucket.bucketName, {
      userId: user.userId,
      data: userData,
    }),
  (record) =&gt; {
    console.log(`Releasing S3 record due`);
    return deleteRecordFromS3(bucket.bucketName, record.recordId);
  }
);
if (uploadToS3Result.isFailure()) {
  return Result.fail(
    new S3Error(uploadToS3Result, [releaseBucket, releaseUser])
  );
}

const { resource: record, release: rollbackUpload } = uploadToS3Result.data;
console.log(&quot;Record uploaded to S3:&quot;, record);

// Step 4: Send SQS message
const sendSQSMessageResult = await acquireRelease(
  () =&gt; sendSQSMessage({ userId: user.userId, recordId: record.recordId }),
  () =&gt; Promise.resolve(Result.succeed(undefined)) // No specific cleanup for SQS
);
if (sendSQSMessageResult.isFailure()) {
  return Result.fail(
    new SQSUploadError(sendSQSMessageResult, [
      rollbackUpload,
      releaseBucket,
      releaseUser,
    ])
  );
}
const sqsResult = sendSQSMessageResult.data;

console.log(&quot;SQS message sent successfully:&quot;, sqsResult);

console.log(&quot;Process completed successfully&quot;);
return Result.succeed(undefined);</code></pre>
<p>In the code I have above, I am relying on the <code>result</code> returned from the <code>acquireRelease</code> function to determine whether the operation was successful or not. If it was not, I am returning a <code>Result.fail</code> with the error and the rollback functions that I want to execute.</p>
<p>The alternative is that you could rollback directly within that block, but in the case that you have that logic encapsulated in a function, you can against surface things to the top-level to handle the rollback.</p>
<aside class="callout callout--warning" aria-label="Warning"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><path d="m21.7 18-8-14a2 2 0 0 0-3.4 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.7-3ZM12 9v4M12 17h.01" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>  <div class="callout__body">  <p>I am still thinking about this approach and whether it is the best way to go
about it. You should consider whether this is the best approach for your
use-case.</p> </div> </aside>
<p>An example of surfacing the rollback logic to the top-level might look like this (assume the above code is in a function called <code>processUserData</code>):</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import { S3Error, SQSUploadError, processUserData } from &quot;./alt-main&quot;;
import { ErrorType, Result } from &quot;./alt-result&quot;;

const unexpectedFailure = () =&gt; {
  throw new Error(&quot;Unexpected error&quot;);
};

// Mock functions (replace these with your actual implementations)
const createUserRecord = (userData: { name: string; email: string }) =&gt;
  Promise.resolve(Result.succeed({ userId: &quot;user123&quot;, ...userData }));

const createS3Bucket = (bucketName: string) =&gt;
  Promise.resolve(Result.succeed({ bucketName }));

const uploadRecordToS3 = (bucketName: string, data: any) =&gt;
  Promise.resolve(Result.succeed({ recordId: &quot;record123&quot; }));

const sendSQSMessage = (message: any) =&gt; unexpectedFailure();

const deleteUserRecord = (
  userId: string
): Promise&lt;Result&lt;{ message: string }, never&gt;&gt; =&gt;
  new Promise((resolve) =&gt; {
    setTimeout(() =&gt; {
      console.log(&quot;Deleted user record&quot;);
      resolve(
        Result.succeed({
          message: &quot;User record deleted&quot;,
        })
      );
    }, 1000);
  });

const deleteS3Bucket = (
  bucketName: string
): Promise&lt;Result&lt;{ message: string }, never&gt;&gt; =&gt;
  new Promise((resolve) =&gt; {
    setTimeout(() =&gt; {
      console.log(&quot;Deleted S3 bucket&quot;);
      resolve(
        Result.succeed({
          message: &quot;S3 bucket deleted&quot;,
        })
      );
    }, 1000);
  });

const deleteRecordFromS3 = (
  bucketName: string,
  recordId: string
): Promise&lt;Result&lt;{ message: string }, never&gt;&gt; =&gt;
  new Promise((resolve) =&gt; {
    setTimeout(() =&gt; {
      console.log(&quot;Deleted S3 record&quot;);
      resolve(
        Result.succeed({
          message: &quot;S3 record deleted&quot;,
        })
      );
    }, 1000);
  });

async function main() {
  // Run the process
  const result = await processUserData(
    { name: &quot;John Doe&quot;, email: &quot;john@example.com&quot; },
    createUserRecord,
    createS3Bucket,
    uploadRecordToS3,
    sendSQSMessage,
    deleteRecordFromS3,
    deleteS3Bucket,
    deleteUserRecord
  );

  if (result.isFailure()) {
    await result.error.rollback();
  }
}

main();</code></pre>
<p>In the above code, the functions that we inject will all succeed except for the <code>sendSQSMessage</code> function. When this function fails, we will surface the error to the top-level and then call the <code>rollback</code> function on the error to rollback the operations.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="s"><code>$ node example.ts
# Successes start here
User record created: { userId: &#39;user123&#39;, name: &#39;John Doe&#39;, email: &#39;john@example.com&#39; }
S3 bucket created: { bucketName: &#39;user-user123-bucket&#39; }
Record uploaded to S3: { recordId: &#39;record123&#39; }
# Oopsy! We failed, so the rollback has started
Releasing S3 record due
Releasing S3 bucket
Releasing user record</code></pre>
<p>As you can see, the rollback logic is executed in reverse order of the operations that were successful (thanks to how we defined them).</p>
<p>If we updated out <code>uploadRecordToS3</code> error to also fail, we can see how our running our code changes:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>// Before
const uploadRecordToS3 = (bucketName: string, data: any) =&gt;
  Promise.resolve(Result.succeed({ recordId: &quot;record123&quot; }));

// After
const uploadRecordToS3 = (bucketName: string, data: any) =&gt; unexpectedFailure();</code></pre>
<p>Now running our code:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="s"><code>$ node example.ts
# Successes start here
User record created: { userId: &#39;user123&#39;, name: &#39;John Doe&#39;, email: &#39;john@example.com&#39; }
S3 bucket created: { bucketName: &#39;user-user123-bucket&#39; }
# Oopsy! We failed, so the rollback has started
Releasing S3 bucket
Releasing user record</code></pre>
<p>In the above, you&#39;ll noticed we&#39;ve only rolled back the S3 bucket and user record.</p>
<h2 id="matching-on-the-error-type">Matching on the error type</h2>
<p>If we want <strong>even more control</strong> around the rollback at the top-level, we could even make use of our custom errors and their tags.</p>
<p>Here is a list of what our custom errors from the code could look like:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>export abstract class RollbackableError&lt;E extends ErrorType&gt; {
  readonly _tag: string;
  readonly reason: Failure&lt;E&gt;;
  private rollbackFns: Array&lt;() =&gt; Promise&lt;Result&lt;unknown, E&gt;&gt;&gt;;

  constructor(
    tag: string,
    reason: Failure&lt;E&gt;,
    rollbackFns: Array&lt;() =&gt; Promise&lt;Result&lt;unknown, E&gt;&gt;&gt; = []
  ) {
    this._tag = tag;
    this.reason = reason;
    this.rollbackFns = rollbackFns;
  }

  async rollback() {
    for (const rollbackFn of this.rollbackFns) {
      await rollbackFn();
    }
  }
}

export class RecordCreateError&lt;
  E extends ErrorType
&gt; extends RollbackableError&lt;E&gt; {
  constructor(
    reason: Failure&lt;E&gt;,
    rollbackFns: Array&lt;() =&gt; Promise&lt;Result&lt;unknown, E&gt;&gt;&gt; = []
  ) {
    super(&quot;RecordCreateError&quot;, reason, rollbackFns);
  }
}

export class S3Error&lt;E extends ErrorType&gt; extends RollbackableError&lt;E&gt; {
  constructor(
    reason: Failure&lt;E&gt;,
    rollbackFns: Array&lt;() =&gt; Promise&lt;Result&lt;unknown, E&gt;&gt;&gt; = []
  ) {
    super(&quot;S3Error&quot;, reason, rollbackFns);
  }
}

export class SQSUploadError&lt;E extends ErrorType&gt; extends RollbackableError&lt;E&gt; {
  constructor(
    reason: Failure&lt;E&gt;,
    rollbackFns: Array&lt;() =&gt; Promise&lt;Result&lt;unknown, E&gt;&gt;&gt; = []
  ) {
    super(&quot;SQSUploadError&quot;, reason, rollbackFns);
  }
}</code></pre>
<p>In the above example, I have an <a href="https://www.tutorialsteacher.com/typescript/abstract-class" target="_blank" rel="noopener noreferrer"><strong>abstract class</strong></a> that could be used to define a custom error that has a <code>rollback</code> function. This function will execute all the rollback functions that were passed to the error.</p>
<p>Because of how the <code>_tag</code> is assigned, we could then use the <code>matchTag</code> logic that we defined in the previous post to determine what kind of error we are dealing with and then execute logic based on what went wrong.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>// ... omit code from before

async function main() {
  // Run the process
  const result = await processUserData(
    { name: &quot;John Doe&quot;, email: &quot;john@example.com&quot; },
    createUserRecord,
    createS3Bucket,
    uploadRecordToS3,
    sendSQSMessage,
    deleteRecordFromS3,
    deleteS3Bucket,
    deleteUserRecord
  );

  await Result.matchTag(result, {
    Success: () =&gt; console.log(&quot;Success&quot;),
    S3Error: (error: S3Error&lt;ErrorType&gt;) =&gt; console.error(&quot;S3Error&quot;, error),
    RecordCreateError: (error: RecordCreateError&lt;ErrorType&gt;) =&gt;
      console.error(&quot;RecordCreateError&quot;, error),
    SQSUploadError: async (error: SQSUploadError&lt;ErrorType&gt;) =&gt; {
      console.error(&quot;SQSUploadError&quot;, error);

      await error.rollback();
    },
  });
}

main();</code></pre>
<p>In the above logic, we are using the <code>matchTag</code> function to determine what kind of error we are dealing with. If it is an <code>SQSUploadError</code>, we are then calling the <code>rollback</code> function on the error to rollback the operations.</p>
<span id="updated-approach-july-15th-2024" class="anchor-alias"><h2 id="updated-approach-july-15-th-2024">Updated approach (July 15th 2024)</h2>
<p>After reviewing some types, I&#39;ve simplified the <code>Result</code> type from the <a href="https://dennisokeeffe.com/blog/2024-07-14-creating-a-result-type-in-typescript#updated-approach-july-15th-2024">previous post</a> and made changes to the <code>acquireRelease</code> function to make it a little more type smart:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import { Result } from &quot;./alt-result&quot;;

type InferResultTypes&lt;T&gt; = T extends Result&lt;infer A, infer E&gt; ? [A, E] : never;

type AcquireRelease&lt;A, E, R = unknown&gt; = {
  resource: A;
  release: () =&gt; Promise&lt;Result&lt;R, E&gt;&gt;;
};

/**
 * Acquires a resource and provides a function to release it.
 *
 * @template T The type of the resource to be acquired.
 * @template E The type of error that might occur during acquire or release (default: Error).
 * @template R The type of the result returned by the release function (default: unknown).
 *
 * @param acquire A function that returns a Promise resolving to a Result containing the resource or an error.
 * @param release A function that takes the acquired resource and returns a Promise resolving to a Result of the release operation.
 *
 * @returns A Promise resolving to a Result containing either an AcquireRelease object or an error.
 *
 * @remarks
 * The R type parameter represents the result type of the release operation.
 * It&#39;s set to &#39;unknown&#39; by default, allowing the release operation to potentially return any type.
 * This can be useful if the release operation needs to return some specific information or status.
 *
 * Rejected promises are treated as defects and not handled in this function.
 * They are thrown as exceptions.
 */
export async function acquireRelease&lt;
  AcquireFn extends () =&gt; Promise&lt;Result&lt;any, any&gt;&gt;,
  ReleaseFn extends (
    resource: InferResultTypes&lt;Awaited&lt;ReturnType&lt;AcquireFn&gt;&gt;&gt;[0]
  ) =&gt; Promise&lt;Result&lt;any, any&gt;&gt;
&gt;(
  acquire: AcquireFn,
  release: ReleaseFn
): Promise&lt;
  Result&lt;
    AcquireRelease&lt;
      InferResultTypes&lt;Awaited&lt;ReturnType&lt;AcquireFn&gt;&gt;&gt;[0],
      InferResultTypes&lt;Awaited&lt;ReturnType&lt;AcquireFn&gt;&gt;&gt;[1],
      InferResultTypes&lt;Awaited&lt;ReturnType&lt;ReleaseFn&gt;&gt;&gt;[0]
    &gt;,
    InferResultTypes&lt;Awaited&lt;ReturnType&lt;AcquireFn&gt;&gt;&gt;[1]
  &gt;
&gt; {
  try {
    const acquireResult = await acquire();

    if (acquireResult.isFailure()) {
      return acquireResult;
    }

    const resource = acquireResult.data;

    return Result.succeed({
      resource,
      release: () =&gt; release(resource),
    });
  } catch (error) {
    return Result.fail(error);
  }
}</code></pre>
<p>In this version:</p>
<ul>
<li>I&#39;ve added a <code>InferResultTypes</code> type that will infer the types of the <code>Result</code> type.</li>
<li>I&#39;ve updated the <code>AcquireRelease</code> type to be more type smart.</li>
<li>I&#39;ve updated the <code>acquireRelease</code> function to be more type smart.</li>
</ul>
<p>These are still experimental and being battle tested, but I will post any updated changes as things happen.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Today&#39;s post looked at how we could implement the <strong>Acquire and Release Pattern</strong> in TypeScript in order to handle a sequence of operations and roll them back if need be.</p>
<p>We also looked at how our pattern can surface the control logic to the top-level which can be powerful for decision making.</p>
<p>In your own work, you could also extend this to optionally roll everything back in parallel or in a specific order. You could also extend this to handle more complex scenarios where you might want to roll back a specific operation and then retry it, or even retry the failing operation a number of times before opting to raise the error and rollback.</p>
<h2 id="resources-and-further-reading">Resources and further reading</h2>
<ul>
<li><a href="https://effect.website/docs/guides/resource-management/patterns/sequence-of-operations-with-compensating-actions-on-failure" target="_blank" rel="noopener noreferrer">EffectTS: Resource Management Patterns</a></li>
<li><a href="https://dennisokeeffe.com/blog/2024-07-14-creating-a-result-type-in-typescript">Creating a Result Type in TypeScript</a></li>
</ul>
<p><strong>Disclaimer</strong>: This blog post used AI to generate the images used for the analogy.</p>
<p><em>Photo credit: <a href="https://unsplash.com/@robanderson72" target="_blank" rel="noopener noreferrer"><code>robanderson72</code></a></em></p>]]></content>
<category term="nodejs"/><category term="typescript"/>
</entry>
<entry>
<title type="html"><![CDATA[Creating a Result Type in TypeScript]]></title>
<id>https://blog.dennisokeeffe.com/blog/2024-07-14-creating-a-result-type-in-typescript</id>
<link href="https://dennisokeeffe.com/blog/2024-07-14-creating-a-result-type-in-typescript"/>
<published>2024-07-14T00:00:00.000Z</published>
<updated>2024-12-05T00:00:00.000Z</updated>
<summary type="html"><![CDATA[In this blog post, we take a look at libraries such as EffectTS, Zod, Valibot and Joi to understand how they approach a Result-like type and how we could approach building our own.]]></summary>
<content type="html"><![CDATA[<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p><strong>Update July 15th 2024:</strong> I&#39;ve ended up removing the <code>matchTag</code>
functionality and some types in the previous version to simplify things a
little more. The updated version will be it&#39;s <a href="#updated-approach-july-15th-2024">own section towards the end</a>.</p> </div> </aside>
<aside class="callout callout--info" aria-label="Note"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path></svg>  <div class="callout__body">  <p><strong>Update Dec 5th 2024:</strong> In a recent migration, I&#39;ve gone and
simplified this right back down to basics for usage. It omits a lot of the
match tag work though, so you may just want to compare and decide for
yourself. You can see that <a href="#updated-approach-dec-5th-2024">approach down here</a>. It also comes after spending a few more months writing more Rust.</p> </div> </aside>
<h2 id="overview">Overview</h2>
<p>When it comes to error management, there are three things that I&#39;ve been thinking about recently that I really like:</p>
<ol>
<li><a href="https://go.dev/blog/error-handling-and-go" target="_blank" rel="noopener noreferrer"><strong>Errors management in Go</strong></a>: I am a big fan of the way Go drives me to handle errors in a very explicit way, right after calling.</li>
<li><a href="https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html" target="_blank" rel="noopener noreferrer"><strong>Errors management in Rust</strong></a>: I really enjoy the <strong>Result</strong> type and the <strong>match</strong> syntax in Rust.</li>
<li><a href="https://effect.website/docs/guides/error-management/two-error-types" target="_blank" rel="noopener noreferrer"><strong>Error management in EffectTS</strong></a>: Thinking about errors are <strong>expected or unexpected</strong> in EffectTS is actually a great paradigm shift as a TypeScript programmer.</li>
</ol>
<p>In particular with <strong>EffectTS</strong>, I think their approach to error handling is a fantastic foundation for writing scalable TypeScript.</p>
<p>At my company, I&#39;ve run into some recent problems that I think <strong>EffectTS</strong> would solve quite nicely. However, achieving buy-in for it at this point in time is difficult (I won&#39;t elaborate on the reasons) and in earnest, I am only hoping to reap the benefits of a few adopted features at this point in time.</p>
<p>So, I&#39;ve been thinking about how I could implement my own <strong>Result</strong> type in TypeScript that could nicely return &quot;successes&quot; or &quot;failures&quot; that gives me nice TypeScript intellisense and type checking around errors. The aim would be to have something <em>relatively simple</em> that could be easily adopted at work, but also easily removed if need be.</p>
<p>In this blog post, we will take a look at libraries such as <strong>EffectTS</strong>, <strong>Zod</strong>, <strong>Valibot</strong> and <strong>Joi</strong> to understand how they approach a <strong>Result-like</strong> type and how we could approach building our own.</p>
<h2 id="what-is-a-result-type">What is a Result type?</h2>
<p>As far as I am talking about, I am looking to write a <strong>Result</strong> type is a type that can represent either a <strong>success</strong> or a <strong>failure</strong>.</p>
<p>I want the <strong>success</strong> type to return data (hopefully via a property like <code>data</code>) and a way to known it&#39;s a success, while the <strong>failure</strong> can safely return an error without throwing an exception.</p>
<p>The aim for <strong>failure</strong> types is that they will represent the &quot;expected errors&quot; i.e. errors that <strong>we expect</strong> could happen. This gives me power over how I handle these errors and how I can recover from them. It also can give me great type checking around these errors to improve developer experience.</p>
<p>Anything unexpected (also known as a <strong>defect</strong> in EffectTS) would be the errors <em>that do</em> end up in the <code>catch</code> part of the <code>try/catch</code> block. My aim here is that this is the stuff that we pay close attention to and aim to patch when these unexpected errors show their face.</p>
<p>Originally, I had a very simple implementation of a <strong>Result</strong> type that looked like this:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>export type Failure&lt;E = unknown&gt; = {
  _tag: &quot;Failure&quot;;
  error: E;
};

export type Success&lt;T = void&gt; = {
  _tag: &quot;Success&quot;;
  data: T;
};

export type Result&lt;T = void, E = unknown&gt; = Failure&lt;E&gt; | Success&lt;T&gt;;

export function succeed&lt;T = void&gt;(data: T): Success&lt;T&gt; {
  return {
    _tag: &quot;Success&quot;,
    data: data,
  };
}

export function fail&lt;E = unknown&gt;(error: E): Failure&lt;E&gt; {
  return {
    _tag: &quot;Failure&quot;,
    error: error,
  };
}</code></pre>
<p>I figured that the <code>Result</code> type would be a union of <code>Failure</code> and <code>Success</code> types, which I could create with the <code>fail</code> and <code>succeed</code> functions:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>const success = succeed(&quot;Hello, world!&quot;); // Success&lt;string&gt;
const failure = fail(&quot;Something went wrong&quot;); // Failure&lt;string&gt;</code></pre>
<p>That being said, it felt super basic and like it was missing some stuff that I wanted, so I decided to snoop into some source code and see how the pros do it.</p>
<h2 id="zod">Zod</h2>
<p>I started with <a href="https://zod.dev/" target="_blank" rel="noopener noreferrer"><strong>Zod</strong></a> due to the amount of usage I spend with it on my personal work.</p>
<p>I use <a href="https://zod.dev/?id=safeparse" target="_blank" rel="noopener noreferrer"><strong>safeParse</strong></a> and <a href="https://zod.dev/?id=safeparseasync" target="_blank" rel="noopener noreferrer"><strong>safeParseAsync</strong></a> frequently, and the API looks little like the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>const result = stringSchema.safeParse(&quot;billie&quot;);
if (!result.success) {
  // handle error then return
  result.error;
} else {
  // do something
  result.data;
}</code></pre>
<p>The API is very simple and intuitive, so it gets brownie points from me.</p>
<p>Looking into the source code for <a href="https://github.com/colinhacks/zod/blob/main/src/types.ts#L244" target="_blank" rel="noopener noreferrer">safeParse</a> and <a href="https://github.com/colinhacks/zod/blob/main/src/types.ts#L274" target="_blank" rel="noopener noreferrer">safeParseAsync</a>, both we returning <code>Promise&lt;SafeParseReturnType&lt;Input, Output&gt;&gt;</code> which is the return type of the <code>handleResult</code> function.</p>
<p>Going further into the <a href="https://github.com/colinhacks/zod/blob/main/src/types.ts#L91" target="_blank" rel="noopener noreferrer">handleResult source code</a>, we look to work with the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>const handleResult = &lt;Input, Output&gt;(
  ctx: ParseContext,
  result: SyncParseReturnType&lt;Output&gt;
):
  | { success: true; data: Output }
  | { success: false; error: ZodError&lt;Input&gt; } =&gt; {
  if (isValid(result)) {
    return { success: true, data: result.value };
  } else {
    if (!ctx.common.issues.length) {
      throw new Error(&quot;Validation failed but no issues detected.&quot;);
    }

    return {
      success: false,
      get error() {
        if ((this as any)._error) return (this as any)._error as Error;
        const error = new ZodError(ctx.common.issues);
        (this as any)._error = error;
        return (this as any)._error;
      },
    };
  }
};</code></pre>
<p>The unsuccessful route returns a <a href="https://zod.dev/?id=error-handling" target="_blank" rel="noopener noreferrer"><strong>ZodError</strong></a>, which both the docs and <a href="https://github.com/colinhacks/zod/blob/main/src/ZodError.ts" target="_blank" rel="noopener noreferrer">ZodError source code</a> show that it is a class that extends <code>Error</code>.</p>
<p>Although it makes perfect sense for Zod&#39;s use case, I&#39;m hoping to return a more generic class for my expected errors <em>that do not extend Error</em>. There reason being is that I don&#39;t want those errors to be throwable.</p>
<h2 id="joi">Joi</h2>
<p>Next, I took a look at <a href="https://joi.dev/" target="_blank" rel="noopener noreferrer"><strong>Joi</strong></a>, another validation library which is the one heavily used at my current company.</p>
<p>The <a href="https://joi.dev/api/?v=17.13.3#_validatevalue-state-prefs" target="_blank" rel="noopener noreferrer"><strong>validate</strong></a> function from <strong>Joi</strong> is what runs the assertions:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>const Joi = require(&quot;joi&quot;);

const schema = Joi.object({
  username: Joi.string().alphanum().min(3).max(30).required(),

  password: Joi.string().pattern(new RegExp(&quot;^[a-zA-Z0-9]{3,30}$&quot;)),

  repeat_password: Joi.ref(&quot;password&quot;),

  access_token: [Joi.string(), Joi.number()],

  birth_year: Joi.number().integer().min(1900).max(2013),

  email: Joi.string().email({
    minDomainSegments: 2,
    tlds: { allow: [&quot;com&quot;, &quot;net&quot;] },
  }),
})
  .with(&quot;username&quot;, &quot;birth_year&quot;)
  .xor(&quot;password&quot;, &quot;access_token&quot;)
  .with(&quot;password&quot;, &quot;repeat_password&quot;);

schema.validate({ username: &quot;abc&quot;, birth_year: 1994 });
// -&gt; { value: { username: &#39;abc&#39;, birth_year: 1994 } }

schema.validate({});
// -&gt; { value: {}, error: &#39;&quot;username&quot; is required&#39; }

// Also -

try {
  const value = await schema.validateAsync({
    username: &quot;abc&quot;,
    birth_year: 1994,
  });
} catch (err) {}</code></pre>
<p>Checking the <a href="https://github.com/hapijs/joi/blob/master/lib/manifest.js#L470" target="_blank" rel="noopener noreferrer">validate source code</a>, it uses the <a href="https://github.com/hapijs/hoek/blob/master/lib/assert.js" target="_blank" rel="noopener noreferrer">hapijs/hoek libraries assert function</a> under the hood.</p>
<p>That too makes use of a <a href="https://github.com/hapijs/hoek/blob/master/lib/assertError.js" target="_blank" rel="noopener noreferrer"><code>AssertError</code> class that extends <code>Error</code></a>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>&quot;use strict&quot;;

const internals = {};

module.exports = class AssertError extends Error {
  name = &quot;AssertError&quot;;

  constructor(message, ctor) {
    super(message || &quot;Unknown error&quot;);

    if (typeof Error.captureStackTrace === &quot;function&quot;) {
      // $lab:coverage:ignore$
      Error.captureStackTrace(this, ctor);
    }
  }
};</code></pre>
<h2 id="valibot">Valibot</h2>
<p><strong>Valibot</strong> was the last assertion library that I checked out. I personally haven&#39;t used <strong>Valibot</strong> much, but I&#39;ve heard the hype.</p>
<p>Similar to <strong>Zod</strong>, <strong>Valibot</strong> has a <a href="https://valibot.dev/api/safeParse/" target="_blank" rel="noopener noreferrer"><strong>safeParse</strong></a> function:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>const EmailSchema = v.pipe(v.string(), v.email());
const result = v.safeParse(EmailSchema, &quot;jane@example.com&quot;); // SafeParseResult&lt;TSchema&gt;

if (result.success) {
  const email = result.output; // string
} else {
  console.log(result.issues);
}</code></pre>
<p>Issues look to be related to the <a href="https://valibot.dev/api/BaseIssue/" target="_blank" rel="noopener noreferrer">BaseIssue</a> interface, as well as making heavy use of the <a href="https://github.com/fabian-hiller/valibot/blob/main/library/src/types/infer.ts#L38" target="_blank" rel="noopener noreferrer">InferIssue utility function</a>.</p>
<p>The type for <strong>SafeParseResult</strong> looks like the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import type {
  BaseIssue,
  BaseSchema,
  BaseSchemaAsync,
  InferIssue,
  InferOutput,
} from &quot;../../types/index.ts&quot;;

/**
 * Safe parse result type.
 */
export type SafeParseResult&lt;
  TSchema extends
    | BaseSchema&lt;unknown, unknown, BaseIssue&lt;unknown&gt;&gt;
    | BaseSchemaAsync&lt;unknown, unknown, BaseIssue&lt;unknown&gt;&gt;
&gt; =
  | {
      /**
       * Whether is&#39;s typed.
       */
      readonly typed: true;
      /**
       * Whether it&#39;s successful.
       */
      readonly success: true;
      /**
       * The output value.
       */
      readonly output: InferOutput&lt;TSchema&gt;;
      /**
       * The issues if any.
       */
      readonly issues: undefined;
    }
  | {
      readonly typed: true;
      readonly success: false;
      readonly output: InferOutput&lt;TSchema&gt;;
      readonly issues: [InferIssue&lt;TSchema&gt;, ...InferIssue&lt;TSchema&gt;[]];
    }
  | {
      readonly typed: false;
      readonly success: false;
      readonly output: unknown;
      readonly issues: [InferIssue&lt;TSchema&gt;, ...InferIssue&lt;TSchema&gt;[]];
    };</code></pre>
<p>Interestingly, the <a href="https://github.com/fabian-hiller/valibot/blob/main/library/src/types/issue.ts#L216" target="_blank" rel="noopener noreferrer">BaseIssue</a> interface does not look to extend from <code>Error</code>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>/**
 * Base issue type.
 */
export interface BaseIssue&lt;TInput&gt; extends Config&lt;BaseIssue&lt;TInput&gt;&gt; {
  /**
   * The issue kind.
   */
  readonly kind: &quot;schema&quot; | &quot;validation&quot; | &quot;transformation&quot;;
  /**
   * The issue type.
   */
  readonly type: string;
  /**
   * The raw input data.
   */
  readonly input: TInput;
  /**
   * The expected property.
   */
  readonly expected: string | null;
  /**
   * The received property.
   */
  readonly received: string;
  /**
   * The error message.
   */
  readonly message: string;
  /**
   * The input requirement.
   */
  readonly requirement?: unknown;
  /**
   * The issue path.
   *
   * TODO: Investigate if it is possible to make the path type safe based on the
   * input.
   */
  readonly path?: [IssuePathItem, ...IssuePathItem[]];
  /**
   * The sub issues.
   */
  readonly issues?: [BaseIssue&lt;TInput&gt;, ...BaseIssue&lt;TInput&gt;[]];
}</code></pre>
<aside class="callout callout--warning" aria-label="Warning"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><path d="m21.7 18-8-14a2 2 0 0 0-3.4 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.7-3ZM12 9v4M12 17h.01" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>  <div class="callout__body">  <p>I could be wrong about its usage. It was a little tricky navigating the source
code for a library I am not too familiar with.</p> </div> </aside>
<span id="effectts" class="anchor-alias"><h2 id="effect-ts">EffectTS</h2>
<p>Finally, I went to check out what <strong>EffectTS</strong> does given how much their paradigm had inspired my initial search.</p>
<p>EffectTS has a <a href="https://github.com/Effect-TS/effect/blob/ca775cec53baebc1a43d9b8852a3ac6726178498/packages/effect/src/Effect.ts#L238" target="_blank" rel="noopener noreferrer"><code>Success</code></a> and <a href="https://github.com/Effect-TS/effect/blob/main/packages/effect/src/Effect.ts#L233" target="_blank" rel="noopener noreferrer"><code>Error</code></a> representation within the <code>Effect</code> namespace.</p>
<p>We can create a &quot;Success&quot; representation by invoking the <code>Effect.succeed</code> function:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import { Effect } from &quot;@effect-ts/core&quot;;

const success = Effect.succeed(&quot;Hello, world!&quot;); // Effect&lt;string, never&gt;</code></pre>
<p>Although this represents a success, the type is in fact still <code>Effect</code>.</p>
<p>The <a href="https://github.com/Effect-TS/effect/blob/main/packages/effect/src/internal/core.ts#L1226" target="_blank" rel="noopener noreferrer">source code for succeed</a> is interesting (and a little trickier to grok):</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>/* @internal */
export const succeed = &lt;A&gt;(value: A): Effect.Effect&lt;A&gt; =&gt; {
  const effect = new EffectPrimitiveSuccess(OpCodes.OP_SUCCESS) as any;
  effect.effect_instruction_i0 = value;
  return effect;
};</code></pre>
<p>Looking into <a href="https://github.com/Effect-TS/effect/blob/main/packages/effect/src/internal/core.ts#L239" target="_blank" rel="noopener noreferrer"><code>EffectPrimitiveSuccess</code></a> led me to the alternative types <code>EffectPrimitive</code> and <code>EffectPrimitiveFailure</code>, where each look to have a similar implementation:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>/** @internal */
class EffectPrimitiveSuccess {
  public effect_instruction_i0 = undefined;
  public effect_instruction_i1 = undefined;
  public effect_instruction_i2 = undefined;
  public trace = undefined;
  [EffectTypeId] = effectVariance;
  constructor(readonly _op: Primitive[&quot;_op&quot;]) {
    // @ts-expect-error
    this._tag = _op;
  }
  [Equal.symbol](this: {}, that: unknown) {
    return (
      exitIsExit(that) &amp;&amp;
      that._op === &quot;Success&quot; &amp;&amp;
      // @ts-expect-error
      Equal.equals(this.effect_instruction_i0, that.effect_instruction_i0)
    );
  }
  [Hash.symbol](this: {}) {
    return pipe(
      // @ts-expect-error
      Hash.string(this._tag),
      // @ts-expect-error
      Hash.combine(Hash.hash(this.effect_instruction_i0)),
      Hash.cached(this)
    );
  }
  get value() {
    return this.effect_instruction_i0;
  }
  pipe() {
    return pipeArguments(this, arguments);
  }
  toJSON() {
    return {
      _id: &quot;Exit&quot;,
      _tag: this._op,
      value: toJSON(this.value),
    };
  }
  toString() {
    return format(this.toJSON());
  }
  [NodeInspectSymbol]() {
    return this.toJSON();
  }
  [Symbol.iterator]() {
    return new SingleShotGen(new YieldWrap(this));
  }
}</code></pre>
<p>I won&#39;t explain too much of this (since it is a tough for me to explain with 100% confidence), but my big assumption here is that the implementation here is required for some important work under the hood, and so for my use case I am likely looking to use something that is simpler in terms of naming conventions, but some of the symbol properties do look important for me to implement.</p>
<p>Although it&#39;s a bit of off scope, something else that is really impressive about EffectTS on top of this though is how they make use of readonly tags. This helps with their conditional logic a lot, and I am a fan of some of their patterns that make use of <code>_tag</code>. I am hoping to make use of this myself in the final result.</p>
<h2 id="implementing-a-result-type">Implementing a Result type</h2>
<p>After reviewing all of the different approaches, I ended up with the following implementation for <code>Result</code>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>export type Success&lt;T&gt; = Result&lt;T, never&gt;;
export type Failure&lt;E extends ErrorType&gt; = Result&lt;never, E&gt;;

export type ErrorType = {
  _tag: string;
  [key: string]: any;
};

type MatchCases&lt;T, E extends ErrorType, U&gt; = {
  Success: (data: T) =&gt; U;
} &amp; {
  [K in E[&quot;_tag&quot;]]: (error: Extract&lt;E, { _tag: K }&gt;) =&gt; U;
};

export class Result&lt;T, E extends ErrorType&gt; {
  protected constructor(
    readonly _tag: &quot;Success&quot; | &quot;Failure&quot;,
    protected readonly value: T | E
  ) {}

  static succeed&lt;T&gt;(data: T): Success&lt;T&gt; {
    return new Result(&quot;Success&quot;, data) as Success&lt;T&gt;;
  }

  static fail&lt;E extends ErrorType&gt;(error: E): Failure&lt;E&gt; {
    return new Result(&quot;Failure&quot;, error) as Failure&lt;E&gt;;
  }

  isSuccess(): this is Success&lt;T&gt; {
    return this._tag === &quot;Success&quot;;
  }

  isFailure(): this is Failure&lt;E&gt; {
    return this._tag === &quot;Failure&quot;;
  }

  get data(): T {
    if (this.isSuccess()) return this.value as T;
    throw new Error(&quot;Cannot get data from a Failure&quot;);
  }

  get error(): E {
    if (this.isFailure()) return this.value as E;
    throw new Error(&quot;Cannot get error from a Success&quot;);
  }

  map&lt;U&gt;(f: (value: T) =&gt; U): Result&lt;U, E&gt; {
    return this.isSuccess()
      ? Result.succeed(f(this.data))
      : (this as unknown as Result&lt;U, E&gt;);
  }

  flatMap&lt;U&gt;(f: (value: T) =&gt; Result&lt;U, E&gt;): Result&lt;U, E&gt; {
    return this.isSuccess() ? f(this.data) : (this as unknown as Result&lt;U, E&gt;);
  }

  equals(that: unknown): boolean {
    return (
      that instanceof Result &amp;&amp;
      this._tag === that._tag &amp;&amp;
      this.value === that.value
    );
  }

  toJSON() {
    return {
      _tag: this._tag,
      [this._tag === &quot;Success&quot; ? &quot;data&quot; : &quot;error&quot;]: this.value,
    };
  }

  toString(): string {
    return JSON.stringify(this.toJSON());
  }

  [Symbol.for(&quot;nodejs.util.inspect.custom&quot;)]() {
    return this.toJSON();
  }

  static matchTag&lt;T, E extends ErrorType, U&gt;(
    result: Result&lt;T, E&gt;,
    cases: MatchCases&lt;T, E, U&gt;
  ): U {
    if (result.isSuccess()) {
      return cases.Success(result.data);
    } else {
      const errorHandler = cases[result.error._tag as keyof typeof cases];
      if (errorHandler) {
        return errorHandler(result.error as any);
      }
      throw new Error(`Unhandled error type: ${result.error._tag}`);
    }
  }
}</code></pre>
<p>Let&#39;s break down the code.</p>
<h3 id="definitions-and-types">Definitions and Types</h3>
<ol>
<li>
<p><strong>Success and Failure Types</strong></p>
<pre class="astro-code night-owl" tabindex="0" data-language="typescript"><code>export type Success&lt;T&gt; = Result&lt;T, never&gt;;
export type Failure&lt;E extends ErrorType&gt; = Result&lt;never, E&gt;;</code></pre>
<ul>
<li><code>Success&lt;T&gt;</code> represents a successful result, containing a value of type <code>T</code>.</li>
<li><code>Failure&lt;E extends ErrorType&gt;</code> represents a failed result, containing an error of type <code>E</code>.</li>
</ul>
</li>
<li>
<p><strong>ErrorType</strong></p>
<pre class="astro-code night-owl" tabindex="0" data-language="typescript"><code>export type ErrorType = {
  _tag: string;
  [key: string]: any;
};</code></pre>
<ul>
<li><code>ErrorType</code> defines the structure for errors. Each error has a <code>_tag</code> (string identifier) and can have additional properties.</li>
</ul>
</li>
<li>
<p><strong>MatchCases</strong></p>
<pre class="astro-code night-owl" tabindex="0" data-language="typescript"><code>type MatchCases&lt;T, E extends ErrorType, U&gt; = {
  Success: (data: T) =&gt; U;
} &amp; {
  [K in E[&quot;_tag&quot;]]: (error: Extract&lt;E, { _tag: K }&gt;) =&gt; U;
};</code></pre>
<ul>
<li><code>MatchCases</code> is a utility type for handling different cases when matching on a <code>Result</code>. It includes:
<ul>
<li>A <code>Success</code> case for handling successful results.</li>
<li>Dynamically creates cases for each error <code>_tag</code> to handle specific error types.</li>
</ul>
</li>
</ul>
</li>
</ol>
<h3 id="result-class">Result Class</h3>
<ol start="4">
<li>
<p><strong>Constructor and Static Methods</strong></p>
<pre class="astro-code night-owl" tabindex="0" data-language="typescript"><code>export class Result&lt;T, E extends ErrorType&gt; {
  protected constructor(
    readonly _tag: &quot;Success&quot; | &quot;Failure&quot;,
    protected readonly value: T | E
  ) {}

  static succeed&lt;T&gt;(data: T): Success&lt;T&gt; {
    return new Result(&quot;Success&quot;, data) as Success&lt;T&gt;;
  }

  static fail&lt;E extends ErrorType&gt;(error: E): Failure&lt;E&gt; {
    return new Result(&quot;Failure&quot;, error) as Failure&lt;E&gt;;
  }</code></pre>
<ul>
<li><code>Result</code> is a generic class representing either a success (<code>Success&lt;T&gt;</code>) or a failure (<code>Failure&lt;E&gt;</code>).</li>
<li>It has a constructor accepting <code>_tag</code> and <code>value</code>.</li>
<li><code>succeed</code> and <code>fail</code> are static methods for creating instances of <code>Success</code> and <code>Failure</code>.</li>
</ul>
</li>
<li>
<p><strong>Type Guards</strong></p>
<pre class="astro-code night-owl" tabindex="0" data-language="typescript"><code>isSuccess(): this is Success&lt;T&gt; {
  return this._tag === &quot;Success&quot;;
}

isFailure(): this is Failure&lt;E&gt; {
  return this._tag === &quot;Failure&quot;;
}</code></pre>
<ul>
<li><code>isSuccess</code> and <code>isFailure</code> are type guards to determine if the result is a success or failure.</li>
</ul>
</li>
<li>
<p><strong>Accessors</strong></p>
<pre class="astro-code night-owl" tabindex="0" data-language="typescript"><code>get data(): T {
  if (this.isSuccess()) return this.value as T;
  throw new Error(&quot;Cannot get data from a Failure&quot;);
}

get error(): E {
  if (this.isFailure()) return this.value as E;
  throw new Error(&quot;Cannot get error from a Success&quot;);
}</code></pre>
<ul>
<li><code>data</code> and <code>error</code> are getters to access the value or error. They throw errors if accessed on the wrong type.</li>
</ul>
</li>
<li>
<p><strong>Transformations</strong></p>
<pre class="astro-code night-owl" tabindex="0" data-language="typescript"><code>map&lt;U&gt;(f: (value: T) =&gt; U): Result&lt;U, E&gt; {
  return this.isSuccess()
    ? Result.succeed(f(this.data))
    : (this as unknown as Result&lt;U, E&gt;);
}

flatMap&lt;U&gt;(f: (value: T) =&gt; Result&lt;U, E&gt;): Result&lt;U, E&gt; {
  return this.isSuccess() ? f(this.data) : (this as unknown as Result&lt;U, E&gt;);
}</code></pre>
<ul>
<li><code>map</code> applies a function to the data if it&#39;s a success, otherwise returns the failure.</li>
<li><code>flatMap</code> applies a function that returns a <code>Result</code> if it&#39;s a success, otherwise returns the failure.</li>
</ul>
</li>
<li>
<p><strong>Equality Check</strong></p>
<pre class="astro-code night-owl" tabindex="0" data-language="typescript"><code>equals(that: unknown): boolean {
  return (
    that instanceof Result &amp;&amp;
    this._tag === that._tag &amp;&amp;
    this.value === that.value
  );
}</code></pre>
<ul>
<li><code>equals</code> checks if another result is equal to the current one by comparing <code>_tag</code> and <code>value</code>.</li>
</ul>
</li>
<li>
<p><strong>Serialization</strong></p>
<pre class="astro-code night-owl" tabindex="0" data-language="typescript"><code>toJSON() {
  return {
    _tag: this._tag,
    [this._tag === &quot;Success&quot; ? &quot;data&quot; : &quot;error&quot;]: this.value,
  };
}

toString(): string {
  return JSON.stringify(this.toJSON());
}

[Symbol.for(&quot;nodejs.util.inspect.custom&quot;)]() {
  return this.toJSON();
}</code></pre>
<ul>
<li><code>toJSON</code> converts the result to a JSON object.</li>
<li><code>toString</code> returns a JSON string representation.</li>
<li><code>[Symbol.for(&quot;nodejs.util.inspect.custom&quot;)]</code> customizes how the result is displayed in Node.js inspection.</li>
</ul>
</li>
<li>
<p><strong>Pattern Matching</strong></p>
<pre class="astro-code night-owl" tabindex="0" data-language="typescript"><code>static matchTag&lt;T, E extends ErrorType, U&gt;(
  result: Result&lt;T, E&gt;,
  cases: MatchCases&lt;T, E, U&gt;
): U {
  if (result.isSuccess()) {
    return cases.Success(result.data);
  } else {
    const errorHandler = cases[result.error._tag as keyof typeof cases];
    if (errorHandler) {
      return errorHandler(result.error as any);
    }
    throw new Error(`Unhandled error type: ${result.error._tag}`);
  }
}</code></pre>
<ul>
<li><code>matchTag</code> matches on the result&#39;s type and invokes the appropriate handler from <code>cases</code> for <code>Success</code> or the specific error type.</li>
</ul>
</li>
</ol>
<h3 id="summary">Summary</h3>
<p>As you can tell, this does take a lot of inspiration from the <strong>EffectTS</strong> implementation, but I&#39;ve tried to simplify it down to the core features that I need as well as using the other validation libraries as inspiration.</p>
<ul>
<li>This code defines a <code>Result</code> class for handling operations that can either succeed or fail.</li>
<li>It includes type guards, transformations (<code>map</code>, <code>flatMap</code>), and utilities for pattern matching (<code>matchTag</code>).</li>
<li><code>Success</code> and <code>Failure</code> are specific instances of <code>Result</code>.</li>
<li><code>ErrorType</code> is a flexible error structure with a <code>_tag</code> for identifying error types.</li>
</ul>
<p>This approach allows for clear and type-safe handling of operations that may succeed or fail, providing strong guarantees about the types involved.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>type TestError =
  | { _tag: &quot;TestError1&quot;; message: string }
  | { _tag: &quot;TestError2&quot;; message: string };

const result = Result.succeed(42); // Result&lt;T, E extends ErrorType&gt;.succeed&lt;number&gt;(data: number): Success&lt;number&gt;

// Result&lt;T, E extends ErrorType&gt;.fail&lt;{
//     _tag: &quot;TestError1&quot;;
//     message: string;
// }&gt;(error: {
//     _tag: &quot;TestError1&quot;;
//     message: string;
// }): Failure&lt;{
//     _tag: &quot;TestError1&quot;;
//     message: string;
// }&gt;
const failure = Result.fail&lt;TestError&gt;({
  _tag: &quot;TestError1&quot;,
  message: &quot;Test error&quot;,
});</code></pre>
<p>The best way to get a feel for how this works is to look at the unit tests that I wrote for this type:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import { Result, ErrorType } from &quot;./alt-result&quot;;

describe(&quot;Result&quot;, () =&gt; {
  // Define some test error types
  type TestError =
    | { _tag: &quot;TestError1&quot;; message: string }
    | { _tag: &quot;TestError2&quot;; message: string };

  describe(&quot;creation and basic methods&quot;, () =&gt; {
    it(&quot;should create a Success result&quot;, () =&gt; {
      const result = Result.succeed(42);
      expect(result.isSuccess()).toBe(true);
      expect(result.isFailure()).toBe(false);
      expect(result.data).toBe(42);
    });

    it(&quot;should create a Failure result&quot;, () =&gt; {
      const error: TestError = { _tag: &quot;TestError1&quot;, message: &quot;Test error&quot; };
      const result = Result.fail(error);
      expect(result.isSuccess()).toBe(false);
      expect(result.isFailure()).toBe(true);
      expect(result.error).toEqual(error);
    });

    it(&quot;should throw when accessing data of a Failure&quot;, () =&gt; {
      const result = Result.fail({ _tag: &quot;TestError1&quot;, message: &quot;Test error&quot; });
      expect(() =&gt; result.data).toThrow(&quot;Cannot get data from a Failure&quot;);
    });

    it(&quot;should throw when accessing error of a Success&quot;, () =&gt; {
      const result = Result.succeed(42);
      expect(() =&gt; result.error).toThrow(&quot;Cannot get error from a Success&quot;);
    });
  });

  describe(&quot;map and flatMap&quot;, () =&gt; {
    it(&quot;should map a Success result&quot;, () =&gt; {
      const result = Result.succeed(42).map((x) =&gt; x * 2);
      expect(result.isSuccess()).toBe(true);
      expect(result.data).toBe(84);
    });

    it(&quot;should not map a Failure result&quot;, () =&gt; {
      const error: TestError = { _tag: &quot;TestError1&quot;, message: &quot;Test error&quot; };
      const result = Result.fail&lt;TestError&gt;(error).map((x) =&gt; x * 2);
      expect(result.isFailure()).toBe(true);
      expect(result.error).toEqual(error);
    });

    it(&quot;should flatMap a Success result&quot;, () =&gt; {
      const result = Result.succeed(42).flatMap((x) =&gt; Result.succeed(x * 2));
      expect(result.isSuccess()).toBe(true);
      expect(result.data).toBe(84);
    });

    it(&quot;should not flatMap a Failure result&quot;, () =&gt; {
      const error: TestError = { _tag: &quot;TestError1&quot;, message: &quot;Test error&quot; };
      const result = Result.fail&lt;TestError&gt;(error).flatMap((x) =&gt;
        Result.succeed(x * 2)
      );
      expect(result.isFailure()).toBe(true);
      expect(result.error).toEqual(error);
    });
  });

  describe(&quot;equals&quot;, () =&gt; {
    it(&quot;should consider two Success results with the same data equal&quot;, () =&gt; {
      const result1 = Result.succeed(42);
      const result2 = Result.succeed(42);
      expect(result1.equals(result2)).toBe(true);
    });

    it(&quot;should consider two Failure results with the same error equal&quot;, () =&gt; {
      const error: TestError = { _tag: &quot;TestError1&quot;, message: &quot;Test error&quot; };
      const result1 = Result.fail(error);
      const result2 = Result.fail(error);
      expect(result1.equals(result2)).toBe(true);
    });

    it(&quot;should consider Success and Failure results not equal&quot;, () =&gt; {
      const success = Result.succeed(42);
      const failure = Result.fail({
        _tag: &quot;TestError1&quot;,
        message: &quot;Test error&quot;,
      });
      expect(success.equals(failure)).toBe(false);
    });
  });

  describe(&quot;matchTag&quot;, () =&gt; {
    it(&quot;should match Success case&quot;, () =&gt; {
      const result = Result.succeed(42);
      const output = Result.matchTag(result as Result&lt;number, TestError&gt;, {
        Success: (data) =&gt; `Success: ${data}`,
        TestError1: (error: TestError) =&gt; `Error1: ${error.message}`,
        TestError2: (error: TestError) =&gt; `Error2: ${error.message}`,
      });
      expect(output).toBe(&quot;Success: 42&quot;);
    });

    it(&quot;should match Failure case&quot;, () =&gt; {
      const result = Result.fail&lt;TestError&gt;({
        _tag: &quot;TestError1&quot;,
        message: &quot;Test error&quot;,
      });
      const output = Result.matchTag(result, {
        Success: (data) =&gt; `Success: ${data}`,
        TestError1: (error: TestError) =&gt; `Error1: ${error.message}`,
        TestError2: (error: TestError) =&gt; `Error2: ${error.message}`,
      });
      expect(output).toBe(&quot;Error1: Test error&quot;);
    });

    it(&quot;should throw for unhandled error types&quot;, () =&gt; {
      const result = Result.fail({ _tag: &quot;UnhandledError&quot; } as ErrorType);
      expect(() =&gt;
        Result.matchTag(result as unknown as Result&lt;number, TestError&gt;, {
          Success: (data) =&gt; `Success: ${data}`,
          TestError1: (error: TestError) =&gt; `Error1: ${error.message}`,
          TestError2: (error: TestError) =&gt; `Error2: ${error.message}`,
        })
      ).toThrow(&quot;Unhandled error type: UnhandledError&quot;);
    });
  });
});</code></pre>
<span id="updated-approach-july-15th-2024" class="anchor-alias"><h2 id="updated-approach-july-15-th-2024">Updated approach (July 15th 2024)</h2>
<p>After playing around a bit more, I&#39;ve done some refactoring. Right now, my current implementation looks like this:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>export type Success&lt;T&gt; = Result&lt;T, never&gt;;
export type Failure&lt;E&gt; = Result&lt;never, E&gt;;

/**
 * !!! EXPERIMENTAL !!!
 * The following class provides a way to enapsulate the result of an operation that can either succeed or fail.
 * It enables the use of functional programming techniques to handle the result of an operation. This can
 * be useful in TypeScript for handling expected errors in a more type-safe way.
 *
 * @experimental
 * @see https://blog.dennisokeeffe.com/blog/2024-07-14-creating-a-result-type-in-typescript
 *
 * @example
 * type TestError =
 *  | { _tag: &quot;TestError1&quot;; message: string }
 *  | { _tag: &quot;TestError2&quot;; message: string };
 *
 *  // Result&lt;T, E extends ErrorType&gt;.succeed&lt;number&gt;(data: number): Success&lt;number&gt;
 * const result = Result.succeed(42);
 *
 * // Result&lt;T, E extends ErrorType&gt;.fail&lt;{
 * //     _tag: &quot;TestError1&quot;;
 * //     message: string;
 * // }&gt;(error: {
 * //     _tag: &quot;TestError1&quot;;
 * //     message: string;
 * // }): Failure&lt;{
 * //     _tag: &quot;TestError1&quot;;
 * //     message: string;
 * // }&gt;
 * const failure = Result.fail&lt;TestError&gt;({
 * _tag: &quot;TestError1&quot;,
 *  message: &quot;Test error&quot;,
 * });
 */
export class Result&lt;T, E&gt; {
  protected constructor(
    readonly _tag: &quot;Success&quot; | &quot;Failure&quot;,
    protected readonly value: T | E
  ) {}

  static succeed&lt;T&gt;(data: T): Success&lt;T&gt; {
    return new Result(&quot;Success&quot;, data) as Success&lt;T&gt;;
  }

  static fail&lt;E&gt;(error: E): Failure&lt;E&gt; {
    return new Result(&quot;Failure&quot;, error) as Failure&lt;E&gt;;
  }

  isSuccess(): this is Success&lt;T&gt; {
    return this._tag === &quot;Success&quot;;
  }

  isFailure(): this is Failure&lt;E&gt; {
    return this._tag === &quot;Failure&quot;;
  }

  get data(): T {
    if (this.isSuccess()) return this.value as T;
    throw new Error(&quot;Cannot get data from a Failure&quot;);
  }

  get error(): E {
    if (this.isFailure()) return this.value as E;
    throw new Error(&quot;Cannot get error from a Success&quot;);
  }

  map&lt;U&gt;(f: (value: T) =&gt; U): Result&lt;U, E&gt; {
    return this.isSuccess()
      ? Result.succeed(f(this.data))
      : (this as unknown as Result&lt;U, E&gt;);
  }

  flatMap&lt;U&gt;(f: (value: T) =&gt; Result&lt;U, E&gt;): Result&lt;U, E&gt; {
    return this.isSuccess() ? f(this.data) : (this as unknown as Result&lt;U, E&gt;);
  }

  equals(that: unknown): boolean {
    return (
      that instanceof Result &amp;&amp;
      this._tag === that._tag &amp;&amp;
      this.value === that.value
    );
  }

  toJSON() {
    return {
      _tag: this._tag,
      [this._tag === &quot;Success&quot; ? &quot;data&quot; : &quot;error&quot;]: this.value,
    };
  }

  toString(): string {
    return JSON.stringify(this.toJSON());
  }

  [Symbol.for(&quot;nodejs.util.inspect.custom&quot;)]() {
    return this.toJSON();
  }
}</code></pre>
<ul>
<li>I&#39;ve removed the <code>matchTag</code> static method as in introduced unnecessary complexity that I think I wanted to think through more. Instead, I&#39;m relying on the <code>_tag</code> property to handle the matching.</li>
<li>I&#39;ve removed the <code>ErrorType</code> type as it introduced unnecessary complexity. I&#39;m happy to just use <code>E</code> as the error type for now to see how it goes.</li>
</ul>
<p>There is also another approach I am mulling over to reduce some <code>as</code> casting for the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>static succeed&lt;T&gt;(data: T): Success&lt;T&gt; {
  return new Result(&quot;Success&quot;, data) as Success&lt;T&gt;;
}

static fail&lt;E&gt;(error: E): Failure&lt;E&gt; {
  return new Result(&quot;Failure&quot;, error) as Failure&lt;E&gt;;
}</code></pre>
<p>That approach would be to convert <code>Result</code> to an abstract class and convert the <code>Success</code> and <code>Failure</code> types into sub classes:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>export abstract class Result&lt;T, E&gt; {
  protected constructor(
    readonly _tag: &quot;Success&quot; | &quot;Failure&quot;,
    protected readonly value: T | E
  ) {}

  static succeed&lt;T&gt;(data: T): Success&lt;T&gt; {
    return new Success(data);
  }

  static fail&lt;E&gt;(error: E): Failure&lt;E&gt; {
    return new Failure(error);
  }

  abstract isSuccess(): this is Success&lt;T&gt;;
  abstract isFailure(): this is Failure&lt;E&gt;;

  get data(): T {
    if (this.isSuccess()) return this.value;
    throw new Error(&quot;Cannot get data from a Failure&quot;);
  }

  get error(): E {
    if (this.isFailure()) return this.value;
    throw new Error(&quot;Cannot get error from a Success&quot;);
  }

  map&lt;U&gt;(f: (value: T) =&gt; U): Result&lt;U, E&gt; {
    return this.isSuccess()
      ? Result.succeed(f(this.data))
      : (this as unknown as Result&lt;U, E&gt;);
  }

  flatMap&lt;U&gt;(f: (value: T) =&gt; Result&lt;U, E&gt;): Result&lt;U, E&gt; {
    return this.isSuccess() ? f(this.data) : (this as unknown as Result&lt;U, E&gt;);
  }

  equals(that: unknown): boolean {
    return (
      that instanceof Result &amp;&amp;
      this._tag === that._tag &amp;&amp;
      this.value === that.value
    );
  }

  toJSON() {
    return {
      _tag: this._tag,
      [this._tag === &quot;Success&quot; ? &quot;data&quot; : &quot;error&quot;]: this.value,
    };
  }

  toString(): string {
    return JSON.stringify(this.toJSON());
  }

  [Symbol.for(&quot;nodejs.util.inspect.custom&quot;)]() {
    return this.toJSON();
  }
}

export class Success&lt;T&gt; extends Result&lt;T, never&gt; {
  constructor(data: T) {
    super(&quot;Success&quot;, data);
  }

  isSuccess(): this is Success&lt;T&gt; {
    return true;
  }

  isFailure(): this is Failure&lt;never&gt; {
    return false;
  }

  get data(): T {
    return this.value;
  }
}

export class Failure&lt;E&gt; extends Result&lt;never, E&gt; {
  constructor(error: E) {
    super(&quot;Failure&quot;, error);
  }

  isSuccess(): this is Success&lt;never&gt; {
    return false;
  }

  isFailure(): this is Failure&lt;E&gt; {
    return true;
  }

  get error(): E {
    return this.value;
  }
}</code></pre>
<p>Both approaches should only require removal of the <code>matchTag</code> tests that I had previously.</p>
<span id="updated-approach-dec-5th-2024" class="anchor-alias"><h2 id="updated-approach-dec-5-th-2024">Updated approach (Dec 5th 2024)</h2>
<p>In a recent data migration, I ended up making use of a <code>Result</code>-ish type again to help capture and propagate any expected errors for my logging to help decide what I needed to do.</p>
<p>I won&#39;t go into too many details for the script itself, but in that approach I ended up stripping everything back to a simple set of types and functions:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>class Ok&lt;T&gt; {
  readonly _tag = &quot;ok&quot;;
  value: T;

  constructor(value: T) {
    this.value = value;
  }
}

class Err&lt;E&gt; {
  readonly _tag = &quot;err&quot;;
  error: E;

  constructor(error: E) {
    this.error = error;
  }
}

type Result&lt;T, E&gt; = Ok&lt;T&gt; | Err&lt;E&gt;;

function err&lt;E&gt;(e: E): Err&lt;E&gt; {
  return new Err(e);
}

function ok&lt;T&gt;(t: T): Ok&lt;T&gt; {
  return new Ok(t);
}</code></pre>
<p>This approach omits a lot of the helper functions that I had for the classes, but it felt like a good balance for the use-case that I had. There can be issues with serialization of the errors for the generic passed in, but it relies a lot more on the call site invocation to handle that.</p>
<p>In use, you could set an expected error and then handle it accordingly:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>class ExpectedError {
  readonly _tag = &quot;ExpectedError&quot;;
}

function resultTest(): Result&lt;string, ExpectedError&gt; {
  const run = true;

  if (run) {
    return err(new ExpectedError());
  }

  return ok(&quot;Success&quot;);
}

const res2 = resultTest();

switch (res2._tag) {
  case &quot;ok&quot;:
    console.log(res2.value);
    break;
  case &quot;err&quot;:
    switch (res2.error._tag) {
      case &quot;ExpectedError&quot;:
        console.log(&quot;Handle something here&quot;);
        break;
    }
    break;
}</code></pre>
<p>Triggering the <code>ExpectedError</code> within that contrived code demonstrates the use case. It&#39;s a bit contrived here with the switch-within-a-switch, but that&#39;s purely for demonstration purposes. In reality, you would make use of a more well-thought-out set of conditional logic.</p>
<p>In my particular use-case, I had a number of Node.js streams where I needed to propagate errors up the chain and log them accordingly. An example of one such stream where I needed to bulk insert into a DynamoDB table at that part of the pipeline:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>class UnprocessedItemsError {
  readonly _tag = &quot;UnprocessedItemsError&quot;;
  readonly barcode: string;

  constructor(barcode: string) {
    this.barcode = barcode;
  }
}

class DynamoDBError {
  readonly _tag = &quot;DynamoDBError&quot;;
  readonly barcode: string;

  constructor(barcode: string) {
    this.barcode = barcode;
  }
}

type BarcodeObj = {
  barcode: string;
};

export async function* processBatchIntoFactTelling(
  source: AsyncIterable&lt;ProcessBatchIntoBaasResult[]&gt;,
  tableName: string,
  client: DynamoDBClient,
  uploadId: string,
  actioner: Actioner,
  walletId: string
): AsyncGenerator&lt;Result&lt;BarcodeObj, UnprocessedItemsError | DynamoDBError&gt;[]&gt; {
  const BATCH_SIZE = 25;

  for await (const batch of source) {
    const unwrapped = batch
      .filter((item) =&gt; item._tag === &quot;ok&quot;)
      .map((item) =&gt; item.value);
    const results: Result&lt;BarcodeObj, UnprocessedItemsError | DynamoDBError&gt;[] =
      [];

    try {
      for (let i = 0; i &lt; unwrapped.length; i += BATCH_SIZE) {
        const writeRequests: any[] = [];
        const currentChunk = unwrapped.slice(i, i + BATCH_SIZE);

        currentChunk.forEach(() =&gt; {
          const timestampISOString = getSequentialTimestamp();
          writeRequests.push({
            PutRequest: {
              Item: marshall({
                // ... omitted DynamoDB item
              }),
            },
          });
        });

        const command = new BatchWriteItemCommand({
          RequestItems: {
            [tableName]: writeRequests,
          },
        });

        const response = await client.send(command);

        if (
          response.UnprocessedItems &amp;&amp;
          response.UnprocessedItems[tableName] &amp;&amp;
          response.UnprocessedItems[tableName].length &gt; 0
        ) {
          results.push(
            ...currentChunk.map((barcodeObj) =&gt;
              err(new UnprocessedItemsError(barcodeObj.barcode))
            )
          );
        } else {
          results.push(...currentChunk.map((item) =&gt; ok(item)));
        }
      }

      yield results;
    } catch (error) {
      console.log(&quot;DynamoDB error&quot;, error);
      yield unwrapped.map((item) =&gt; err(new DynamoDBError(item.barcode)));
    }
  }
}</code></pre>
<p>This isn&#39;t as specific as some of the other streams, but in this particular case I was interested in unprocessed items or general failures.</p>
<p>I also had another function that helped to split the stream by the take so I could write the logs into two separate files to prepare for retries:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>export const splitByTag = &lt;T, U&gt;(array: (Ok&lt;T&gt; | Err&lt;U&gt;)[]) =&gt; {
  const ok: Ok&lt;T&gt;[] = [];
  const err: Err&lt;U&gt;[] = [];

  return array.reduce(
    (result, item) =&gt; {
      const { ok, err } = result;

      if (item._tag === &quot;ok&quot;) {
        return { ...result, ok: [...ok, item] };
      } else {
        return { ...result, err: [...err, item] };
      }
    },
    { ok, err }
  );
};</code></pre>
<p>That same <code>_tag</code> property could be applied further for making decisions on what to do for specific errors and help type narrow down even further. It was very nice to work with.</p>
<p>I also ended up implementing a similar <code>Some</code> type inspired by Rust which I might go into detail in for another post which worked well in representing optional values that could also apply the same conditional logic thanks to the <code>_tag</code> property instead of using something like <code>undefined</code>. Once again though, although the developer experience of using this feels enhanced, it&#39;s an abstraction that likely comes at a big cost if performance is the number one metric, but I haven&#39;t bothered with any benchmarks. It was mostly about having code that was easy to pick up again and reason with.</p>
<h2 id="conclusion">Conclusion</h2>
<p>At the moment, I am content with this implementation. As you&#39;ll see in some upcoming posts, it balances the complexity of the implementation with it&#39;s use-case power.</p>
<p>Please note that this is still early days and a little raw, which the end-game meaning to be a useful utility that might be a stepping stone towards something more powerful.</p>
<p>If you find some obvious improvements or have some feedback, please feel free to reach out to me on <a href="https://twitter.com/okeeffed" target="_blank" rel="noopener noreferrer">Twitter</a>. I would be very keen to hear your thoughts.</p>
<h2 id="resources-and-further-reading">Resources and further reading</h2>
<ul>
<li><a href="https://effect.website/docs/guides/error-management/two-error-types" target="_blank" rel="noopener noreferrer">EffectTS: Error Management Guide</a></li>
<li><a href="https://go.dev/blog/error-handling-and-go" target="_blank" rel="noopener noreferrer">Go Blog: Error Handling and Go</a></li>
<li><a href="https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html" target="_blank" rel="noopener noreferrer">Rust Book: Recoverable Errors with Result</a></li>
<li><a href="https://zod.dev/?id=safeparse" target="_blank" rel="noopener noreferrer">Zod Documentation: Safe Parsing</a></li>
<li><a href="https://joi.dev/api/?v=17.13.3#validate" target="_blank" rel="noopener noreferrer">Joi API Reference: Validation</a></li>
<li><a href="https://valibot.dev/api/safeParse/" target="_blank" rel="noopener noreferrer">Valibot API: Safe Parsing</a></li>
</ul>
<p><strong>Disclaimer</strong>: This blog post used AI to generate the images used for the analogy.</p>
<p><em>Photo credit: <a href="https://unsplash.com/@pawel_czerwinski" target="_blank" rel="noopener noreferrer"><code>pawel_czerwinski</code></a></em></p>]]></content>
<category term="nodejs"/><category term="typescript"/>
</entry>
<entry>
<title type="html"><![CDATA[Node.js Streams in Action with the AWS CDK]]></title>
<id>https://blog.dennisokeeffe.com/blog/2024-07-14-nodejs-streams-in-action-with-the-aws-cdk</id>
<link href="https://dennisokeeffe.com/blog/2024-07-14-nodejs-streams-in-action-with-the-aws-cdk"/>
<published>2024-07-14T00:00:00.000Z</published>
<updated>2024-07-14T00:00:00.000Z</updated>
<summary type="html"><![CDATA[We tie together everything that we've learned so far about Node.js streams and apply it through a practical example using the AWS CDK, AWS S3 and AWS Transfer Family.]]></summary>
<content type="html"><![CDATA[<h2 id="overview">Overview</h2>
<p>We&#39;ve come towards the end of the <a href="https://dennisokeeffe.com/blog/series/node-js-streams">Node.js Streams blog series</a>. Everything we&#39;ve covered so far will take you a long way in understanding how to work with streams in Node.js.</p>
<p>In this post, we&#39;re going to introduce the <code>PassThrough</code> stream type and then take everything that we&#39;ve learned so far and apply it through a practical (but somewhat contrived) example using the AWS CDK, AWS S3 and AWS Transfer Family.</p>
<p>If you&#39;re here because you are most interested in configuring AWS Transfer Family, then jump down to the project implementation.</p>
<span id="what-are-passthrough-streams" class="anchor-alias"><h2 id="what-are-pass-through-streams">What are PassThrough streams?</h2>
<p>Previously, we covered the four fundamental stream types:</p>
<ol>
<li><a href="https://dennisokeeffe.com/blog/2024-07-08-readable-streams-in-nodejs">Readable streams</a></li>
<li><a href="https://dennisokeeffe.com/blog/2024-07-10-writable-streams-in-nodejs">Writable streams</a></li>
<li><a href="https://dennisokeeffe.com/blog/2024-07-11-duplex-streams-in-nodejs">Duplex streams</a></li>
<li><a href="https://dennisokeeffe.com/blog/2024-07-12-transform-streams-in-nodejs">Transform streams</a></li>
</ol>
<p>So how does <code>stream.PassThrough</code> fit into the picture?</p>
<p>A <a href="https://nodejs.org/api/stream.html#class-streampassthrough" target="_blank" rel="noopener noreferrer"><strong>PassThrough stream</strong></a> in Node.js is a special type of <code>Transform</code> stream that simply passes data through from its writable side to its readable side without modifying it. It&#39;s essentially a &quot;do-nothing&quot; transform stream that can be useful in various scenarios.</p>
<p>In general, Node.js docs describe the <code>PassThrough</code> stream&#39;s primary purpose as being for examples and testing, but there are also some novel use cases for <code>PassThrough</code> streams.</p>
<p>Some of these novel uses of a <code>PassThrough</code> stream are:</p>
<ol>
<li>To provide a placeholder for a transform operation that may be added later.</li>
<li>To delay the piping of data from one stream to another.</li>
<li>To observe data passing through a pipeline without altering it.</li>
</ol>
<p>(3) is particularly useful when you are tying together third-party stream types that you want to pipe through pipelines or when demultiplexing streams. We will be using the <code>PassThrough</code> stream later in our example to do just that.</p>
<p>For clarity, it&#39;s not necessarily a tool that you&#39;ll use everyday, but I have found some use cases.</p>
<aside class="callout callout--warning" aria-label="Warning"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><path d="m21.7 18-8-14a2 2 0 0 0-3.4 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.7-3ZM12 9v4M12 17h.01" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>  <div class="callout__body">  <p>Full disclosure: I have found some use cases, but maybe you should scrutinize
my use case. There might be a better way to do it.</p> </div> </aside>
<span id="how-to-think-about-passthrough-streams" class="anchor-alias"><h2 id="how-to-think-about-pass-through-streams">How to think about PassThrough streams</h2>
<p>With our four fundamental data types, we&#39;ve used water-related analogies to help understand how streams work in Node.js.</p>
<ol>
<li><strong>Readable streams</strong>: Think of a water tap.</li>
<li><strong>Writable streams</strong>: Think of a water bucket.</li>
<li><strong>Duplex streams</strong>: A two-way water pipe (with valves to represent the read/write elements).</li>
<li><strong>Transform streams</strong>: A pipe going through a water treatment plant.</li>
</ol>
<p>So how do we think about <code>PassThrough</code> streams?</p>
<p>We can think of them as a <strong>clear, transparent water pipe</strong>. The water flows from one end to another <strong>without any modification</strong>.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2024-07-14-nodejs-streams-in-action-with-the-aws-cdk/clear-water-pipe.webp" alt="We think about Node.js PassThrough streams as a clear, transparent water pipe" loading="lazy" decoding="async"/><figcaption>We think about Node.js PassThrough streams as a clear, transparent water pipe</figcaption></figure>
<p>To push forward the idea of this analogy:</p>
<ul>
<li>Like a <strong>PassThrough</strong> stream, a transparent pipe section <strong>allows water to flow through without changing it</strong>.</li>
<li>You can <strong>observe the water (data)</strong> as it passes through.</li>
<li>It can be <strong>easily inserted into an existing pipeline</strong> without affecting the flow.</li>
<li>It provides a point where <strong>you could potentially add a &quot;filter&quot; or other &quot;treatment&quot;</strong> later if needed.</li>
</ul>
<p>With that out of the way, let&#39;s move on onto our project.</p>
<span id="uploading-and-downloading-a-csv-file-to-aws-s3-using-the-aws-cdk" class="anchor-alias"><h2 id="uploading-and-downloading-a-csv-file-to-aws-s-3-using-the-aws-cdk">Uploading and downloading a CSV file to AWS S3 using the AWS CDK</h2>
<p>Our project today is to create an &quot;upload&quot; and &quot;download&quot; script to exemplify some of the concepts we&#39;ve learned throughout the series.</p>
<p>Here is the overview of what we are building:</p>
<ul>
<li>We will be using the AWS CDK to create an S3 bucket and an AWS Transfer Family server.</li>
<li>We will create a special <strong>Duplex</strong> stream that will act as a demultiplexer.</li>
<li>Our upload script will have <strong>three pipelines</strong>: one to generate a CSV stream from an array of objects which is compressed and then demultiplexed, while the other two will be writable from our demux. One will upload the compressed CSV file to S3, and the other will SFTP the CSV file to our Transfer Family Server (and end up in the same bucket).</li>
<li>Our download script will have <strong>four pipelines</strong>: one to download our S3 upload file, decompress it and then demultiplex it. The two receivers from the demux will write our the CSV file and and YAML file to disk (just for fun). The fourth pipeline will download the compressed CSV file via SFTP from our Transfer Family Server, decompress it then write it to disk.</li>
</ul>
<p>You can see <a href="https://github.com/okeeffed/demo-stream-to-s3-and-back" target="_blank" rel="noopener noreferrer">the full project code on my GitHub repository</a>.</p>
<h2 id="prerequisites">Prerequisites</h2>
<p>It is expected that you are familiar with the following (at least to some extent):</p>
<ol>
<li><a href="https://docs.aws.amazon.com/cdk/v2/guide/home.html" target="_blank" rel="noopener noreferrer">AWS CDK</a></li>
<li><a href="https://aws.amazon.com/pm/serv-s3/" target="_blank" rel="noopener noreferrer">AWS S3</a></li>
<li><a href="https://aws.amazon.com/aws-transfer-family/" target="_blank" rel="noopener noreferrer">AWS Transfer Family</a></li>
<li><a href="https://dennisokeeffe.com/blog/series/node-js-streams">Have read the Node.js stream series</a></li>
</ol>
<h2 id="setting-up-the-project">Setting up the project</h2>
<p>First, we need to initialize our CDK TypeScript project. We can do that by running the following commands:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="s"><code>$ mkdir demo-stream-to-s3-and-back
$ cd demo-stream-to-s3-and-back
$ npx cdk init app --language typescript</code></pre>
<p>Once the project is initialized, we can install the necessary dependencies for our other scripts:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="s"><code>$ npm i @aws-sdk/client-s3 @aws-sdk/lib-storage fast-csv ssh2-sftp-client yaml
$ npm i --save-dev tsx</code></pre>
<p>Finally, let&#39;s configure some files for our project:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="s"><code>$ mkdir src
$ touch src/upload.ts src/download.ts src/</code></pre>
<h2 id="creating-an-asymmetric-ssh-key-pair">Creating an asymmetric SSH key pair</h2>
<p>First step is for us to create an asymmetric SSH key pair. We will use the public key for our SFTP server.</p>
<p>From the root directory of our project, run the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="s"><code>$ ssh-keygen -t rsa -b 2048 -f sftp_key</code></pre>
<p>Running this code will generate two files: <code>sftp_key</code> and <code>sftp_key.pub</code>. We will use the <strong>public key</strong> in our CDK code, while the <strong>private key</strong> will need to be referenced in our local code.</p>
<aside class="callout callout--warning" aria-label="Warning"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><path d="m21.7 18-8-14a2 2 0 0 0-3.4 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.7-3ZM12 9v4M12 17h.01" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>  <div class="callout__body">  <p>If you are committing you own work, make sure to git ignore these files.</p> </div> </aside>
<span id="creating-the-s3-bucket-and-transfer-family-server" class="anchor-alias"><h2 id="creating-the-s-3-bucket-and-transfer-family-server">Creating the S3 bucket and Transfer Family server</h2>
<p>Next, let&#39;s write our AWS CDK infrastructure code.</p>
<p>If you followed my naming convention above and called the project directory <code>demo-stream-to-s3-and-back</code>, then you will have a file <code>lib/demo-stream-to-s3-and-back-stack.ts</code> already created for you.</p>
<p>Update that file with the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import * as cdk from &quot;aws-cdk-lib&quot;;
import { Construct } from &quot;constructs&quot;;
import * as s3 from &quot;aws-cdk-lib/aws-s3&quot;;
import * as transfer from &quot;aws-cdk-lib/aws-transfer&quot;;
import * as iam from &quot;aws-cdk-lib/aws-iam&quot;;
import { readFileSync } from &quot;fs&quot;;
import * as path from &quot;path&quot;;

const SFTP_PUBLIC_KEY = readFileSync(path.join(__dirname, &quot;../sftp_key.pub&quot;));

export class DemoStreamToS3AndBackStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const bucket = new s3.Bucket(this, &quot;StreamToS3AndBackBucket&quot;, {
      bucketName: &quot;stream-to-s3-and-back&quot;,
      versioned: true,
      encryption: s3.BucketEncryption.S3_MANAGED,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });

    // Create an IAM role for SFTP users
    const sftpUserRole = new iam.Role(this, &quot;SftpUserRole&quot;, {
      assumedBy: new iam.ServicePrincipal(&quot;transfer.amazonaws.com&quot;),
    });

    // Grant read/write permissions to the S3 bucket
    bucket.grantReadWrite(sftpUserRole);

    // Create the SFTP server
    const server = new transfer.CfnServer(this, &quot;SftpServer&quot;, {
      protocols: [&quot;SFTP&quot;],
      identityProviderType: &quot;SERVICE_MANAGED&quot;,
      loggingRole: sftpUserRole.roleArn,
    });

    // Create an SFTP user
    const user = new transfer.CfnUser(this, &quot;SftpUser&quot;, {
      userName: &quot;testuser&quot;,
      serverId: server.attrServerId,
      role: sftpUserRole.roleArn,
      homeDirectory: `/${bucket.bucketName}`,
      sshPublicKeys: [SFTP_PUBLIC_KEY.toString()],
    });

    // Output the SFTP server endpoint
    new cdk.CfnOutput(this, &quot;SftpEndpoint&quot;, {
      value: `${server.attrServerId}.server.transfer.${this.region}.amazonaws.com`,
      description: &quot;SFTP Server Endpoint&quot;,
    });
  }
}</code></pre>
<p>The above code does the following:</p>
<ul>
<li><strong>Import Necessary Modules:</strong> Import AWS CDK libraries, Node.js modules for file handling, and necessary AWS services.</li>
<li><strong>Read SFTP Public Key:</strong> Read the SFTP public key from a file.</li>
<li><strong>Define the Stack Class:</strong> Define a class that extends the CDK <code>Stack</code> to encapsulate all resources.</li>
<li><strong>Create an S3 Bucket:</strong> Create an S3 bucket with versioning, encryption, and a removal policy.</li>
<li><strong>Create an IAM Role for SFTP Users:</strong> Create an IAM role to be assumed by the SFTP service.</li>
<li><strong>Grant Permissions to the S3 Bucket:</strong> Grant read and write permissions to the IAM role for the S3 bucket.</li>
<li><strong>Create the SFTP Server:</strong> Create an SFTP server using AWS Transfer Family with the IAM role for logging.</li>
<li><strong>Create an SFTP User:</strong> Create an SFTP user associated with the server and provide the SSH public key.</li>
<li><strong>Output the SFTP Server Endpoint:</strong> Output the SFTP server endpoint URL for connecting to the server.</li>
</ul>
<p>We are referencing the same S3 bucket we will use in uploads for our SFTP server to simplify things.</p>
<p>Once this is done, we can deploy our stack with the following command:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="s"><code>$ npx cdk deploy</code></pre>
<p>The above step <strong>expects</strong> that your AWS credentials are set up correctly. If you have not done so, please follow a guide <a href="https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html" target="_blank" rel="noopener noreferrer">like this one for the AWS CLI</a>.</p>
<p>Make sure to take note of the output for the SFTP server endpoint.</p>
<p>Once that is done, let&#39;s move onto our code to interface with our infrastructure.</p>
<h2 id="writing-our-demultiplexer">Writing our demultiplexer</h2>
<p>We covered a version of multiplexing in our <a href="https://dennisokeeffe.com/blog/2024-07-11-duplex-streams-in-nodejs#use-cases-for-duplex-streams">Duplex streams post</a>. In this case, we will be writing a demultiplexer.</p>
<p>Our demultiplexer will take in <code>PassThrough</code> stream types in the constructor and ensure that they are written to when we are processing data.</p>
<p>Let&#39;s see how that looks. Add the following to <code>src/demultiplexer.ts</code>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import { Duplex, PassThrough } from &quot;node:stream&quot;;

/**
 * We create a demultiplexer to write out to two different
 * streams.
 *
 * 1. To handle writing the CSV file.
 * 2. To handle writing the JSON file.
 *
 * Reading is handled by the pipeline implementation.
 */
export class Demultiplexer extends Duplex {
  private outputStreams: PassThrough[];

  constructor(outputStreams: PassThrough[]) {
    super({
      objectMode: true,
    });
    this.outputStreams = outputStreams;
  }

  _write(
    chunk: any,
    _encoding: BufferEncoding,
    callback: (error?: Error | null) =&gt; void
  ): void {
    for (const outputStream of this.outputStreams) {
      outputStream.write(chunk);
    }
    callback();
  }

  _final(callback: (error?: Error | null) =&gt; void): void {
    for (const outputStream of this.outputStreams) {
      outputStream.end();
    }
    callback();
  }
}</code></pre>
<p>Our demultiplexer has implementations for both <code>_write</code> and <code>_final</code>. The <code>_write</code> method writes the chunk to all the output streams, while the <code>_final</code> method ends all the output streams.</p>
<p>Although this is not use case, a simple example of this in use:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import { PassThrough } from &quot;node:stream&quot;;

const outputStream1 = new PassThrough();
const outputStream2 = new PassThrough();
const demux = new Demultiplexer([outputStream1, outputStream2]);</code></pre>
<p>This would configure our multiplex to write messages to both <code>outputStream1</code> and <code>outputStream2</code>.</p>
<p>The implementation itself isn&#39;t super flexible. In hindsight, you would ideally allow anything that is a <code>Writable</code> stream to be passed in. But it will suffice for our example (and refactoring this could be some nice homework).</p>
<h2 id="writing-our-upload-script">Writing our upload script</h2>
<p>Now that we have our infrastructure set up and our demultiplexer ready, let&#39;s write our upload script.</p>
<p>Add the following to <code>src/upload.ts</code>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import { readFileSync } from &quot;fs&quot;;
import { pipeline } from &quot;node:stream/promises&quot;;
import { format } from &quot;fast-csv&quot;;
import { createGzip } from &quot;node:zlib&quot;;
import { S3Client } from &quot;@aws-sdk/client-s3&quot;;
import { PassThrough, Readable } from &quot;node:stream&quot;;
import { Upload } from &quot;@aws-sdk/lib-storage&quot;;
import { Demultiplexer } from &quot;./demultiplexer&quot;;
import * as path from &quot;node:path&quot;;
import Client = require(&quot;ssh2-sftp-client&quot;);

const BUCKET_NAME = &quot;stream-to-s3-and-back&quot;;
const sftp = new Client();
const s3Client = new S3Client();

const config = {
  host: &quot;cdk-output.server.transfer.ap-southeast-2.amazonaws.com&quot;,
  port: 22,
  username: &quot;testuser&quot;,
  privateKey: readFileSync(path.join(__dirname, &quot;../sftp_key&quot;)),
};

async function uploadToS3() {
  try {
    const data = [
      {
        flavor: &quot;vanilla&quot;,
        topping: &quot;sprinkles&quot;,
      },
      {
        flavor: &quot;chocolate&quot;,
        topping: &quot;fudge&quot;,
      },
      {
        flavor: &quot;strawberry&quot;,
        topping: &quot;whipped cream&quot;,
      },
    ];

    await sftp.connect(config);
    console.log(&quot;SFTP connected successfully&quot;);

    // We use a PassThrough to support uploading
    // via the pipeline API.
    const uploadToS3PassThrough = new PassThrough();
    const upload = new Upload({
      client: s3Client,
      params: {
        Bucket: BUCKET_NAME,
        Key: &quot;output.csv.gz&quot;,
        Body: uploadToS3PassThrough,
      },
    });

    // PassThroughs for Demux
    const s3UploadPassThrough = new PassThrough({
      objectMode: true,
    });

    const sftpUploadPassThrough = new PassThrough({
      objectMode: true,
    });
    const demux = new Demultiplexer([
      s3UploadPassThrough,
      sftpUploadPassThrough,
    ]);

    await Promise.all([
      pipeline(
        Readable.from(data),
        format({ quoteColumns: true, headers: [&quot;flavor&quot;, &quot;topping&quot;] }),
        createGzip(),
        demux
      ),
      pipeline(s3UploadPassThrough, uploadToS3PassThrough),
      pipeline(
        sftpUploadPassThrough,
        sftp.createWriteStream(`/${BUCKET_NAME}/sftp-upload.csv.gz`)
      ),
    ]);

    await sftp.end();
    await upload.done();

    console.log(&quot;File processed and uploaded successfully&quot;);
  } catch (error) {
    console.error(&quot;Error:&quot;, error);
  }
}

async function main() {
  await uploadToS3();
}

main();</code></pre>
<aside class="callout callout--warning" aria-label="Warning"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><path d="m21.7 18-8-14a2 2 0 0 0-3.4 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.7-3ZM12 9v4M12 17h.01" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>  <div class="callout__body">  <p>Be sure to replace <code>cdk-output.server.transfer.ap-southeast-2.amazonaws.com</code>
with the endpoint you received from the CDK output.</p> </div> </aside>
<p>This script is more complex than the others we&#39;ve written so far. Although <em>it is a little contrived</em>. Here&#39;s a breakdown of what it does:</p>
<ul>
<li><strong>Import Necessary Modules:</strong> Import various Node.js and AWS SDK modules for file handling, streaming, CSV formatting, compression, and S3 operations.</li>
<li><strong>Define Constants:</strong> Define the S3 bucket name, SFTP client instance, and S3 client instance.</li>
<li><strong>Read SFTP Private Key:</strong> Read the SFTP private key from a file.</li>
<li><strong>Set SFTP Configuration:</strong> Set up the SFTP connection configuration.</li>
<li><strong>Define <code>uploadToS3</code> Function:</strong> Define an asynchronous function to handle the data processing and uploading.</li>
<li><strong>Prepare Data:</strong> Create an array of data to be uploaded.</li>
<li><strong>Connect to SFTP:</strong> Connect to the SFTP server using the configuration.</li>
<li><strong>Create PassThrough Streams:</strong> Create PassThrough streams to facilitate the pipeline process.</li>
<li><strong>Create S3 Upload Object:</strong> Create an upload object for S3 using the AWS SDK.</li>
<li><strong>Create Demultiplexer:</strong> Create a Demultiplexer instance to split the data stream.</li>
<li><strong>Setup Pipelines:</strong> Set up parallel pipelines for processing data, compressing it, and sending it to both S3 and SFTP.</li>
<li><strong>End SFTP Connection:</strong> End the SFTP connection after the upload is complete.</li>
<li><strong>Log Success or Error:</strong> Log the success message or handle errors if they occur.</li>
<li><strong>Define <code>main</code> Function:</strong> Define a main function to call <code>uploadToS3</code>.</li>
<li><strong>Execute <code>main</code> Function:</strong> Execute the main function to start the process.</li>
</ul>
<p>You can visualize the process a bit better with the following flow chart:</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2024-07-14-nodejs-streams-in-action-with-the-aws-cdk/upload-flow-chart.webp" alt="Upload script flow chart" loading="lazy" decoding="async"/><figcaption>Upload script flow chart</figcaption></figure>
<p>The significant part is our pipelines:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>await Promise.all([
  pipeline(
    Readable.from(data),
    format({ quoteColumns: true, headers: [&quot;flavor&quot;, &quot;topping&quot;] }),
    createGzip(),
    demux
  ),
  pipeline(s3UploadPassThrough, uploadToS3PassThrough),
  pipeline(
    sftpUploadPassThrough,
    sftp.createWriteStream(`/${BUCKET_NAME}/sftp-upload.csv.gz`)
  ),
]);</code></pre>
<p>We are using the <code>pipeline</code> function to chain together our streams.</p>
<p>The first pipeline generates the CSV data, formats it, compresses it, and then demultiplexes it. The second pipeline sends the data to S3. The third pipeline sends the data to the SFTP server.</p>
<p>Our second and third streams are receivers for our demultiplexer. The <code>PassThrough</code> is used as both the destination of our compression pipeline, and is also the source for our second and third pipelines that are doing two very different things.</p>
<p>It is also important for us to await the end of the remote streams in our case. That is where the following code comes into play:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>await sftp.end();
await upload.done();</code></pre>
<p>Without it, our script would end prematurely before the data is fully uploaded.</p>
<p>Next, we can run our script with the following command:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="s"><code>$ npx tsx src/upload.ts
SFTP connected successfully
File processed and uploaded successfully</code></pre>
<aside class="callout callout--warning" aria-label="Warning"> <span class="callout__icon" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22"><path d="m21.7 18-8-14a2 2 0 0 0-3.4 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.7-3ZM12 9v4M12 17h.01" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>  <div class="callout__body">  <p>The above assumes that you&#39;ve configured your AWS credentials correctly and
already deployed your infrastructure.</p> </div> </aside>
<p>At this point, if you jump onto your AWS S3 bucket, you should see a file called <code>output.csv.gz</code> that contains the compressed CSV file as well as a file called <code>sftp-upload.csv.gz</code> that contains the same file that we uploaded through SFTP.</p>
<h2 id="writing-our-download-script">Writing our download script</h2>
<p>At this point, we have successfully uploaded a file to S3 and SFTP. Now, let&#39;s write a script to download the file from S3 and SFTP, decompress it, and write it to disk.</p>
<p>Add the following to <code>src/download.ts</code>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import { createWriteStream, readFileSync } from &quot;node:fs&quot;;
import { parse } from &quot;fast-csv&quot;;
import { createGunzip } from &quot;node:zlib&quot;;
import { S3Client, GetObjectCommand } from &quot;@aws-sdk/client-s3&quot;;
import { PassThrough, Readable, Transform } from &quot;node:stream&quot;;
import { pipeline } from &quot;node:stream/promises&quot;;
import { stringify } from &quot;yaml&quot;;
import { Demultiplexer } from &quot;./demultiplexer&quot;;
import * as path from &quot;node:path&quot;;
import Client = require(&quot;ssh2-sftp-client&quot;);

const BUCKET_NAME = &quot;stream-to-s3-and-back&quot;;
const sftp = new Client();
const s3Client = new S3Client();

const config = {
  host: &quot;cdk-output.server.transfer.ap-southeast-2.amazonaws.com&quot;,
  port: 22,
  username: &quot;testuser&quot;,
  privateKey: readFileSync(path.join(__dirname, &quot;../sftp_key&quot;)),
};

const yamlTransformStream = new Transform({
  objectMode: true,
  transform(chunk, _encoding, callback) {
    this.push(stringify([chunk]) + &quot;\n&quot;);
    callback();
  },
});

async function downloadFromS3() {
  try {
    await sftp.connect(config);
    console.log(&quot;SFTP connected successfully&quot;);

    const { Body: s3UploadBody } = await s3Client.send(
      new GetObjectCommand({
        Bucket: BUCKET_NAME,
        Key: &quot;output.csv.gz&quot;,
      })
    );

    if (s3UploadBody instanceof Readable === false) {
      throw new Error(&quot;S3 object body is not a readable stream&quot;);
    }

    const csvStreamPassThrough = new PassThrough({
      objectMode: true,
    });
    const csvOutput = createWriteStream(&quot;downloaded_output.csv&quot;);

    const yamlStreamPassThrough = new PassThrough({
      objectMode: true,
    });
    const yamlOutput = createWriteStream(&quot;downloaded_output.yaml&quot;);

    const demux = new Demultiplexer([
      csvStreamPassThrough,
      yamlStreamPassThrough,
    ]);

    // Create a write stream for the SFTP CSV output
    const sftpCsvOutput = createWriteStream(&quot;downloaded_sftp_output.csv&quot;);
    const sftpReadableStream = sftp.createReadStream(&quot;output.csv.gz&quot;);

    // Buffer everything into the demux stream
    await Promise.all([
      // S3 download streams
      pipeline(s3UploadBody, createGunzip(), demux),
      pipeline(csvStreamPassThrough, csvOutput),
      pipeline(
        yamlStreamPassThrough,
        parse({ headers: true, objectMode: true }),
        yamlTransformStream,
        yamlOutput
      ),
      // SFTP download stream
      pipeline(sftpReadableStream, createGunzip(), sftpCsvOutput),
    ]);

    await sftp.end();

    console.log(&quot;File downloaded, decompressed, and processed successfully&quot;);
  } catch (error) {
    console.error(&quot;Error:&quot;, error);
  }
}

async function main() {
  await downloadFromS3();
}

main();</code></pre>
<p>Again, you will need to update your SFTP endpoint in the script.</p>
<p>Let&#39;s breakdown what we are doing here:</p>
<ul>
<li><strong>Import Necessary Modules:</strong> Import Node.js and AWS SDK modules for file handling, streaming, CSV parsing, decompression, YAML stringifying, and S3 operations.</li>
<li><strong>Define Constants:</strong> Define the S3 bucket name, SFTP client instance, and S3 client instance.</li>
<li><strong>Read SFTP Private Key:</strong> Read the SFTP private key from a file.</li>
<li><strong>Set SFTP Configuration:</strong> Set up the SFTP connection configuration.</li>
<li><strong>Define YAML Transform Stream:</strong> Create a transform stream to convert CSV data to YAML format.</li>
<li><strong>Define <code>downloadFromS3</code> Function:</strong> Define an asynchronous function to handle data downloading, decompressing, and processing.</li>
<li><strong>Connect to SFTP:</strong> Connect to the SFTP server using the configuration.</li>
<li><strong>Download S3 Object:</strong> Use AWS SDK to download the compressed CSV file from S3.</li>
<li><strong>Check S3 Body Type:</strong> Ensure the downloaded S3 object body is a readable stream.</li>
<li><strong>Create PassThrough Streams:</strong> Create PassThrough streams to facilitate the pipeline process.</li>
<li><strong>Create Write Streams:</strong> Create write streams for local CSV and YAML files, and for SFTP CSV output.</li>
<li><strong>Create Demultiplexer:</strong> Create a Demultiplexer instance to split the data stream into CSV and YAML streams.</li>
<li><strong>Setup Pipelines:</strong> Set up parallel pipelines for decompressing S3 data, splitting streams, and writing to local files.</li>
<li><strong>Decompress SFTP Data:</strong> Set up a pipeline to download, decompress, and write SFTP CSV data to a local file.</li>
<li><strong>End SFTP Connection:</strong> End the SFTP connection after the download and processing are complete.</li>
<li><strong>Log Success or Error:</strong> Log the success message or handle errors if they occur.</li>
<li><strong>Define <code>main</code> Function:</strong> Define a main function to call <code>downloadFromS3</code>.</li>
<li><strong>Execute <code>main</code> Function:</strong> Execute the main function to start the process.</li>
</ul>
<p>Another flow chart to help visualize how the code is executing:</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2024-07-14-nodejs-streams-in-action-with-the-aws-cdk/download-flow-chart.webp" alt="Download script flow chart" loading="lazy" decoding="async"/><figcaption>Download script flow chart</figcaption></figure>
<p>Again, it&#39;s the pipelines that play the most integral role:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>// Buffer everything into the demux stream
await Promise.all([
  // S3 download streams
  pipeline(s3UploadBody, createGunzip(), demux),
  pipeline(csvStreamPassThrough, csvOutput),
  pipeline(
    yamlStreamPassThrough,
    parse({ headers: true, objectMode: true }),
    yamlTransformStream,
    yamlOutput
  ),
  // SFTP download stream
  pipeline(sftpReadableStream, createGunzip(), sftpCsvOutput),
]);</code></pre>
<p>Our first three pipelines work with the multiplexer to download the content from S3, then split that content into streams that write a CSV file and a YAML file respectively.</p>
<p>The fourth pipeline downloads the compressed CSV file from SFTP, decompresses it, and writes it to disk.</p>
<p>We can run this script with the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="s"><code>$ npx tsx src/download.ts

SFTP connected successfully
File downloaded, decompressed, and processed successfully</code></pre>
<p>We will end up with three files written to disk:</p>
<ol>
<li><code>downloaded_output.csv</code>: The CSV file downloaded from S3.</li>
<li><code>downloaded_output.yaml</code>: The YAML file generated from the CSV file.</li>
<li><code>downloaded_sftp_output.csv</code>: The CSV file downloaded from the SFTP server.</li>
</ol>
<p>The content of (1) and (3) should be identical:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="csv"><code>&quot;flavor&quot;,&quot;topping&quot;
&quot;vanilla&quot;,&quot;sprinkles&quot;
&quot;chocolate&quot;,&quot;fudge&quot;
&quot;strawberry&quot;,&quot;whipped cream&quot;</code></pre>
<p>While the content of (2) should be:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="yaml"><code>- flavor: vanilla
  topping: sprinkles

- flavor: chocolate
  topping: fudge

- flavor: strawberry
  topping: whipped cream</code></pre>
<p>Please be aware that awaiting the end of the remote streams is important again. We run into the same problem of prematurely ending the stream without it.</p>
<p>Otherwise, we have successfully uploaded a file to S3 and SFTP, downloaded it, decompressed it, and written it to disk!</p>
<h2 id="teardown">Teardown</h2>
<p>Once you are done with the project, you can tear down the infrastructure with the following command:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="s"><code>$ npx cdk destroy</code></pre>
<p>Please be aware that all the content from the bucket needs to be deleted beforehand. If you get an error teardown, head to the AWS console and manually delete the stack and bucket... and definitely please do. Transfer Family is sadly a bit $$$.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Congratulations on making it through the <a href="https://dennisokeeffe.com/blog/series/node-js-streams">Node.js Streams blog series</a>! By now, you should have a solid understanding of how to work with streams in Node.js, including the different types of streams and how they can be utilized.</p>
<p>In this post, we introduced the <code>PassThrough</code> stream and demonstrated its use in a practical example involving AWS CDK, AWS S3, and AWS Transfer Family. We explored how to create a pipeline for uploading and downloading files using Node.js streams, showcasing the versatility and power of streams in handling complex data flows.</p>
<p>Our key takeaways and lessons today:</p>
<ul>
<li><strong>PassThrough Streams:</strong> These streams allow data to pass through without modification, making them useful for observing data flow, delaying piping, or providing placeholders for future transformations.</li>
<li><strong>AWS Integration:</strong> Leveraging AWS CDK, S3, and Transfer Family, we created a robust data pipeline for handling file uploads and downloads.</li>
<li><strong>Demultiplexer:</strong> Implementing a custom demultiplexer stream allowed us to split data streams into multiple outputs efficiently.</li>
<li><strong>Practical Application:</strong> By applying these concepts, we demonstrated how to handle real-world scenarios involving data processing and transfer in a scalable and efficient manner.</li>
</ul>
<p>This journey through Node.js streams, from understanding the basics to implementing advanced pipelines, equips you with the knowledge to tackle various data processing tasks. Streams provide a powerful abstraction for handling data flows, and with the skills you&#39;ve gained, you can now confidently apply them to your projects.</p>
<h2 id="resources-and-further-reading">Resources and further reading</h2>
<ul>
<li><a href="https://dennisokeeffe.com/blog/2024-07-05-understanding-nodejs-streams">Understanding Node.js Streams Blog Post</a></li>
<li><a href="https://dennisokeeffe.com/blog/2024-07-08-readable-streams-in-nodejs">Readable Streams in Node.js</a></li>
<li><a href="https://dennisokeeffe.com/blog/2024-07-10-writable-streams-in-nodejs">Writable Streams in Node.js</a></li>
<li><a href="https://dennisokeeffe.com/blog/2024-07-11-duplex-streams-in-nodejs">Duplex Streams in Node.js</a></li>
<li><a href="https://dennisokeeffe.com/blog/2024-07-12-transform-streams-in-nodejs">Transform Streams in Node.js</a></li>
<li><a href="https://nodejs.org/api/stream.html#streampipelinesource-transforms-destination-options" target="_blank" rel="noopener noreferrer">Node.js Stream Pipeline API Documentation</a></li>
<li><a href="https://dennisokeeffe.com/blog/series/node-js-streams">Node.js Streams series</a></li>
<li><a href="https://dennisokeeffe.com/blog/2024-07-12-transform-streams-in-nodejs">Transform streams</a></li>
<li><a href="https://nodejs.org/api/stream.html#class-streampassthrough" target="_blank" rel="noopener noreferrer">PassThrough stream</a></li>
<li><a href="https://docs.aws.amazon.com/cdk/v2/guide/home.html" target="_blank" rel="noopener noreferrer">AWS CDK</a></li>
<li><a href="https://aws.amazon.com/pm/serv-s3/" target="_blank" rel="noopener noreferrer">AWS S3</a></li>
<li><a href="https://aws.amazon.com/aws-transfer-family/" target="_blank" rel="noopener noreferrer">AWS Transfer Family</a></li>
<li><a href="https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html" target="_blank" rel="noopener noreferrer">AWS CLI configuration guide</a></li>
<li><a href="https://github.com/okeeffed/demo-stream-to-s3-and-back" target="_blank" rel="noopener noreferrer">GitHub repository for the full project code</a></li>
</ul>
<p><strong>Disclaimer</strong>: This blog post used AI to generate the images used for the analogy.</p>
<p><em>Photo credit: <a href="https://unsplash.com/@robanderson72" target="_blank" rel="noopener noreferrer"><code>robanderson72</code></a></em></p>]]></content>
<category term="nodejs"/>
</entry>
<entry>
<title type="html"><![CDATA[Stream Pipelines in Node.js]]></title>
<id>https://blog.dennisokeeffe.com/blog/2024-07-13-stream-pipelines-in-nodejs</id>
<link href="https://dennisokeeffe.com/blog/2024-07-13-stream-pipelines-in-nodejs"/>
<published>2024-07-13T00:00:00.000Z</published>
<updated>2024-07-13T00:00:00.000Z</updated>
<summary type="html"><![CDATA[We looking into the pipeline function from the Node.js stream module.]]></summary>
<content type="html"><![CDATA[<h2 id="overview">Overview</h2>
<p>During the <a href="https://dennisokeeffe.com/blog/series/node-js-streams">Node.js Streams blog series</a>, we covered the <a href="https://dennisokeeffe.com/blog/2024-07-05-understanding-nodejs-streams">core fundamentals you should know about Node.js streams</a> and spent a blog post diving into both <a href="https://dennisokeeffe.com/blog/2024-07-08-readable-streams-in-nodejs"><strong>Readable streams</strong></a> and <a href="https://dennisokeeffe.com/blog/2024-07-10-writable-streams-in-nodejs"><strong>Writable</strong> streams</a>, we&#39;ve covered the fundamentals of what you should know about streams in Node.js, as well as the four fundamental stream types.</p>
<p>In this post, we are diving into to the <strong>pipeline</strong> function from the Node.js stream module: how it can be of use and how we can use it for asynchronous operations.</p>
<h2 id="how-to-think-about-pipeline-streams">How to think about pipeline streams</h2>
<p>The <code>stream.pipeline</code> function is a utility function that allows us to easily pipe streams together. It is very aptly name, so you can think of it as our water network that bridges between the different streams in our analogies that we&#39;ve used so far.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2024-07-13-stream-pipelines-in-nodejs/pipelines-analogy.webp" alt="Water buckets connected by pipes can be used to think of pipeline streams in Node.js" loading="lazy" decoding="async"/><figcaption>Water buckets connected by pipes can be used to think of pipeline streams in Node.js</figcaption></figure>
<h2 id="creating-our-first-pipeline">Creating our first pipeline</h2>
<p>We can export the <code>pipeline</code> function from the <code>node:stream</code> module and use it to connect streams together.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const { pipeline, Readable, Writable } = require(&quot;node:stream&quot;);

const data = [&quot;Hello&quot;, &quot;World!&quot;];
const readStream = Readable.from(data);

// Create a writable function
const writeStream = new Writable({
  write(chunk, _encoding, callback) {
    console.log(&quot;Writing:&quot;, chunk.toString());
    callback();
  },
});

pipeline(readStream, writeStream, (err) =&gt; {
  if (err) {
    console.error(&quot;Pipeline failed.&quot;, err);
  } else {
    console.log(&quot;Pipeline succeeded.&quot;);
  }
});</code></pre>
<p>In the above example, we create a <code>Readable</code> stream from an array of strings and a <code>Writable</code> stream that logs the data to the console. We then use the <code>pipeline</code> function to connect the two streams together.</p>
<p>Running it gives the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>$ node pipelines/simple.js
Writing: Hello
Writing: World!
Pipeline succeeded.</code></pre>
<p>This simple example demonstrates how we can use the <code>pipeline</code> function to connect streams together.</p>
<p>For a more complex pipeline (well, more complex than above), we can connect multiple streams together:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const { pipeline, Readable, Writable, Transform } = require(&quot;node:stream&quot;);

const data = [&quot;Hello&quot;, &quot;World!&quot;];
const readStream = Readable.from(data);

// Create a writable function
const writeStream = new Writable({
  write(chunk, _encoding, callback) {
    console.log(&quot;Writing:&quot;, chunk.toString());
    callback();
  },
});

const lowerCaseTransform = new Transform({
  transform(chunk, _encoding, callback) {
    let str = chunk.toString();
    this.push(`${str.slice(0, 1).toUpperCase()}${str.slice(1)}`);
    callback();
  },
});

const addSuffixTransform = new Transform({
  transform(chunk, _encoding, callback) {
    this.push(`${chunk.toString()}-0123`);
    callback();
  },
});

pipeline(
  readStream,
  lowerCaseTransform,
  addSuffixTransform,
  writeStream,
  (err) =&gt; {
    if (err) {
      console.error(&quot;Pipeline failed.&quot;, err);
    } else {
      console.log(&quot;Pipeline succeeded.&quot;);
    }
  }
);</code></pre>
<p>In this case, we add in a couple of transforms just to make some string modifications.</p>
<p>The result:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>$ node pipelines/slightly-more-complex.js
Writing: hello-0123
Writing: world!-0123
Pipeline succeeded.</code></pre>
<p>The follow sequence diagram can you help you visualize how the pipeline is operating:</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2024-07-13-stream-pipelines-in-nodejs/pipeline-1.webp" alt="Sequence diagram for our slightly-more-complex pipeline" loading="lazy" decoding="async"/><figcaption>Sequence diagram for our slightly-more-complex pipeline</figcaption></figure>
<h2 id="why-use-the-pipeline-function">Why use the pipeline function?</h2>
<p>The <code>pipeline</code> function from the <code>stream</code> module in Node.js offers several important benefits:</p>
<ol>
<li>
<p><strong>Automatic error handling</strong>:</p>
<ul>
<li><code>pipeline</code> automatically handles errors from any of the streams in the pipeline.</li>
<li>It ensures that if an error occurs in any stream, all streams are properly closed.</li>
<li>This prevents potential memory leaks and resource issues.</li>
</ul>
</li>
<li>
<p><strong>Simplified code</strong>:</p>
<ul>
<li>It reduces the boilerplate code needed to properly manage multiple streams.</li>
<li>You don&#39;t need to manually handle &#39;error&#39; and &#39;end&#39; events for each stream.</li>
</ul>
</li>
<li>
<p><strong>Backpressure management</strong>:</p>
<ul>
<li><code>pipeline</code> automatically manages backpressure between the streams.</li>
<li>It ensures that faster streams don&#39;t overwhelm slower ones by pausing and resuming as needed.</li>
</ul>
</li>
<li>
<p><strong>Cleanup on completion</strong>:</p>
<ul>
<li>It automatically cleans up resources when the pipeline finishes or errors out.</li>
<li>This includes closing file descriptors and freeing up memory.</li>
</ul>
</li>
<li>
<p><strong>Promise support</strong>:</p>
<ul>
<li>When used with <code>util.promisify</code> or when directly imported from <code>node:stream/promises</code>, it can return a <code>Promise</code>, allowing for easier use with async/await syntax.</li>
</ul>
</li>
<li>
<p><strong>Multiple streams</strong>:</p>
<ul>
<li>It can handle any number of streams, making it easy to create complex data processing pipelines.</li>
</ul>
</li>
<li>
<p><strong>Improved performance</strong>:</p>
<ul>
<li>The internal implementation is optimized for performance, especially when dealing with object mode streams.</li>
</ul>
</li>
<li>
<p><strong>Consistent behavior</strong>:</p>
<ul>
<li>It ensures consistent behavior across different types of streams (<strong>readable</strong>, <strong>writable</strong>, <strong>transform</strong>).</li>
</ul>
</li>
<li>
<p><strong>Error propagation</strong>:</p>
<ul>
<li>Errors are properly propagated through the pipeline, ensuring that error handling is centralized and reliable.</li>
</ul>
</li>
<li>
<p><strong>Ordering guarantee</strong>:</p>
<ul>
<li>It guarantees that data will be processed in the order it&#39;s received, which is particularly important for certain types of data processing.</li>
</ul>
</li>
</ol>
<p>Here&#39;s another simple example to illustrate its use with other Node.js modules:</p>
<p>Using <code>pipeline</code> is generally considered a best practice when working with streams in Node.js due to these benefits.</p>
<h2 id="asynchronous-streams">Asynchronous streams</h2>
<p>What happens if our streams happen to be asynchronous? We can import <code>pipeline</code> from the <code>node:stream/promises</code> module to handle this.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const { Readable, Transform, Writable } = require(&quot;node:stream&quot;);
const { pipeline } = require(&quot;node:stream/promises&quot;);

// Simulated async operation
const asyncOperation = (num) =&gt; {
  return new Promise((resolve) =&gt; {
    setTimeout(() =&gt; {
      resolve(num * 2);
    }, Math.random() * 1000); // Random delay to simulate varying processing times
  });
};

// Source stream
class NumberSource extends Readable {
  constructor(options) {
    super(options);
    this.current = 0;
    this.max = 10;
  }

  _read() {
    if (this.current &lt; this.max) {
      this.push(this.current.toString());
      this.current++;
    } else {
      this.push(null);
    }
  }
}

// Async transform stream
class AsyncDoubler extends Transform {
  constructor(options) {
    super({ ...options, objectMode: true });
  }

  _transform(chunk, encoding, callback) {
    const num = parseInt(chunk, 10);
    asyncOperation(num)
      .then((result) =&gt; {
        this.push(`${num} doubled is ${result}\n`);
        callback();
      })
      .catch(callback);
  }
}

// Destination stream
class ConsoleWriter extends Writable {
  _write(chunk, encoding, callback) {
    console.log(chunk.toString().trim());
    callback();
  }
}

async function runPipeline() {
  try {
    await pipeline(new NumberSource(), new AsyncDoubler(), new ConsoleWriter());
    console.log(&quot;Pipeline succeeded&quot;);
  } catch (err) {
    console.error(&quot;Pipeline failed&quot;, err);
  }
}

runPipeline();</code></pre>
<p>In this example, we have a <code>NumberSource</code> readable stream that emits numbers from 0 to 9, an <code>AsyncDoubler</code> transform stream that doubles the numbers asynchronously, and a <code>ConsoleWriter</code> writable stream that logs the doubled numbers to the console.</p>
<p>Running the code gives the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>$ node pipelines/async.js
0 doubled is 0
1 doubled is 2
2 doubled is 4
3 doubled is 6
4 doubled is 8
5 doubled is 10
6 doubled is 12
7 doubled is 14
8 doubled is 16
9 doubled is 18
Pipeline succeeded</code></pre>
<p>We can visualize the asynchronous pipeline as follows:</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2024-07-13-stream-pipelines-in-nodejs/pipeline-2.webp" alt="Sequence diagram for our asynchronous pipeline" loading="lazy" decoding="async"/><figcaption>Sequence diagram for our asynchronous pipeline</figcaption></figure>
<p>If we had tried running the <code>pipeline</code> without supporting async, the code would have executed without waiting:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>// WHAT NOT TO DO
const { pipeline, Readable, Transform, Writable } = require(&quot;node:stream&quot;);
// const { pipeline } = require(&quot;stream/promises&quot;);

// Simulated async operation
const asyncOperation = (num) =&gt; {
  return new Promise((resolve) =&gt; {
    setTimeout(() =&gt; {
      resolve(num * 2);
    }, Math.random() * 1000); // Random delay to simulate varying processing times
  });
};

// Source stream
class NumberSource extends Readable {
  constructor(options) {
    super(options);
    this.current = 0;
    this.max = 10;
  }

  _read() {
    if (this.current &lt; this.max) {
      this.push(this.current.toString());
      this.current++;
    } else {
      this.push(null);
    }
  }
}

// Async transform stream
class AsyncDoubler extends Transform {
  constructor(options) {
    super({ ...options, objectMode: true });
  }

  _transform(chunk, encoding, callback) {
    const num = parseInt(chunk, 10);
    asyncOperation(num)
      .then((result) =&gt; {
        this.push(`${num} doubled is ${result}\n`);
        callback();
      })
      .catch(callback);
  }
}

// Destination stream
class ConsoleWriter extends Writable {
  _write(chunk, encoding, callback) {
    console.log(chunk.toString().trim());
    callback();
  }
}

async function runPipeline() {
  try {
    pipeline(
      new NumberSource(),
      new AsyncDoubler(),
      new ConsoleWriter(),
      (err) =&gt; {
        if (err) {
          console.error(&quot;Pipeline failed&quot;, err);
        } else {
          console.log(&quot;Pipeline succeeded&quot;);
        }
      }
    );

    console.log(&quot;HERE ALREADY&quot;);
    process.exit();
  } catch (err) {
    console.error(&quot;Pipeline failed&quot;, err);
  }
}

runPipeline();</code></pre>
<p>Running the above would exit the code before the streams ran.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>$ node pipelines/async-incorrect.js
HERE ALREADY</code></pre>
<h2 id="multiplexer-example">Multiplexer example</h2>
<p>We won&#39;t go too deep into other <code>pipeline</code> examples in this post as I will be doing so in some follow-up posts, but we will finish off looking at an example using a <code>Duplex</code> stream to create a multiplexer.</p>
<p>In the following example, we create a multiplexer stream that takes two input streams and multiplexes them into a single output stream.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const { Duplex, pipeline, Transform } = require(&quot;node:stream&quot;);
const { createReadStream, createWriteStream } = require(&quot;node:fs&quot;);

class Multiplexer extends Duplex {
  constructor(options) {
    super(options);
    this.sources = [];
    this.currentSource = 0;
    this.activeSourcesCount = 0;
  }

  _read(size) {
    this.readFromSources(size);
  }

  readFromSources(size) {
    if (this.activeSourcesCount === 0) {
      this.push(null);
      return;
    }

    let chunk;
    do {
      const source = this.sources[this.currentSource];
      chunk = source.read(size);

      if (chunk !== null) {
        this.push(chunk);
        return;
      }

      this.currentSource = (this.currentSource + 1) % this.sources.length;
    } while (this.currentSource !== 0);

    // If we&#39;ve gone through all sources without reading, wait for more data
    this.sources.forEach((source) =&gt; {
      source.once(&quot;readable&quot;, () =&gt; this.readFromSources(size));
    });
  }

  _write(chunk, encoding, callback) {
    // For simplicity, we&#39;re not implementing write functionality
    callback();
  }

  addSource(source) {
    this.sources.push(source);
    this.activeSourcesCount++;
    source.on(&quot;end&quot;, () =&gt; {
      this.activeSourcesCount--;
      if (this.activeSourcesCount === 0) {
        this.push(null);
      }
    });
  }
}

const upperCaseTransform = new Transform({
  transform(chunk, encoding, callback) {
    callback(null, chunk.toString().toUpperCase());
  },
});

// Create input streams
const input1 = createReadStream(&quot;file1.txt&quot;);
const input2 = createReadStream(&quot;file2.txt&quot;);

// Create output stream
const output = createWriteStream(&quot;output.txt&quot;);

// Create multiplexer
const multiplexer = new Multiplexer();
multiplexer.addSource(input1);
multiplexer.addSource(input2);

// Use pipeline to connect streams
pipeline(multiplexer, upperCaseTransform, output, (err) =&gt; {
  if (err) {
    console.error(&quot;Pipeline failed:&quot;, err);
  } else {
    console.log(&quot;Pipeline succeeded&quot;);
  }
});</code></pre>
<p>In the example above, we have two sources <code>file1.txt</code> and <code>file2.txt</code> that will be multiplexed into a single output stream. The output stream will be transformed to uppercase before being written to <code>output.txt</code>.</p>
<p>If our files are <code>Hello, world!</code> and <code>Streams are fun</code> for <code>file1.txt</code> and <code>file2.txt</code> respectively, then an example output we can get at <code>output.txt</code> is:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="txt"><code>HELLO, WORLD!STREAMS ARE FUN</code></pre>
<p>The sequence diagram for the multiplexer pipeline can be visualized as follows:</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2024-07-13-stream-pipelines-in-nodejs/pipeline-3.webp" alt="Multiplexer pipeline" loading="lazy" decoding="async"/><figcaption>Multiplexer pipeline</figcaption></figure>
<p>In the current implementation:</p>
<ul>
<li>The multiplexer attempts to read from each source in turn.</li>
<li>Whichever source has data available first will be read from and its data pushed to the output.</li>
<li>This can lead to interleaving of data from different sources in an unpredictable way, especially if the sources become readable at different times or at different rates.</li>
</ul>
<p>That means that the output may not be in the order that the data is written to <code>output.txt</code> is not guaranteed.</p>
<span id="usage-with-other-streams-from-nodejs-modules" class="anchor-alias"><h2 id="usage-with-other-streams-from-node-js-modules">Usage with other streams from Node.js modules</h2>
<p>Finally, let&#39;s recap some streams from native Node.js modules that we&#39;ve seen before in previous posts and how we can use them with <code>pipeline</code>.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="javascript"><code>const { pipeline } = require(&quot;node:stream&quot;);
const { createGzip } = require(&quot;node:zlib&quot;);
const { createReadStream, createWriteStream } = require(&quot;node:fs&quot;);

pipeline(
  createReadStream(&quot;input.txt&quot;),
  createGzip(),
  createWriteStream(&quot;output.txt.gz&quot;),
  (err) =&gt; {
    if (err) {
      console.error(&quot;Pipeline failed&quot;, err);
    } else {
      console.log(&quot;Pipeline succeeded&quot;);
    }
  }
);</code></pre>
<p>In this example, <code>pipeline</code> manages the entire process of reading a file, compressing it, and writing the compressed data to a new file. It handles all the necessary error checking and resource management, significantly simplifying the code compared to manually piping these streams together and handling all the events yourself.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Throughout this blog series, we&#39;ve delved deeply into the intricate world of <strong>Node.js streams</strong>, exploring their core concepts, different stream types, and practical use cases. Starting from the fundamentals, we learned about <strong>Readable</strong> and <strong>Writable</strong> streams and progressed to more advanced topics like <strong>Transform</strong> and <strong>Duplex</strong> streams.</p>
<p>In this post, we focused on the <code>pipeline</code> function, which simplifies the process of connecting multiple streams. We demonstrated how to create basic and complex pipelines, including handling asynchronous streams with ease using <code>stream/pipeline</code>. Additionally, we introduced a multiplexer example to show how multiple input streams can be combined into a single output stream, demonstrating the flexibility and power of Node.js streams.</p>
<p>Streams are a powerful feature in Node.js, providing a way to process data efficiently and effectively. Whether you&#39;re working with file I/O, network communication, or any other data processing tasks, mastering streams can significantly enhance your Node.js applications.</p>
<p>In future posts, I will be working through some more specific use cases and examples of how to use streams in Node.js with destinations like S3 buckets.</p>
<h2 id="resources-and-further-reading">Resources and further reading</h2>
<ul>
<li><a href="https://dennisokeeffe.com/blog/2024-07-05-understanding-nodejs-streams">Understanding Node.js Streams Blog Post</a></li>
<li><a href="https://dennisokeeffe.com/blog/2024-07-08-readable-streams-in-nodejs">Readable Streams in Node.js</a></li>
<li><a href="https://dennisokeeffe.com/blog/2024-07-10-writable-streams-in-nodejs">Writable Streams in Node.js</a></li>
<li><a href="https://dennisokeeffe.com/blog/2024-07-11-duplex-streams-in-nodejs">Duplex Streams in Node.js</a></li>
<li><a href="https://dennisokeeffe.com/blog/2024-07-12-transform-streams-in-nodejs">Transform Streams in Node.js</a></li>
<li><a href="https://nodejs.org/api/stream.html#streampipelinesource-transforms-destination-options" target="_blank" rel="noopener noreferrer">Node.js Stream Pipeline API Documentation</a></li>
<li><a href="https://dennisokeeffe.com/blog/series/node-js-streams">Node.js Streams series</a></li>
<li><a href="https://nodejs.org/api/crypto.html" target="_blank" rel="noopener noreferrer">Node.js Crypto module</a></li>
<li><a href="https://nodejs.org/api/zlib.html" target="_blank" rel="noopener noreferrer">Node.js zlib module</a></li>
</ul>
<p><strong>Disclaimer</strong>: This blog post used AI to generate the images used for the analogy.</p>
<p><em>Photo credit: <a href="https://unsplash.com/@jagodakondratiuk" target="_blank" rel="noopener noreferrer"><code>jagodakondratiuk</code></a></em></p>]]></content>
<category term="nodejs"/>
</entry>
<entry>
<title type="html"><![CDATA[Transform Streams in Node.js]]></title>
<id>https://blog.dennisokeeffe.com/blog/2024-07-12-transform-streams-in-nodejs copy</id>
<link href="https://dennisokeeffe.com/blog/2024-07-12-transform-streams-in-nodejs"/>
<published>2024-07-12T00:00:00.000Z</published>
<updated>2024-07-12T00:00:00.000Z</updated>
<summary type="html"><![CDATA[What are Transform streams in Node.js? How do they work? We cover the fourth and final fundamental stream type in this post.]]></summary>
<content type="html"><![CDATA[<h2 id="overview">Overview</h2>
<p>At this point in our <a href="https://dennisokeeffe.com/blog/series/node-js-streams">Node.js Streams blog series</a>, we covered the <a href="https://dennisokeeffe.com/blog/2024-07-05-understanding-nodejs-streams">core fundamentals you should know about Node.js streams</a> and spent a blog post diving into both <a href="https://dennisokeeffe.com/blog/2024-07-08-readable-streams-in-nodejs"><strong>Readable streams</strong></a> and <a href="https://dennisokeeffe.com/blog/2024-07-10-writable-streams-in-nodejs"><strong>Writable</strong> streams</a>.</p>
<p>In this shorter blog post, we will cover the fourth and final fundamental stream type <strong>Transform</strong> streams in this blog post.</p>
<h2 id="how-to-think-about-transform-streams">How to think about Transform streams</h2>
<p><strong>Transform</strong> streams are a special type of <strong>Duplex</strong> stream (which we covered in the <a href="https://dennisokeeffe.com/blog/2024-07-11-duplex-streams-in-nodejs">last blog post</a>). We use <strong>Transform</strong> streams for transforming data (very apt naming) as it passes through the stream.</p>
<p>In the <a href="https://dennisokeeffe.com/blog/2024-07-05-understanding-nodejs-streams">Node.js streams fundamentals blog post</a>, the mental model we used for <strong>Transform</strong> streams was the <strong>the water treatment plant</strong>.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2024-07-05-understanding-nodejs-streams/water-treatment-plan.webp" alt="The water treatment plant is our mental model for Transform streams in Node.js" loading="lazy" decoding="async"/><figcaption>The water treatment plant is our mental model for Transform streams in Node.js</figcaption></figure>
<p>Some parallels with the analogy to make it memorable:</p>
<ol>
<li><strong>Dual Nature</strong>: Transform streams are both readable and writable. A water treatment plant receives untreated water (input) and outputs clean water (output).</li>
<li><strong>Data Transformation</strong>: Modifies input data before passing it as output. Similarly, various processes in the plant (filtration, chemical treatment) change the water&#39;s composition.</li>
<li><strong>Streaming Processing</strong>: Handles data in chunks, not all at once. We can think of this like how water flows continuously through the plant, not in single large batches.</li>
<li><strong>Backpressure Handling</strong>: Manages flow when output is slower than input. Reservoirs or holding tanks regulate water flow when treatment is slower than intake.</li>
<li><strong>Piping</strong>: Can be easily connected to other streams. Treatment plants can also be connected to various water sources and distribution systems</li>
<li><strong>Error Handling</strong>: Manages issues during transformation. We can think of them like the safety protocols and backup systems in the plant handle treatment failures.</li>
</ol>
<p>These analogies help tie together the core concepts of <strong>Transform</strong> streams, as well as a few of the fundamentals that we covered at the start of this series.</p>
<p>With that out of the way, let&#39;s see how we can create <strong>Transform</strong> streams in Node.js.</p>
<h2 id="how-to-create-transform-streams">How to create Transform streams</h2>
<p>Similar to all the previous posts, we can create <strong>Transform</strong> streams using the <code>node:stream</code> module in a few ways:</p>
<ol>
<li><strong>Using the <code>stream.Transform</code> class</strong>: We can extend the <code>stream.Transform</code> class to create our <strong>Transform</strong> stream.</li>
<li><strong>Using the <code>stream.Transform</code> constructor</strong>: We can use the <code>stream.Transform</code> constructor to create a <strong>Transform</strong> stream.</li>
</ol>
<span id="extending-the-streamtransform-class" class="anchor-alias"><h3 id="extending-the-stream-transform-class">Extending the <code>stream.Transform</code> class</h3>
<p>If we extend the <code>stream.Transform</code> class, we can provide a <code>_transform</code> method that will be called for each chunk of data that passes through the stream.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const { Transform } = require(&quot;stream&quot;);

class UpperCaseTransform extends Transform {
  constructor() {
    super();
  }

  _transform(chunk, encoding, callback) {
    try {
      this.push(chunk.toString().toUpperCase());
      callback();
    } catch (error) {
      callback(error);
    }
  }
}

const upperCaseTransform = new UpperCaseTransform();

process.stdin.pipe(upperCaseTransform).pipe(process.stdout);</code></pre>
<p>In the above example, we pipe the <code>process.stdin</code> readable stream to our <code>UpperCaseTransform</code> stream, which converts the input to uppercase. We then pipe the output to <code>process.stdout</code>.</p>
<span id="using-the-streamtransform-constructor" class="anchor-alias"><h3 id="using-the-stream-transform-constructor">Using the <code>stream.Transform</code> constructor</h3>
<p>We could also have implemented the stream calling <code>new Transform</code> directly:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const { Transform } = require(&quot;stream&quot;);

const upperCaseTransform = new Transform({
  transform(chunk, encoding, callback) {
    this.push(chunk.toString().toUpperCase());
    callback();
  },
});

process.stdin.pipe(upperCaseTransform).pipe(process.stdout);</code></pre>
<p>When creating an instance of a <code>Transform</code> stream, we supply a <code>transform</code> function that will be called for each chunk of data that passes through the stream.</p>
<p>The above code works the same as the previous example, but it&#39;s a bit more concise.</p>
<h2 id="use-cases-for-transform-streams">Use cases for Transform streams</h2>
<p>The use cases are a subset of what we covered in our <a href="https://dennisokeeffe.com/blog/2024-07-11-duplex-streams-in-nodejs#use-cases-for-duplex-streams">use cases for Duplex streams</a> in the prior post.</p>
<p>I won&#39;t recap the entire list here, but here are a few examples of where you might use <strong>Transform</strong> streams:</p>
<h3 id="compression-streams">Compression streams</h3>
<p>With <code>zlib</code>, we can create a <code>gzip</code> compression <code>Transform</code> stream. We&#39;ve covered this example before in the <a href="https://dennisokeeffe.com/blog/2024-07-10-writable-streams-in-nodejs">Writable streams blog post</a>.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const zlib = require(&quot;node:zlib&quot;);
const gzip = zlib.createGzip();
// gzip is a transform stream (both readable and writable) - we cover this more in a future post</code></pre>
<p>Here is the more complete example where we compress a file using <code>gzip</code> and write it out to a new file:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const fs = require(&quot;node:fs&quot;);
const zlib = require(&quot;node:zlib&quot;);

const readStream = fs.createReadStream(&quot;input.txt&quot;);
const writeStream = fs.createWriteStream(&quot;input.txt.gz&quot;);
const gzip = zlib.createGzip();

readStream.pipe(gzip).pipe(writeStream);

writeStream.on(&quot;finish&quot;, () =&gt; console.log(&quot;File successfully compressed&quot;));</code></pre>
<h3 id="crypto-streams">Crypto streams</h3>
<p>We&#39;ve also covered this example before in the <a href="https://dennisokeeffe.com/blog/2024-07-10-writable-streams-in-nodejs">Writable streams blog post</a>.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const crypto = require(&quot;crypto&quot;);
const cipher = crypto.createCipher(&quot;aes192&quot;, &quot;secret&quot;);
// cipher is a transform stream (both readable and writable) - we cover this more in a future post</code></pre>
<p>The <strong>Transform</strong> stream here is the <code>cipher</code> stream. We can use it to encrypt data as it passes through the stream.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const crypto = require(&quot;node:crypto&quot;);
const fs = require(&quot;node:fs&quot;);

const algorithm = &quot;aes-192-cbc&quot;;
const password = &quot;Password used to generate key&quot;;

const key = crypto.scryptSync(password, &quot;salt&quot;, 24);
const iv = Buffer.alloc(16, 0);

const cipher = crypto.createCipheriv(algorithm, key, iv);

const input = fs.createReadStream(&quot;input.txt&quot;);
const output = fs.createWriteStream(&quot;encrypted.txt&quot;);

input.pipe(cipher).pipe(output);

output.on(&quot;finish&quot;, () =&gt; console.log(&quot;File encrypted successfully&quot;));</code></pre>
<h2 id="conclusion">Conclusion</h2>
<p>Today&#39;s post is a short one that covers the special type of <strong>Duplex</strong> stream: <strong>Transform</strong> streams.</p>
<p>We&#39;ve covered the water treatment plant analogy to help you remember the core concepts of <strong>Transform</strong> streams, how to create them, and a few use cases where you might use them. You should review the previous posts in this series to get a full understanding of how <strong>Transform</strong> streams fit into the broader picture of Node.js streams, particularly now that you&#39;re understanding of Node.js streams is becoming more complete.</p>
<p>In the next post, we will cover the <strong>pipeline</strong> API in Node.js, which is a powerful way to connect streams together.</p>
<h2 id="resources-and-further-reading">Resources and further reading</h2>
<ul>
<li><a href="https://dennisokeeffe.com/blog/2024-07-05-understanding-nodejs-streams">Understanding Node.js Streams Blog Post</a></li>
<li><a href="https://dennisokeeffe.com/blog/2024-07-08-readable-streams-in-nodejs">Readable Streams in Node.js</a></li>
<li><a href="https://dennisokeeffe.com/blog/2024-07-10-writable-streams-in-nodejs">Writable Streams in Node.js</a></li>
<li><a href="https://dennisokeeffe.com/blog/2024-07-11-duplex-streams-in-nodejs">Duplex Streams in Node.js</a></li>
<li><a href="https://nodejs.org/api/stream.html#streampipelinesource-transforms-destination-options" target="_blank" rel="noopener noreferrer">Node.js Stream Pipeline API Documentation</a></li>
<li><a href="https://dennisokeeffe.com/blog/series/node-js-streams">Node.js Streams series</a></li>
<li><a href="https://nodejs.org/api/crypto.html" target="_blank" rel="noopener noreferrer">Node.js Crypto module</a></li>
<li><a href="https://nodejs.org/api/zlib.html" target="_blank" rel="noopener noreferrer">Node.js zlib module</a></li>
</ul>
<p><strong>Disclaimer</strong>: This blog post used AI to generate the images used for the analogy.</p>
<p><em>Photo credit: <a href="https://unsplash.com/@blendertimer" target="_blank" rel="noopener noreferrer"><code>blendertimer</code></a></em></p>]]></content>
<category term="nodejs"/>
</entry>
<entry>
<title type="html"><![CDATA[Duplex Streams in Node.js]]></title>
<id>https://blog.dennisokeeffe.com/blog/2024-07-11-duplex-streams-in-nodejs</id>
<link href="https://dennisokeeffe.com/blog/2024-07-11-duplex-streams-in-nodejs"/>
<published>2024-07-11T00:00:00.000Z</published>
<updated>2024-07-11T00:00:00.000Z</updated>
<summary type="html"><![CDATA[What are Duplex streams in Node.js? How do they work? We cover the third fundamental stream type in this post.]]></summary>
<content type="html"><![CDATA[<h2 id="overview">Overview</h2>
<p>At this point in our <a href="https://dennisokeeffe.com/blog/series/node-js-streams">Node.js Streams blog series</a>, we covered the <a href="https://dennisokeeffe.com/blog/2024-07-05-understanding-nodejs-streams">core fundamentals you should know about Node.js streams</a> and spent a blog post diving into both <a href="https://dennisokeeffe.com/blog/2024-07-08-readable-streams-in-nodejs"><strong>Readable streams</strong></a> and <a href="https://dennisokeeffe.com/blog/2024-07-10-writable-streams-in-nodejs"><strong>Writable</strong> streams</a>.</p>
<p>The next fundamental stream type that we&#39;re going to look into are <a href="https://nodejs.org/api/stream.html#duplex-and-transform-streams" target="_blank" rel="noopener noreferrer"><strong>Duplex streams</strong></a>.</p>
<h2 id="how-to-think-about-duplex-streams">How to think about Duplex streams</h2>
<p>Let&#39;s recap our mental model on <strong>Duplex</strong> streams: the <strong>two-way water pipe</strong>.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2024-07-05-understanding-nodejs-streams/water-pipe.webp" alt="Two-way water pipe: our mental model for Duplex streams" loading="lazy" decoding="async"/><figcaption>Two-way water pipe: our mental model for Duplex streams</figcaption></figure>
<p>To understand this analogy a bit more, let&#39;s place flesh it out a little bit more:</p>
<ul>
<li><strong>Bidirectional flow</strong>: Like a <strong>two-way water pipe</strong> that <strong>allows water to flow in both directions</strong>, a <strong>Duplex</strong> stream <strong>enables data to flow both ways</strong>.</li>
<li><strong>Separate channels</strong>: Imagine the pipe having <strong>two separate channels</strong>, one for each direction. This represents how <strong>Duplex</strong> streams manage <strong>separate internal buffers for reading and writing</strong>.</li>
<li><strong>Valves (methods)</strong>:
<ul>
<li><code>_read()</code> is like opening a valve to <strong>allow water (data) to flow out</strong>.</li>
<li><code>_write()</code> is like opening a valve to <strong>let water (data) flow in</strong>.</li>
</ul>
</li>
<li><strong>Pressure (backpressure)</strong>:
<ul>
<li>If water is flowing in faster than it can flow out, pressure builds up. This is analogous to <strong>backpressure</strong> in streams.</li>
<li>The pipe might have a <strong>mechanism to signal when it&#39;s full</strong>, similar to how <strong>streams emit events or return values to indicate backpressure</strong>.</li>
</ul>
</li>
<li><strong>Flow control</strong>: Imagine <strong>regulators on each end of the pipe</strong> that can <strong>slow down or speed up the flow</strong>. This represents how <strong>Duplex</strong> streams can <strong>pause and resume</strong> data flow.</li>
<li><strong>Pipe connections (piping)</strong>: You can connect multiple two-way pipes to create a complex water system, just as you can pipe multiple <strong>Duplex</strong> streams together.</li>
<li><strong>Closing the pipe</strong>: The process of <strong>properly shutting off both ends of the pipe</strong> is similar to the <code>_final()</code> method ensuring <strong>all data is processed before closing</strong>.</li>
<li><strong>Leaks (error handling)</strong>: A <strong>leak in the pipe</strong> is like an <strong>error</strong> in the stream, requiring proper handling to prevent data loss.</li>
<li><strong>Water types (modes)</strong>: The pipe could carry either a <strong>continuous flow of water (binary mode)</strong> or <strong>discrete objects like fish (object mode)</strong>.</li>
</ul>
<p>This analogy can help readers visualize the abstract concepts of <strong>Duplex</strong> streams in a more concrete, familiar context.</p>
<p>Alternatives to this would be to take the analogy of <strong>walkie talkies</strong>, or a <strong>two-way street with cars coming and going in both directions</strong>. Choose whatever helps you visualize things best. I stick with the two-way water pipe analogy to fit in better with all the water-themed analogies I used for Node.js streams.</p>
<p>You can also think of <strong>Duplex</strong> streams as a combination of <strong>Readable</strong> and <strong>Writable</strong> streams, meaning that our previous deep-dives analogies for those can also apply to help you solidify your understanding of <strong>Duplex</strong> streams.</p>
<h2 id="how-to-create-duplex-streams">How to create Duplex streams</h2>
<p>We can create duplex streams using the <code>node:stream</code> module. There are a number of ways:</p>
<ol>
<li>Extending the <code>Duplex</code> class.</li>
<li>Using the <code>new Duplex()</code> constructor.</li>
<li>Using <code>Duplex.from</code>.</li>
</ol>
<h3 id="extending-the-duplex-class">Extending the Duplex class</h3>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const { Duplex } = require(&quot;stream&quot;);

class MyDuplex extends Duplex {
  constructor(options) {
    super(options);
    this.data = [&quot;Hello&quot;, &quot;World&quot;, &quot;Duplex&quot;, &quot;Stream&quot;];
  }

  _read(size) {
    const chunk = this.data.shift();
    if (chunk) {
      this.push(chunk);
    } else {
      this.push(null);
    }
  }

  _write(chunk, encoding, callback) {
    console.log(&quot;Received:&quot;, chunk.toString());
    callback();
  }
}

const myDuplex = new MyDuplex();

myDuplex.on(&quot;data&quot;, (chunk) =&gt; console.log(&quot;Read:&quot;, chunk.toString()));
myDuplex.write(&quot;Some data&quot;);</code></pre>
<p>If you have followed along with my previous posts, you will notice that the <code>_read</code> and <code>_write</code> methods are similar to the ones we used for <strong>Readable</strong> and <strong>Writable</strong> streams. The key difference is that <strong>Duplex</strong> streams implement both methods.</p>
<p>The above code creates a <strong>Duplex</strong> stream that reads data from an internal array and writes data to the console.</p>
<p>When you run <code>node script.js</code>, you should see the following output:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>$ node duplex/one.js
Received: Some data
Read: Hello
Read: World
Read: Duplex
Read: Stream</code></pre>
<p>Here&#39;s a sequence diagram visual:</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2024-07-11-duplex-streams-in-nodejs/seq-diagram-1.webp" alt="Visualizing our Duplex stream" loading="lazy" decoding="async"/><figcaption>Visualizing our Duplex stream</figcaption></figure>
<h3 id="using-the-new-duplex-constructor">Using the new Duplex() constructor</h3>
<p>Similar to <strong>Readable</strong> and <strong>Writable</strong>, we do not need to extend the class if we choose that we do not want to.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const { Duplex } = require(&quot;node:stream&quot;);

const duplex = new Duplex({
  read(size) {
    // Implement read logic
  },
  write(chunk, encoding, callback) {
    // Implement write logic
    callback();
  },
});</code></pre>
<span id="using-duplexfrom" class="anchor-alias"><h3 id="using-duplex-from">Using Duplex.from</h3>
<p>With <code>Duplex.from</code>, we can write the implementation for the <code>readable</code> and <code>writable</code> properties:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const { Duplex } = require(&quot;node:stream&quot;);

const duplex = Duplex.from({
  readable: (async function* () {
    yield &quot;data&quot;;
    yield &quot;more data&quot;;
  })(),
  writable: new WritableStream({
    write(chunk) {
      console.log(&quot;Received:&quot;, chunk);
    },
  }),
});</code></pre>
<p>Alternatively, we can pass in existing <strong>Readable</strong> and <strong>Writable</strong> streams:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const { Duplex } = require(&quot;node:stream&quot;);
const { PassThrough } = require(&quot;node:stream&quot;);

const readable = new PassThrough();
const writable = new PassThrough();

const duplex = Duplex.from({ readable, writable });</code></pre>
<h2 id="use-cases-for-duplex-streams">Use cases for Duplex streams</h2>
<p>Duplex streams have several practical use cases in Node.js applications. Here are some common scenarios where Duplex streams are particularly useful:</p>
<ol>
<li>
<p><strong>Network sockets</strong>:</p>
<ul>
<li>TCP sockets</li>
<li>WebSocket connections</li>
<li>Implementing custom network protocols</li>
</ul>
</li>
<li>
<p><strong>Database connections</strong>:</p>
<ul>
<li>Streaming queries and results</li>
<li>Real-time data synchronization</li>
</ul>
</li>
<li>
<p><strong>File I/O</strong>:</p>
<ul>
<li>Simultaneous reading and writing of files</li>
<li>Implementing file-based IPC (Inter-Process Communication)</li>
</ul>
</li>
<li>
<p><strong>Encryption/Decryption</strong>:</p>
<ul>
<li>Creating streams that can both encrypt and decrypt data</li>
</ul>
</li>
<li>
<p><strong>Compression/Decompression</strong>:</p>
<ul>
<li>Implementing streams that can both compress and decompress data</li>
</ul>
</li>
<li>
<p><strong>Parser/Serializer</strong>:</p>
<ul>
<li>Creating streams that can parse incoming data and serialize outgoing data</li>
</ul>
</li>
<li>
<p><strong>Protocol implementation</strong>:</p>
<ul>
<li>HTTP/2 streams</li>
<li>Custom bi-directional protocols</li>
</ul>
</li>
<li>
<p><strong>Proxy servers</strong>:</p>
<ul>
<li>Forwarding requests and responses between client and server</li>
</ul>
</li>
<li>
<p><strong>Telnet-like interfaces</strong>:</p>
<ul>
<li>Creating interactive command-line tools</li>
</ul>
</li>
<li>
<p><strong>Middleware in stream processing pipelines</strong>:</p>
<ul>
<li>Implementing transformations that require both read and write capabilities</li>
</ul>
</li>
<li>
<p><strong>Testing and mocking</strong>:</p>
<ul>
<li>Simulating complex I/O scenarios in unit tests</li>
</ul>
</li>
<li>
<p><strong>Multiplexing/Demultiplexing</strong>:</p>
<ul>
<li>Combining multiple streams into one or splitting one stream into many</li>
</ul>
</li>
<li>
<p><strong>Buffering and flow control</strong>:</p>
<ul>
<li>Implementing custom buffering strategies for data flow</li>
</ul>
</li>
</ol>
<p>Let&#39;s go through some simple examples for some of these.</p>
<h3 id="network-sockets">Network sockets</h3>
<p>Here we show a TCP server and client operating as <strong>Duplex</strong> streams:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const net = require(&quot;net&quot;);

const server = net.createServer((socket) =&gt; {
  socket.write(&quot;Welcome to the server!\n&quot;);
  socket.on(&quot;data&quot;, (data) =&gt; {
    console.log(&quot;Received:&quot;, data.toString());
    socket.write(`Echo: ${data}`);
  });
});

server.listen(3000, () =&gt; {
  const client = new net.Socket();
  client.connect(3000, &quot;localhost&quot;, () =&gt; {
    client.write(&quot;Hello, server!&quot;);
  });

  client.on(&quot;data&quot;, (data) =&gt; {
    console.log(&quot;Server says:&quot;, data.toString());
  });
});</code></pre>
<p>In this example, we create a TCP server and client using Node&#39;s <strong>net</strong> module. The server listens for connections, echoes back any received data, and sends a welcome message. The client connects to the server, sends a message, and logs any responses.</p>
<p>Both the <strong>server socket</strong> and <strong>client socket</strong> are <strong>Duplex</strong> streams, allowing bidirectional communication.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2024-07-11-duplex-streams-in-nodejs/seq-diagram-2.webp" alt="Sequence diagram for how the network socket above works" loading="lazy" decoding="async"/><figcaption>Sequence diagram for how the network socket above works</figcaption></figure>
<span id="file-io" class="anchor-alias"><h3 id="file-i-o">File I/O</h3>
<p>In this case we simultaneously read and write:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const fs = require(&quot;fs&quot;);
const { Duplex } = require(&quot;stream&quot;);

class FileDuplex extends Duplex {
  constructor(options) {
    super(options);
    this.readFd = fs.openSync(&quot;input.txt&quot;, &quot;r&quot;);
    this.writeFd = fs.openSync(&quot;output.txt&quot;, &quot;w&quot;);
  }

  _read(size) {
    const buf = Buffer.alloc(size);
    fs.read(this.readFd, buf, 0, size, null, (err, bytesRead) =&gt; {
      if (bytesRead &gt; 0) {
        this.push(buf.slice(0, bytesRead));
      } else {
        this.push(null);
      }
    });
  }

  _write(chunk, encoding, callback) {
    fs.write(this.writeFd, chunk, callback);
  }

  _final(callback) {
    fs.close(this.readFd, (err) =&gt; {
      fs.close(this.writeFd, callback);
    });
  }
}

const fileDuplex = new FileDuplex();
fileDuplex.pipe(process.stdout);
fileDuplex.write(&quot;This will be written to output.txt\n&quot;);</code></pre>
<p>This example implements a custom <strong>Duplex</strong> stream for simultaneous file reading and writing. It reads from &#39;<strong>input.txt</strong>&#39; and writes to &#39;<strong>output.txt</strong>&#39;. The <code>_read</code> method reads chunks from the input file, while the <code>_write</code> method writes data to the output file. This demonstrates how <strong>Duplex</strong> streams can be used for concurrent file operations.</p>
<span id="encryptiondecryption" class="anchor-alias"><h3 id="encryption-decryption">Encryption/Decryption</h3>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const { Duplex } = require(&quot;stream&quot;);
const crypto = require(&quot;crypto&quot;);

class CipherDuplex extends Duplex {
  constructor(options) {
    super(options);
    this.key = crypto.randomBytes(32);
    this.iv = crypto.randomBytes(16);
  }

  _read(size) {}

  _write(chunk, encoding, callback) {
    const cipher = crypto.createCipheriv(&quot;aes-256-cbc&quot;, this.key, this.iv);
    let encrypted = cipher.update(chunk);
    encrypted = Buffer.concat([encrypted, cipher.final()]);
    this.push(encrypted);
    callback();
  }
}

const cipherDuplex = new CipherDuplex();
cipherDuplex.on(&quot;data&quot;, (data) =&gt; {
  console.log(&quot;Encrypted:&quot;, data.toString(&quot;hex&quot;));
});
cipherDuplex.write(&quot;Secret message&quot;);</code></pre>
<p>We have the <strong>CipherDuplex</strong> stream that encrypts incoming data using <strong>AES-256-CBC</strong> encryption. It generates a random key and initialization vector (IV) for the encryption. When data is written to the stream, it&#39;s encrypted and then pushed as readable data. This shows how <strong>Duplex</strong> streams can be used for on-the-fly data transformation.</p>
<span id="compressiondecompression" class="anchor-alias"><h3 id="compression-decompression">Compression/Decompression</h3>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const zlib = require(&quot;zlib&quot;);
const { pipeline } = require(&quot;stream&quot;);

const compressDecompress = zlib.createGzip();

pipeline(
  process.stdin,
  compressDecompress,
  zlib.createGunzip(),
  process.stdout,
  (err) =&gt; {
    if (err) {
      console.error(&quot;Pipeline failed&quot;, err);
    } else {
      console.log(&quot;Pipeline succeeded&quot;);
    }
  }
);

// Usage: echo &quot;Hello, world!&quot; | node script.js</code></pre>
<p>This example uses <strong>zlib</strong>&#39;s built-in <strong>Duplex</strong> streams for <strong>compression and decompression</strong>. It sets up a <strong>pipeline</strong> that compresses input from stdin, immediately decompresses it, and then writes the result to <strong>stdout</strong>. This demonstrates how <strong>Duplex</strong> streams can be chained together for complex data processing.</p>
<span id="parserserializer" class="anchor-alias"><h3 id="parser-serializer">Parser/serializer</h3>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const { Duplex } = require(&quot;stream&quot;);

class JSONDuplex extends Duplex {
  constructor(options) {
    super({ ...options, objectMode: true });
  }

  _read(size) {}

  _write(chunk, encoding, callback) {
    try {
      const parsed = JSON.parse(chunk);
      this.push(parsed);
      callback();
    } catch (err) {
      callback(err);
    }
  }

  _final(callback) {
    this.push(null);
    callback();
  }
}

const jsonDuplex = new JSONDuplex();

jsonDuplex.on(&quot;data&quot;, (data) =&gt; {
  console.log(&quot;Parsed object:&quot;, data);
});

jsonDuplex.write(&#39;{&quot;name&quot;: &quot;John&quot;, &quot;age&quot;: 30}&#39;);
jsonDuplex.write(&#39;{&quot;city&quot;: &quot;New York&quot;, &quot;country&quot;: &quot;USA&quot;}&#39;);</code></pre>
<p>Here we create a <strong>JSONDuplex</strong> stream that parses incoming JSON strings into JavaScript objects. It operates in <strong>object mode</strong>, allowing it to push parsed objects directly. This showcases how <strong>Duplex</strong> streams can be used for data format conversion in both directions, although this example only implements the parsing direction.</p>
<span id="multiplexingdemultiplexing" class="anchor-alias"><h3 id="multiplexing-demultiplexing">Multiplexing/Demultiplexing</h3>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const { Duplex, PassThrough } = require(&quot;stream&quot;);

class MultiplexDuplex extends Duplex {
  constructor(options) {
    super(options);
    this.streams = new Map();
  }

  _read(size) {}

  _write(chunk, encoding, callback) {
    const { streamId, data } = JSON.parse(chunk);
    if (!this.streams.has(streamId)) {
      this.streams.set(streamId, new PassThrough());
    }
    this.streams.get(streamId).write(data);
    callback();
  }

  getStream(streamId) {
    if (!this.streams.has(streamId)) {
      this.streams.set(streamId, new PassThrough());
    }
    return this.streams.get(streamId);
  }
}

const multiplexer = new MultiplexDuplex();

const stream1 = multiplexer.getStream(1);
const stream2 = multiplexer.getStream(2);

stream1.on(&quot;data&quot;, (data) =&gt; console.log(&quot;Stream 1:&quot;, data.toString()));
stream2.on(&quot;data&quot;, (data) =&gt; console.log(&quot;Stream 2:&quot;, data.toString()));

multiplexer.write(JSON.stringify({ streamId: 1, data: &quot;Hello from stream 1&quot; }));
multiplexer.write(JSON.stringify({ streamId: 2, data: &quot;Hello from stream 2&quot; }));</code></pre>
<p>This example implements a <strong>MultiplexDuplex</strong> stream that can handle <strong>multiple logical streams</strong> over a single Duplex stream. It uses a <strong>streamId</strong> to differentiate between different logical streams. Data written to the multiplexer is routed to the appropriate <strong>PassThrough</strong> stream based on the <strong>streamId</strong>. This demonstrates how <strong>Duplex</strong> streams can be used to implement complex communication protocols.</p>
<h2 id="conclusion">Conclusion</h2>
<p>In this journey through the world of Node.js streams, we&#39;ve delved into the fundamental concepts of streams, explored <strong>Readable</strong> and <strong>Writable</strong> streams in detail, and now, we&#39;ve uncovered the intricacies of <strong>Duplex</strong> streams. By understanding <strong>Duplex</strong> streams through the analogy of a <strong>two-way water pipe</strong>, we&#39;ve gained insight into how data can flow bidirectionally, the importance of backpressure, and the mechanisms for controlling this flow.</p>
<p>From extending the <code>Duplex</code> class to using the <code>new Duplex()</code> constructor and <code>Duplex.from</code>, we&#39;ve seen multiple ways to create Duplex streams. These streams offer a powerful way to handle scenarios where both reading and writing are essential, such as network sockets, file I/O, encryption, compression, and complex protocols.</p>
<p>By exploring practical use cases, we&#39;ve demonstrated how Duplex streams can be applied to real-world problems, making them an indispensable tool in your Node.js toolkit. Whether you&#39;re building a custom network protocol, implementing a parser, or creating a multiplexing system, Duplex streams provide the flexibility and control you need.</p>
<p>As we continue this series, we&#39;ll look into other advanced stream types and techniques, helping you master the full spectrum of stream capabilities in Node.js. Stay tuned for more insights and practical examples in our next post on <strong>Transform streams</strong>. Thank you for following along, and happy streaming!</p>
<h2 id="resources-and-further-reading">Resources and further reading</h2>
<ul>
<li><a href="https://dennisokeeffe.com/blog/2024-07-05-understanding-nodejs-streams">Understanding Node.js Streams Blog Post</a></li>
<li><a href="https://dennisokeeffe.com/blog/2024-07-08-readable-streams-in-nodejs">Readable Streams in Node.js</a></li>
<li><a href="https://dennisokeeffe.com/blog/2024-07-10-writable-streams-in-nodejs">Writable Streams in Node.js</a></li>
<li><a href="https://nodejs.org/api/stream.html#streampipelinesource-transforms-destination-options" target="_blank" rel="noopener noreferrer">Node.js Stream Pipeline API Documentation</a></li>
<li><a href="https://dennisokeeffe.com/blog/series/node-js-streams">Node.js Streams series</a></li>
<li><a href="https://nodejs.org/api/crypto.html" target="_blank" rel="noopener noreferrer">Node.js Crypto module</a></li>
<li><a href="https://nodejs.org/api/zlib.html" target="_blank" rel="noopener noreferrer">Node.js zlib module</a></li>
</ul>
<p><strong>Disclaimer</strong>: This blog post used AI to generate the images used for the analogy.</p>
<p><em>Photo credit: <a href="https://unsplash.com/@pawel_czerwinski" target="_blank" rel="noopener noreferrer"><code>pawel_czerwinski</code></a></em></p>]]></content>
<category term="nodejs"/>
</entry>
<entry>
<title type="html"><![CDATA[Writable Streams in Node.js]]></title>
<id>https://blog.dennisokeeffe.com/blog/2024-07-10-writable-streams-in-nodejs</id>
<link href="https://dennisokeeffe.com/blog/2024-07-10-writable-streams-in-nodejs"/>
<published>2024-07-10T00:00:00.000Z</published>
<updated>2024-07-10T00:00:00.000Z</updated>
<summary type="html"><![CDATA[What are Writable streams in Node.js? How do they work? What are some common use cases for Writable streams? We continue our exploration into streams in this post.]]></summary>
<content type="html"><![CDATA[<h2 id="overview">Overview</h2>
<p>Previously, we covered <a href="https://dennisokeeffe.com/blog/2024-07-05-understanding-nodejs-streams">everything we need to know to understand Node.js streams</a>, followed by a look into <a href="https://dennisokeeffe.com/blog/2024-07-08-readable-streams-in-nodejs"><strong>Readable streams</strong></a>.</p>
<p>In this post, we move onto <strong>Writable</strong> streams.&quot;</p>
<h2 id="how-to-think-about-writable-streams">How to think about Writable streams</h2>
<p>Previously, we started that a mental model for remembering <strong>Writable</strong> streams is to compare them to a <strong>water barrel</strong> (or a <strong>water bucket</strong> if that works better for you).</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2024-07-05-understanding-nodejs-streams/water-barrel.webp" alt="The happy-go-lucky water barrel: our mental starting point for Readable streams" loading="lazy" decoding="async"/><figcaption>The happy-go-lucky water barrel: our mental starting point for Readable streams</figcaption></figure>
<p>Here is a little more depth to the analogy and things that you need to know:</p>
<ul>
<li><strong>Bucket (Stream Buffer):</strong> The bucket represents the <strong>internal buffer</strong> of a <strong>Writable</strong> stream. It has a <strong>finite capacity</strong> to hold water (data).</li>
<li><strong>Pouring Water (Writing Data):</strong> When you <strong>write data to a stream</strong>, it&#39;s like <strong>pouring water into the bucket</strong>. The <code>write()</code> method is analogous to pouring water.</li>
<li><strong>Bucket Capacity (<code>highWaterMark</code>):</strong> The bucket has a <strong>maximum capacity</strong>, similar to the <code>highWaterMark</code> in <strong>Writable</strong> streams. This is the point at which the bucket is considered &quot;full&quot;.</li>
<li><strong>Overflow (Backpressure):</strong> If you try to pour more water when the bucket is already full, it will overflow. In streams, this triggers <strong>backpressure</strong>, and the <code>write()</code> method returns <strong>false</strong>.</li>
<li><strong>Draining the Bucket (Processing Data):</strong> As the stream <strong>processes data</strong>, it&#39;s like <strong>water draining from the bucket</strong>. When enough water has drained (data processed), the stream emits a &#39;<strong>drain</strong>&#39; event.</li>
<li><strong>Continuous Flow (Flowing Mode):</strong> Imagine a system where <strong>water is constantly poured in and drained out</strong>. This is similar to a <strong>stream in flowing mode</strong>, where data is written and processed continuously.</li>
<li><strong>Pausing the Flow (Paused Mode):</strong> If you <strong>pause the water flow</strong>, it&#39;s like <strong>pausing a stream</strong>. The <code>pause()</code> method is used to pause the stream. This manual process is useful when you need to control the flow of data.</li>
<li><strong>Sealing the bucket (Ending the Stream):</strong> In a <strong>Writable</strong> stream, calling <code>end()</code> is like saying &quot;This is the last bit of data I&#39;m adding to the bucket, and once it&#39;s processed, consider the operation complete.&quot; It&#39;s more about finalizing the writing process than &quot;closing a source&quot;.</li>
<li><strong>Bucket Emptied (Finish Event):</strong> When all the water has drained from the bucket, it&#39;s empty. This is analogous to the &#39;<strong>finish</strong>&#39; event in a <strong>Writable</strong> stream, indicating <strong>all data has been processed</strong>.</li>
<li><strong>Leaks (Error Handling):</strong> This is more for a mental model, but if there&#39;s a leak in the bucket, it&#39;s like an <strong>error</strong> in the stream. Proper error handling is crucial in both cases.</li>
<li><strong>Custom Buckets (Custom Writable Streams):</strong> Just as you might have specialized buckets for different liquids, you can create custom <strong>Writable</strong> streams for specific data types or operations. This is done by extending a class from <code>stream.Writable</code>. See the section on <code>stream.Writable</code> class for more clarification.</li>
</ul>
<p>Here is a lifecycle sequence diagram to help you visualize the analogy:</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2024-07-10-writable-streams-in-nodejs/Writable-analogy-seq-diagram.webp" alt="Our water bucket/barrel analogy in action" loading="lazy" decoding="async"/><figcaption>Our water bucket/barrel analogy in action</figcaption></figure>
<h2 id="how-to-create-writable-streams">How to create Writable streams</h2>
<p>The <code>node:stream</code> module provides a <code>Writable</code> class which you use to create <code>Writable</code> streams.</p>
<ol>
<li>Extending the <code>stream.Writable</code> class</li>
<li>Creating an instance using the <code>stream.Writable</code> class</li>
<li>Using a factory method from <code>stream.Writable</code></li>
</ol>
<span id="extending-the-streamwritable-class" class="anchor-alias"><h3 id="extending-the-stream-writable-class">Extending the <code>stream.Writable</code> class</h3>
<p>In this scenario, you create your own class and provide a <code>_write</code> method for handling the data processing.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const { Writable } = require(&quot;node:stream&quot;);
class MyWritable extends Writable {
  _write(chunk, encoding, callback) {
    // Implementation
  }
}</code></pre>
<span id="creating-an-instance-using-the-streamwritable-class" class="anchor-alias"><h3 id="creating-an-instance-using-the-stream-writable-class">Creating an instance using the <code>stream.Writable</code> class</h3>
<p>Alternative, we can just create an instance of a <code>Writable</code> stream directly.</p>
<p>This is similar to extending a class, but we make our implementation for processing data directly in the <code>write</code> method.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const { Writable } = require(&quot;node:stream&quot;);
const writable = new Writable({
  write(chunk, encoding, callback) {
    // Implementation
  },
});</code></pre>
<p>For example, we could create a <code>LogWriter</code> class that logs the chunks as it processes data:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const { Writable } = require(&quot;node:stream&quot;);

class LogWriter extends Writable {
  _write(chunk, encoding, callback) {
    console.log(`[${new Date().toISOString()}] ${chunk.toString()}`);
    callback();
  }
}

const logger = new LogWriter();
logger.write(&quot;Application started&quot;);
logger.write(&quot;User logged in&quot;);
logger.end(&quot;Application closed&quot;);</code></pre>
<span id="using-a-factory-method-from-streamwritable" class="anchor-alias"><h3 id="using-a-factory-method-from-stream-writable">Using a factory method from <code>stream.Writable</code></h3>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const stream = require(&quot;node:stream&quot;);
const writable = stream.Writable({
  write(chunk, encoding, callback) {
    // Implementation
  },
});</code></pre>
<h2 id="other-examples-of-writable-streams">Other examples of Writable streams</h2>
<p>In addition to using the <code>stream.Writable</code> class, some Node.js modules provide several built-in <code>Writable</code> streams for common use cases:</p>
<ol start="4">
<li><code>fs.createWriteStream</code></li>
<li><code>http</code> response objects</li>
<li><code>process.stdout</code> and <code>process.stderr</code></li>
<li><code>child_process</code> stdin</li>
<li>Compression streams</li>
<li>Crypto streams</li>
<li>Database write streams (e.g. MongoDB)</li>
</ol>
<span id="fscreatewritestream" class="anchor-alias"><h3 id="fs-create-write-stream">fs.createWriteStream</h3>
<p>With <code>fs.createWriteStream</code>, you can write data to a file using a <code>Writable</code> stream.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const fs = require(&quot;node:fs&quot;);
const writableStream = fs.createWriteStream(&quot;output.txt&quot;);</code></pre>
<p>For example, we could create a log file:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const fs = require(&quot;node:fs&quot;);

const writableStream = fs.createWriteStream(&quot;access.log&quot;, { flags: &quot;a&quot; });

function logAccess(ip, route) {
  writableStream.write(`${new Date().toISOString()} ${ip} ${route}\n`);
}

logAccess(&quot;192.168.1.1&quot;, &quot;/home&quot;);
logAccess(&quot;192.168.1.2&quot;, &quot;/login&quot;);</code></pre>
<h3 id="http-response-objects">http response objects</h3>
<p>The <code>res</code> or &quot;response&quot; argument in an HTTP server callback is a <code>Writable</code> stream.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const http = require(&quot;node:http&quot;);
const server = http.createServer((req, res) =&gt; {
  // res is a writable stream
  res.write(&quot;Hello&quot;);
  res.end(&quot;World&quot;);
});</code></pre>
<p>We could use it for streaming back simple responses to the client:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const http = require(&quot;node:http&quot;);

const server = http.createServer((req, res) =&gt; {
  res.writeHead(200, { &quot;Content-Type&quot;: &quot;text/html&quot; });
  res.write(&quot;&lt;html&gt;&lt;body&gt;&quot;);
  res.write(&quot;&lt;h1&gt;Welcome to my server!&lt;/h1&gt;&quot;);
  res.write(`&lt;p&gt;You requested: ${req.url}&lt;/p&gt;`);
  res.end(&quot;&lt;/body&gt;&lt;/html&gt;&quot;);
});

server.listen(3000, () =&gt; console.log(&quot;Server running on port 3000&quot;));</code></pre>
<p>In the above example, we are streaming an HTML response to the client.</p>
<span id="processstdout-and-processstderr" class="anchor-alias"><h3 id="process-stdout-and-process-stderr">process.stdout and process.stderr</h3>
<p>Both <code>process.stdout</code> and <code>process.stderr</code> are <code>Writable</code> streams that write to the console.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>process.stdout.write(&quot;Log to console\n&quot;);
process.stderr.write(&quot;Log error to console\n&quot;);</code></pre>
<span id="child_process-stdin" class="anchor-alias"><h3 id="child-process-stdin">child_process stdin</h3>
<p><code>child_process</code> provides a way to spawn child processes and interact with them using <code>stdin</code>, <code>stdout</code> and <code>stderr</code>.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const { spawn } = require(&quot;node:child_process&quot;);
const child = spawn(&quot;wc&quot;);
// child.stdin is a writable stream
child.stdin.write(&quot;Hello World&quot;);
child.stdin.end();</code></pre>
<p>For example, we could use <code>child_process</code> to spawn a <code>grep</code> process that searches for the word &quot;hello&quot; in the input stream and logs the output to the console:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const { spawn } = require(&quot;node:child_process&quot;);

const child = spawn(&quot;grep&quot;, [&quot;hello&quot;]);

child.stdout.on(&quot;data&quot;, (data) =&gt; {
  console.log(`grep found: ${data}`);
});

child.stdin.write(&quot;hello world\n&quot;);
child.stdin.write(&quot;goodbye world\n&quot;);
child.stdin.end();</code></pre>
<h3 id="compression-streams">Compression streams</h3>
<p>Although we haven&#39;t covered <code>Transform</code> streams in-depth yet, if you&#39;ve followed along with the series so far then it will be come trivial to understand how they work.</p>
<p>In the case of <code>zlib</code>, we can create a <code>gzip</code> compression <code>Transform</code> stream that is both readable and writable:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const zlib = require(&quot;node:zlib&quot;);
const gzip = zlib.createGzip();
// gzip is a transform stream (both readable and writable) - we cover this more in a future post</code></pre>
<p>An example of this in action where we compress a file using gzip compression and write it out to a new file:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const fs = require(&quot;node:fs&quot;);
const zlib = require(&quot;node:zlib&quot;);

const readStream = fs.createReadStream(&quot;input.txt&quot;);
const writeStream = fs.createWriteStream(&quot;input.txt.gz&quot;);
const gzip = zlib.createGzip();

readStream.pipe(gzip).pipe(writeStream);

writeStream.on(&quot;finish&quot;, () =&gt; console.log(&quot;File successfully compressed&quot;));</code></pre>
<h3 id="crypto-streams">Crypto streams</h3>
<p>Similar to above, <code>cipher</code> in our below example is a <code>Transform</code> stream that is both readable and writable.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const crypto = require(&quot;crypto&quot;);
const cipher = crypto.createCipher(&quot;aes192&quot;, &quot;secret&quot;);
// cipher is a transform stream (both readable and writable) - we cover this more in a future post</code></pre>
<p>An example of this in action where we encrypt a file using AES-192-CBC encryption and write it out to a new file:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const crypto = require(&quot;node:crypto&quot;);
const fs = require(&quot;node:fs&quot;);

const algorithm = &quot;aes-192-cbc&quot;;
const password = &quot;Password used to generate key&quot;;

const key = crypto.scryptSync(password, &quot;salt&quot;, 24);
const iv = Buffer.alloc(16, 0);

const cipher = crypto.createCipheriv(algorithm, key, iv);

const input = fs.createReadStream(&quot;input.txt&quot;);
const output = fs.createWriteStream(&quot;encrypted.txt&quot;);

input.pipe(cipher).pipe(output);

output.on(&quot;finish&quot;, () =&gt; console.log(&quot;File encrypted successfully&quot;));</code></pre>
<h3 id="database-write-streams">Database write streams</h3>
<p>Finally, some databases provide <code>Writable</code> streams for writing data. For example, MongoDB provides a <code>GridFSBucket</code> class for working with large files.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const mongodb = require(&quot;mongodb&quot;);
const bucket = new mongodb.GridFSBucket(db);
const uploadStream = bucket.openUploadStream(&quot;file.txt&quot;);</code></pre>
<p>Here is a more complete example:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const { MongoClient } = require(&quot;mongodb&quot;);
const fs = require(&quot;node:fs&quot;);

async function uploadFile(filePath, fileName) {
  const client = await MongoClient.connect(&quot;mongodb://localhost:27017&quot;);
  const db = client.db(&quot;myDatabase&quot;);
  const bucket = new MongoClient.GridFSBucket(db);

  const uploadStream = bucket.openUploadStream(fileName);
  const readStream = fs.createReadStream(filePath);

  readStream.pipe(uploadStream);

  return new Promise((resolve, reject) =&gt; {
    uploadStream.on(&quot;finish&quot;, resolve);
    uploadStream.on(&quot;error&quot;, reject);
  });
}

uploadFile(&quot;largefile.zip&quot;, &quot;backup.zip&quot;)
  .then(() =&gt; console.log(&quot;File uploaded successfully&quot;))
  .catch(console.error);</code></pre>
<p>You can also write streams to remote sources like S3 buckets, etc.</p>
<h2 id="writable-stream-fundamentals-in-action">Writable stream fundamentals in action</h2>
<p>To cap off today&#39;s blog post, let&#39;s take a look at a <strong>Writable</strong> stream that covers the fundamentals that we have covered in the series previously (backpressure, buffering etc.).</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const { Writable } = require(&quot;stream&quot;);
const fs = require(&quot;fs&quot;);

class SlowWriter extends Writable {
  constructor(options) {
    // 1. Stream types: This is a Writable stream
    // 7. Object Mode: We&#39;re enabling object mode
    super({ ...options, objectMode: true });

    // 2. Buffering: We&#39;re setting a custom highWaterMark
    this.highWaterMark = options.highWaterMark || 2;

    this.outputFile = fs.createWriteStream(&quot;output.txt&quot;, { flags: &quot;a&quot; });
  }

  _write(chunk, encoding, callback) {
    // Simulate slow writing
    setTimeout(() =&gt; {
      // 7. Object Mode: We can write objects directly
      const output = `${JSON.stringify(chunk)}\n`;

      // 3. Backpressure: This slow write operation may cause backpressure
      this.outputFile.write(output, (err) =&gt; {
        if (err) {
          callback(err);
        } else {
          console.log(&quot;Chunk written&quot;);
          callback();
        }
      });
    }, 1000);
  }

  _final(callback) {
    this.outputFile.end(callback);
  }
}

// Usage:
const slowWriter = new SlowWriter({ highWaterMark: 2 });

// 6. Events: Listening for events
slowWriter.on(&quot;finish&quot;, () =&gt; console.log(&quot;All writes are complete.&quot;));
slowWriter.on(&quot;drain&quot;, () =&gt;
  console.log(&quot;Stream is no longer full, can resume writing.&quot;)
);

// 5. Piping: Creating a readable stream to pipe into our writable
const { Readable } = require(&quot;stream&quot;);
const fastReader = new Readable({
  objectMode: true,
  read() {},
});

// 4. Modes of Operation: This is in flowing mode due to the pipe
fastReader.pipe(slowWriter);

// Simulate fast data production
for (let i = 0; i &lt; 10; i++) {
  // 3. Backpressure: This may trigger backpressure due to slow writing
  const canContinue = fastReader.push({ data: `Fast data ${i}` });
  if (!canContinue) {
    console.log(&quot;Backpressure detected, waiting for drain&quot;);
    // In a real scenario, you&#39;d wait for the &#39;drain&#39; event before continuing
  }
}

fastReader.push(null); // Signal the end of the readable stream</code></pre>
<p>In the above example, we demonstrate:</p>
<ol>
<li><strong>Stream types</strong>: It creates a custom <strong>Writable</strong> stream (<strong>SlowWriter</strong>) and uses a <strong>Readable</strong> stream (<strong>fastReader</strong>).</li>
<li><strong>Buffering</strong>: The <code>highWaterMark</code> is set to control the <strong>internal buffer size</strong>.</li>
<li><strong>Backpressure</strong>: The slow writing operation simulated by <code>setTimeout</code> may cause <strong>backpressure</strong>, which is handled by the stream automatically.</li>
<li><strong>Modes of Operation</strong>: The <strong>readable</strong> stream is put into <strong>flowing mode</strong> when piped to the <strong>writable</strong> stream.</li>
<li><strong>Piping</strong>: The <strong>readable</strong> stream is piped to the <strong>writable</strong> stream, automatically managing data flow.</li>
<li><strong>Events</strong>: The code listens for &#39;<strong>finish</strong>&#39; and &#39;<strong>drain</strong>&#39; <strong>events</strong> on the <strong>writable</strong> stream.</li>
<li><strong>Object Mode</strong>: Both the <strong>readable</strong> and <strong>writable</strong> streams are set to <strong>object mode</strong>, allowing them to work with JavaScript objects.</li>
</ol>
<p>For a visual guide on the above:</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2024-07-10-writable-streams-in-nodejs/writable-fundamentals-seq-diagram.webp" alt="A sequence diagram of our SlowWriter in action" loading="lazy" decoding="async"/><figcaption>A sequence diagram of our SlowWriter in action</figcaption></figure>
<h2 id="conclusion">Conclusion</h2>
<p>Understanding <strong>Writable</strong> streams in Node.js is crucial for managing data flow in your applications. By drawing parallels to everyday concepts like water barrels and buckets, we&#39;ve explored how <strong>Writable</strong> streams handle data, manage backpressure, and process data efficiently. We&#39;ve also demonstrated various ways to implement and utilize <strong>Writable</strong> streams, from logging and file writing to more advanced use cases like compression and encryption.</p>
<p>As you continue your journey with Node.js streams, remember that mastering these concepts can greatly enhance your ability to build robust, high-performance applications. Whether you&#39;re handling large file uploads, streaming data to a client, or processing logs, <strong>Writable</strong> streams offer a powerful toolset for efficient data handling. Keep experimenting with different stream types and explore the endless possibilities they provide.</p>
<p>Thank you for following along with this series on Node.js streams. Stay tuned for our next post, where we&#39;ll dive into <strong>Transform</strong> streams and further expand our understanding of Node.js stream fundamentals.</p>
<h2 id="resources-and-further-reading">Resources and further reading</h2>
<ul>
<li><a href="https://dennisokeeffe.com/blog/2024-07-05-understanding-nodejs-streams">Understanding Node.js Streams Blog Post</a></li>
<li><a href="https://dennisokeeffe.com/blog/2024-07-08-readable-streams-in-nodejs">Readable Streams in Node.js</a></li>
<li><a href="https://nodejs.org/api/stream.html#streampipelinesource-transforms-destination-options" target="_blank" rel="noopener noreferrer">Node.js Stream Pipeline API Documentation</a></li>
<li><a href="https://dennisokeeffe.com/blog/series/node-js-streams">Node.js Streams series</a></li>
<li><a href="https://nodejs.org/api/crypto.html" target="_blank" rel="noopener noreferrer">Node.js Crypto module</a></li>
<li><a href="https://nodejs.org/api/zlib.html" target="_blank" rel="noopener noreferrer">Node.js zlib module</a></li>
<li><a href="https://nodejs.org/en/learn/manipulating-files/writing-files-with-nodejs" target="_blank" rel="noopener noreferrer">Node.js Blog | Writing files with Node.js</a></li>
</ul>
<p><strong>Disclaimer</strong>: This blog post used AI to generate the images used for the analogy.</p>
<p><em>Photo credit: <a href="https://unsplash.com/@robanderson72" target="_blank" rel="noopener noreferrer"><code>robanderson72</code></a></em></p>]]></content>
<category term="nodejs"/>
</entry>
<entry>
<title type="html"><![CDATA[Readable Streams in Node.js]]></title>
<id>https://blog.dennisokeeffe.com/blog/2024-07-08-readable-streams-in-nodejs</id>
<link href="https://dennisokeeffe.com/blog/2024-07-08-readable-streams-in-nodejs"/>
<published>2024-07-08T00:00:00.000Z</published>
<updated>2024-07-09T00:00:00.000Z</updated>
<summary type="html"><![CDATA[What are readable streams in Node.js? How do they work? What are some common use cases for readable streams? Let's explore these questions in this post.]]></summary>
<content type="html"><![CDATA[<h2 id="overview">Overview</h2>
<p>In my previous post, we learned <a href="https://dennisokeeffe.com/blog/2024-07-05-understanding-nodejs-streams">everything we need to know to understand Node.js streams</a>. We covered the four fundamental stream types in Node.js: <strong>Writable</strong>, <strong>Readable</strong>, <strong>Duplex</strong>, and <strong>Transform</strong>. We also discussed the benefits of using streams, key concepts like buffering, backpressure, and piping, and the importance of events in stream processing.</p>
<p>In this post, we will dive deeper into <code>Readable</code> streams in particular: how they are created, how we can consume data from them, and some a number of examples to get you comfortable.</p>
<h2 id="how-to-create-readable-streams">How to create Readable streams</h2>
<p>In our previous post, we started with the mental model that <strong>Readable</strong> streams are like <strong>water taps</strong>. In the same way that we can turn on the tap, change the flow rate, or turn it off, a <strong>Readable</strong> stream provides data in chunks that we can consume, pause, resume, or end.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2024-07-05-understanding-nodejs-streams/tap.webp" alt="The glorious water tap: our mental starting point for Readable streams" loading="lazy" decoding="async"/><figcaption>The glorious water tap: our mental starting point for Readable streams</figcaption></figure>
<p>But how can we create a <strong>Readable</strong> stream in Node.js? There are a number of ways, but let&#39;s start with what the <code>node:stream</code> module provides.</p>
<ol>
<li>stream.Readable.from()</li>
<li>stream.from()</li>
<li>stream.Readable class</li>
<li>stream.fromWeb()</li>
</ol>
<p>From top-to-bottom, I&#39;ve listed out what I think are the most approachable ways to create a <strong>Readable</strong> stream in Node.js from the <code>node:stream</code> module, so let&#39;s go through them each.</p>
<span id="streamreadablefrom" class="anchor-alias"><h3 id="stream-readable-from">stream.Readable.from()</h3>
<p>The <code>stream.Readable.from()</code> method creates a <strong>Readable</strong> stream from an iterable object or an async iterable object. This method is useful when you have an iterable data source, for example an array of strings, that you want to convert into a stream.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const { Readable } = require(&quot;stream&quot;);
const readableStream = Readable.from([&quot;Hello&quot;, &quot; &quot;, &quot;World&quot;, &quot;!&quot;]);</code></pre>
<p>In the above example, we create a <strong>Readable</strong> stream from an array of strings. The <code>Readable.from()</code> method automatically converts the array into a stream that emits each string as a chunk of data.</p>
<p>For the sake of completion, here is a reminder of how that <strong>Readable</strong> stream could be paired with a <strong>Writable</strong> stream to pipe data from one to the other:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const { Readable, Writable } = require(&quot;node:stream&quot;);
const fs = require(&quot;node:fs&quot;);

// Create a Readable stream from an array of strings
const readableStream = Readable.from([&quot;Hello&quot;, &quot; &quot;, &quot;World&quot;, &quot;!&quot;]);

// Create a Writable stream (in this case, writing to a file)
const writableStream = fs.createWriteStream(&quot;output.txt&quot;);

// Pipe the Readable stream to the Writable stream
readableStream.pipe(writableStream);

// Handle the &#39;finish&#39; event on the Writable stream
writableStream.on(&quot;finish&quot;, () =&gt; {
  console.log(&quot;Finished writing to file&quot;);
});

// Handle any errors
writableStream.on(&quot;error&quot;, (err) =&gt; {
  console.error(&quot;An error occurred:&quot;, err);
});</code></pre>
<p>In the above scenario, our <code>readableStream</code> is piped to the <code>writeableStream</code> that writes to a file <code>output.txt</code>.</p>
<p>The array of <code>[&quot;Hello&quot;, &quot; &quot;, &quot;World&quot;, &quot;!&quot;]</code> here is considered an <strong>iterable</strong> object with four chunks. Each element of the array is treated as a separate chunk in the resulting stream.</p>
<p>The <code>writeableStream</code> consumes each chunk of data and writes it to the file <code>output.txt</code>. The <code>finish</code> event is emitted when all data has been written to the file, and the <code>error</code> event is emitted if an error occurs during the writing process.</p>
<p>To solidify this, here is a verbose sequence diagram:</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2024-07-08-readable-streams-in-nodejs/readable-writeable-seq-diagram.webp" alt="An overview of how the above code is processed chunk-by-chunk" loading="lazy" decoding="async"/><figcaption>An overview of how the above code is processed chunk-by-chunk</figcaption></figure>
<span id="streamfrom" class="anchor-alias"><h3 id="stream-from">stream.from()</h3>
<p>This is function that can be imported directly from the <code>node:stream</code> module and is a shorthand for <code>stream.Readable.from()</code>.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const { from, Readable } = require(&quot;node:stream&quot;);

// These two lines are equivalent:
const stream1 = from([&quot;Hello&quot;, &quot;World&quot;]);
const stream2 = Readable.from([&quot;Hello&quot;, &quot;World&quot;]);</code></pre>
<span id="streamreadable" class="anchor-alias"><h3 id="stream-readable">stream.Readable()</h3>
<p>The <code>stream.Readable</code> class is a base class for creating custom <strong>Readable</strong> streams. You can extend this class to create your own <strong>Readable</strong> streams by implementing the <code>_read()</code> method.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const { Readable } = require(&quot;stream&quot;);

class MyReadable extends Readable {
  constructor(options, data) {
    super(options);
    this.data = data;
  }

  _read() {
    if (this.data.length) {
      this.push(this.data.shift());
    } else {
      this.push(null);
    }
  }
}

const myReadable = new MyReadable([&quot;Hello&quot;, &quot; &quot;, &quot;World&quot;, &quot;!&quot;]);</code></pre>
<p>In the above example, we create a custom <strong>Readable</strong> stream by extending the <code>stream.Readable</code> class. The <code>_read()</code> method is implemented to push data chunks from the <code>data</code> array to the stream. When all data has been pushed, <code>null</code> is pushed to signal the end of the stream.</p>
<span id="streamfromweb" class="anchor-alias"><h3 id="stream-from-web">stream.fromWeb()</h3>
<p><code>stream.fromWeb()</code> is designed to convert a Web API ReadableStream into a Node.js Readable stream. It&#39;s not always &quot;from the web&quot; in the sense of network requests; it&#39;s about compatibility between Web API streams and Node.js streams.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const { fromWeb } = require(&quot;node:stream/web&quot;);

// Create a Web API ReadableStream (this could come from a fetch() response, for example)
const webReadableStream = new ReadableStream({
  start(controller) {
    controller.enqueue(&quot;Hello&quot;);
    controller.enqueue(&quot;World&quot;);
    controller.close();
  },
});

// Convert it to a Node.js Readable stream
const nodeReadableStream = fromWeb(webReadableStream);

// Use the Node.js stream
nodeReadableStream.on(&quot;data&quot;, (chunk) =&gt; {
  console.log(chunk.toString());
});

nodeReadableStream.on(&quot;end&quot;, () =&gt; {
  console.log(&quot;Stream ended&quot;);
});</code></pre>
<p>In the above example, we create a Web API <code>ReadableStream</code> that emits two chunks of data (&quot;Hello&quot; and &quot;World&quot;) and then closes. We then convert this Web API stream to a Node.js <strong>Readable</strong> stream using <code>stream.fromWeb()</code>.</p>
<p>For advanced more practical example using the web <code>fetch</code> API:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const { fromWeb } = require(&quot;node:stream/web&quot;);
const { createWriteStream } = require(&quot;node:fs&quot;);
const { pipeline } = require(&quot;node:stream/promises&quot;);

async function downloadFile(url, outputPath) {
  const response = await fetch(url);
  if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);

  // response.body is a Web API ReadableStream
  const nodeStream = fromWeb(response.body);

  // Create a writable stream to the output file
  const fileStream = createWriteStream(outputPath);

  // Use pipeline to pipe the data and handle cleanup
  await pipeline(nodeStream, fileStream);

  console.log(`File downloaded to ${outputPath}`);
}

downloadFile(&quot;https://example.com/somefile.txt&quot;, &quot;output.txt&quot;).catch(
  console.error
);</code></pre>
<p>In the above, we use the <code>fetch</code> API to download a file from a URL. The response body is a Web API <code>ReadableStream</code>, which we convert to a Node.js <strong>Readable</strong> stream using <code>stream.fromWeb()</code>. We then pipe this stream to a <strong>Writable</strong> stream that writes the data to a file.</p>
<blockquote>
<p>Although we haven&#39;t touched on it, we make use of the <a href="https://nodejs.org/api/stream.html#streampipelinesource-transforms-destination-options" target="_blank" rel="noopener noreferrer"><code>pipeline</code></a> API from <code>node:stream</code> in the above example.</p>
</blockquote>
<p>A visual look at the download sequence:</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2024-07-08-readable-streams-in-nodejs/pipeline-from-web.webp" alt="A sequence diagram of how the above downloadFile function works" loading="lazy" decoding="async"/><figcaption>A sequence diagram of how the above downloadFile function works</figcaption></figure>
<h2 id="other-ways-to-create-a-readable-stream">Other ways to create a Readable stream</h2>
<span id="processstdin" class="anchor-alias"><h3 id="process-stdin">process.stdin</h3>
<p><code>process.stdin</code> provides a built-in <strong>Readable</strong> stream for standard input that you can hook into thanks streams implementing the EventEmitter class:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>process.stdin.on(&quot;data&quot;, (chunk) =&gt; {
  console.log(&quot;Received input:&quot;, chunk.toString());
});</code></pre>
<span id="httpincomingmessage" class="anchor-alias"><h3 id="http-incoming-message">http.IncomingMessage</h3>
<p>Another built-in <strong>Readable</strong> stream created when receiving an HTTP request from Node.js&#39; <code>node:http</code> module:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const http = require(&quot;node:http&quot;);

const server = http.createServer((req, res) =&gt; {
  // req is an instance of http.IncomingMessage
  req.on(&quot;data&quot;, (chunk) =&gt; {
    console.log(&quot;Received data:&quot;, chunk.toString());
  });
});

server.listen(3000);</code></pre>
<span id="child_processspawn" class="anchor-alias"><h3 id="child-process-spawn">child_process.spawn()</h3>
<p><code>child_process.spawn()</code> creates a child process that provides <strong>Readable</strong> streams for standard output and standard error:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const { spawn } = require(&quot;node:child_process&quot;);

const child = spawn(&quot;ls&quot;, [&quot;-l&quot;]);
child.stdout.on(&quot;data&quot;, (data) =&gt; {
  console.log(`stdout: ${data}`);
});</code></pre>
<h3 id="compression">Compression</h3>
<p>Built-in modules like <code>zlib</code> provide <strong>Readable</strong> streams for compression and decompression:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const zlib = require(&quot;node:zlib&quot;);
const fs = require(&quot;node:fs&quot;);

const readStream = fs.createReadStream(&quot;file.txt&quot;);
const gunzip = zlib.createGunzip();
const uncompressedStream = readStream.pipe(gunzip);</code></pre>
<h3 id="encryption">Encryption</h3>
<p>Finally for our last example, we can use the <code>crypto</code> module to create streams for cryptographic operations:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const crypto = require(&quot;node:crypto&quot;);

const key = crypto.randomBytes(32);
const iv = crypto.randomBytes(16);
const decipher = crypto.createDecipheriv(&quot;aes-256-cbc&quot;, key, iv);

// Use decipher as a Readable stream</code></pre>
<p>For a more complete example for encryption:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const crypto = require(&quot;node:crypto&quot;);
const { pipeline } = require(&quot;node:stream/promises&quot;);
const fs = require(&quot;node:fs&quot;);

// Generate a random key and IV
const key = crypto.randomBytes(32);
const iv = crypto.randomBytes(16);

// Create some sample encrypted data
const algorithm = &quot;aes-256-cbc&quot;;
const cipher = crypto.createCipheriv(algorithm, key, iv);
let encrypted = cipher.update(&quot;This is a secret message&quot;, &quot;utf8&quot;, &quot;hex&quot;);
encrypted += cipher.final(&quot;hex&quot;);

// Write the encrypted data to a file
fs.writeFileSync(&quot;encrypted.txt&quot;, encrypted);

// Create a decipher (Readable stream)
const decipher = crypto.createDecipheriv(algorithm, key, iv);

// Create a Readable stream from the encrypted file
const readStream = fs.createReadStream(&quot;encrypted.txt&quot;, { encoding: &quot;hex&quot; });

// Create a Writable stream for the decrypted output
const writeStream = fs.createWriteStream(&quot;decrypted.txt&quot;);

// Use the pipeline to decrypt the data
async function decryptFile() {
  try {
    await pipeline(readStream, decipher, writeStream);
    console.log(&quot;File successfully decrypted&quot;);
  } catch (err) {
    console.error(&quot;Pipeline failed&quot;, err);
  }
}

decryptFile();</code></pre>
<p>This last example is a little tough to follow (I&#39;ve synchronously written the encrypted data to a file), but here is a sequence diagram to help:</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2024-07-08-readable-streams-in-nodejs/encrypt-decrypt-seq.webp" alt="A sequence diagram of how the above code executes" loading="lazy" decoding="async"/><figcaption>A sequence diagram of how the above code executes</figcaption></figure>
<h2 id="readable-stream-fundamentals-in-action">Readable stream fundamentals in action</h2>
<p>We&#39;re going to cap off this blog post by recapping some of the core stream concepts we covered in the <a href="https://dennisokeeffe.com/blog/2024-07-05-understanding-nodejs-streams">previous post Understanding Node.js Streams</a>.</p>
<p>The following will be a quick-fire round of examples to help you get comfortable with the concepts we have covered.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const { Readable } = require(&quot;node:stream&quot;);

// Create a custom Readable stream
class NumberStream extends Readable {
  constructor(max, options) {
    super(options);
    this.max = max;
    this.current = 1;
  }

  _read() {
    if (this.current &lt;= this.max) {
      const data = { number: this.current };
      this.push(JSON.stringify(data));
      this.current++;
    } else {
      this.push(null); // Signal the end of the stream
    }
  }
}

// Create instances of our custom stream
const numberStream = new NumberStream(5, {
  objectMode: false,
  highWaterMark: 64, // Buffer size
});
const objectStream = new NumberStream(5, {
  objectMode: true,
  highWaterMark: 2, // Buffer size
});

// Example 1: Using events (paused mode)
numberStream.on(&quot;readable&quot;, () =&gt; {
  let chunk;
  while (null !== (chunk = numberStream.read())) {
    console.log(&quot;Received chunk:&quot;, chunk.toString());
  }
});

numberStream.on(&quot;end&quot;, () =&gt; {
  console.log(&quot;Stream ended&quot;);
});

// Example 2: Using flowing mode
objectStream.on(&quot;data&quot;, (chunk) =&gt; {
  console.log(&quot;Received object:&quot;, chunk);
});

objectStream.on(&quot;end&quot;, () =&gt; {
  console.log(&quot;Object stream ended&quot;);
});

// Example 3: Demonstrating backpressure
const slowConsumer = new Readable({
  read() {},
});

slowConsumer.on(&quot;data&quot;, (chunk) =&gt; {
  console.log(&quot;Slow consumer received:&quot;, chunk.toString());
});

const fastProducer = new Readable({
  read() {
    for (let i = 0; i &lt; 1000; i++) {
      if (!this.push(`Data ${i}\n`)) {
        console.log(&quot;Backpressure applied&quot;);
        return;
      }
    }
    this.push(null);
  },
});

// Piping with backpressure handling
fastProducer.pipe(slowConsumer);</code></pre>
<p>In the above example, we cover the following concepts:</p>
<ol>
<li>
<p><strong>Buffering</strong>: The <code>highWaterMark</code> option is set in the <code>NumberStream</code> constructor, controlling the internal buffer size.</p>
</li>
<li>
<p><strong>Backpressure</strong>: In Example 3, we demonstrate backpressure between a fast producer and a slow consumer.</p>
</li>
<li>
<p><strong>Modes of Operation</strong>:</p>
<ul>
<li>Example 1 uses the paused mode, manually calling <code>read()</code>.</li>
<li>Example 2 uses the flowing mode with the &#39;data&#39; event.</li>
</ul>
</li>
<li>
<p><strong>Piping</strong>: Example 3 uses <code>pipe()</code> to connect the fast producer to the slow consumer.</p>
</li>
<li>
<p><strong>Events</strong>: We use <code>readable</code>, <code>data</code>, and <code>end</code> events to interact with the streams.</p>
</li>
<li>
<p><strong>Object Mode</strong>: The <code>objectStream</code> is created with <code>objectMode: true</code>, allowing it to emit JavaScript objects.</p>
</li>
</ol>
<p>On top of this:</p>
<ul>
<li>The <code>NumberStream</code> class demonstrates how to create a custom Readable stream.</li>
<li>The <code>_read</code> method is called internally by the stream to generate data.</li>
<li>In object mode, we directly push JavaScript objects. In non-object mode, we <code>stringify</code> the objects.</li>
<li>The backpressure example shows how a fast producer will pause when the consumer&#39;s buffer fills up.</li>
<li>Piping automatically handles backpressure and data flow between streams.</li>
</ul>
<h2 id="conclusion">Conclusion</h2>
<p><strong>Readable</strong> streams in Node.js offer a powerful and flexible way to handle data in chunks, making it easier to process large datasets efficiently. By understanding the various methods to create readable streams, such as <code>stream.Readable.from()</code>, <code>stream.from()</code>, <code>stream.Readable</code>, and <code>stream.fromWeb()</code>, you can leverage these tools to build robust data processing applications.</p>
<p>We also explored several practical examples, including piping streams, handling HTTP requests, working with child processes, and performing compression and encryption. Each of these examples highlights the versatility of readable streams and their applicability in different scenarios.</p>
<p>As you continue to work with Node.js streams, remember the key concepts of <strong>buffering</strong>, <strong>backpressure</strong>, and the importance of <strong>event handling</strong> to manage your streams effectively. It all takes practice.</p>
<p>For further reading and more advanced use cases, be sure to check out the official Node.js documentation and other related resources. We&#39;ll be looking into <strong>Writeable</strong> streams next time!</p>
<h2 id="resources-and-further-reading">Resources and further reading</h2>
<ul>
<li><a href="https://dennisokeeffe.com/blog/2024-07-05-understanding-nodejs-streams">Understanding Node.js Streams Blog Post</a></li>
<li><a href="https://nodejs.org/api/stream.html#streampipelinesource-transforms-destination-options" target="_blank" rel="noopener noreferrer">Node.js Stream Pipeline API Documentation</a></li>
<li><a href="https://dennisokeeffe.com/blog/series/node-js-streams">Node.js Streams series</a></li>
</ul>
<p><strong>Disclaimer</strong>: This blog post used AI to generate the images used for the analogy.</p>
<p><em>Photo credit: <a href="https://unsplash.com/@terminath0r" target="_blank" rel="noopener noreferrer"><code>terminath0r</code></a></em></p>]]></content>
<category term="nodejs"/>
</entry>
<entry>
<title type="html"><![CDATA[Understanding Node.js Streams]]></title>
<id>https://blog.dennisokeeffe.com/blog/2024-07-05-understanding-nodejs-streams</id>
<link href="https://dennisokeeffe.com/blog/2024-07-05-understanding-nodejs-streams"/>
<published>2024-07-05T00:00:00.000Z</published>
<updated>2024-07-05T00:00:00.000Z</updated>
<summary type="html"><![CDATA[Everything you need to prepare yourself to know Node.js Streams.]]></summary>
<content type="html"><![CDATA[<h2 id="overview">Overview</h2>
<p>Streams can be a less approachable topic for those working with Node.js.</p>
<p>Within Node.js, there are <a href="https://nodejs.org/api/stream.html#types-of-streams" target="_blank" rel="noopener noreferrer">four fundamental stream types</a> that exist:</p>
<ol>
<li><strong>Writeable</strong>: Streams that you can write data to.</li>
<li><strong>Readable</strong>: Streams that you can read data from.</li>
<li><strong>Duplex</strong>: Streams that are both readable and writeable.</li>
<li><strong>Transform</strong>: A type of Duplex stream that can modify or transform the data as it is written and read.</li>
</ol>
<p>Over the course of this post, we will working through the fundamentals that you should understand about Node.js streams and why they exist.</p>
<p>In this post, we will not deep dive into each of the four stream types. Even though I will be introducing stream code in this blog post, we specifically will focus on the foundational concepts that you should understand about streams. In the posts following, will consolidate our knowledge by diving deeper into each stream type.</p>
<span id="how-to-think-about-the-four-fundamental-nodejs-stream-types" class="anchor-alias"><h2 id="how-to-think-about-the-four-fundamental-node-js-stream-types">How to think about the four fundamental Node.js Stream types</h2>
<p>To help you understand the four stream types, I will introduce four analogies to think about each stream type orientated around water so that you connect the dots between the four types, but I will also add an alternative analogy to help consolidate your mental model of the four fundamental stream types. These aim to help bridge the gap between the abstract concept of streams and something more tangible.</p>
<h3 id="1-writable-streams-the-water-bucket-the-notebook">1. Writable Streams: The Water Bucket, The Notebook</h3>
<p><strong>Writable</strong> streams are like <strong>water buckets that collect data (water)</strong>. Just as a bucket can be filled with water from various sources, a writable stream can <strong>receive data from different inputs</strong>. You can pour data into it chunk by chunk, and it will hold that data until it&#39;s full or you&#39;re done writing. This makes writable streams <strong>perfect for scenarios where you need to collect and store data</strong>, such as writing to files or sending responses in a web server.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2024-07-05-understanding-nodejs-streams/water-bucket.webp" alt="A water bucket can be used to think about Writeable Streams in Node.js" loading="lazy" decoding="async"/><figcaption>A water bucket can be used to think about Writeable Streams in Node.js</figcaption></figure>
<p>Alternatively, you could think of <strong>Writable</strong> streams are like a <strong>notebook where you can write information</strong>. Just as you can open a notebook and jot down thoughts, ideas, or data page by page, a writable stream allows you to <strong>input data chunk by chunk</strong>. You control <strong>when to write, how much to write, and when you&#39;re finished</strong>.</p>
<p>Examples of <strong>Writable</strong> streams include:</p>
<ul>
<li><strong>File Writing</strong>: Writing data to a file using <code>fs.createWriteStream()</code>.</li>
<li><strong>HTTP Responses</strong>: Sending responses to clients in a web server.</li>
<li><strong>Database Inserts</strong>: Streaming large amounts of data into a database.</li>
<li><strong>Console Output</strong>: Writing to <code>process.stdout</code> for command-line applications.</li>
</ul>
<p>A simple example of creating a writable stream to a file <code>output.txt</code> in Node.js and writing in the text <strong>Hello, World!</strong>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const fs = require(&quot;node:fs&quot;);
const writeStream = fs.createWriteStream(&quot;output.txt&quot;);
writeStream.write(&quot;Hello, World!&quot;);
writeStream.end();</code></pre>
<h3 id="2-readable-streams-the-tap-the-fantasy-novel">2. Readable Streams: The Tap, The Fantasy Novel</h3>
<p><strong>Readable</strong> streams can be thought of as water taps. Like a tap that can be turned on to release a steady flow of water, a readable stream provides a controlled source of data. You can &quot;open&quot; the stream to start receiving data, control its flow, and &quot;close&quot; it when you&#39;re done. This makes readable streams ideal for scenarios where you have a source of data that you want to access and process at your own pace, such as reading from a file or receiving a response from an HTTP request. Just as you can adjust a tap to control the water flow, you can also manage how you consume data from a readable stream.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2024-07-05-understanding-nodejs-streams/tap.webp" alt="A water tap can be used to think about Readable Streams in Node.js" loading="lazy" decoding="async"/><figcaption>A water tap can be used to think about Readable Streams in Node.js</figcaption></figure>
<p>You could also think about a <strong>fantasy novel</strong> (or in fact, any published book that you read). As you flip through the pages, you&#39;re consuming the story bit by bit, much like how a <strong>readable stream provides data in chunks</strong>. You can <strong>pause your reading</strong> (pause the stream), <strong>continue where you left off</strong> (resume the stream), or <strong>decide you&#39;re done reading for now</strong> (end the stream).</p>
<p>Examples of <strong>Readable</strong> streams include:</p>
<ul>
<li><strong>File Reading</strong>: Reading data from a file using <code>fs.createReadStream()</code>.</li>
<li><strong>HTTP Requests</strong>: Receiving data from client requests in a web server.</li>
<li><strong>Database Queries</strong>: Streaming large result sets from a database.</li>
<li><strong>User Input</strong>: Reading from <code>process.stdin</code> for command-line interfaces.</li>
</ul>
<p>An example of creating a readable stream from a file <code>input.txt</code> in Node.js:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const fs = require(&quot;node:fs&quot;);
const readStream = fs.createReadStream(&quot;input.txt&quot;);
readStream.on(&quot;data&quot;, (chunk) =&gt; {
  console.log(`Received ${chunk.length} bytes of data.`);
});</code></pre>
<h3 id="3-duplex-streams-the-two-way-water-pipe-the-walkie-talkie">3. Duplex Streams: The Two-Way Water Pipe, The Walkie-Talkie</h3>
<p><strong>Duplex</strong> streams are like two-way water pipes. Imagine a <strong>pipe system where water can flow in both directions simultaneously</strong>. One end of the pipe can <strong>receive water</strong> (write data), while the other end can <strong>release water</strong> (read data). This bidirectional flow makes duplex streams perfect for <strong>scenarios where you need to both send and receive data</strong>, such as in network sockets or inter-process communication.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2024-07-05-understanding-nodejs-streams/water-pipe.webp" alt="A two-way pipe can be used to think about Readable Streams in Node.js" loading="lazy" decoding="async"/><figcaption>A two-way pipe can be used to think about Readable Streams in Node.js</figcaption></figure>
<p>Admittedly for me, <strong>Duplex</strong> streams were the ones that took me more time to understand. My alternative is that <strong>Duplex</strong> streams are akin to a <strong>walkie-talkie</strong>. With a <strong>walkie-talkie</strong>, you can both speak (write data) and listen (read data) through the same device. Similarly, a <strong>duplex stream can both accept input and provide output</strong>, making it perfect for two-way communication scenarios.</p>
<p>Examples of <strong>Duplex</strong> streams include:</p>
<ul>
<li><strong>TCP Sockets</strong>: Implementing network communication where data flows both ways.</li>
<li><strong>SSH Tunnels</strong>: Establishing secure, bidirectional communication channels.</li>
<li><strong>Websockets</strong>: Creating real-time, full-duplex communication in web applications.</li>
<li><strong>Inter-process Communication</strong>: Facilitating two-way communication between processes.</li>
</ul>
<p>An example of creating a duplex stream using a TCP server in Node.js by creating a server that listens on port <code>8080</code>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const net = require(&quot;node:net&quot;);
const server = net.createServer((socket) =&gt; {
  socket.write(&quot;Server: Hello!&quot;);
  socket.on(&quot;data&quot;, (data) =&gt; {
    console.log(&quot;Received:&quot;, data.toString());
  });
});
server.listen(8080);</code></pre>
<p>In the above code, we have a <code>socket</code> variable that is a duplex stream. We write to the socket using <code>socket.write()</code> and read from the socket using <code>socket.on(&quot;data&quot;, ...)</code>.</p>
<h3 id="4-transform-streams-the-water-treatment-plant-the-factory-assembly-line">4. Transform Streams: The Water Treatment Plant, The Factory Assembly Line</h3>
<p><strong>Transform</strong> streams you can think of as <strong>water treatment plants</strong>. Just as a treatment plant takes in raw water, processes it, and outputs clean water, a transform stream <strong>receives input data, manipulates or &quot;transforms&quot; it in some way, and then outputs the modified data</strong>. This makes transform streams excellent for <strong>scenarios where you need to modify data on the fly, such as compression, encryption, or any kind of data parsing or conversion</strong>.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2024-07-05-understanding-nodejs-streams/water-treament-plan.webp" alt="A water treatment plant can be used to think about Transform Streams in Node.js" loading="lazy" decoding="async"/><figcaption>A water treatment plant can be used to think about Transform Streams in Node.js</figcaption></figure>
<p><strong>Transform</strong> streams can be likened to a factory assembly line. In a factory, raw materials enter at one end, undergo various modifications or enhancements as they move along the line, and emerge as finished products at the other end. Similarly, a transform stream takes input data, processes or modifies it in some way, and outputs the transformed data.</p>
<p>Examples of <strong>Transform</strong> streams include:</p>
<ul>
<li><strong>Data Compression</strong>: Compressing or decompressing data on-the-fly.</li>
<li><strong>Encryption/Decryption</strong>: Securing data as it&#39;s being transmitted.</li>
<li><strong>Data Parsing</strong>: Converting between data formats (e.g., <strong>CSV</strong> to <strong>JSON</strong>).</li>
<li><strong>Text Encoding</strong>: Changing character encodings of text data.</li>
</ul>
<p>Here is a simple example of a transform stream that converts input text to uppercase:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const { Transform } = require(&quot;node:stream&quot;);
const upperCaseTransform = new Transform({
  transform(chunk, encoding, callback) {
    this.push(chunk.toString().toUpperCase());
    callback();
  },
});

process.stdin.pipe(upperCaseTransform).pipe(process.stdout);</code></pre>
<p>We could run the following in the terminal to see it in action:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="sh"><code>$ echo &quot;Hello&quot; | node basic-transform.js
HELLO</code></pre>
<h3 id="understanding-the-difference-between-duplex-and-transform-streams">Understanding the difference between Duplex and Transform streams</h3>
<p>Transform streams are actually a special type of duplex stream. They can be thought of as duplex streams with a built-in relationship between the input and output. Using out analogy of <strong>the factory line</strong> or <strong>the water treatment plant</strong>, we understand that the input and output are related in some way. For <strong>Transform</strong> streams, data that comes in can be changed in some way before it goes out.</p>
<p>On the other hand, <strong>Duplex</strong> streams are more general. They can be used for two-way communication, but the input and output are not necessarily related. You can write data in one format and read it in another, or vice versa. In our example of the <strong>walkie talkie</strong>, what you talk about and what you hear are not necessarily directly related.</p>
<span id="the-why-behind-nodejs-streams" class="anchor-alias"><h2 id="the-why-behind-node-js-streams">The &quot;why&quot; behind Node.js Streams</h2>
<p>Node.js streams offer several benefits that make them a powerful tool for data processing and manipulation. Here are some key advantages of using streams in Node.js applications:</p>
<ol>
<li>
<p><strong>Memory Efficiency</strong></p>
<ul>
<li>Process large amounts of data <strong>without loading it all into memory at once</strong>.</li>
<li>Ideal for working with <strong>files or datasets larger than available Random Access Memory</strong> (RAM).</li>
</ul>
</li>
<li>
<p><strong>Time Efficiency</strong></p>
<ul>
<li>Start processing data <strong>as soon as it&#39;s available</strong>, without waiting for the entire dataset.</li>
<li><strong>Reduces overall processing time</strong> for large datasets.</li>
</ul>
</li>
<li>
<p><strong>Composability</strong></p>
<ul>
<li>Easily chain multiple stream operations together using <code>.pipe()</code>.</li>
<li>Create complex data processing pipelines with modular, reusable components.</li>
</ul>
</li>
<li>
<p><strong>Backpressure Handling</strong></p>
<ul>
<li>Built-in mechanisms to <strong>manage the flow of data</strong> between fast producers and slow consumers.</li>
<li><strong>Prevents memory overflow</strong> and <strong>system crashes</strong>.</li>
</ul>
</li>
<li>
<p><strong>Abstraction of Data Sources and Destinations</strong></p>
<ul>
<li><strong>Consistent interface</strong> for various types of input/output (files, network, etc.).</li>
<li>Write code that <strong>works with different data sources</strong> without major changes.</li>
</ul>
</li>
<li>
<p><strong>Event-Driven Architecture</strong></p>
<ul>
<li>Aligns well with Node.js&#39;s event-driven, non-blocking I/O model.</li>
<li><strong>Enables responsive applications</strong> even when dealing with large datasets.</li>
</ul>
</li>
<li>
<p><strong>Built-in to Core Modules</strong></p>
<ul>
<li>Native support in many Node.js core modules (<code>fs</code>, <code>http</code>, <code>crypto</code>, etc.).</li>
<li>Seamless integration with Node.js ecosystem.</li>
</ul>
</li>
<li>
<p><strong>Asynchronous Processing</strong></p>
<ul>
<li>Natural fit for <strong>asynchronous operations</strong>, allowing for <strong>efficient multitasking</strong>.</li>
<li>Keeps the <strong>event loop</strong> free for other operations.</li>
</ul>
</li>
<li>
<p><strong>Transformations</strong></p>
<ul>
<li>Easy to <strong>implement data transformations</strong> on-the-fly.</li>
<li>Supports operations like <strong>compression, encryption, or format conversion</strong> as data flows.</li>
</ul>
</li>
<li>
<p><strong>Improved Performance in Network Operations</strong></p>
<ul>
<li>Efficient for <strong>sending and receiving data</strong> over networks.</li>
<li><strong>Reduces latency</strong> in client-server communications.</li>
</ul>
</li>
<li>
<p><strong>Customizability</strong></p>
<ul>
<li>Can create custom streams for specific use cases.</li>
<li>Extend built-in stream classes for tailored functionality.</li>
</ul>
</li>
<li>
<p><strong>Interoperability</strong></p>
<ul>
<li>Works well with other Node.js features and third-party libraries.</li>
<li>Many <strong>npm</strong> packages provide stream interfaces for various data sources and sinks.</li>
</ul>
</li>
<li>
<p><strong>Lazy Evaluation</strong></p>
<ul>
<li><strong>Process data only when it&#39;s needed</strong>, improving application responsiveness.</li>
<li>Useful for scenarios where not all data may be used.</li>
</ul>
</li>
<li>
<p><strong>Standard Interface</strong></p>
<ul>
<li>Provides a consistent API across different types of streams.</li>
<li>Reduces the learning curve for working with various data sources and sinks.</li>
</ul>
</li>
</ol>
<p>These benefits make Node.js streams a powerful tool for efficient, scalable, and flexible data handling in a wide range of applications.</p>
<span id="nodejs-streams-concepts-that-you-should-know" class="anchor-alias"><h2 id="node-js-streams-concepts-that-you-should-know">Node.js Streams concepts that you should know</h2>
<p>On top of the four fundamental stream types, it is important to keep in mind a few foundational key terms that relate to streams:</p>
<ol>
<li><strong>Stream types</strong>: The <em>already-mentioned</em> four foundational stream types (Writeable, Readable, Duplex, and Transform).</li>
<li><strong>Buffering</strong>: Temporary data storage within streams, controlled by <code>highWaterMark</code> to manage memory usage.</li>
<li><strong>Backpressure</strong>: Mechanism to balance data flow between fast producers and slow consumers, preventing memory issues.</li>
<li><strong>Modes of Operation</strong>: <em>Flowing</em> (automatic data reading) vs <em>Paused</em> (manual data reading) modes for controlling data consumption.</li>
<li><strong>Piping</strong>: Connecting streams to automatically manage data flow and backpressure between them.</li>
<li><strong>Events</strong>: Asynchronous notifications for stream states and data availability, crucial for stream interaction.</li>
<li><strong>Object Mode</strong>: An option with streams that enable the capability to process JavaScript objects instead of just buffers or strings.</li>
</ol>
<p>Understanding these concepts provides a solid foundation for working with Node.js streams effectively. It helps in making informed decisions about stream usage, handling data efficiently, and avoiding common pitfalls in stream-based applications.</p>
<p>Let&#39;s walk through each of these concepts in more detail.</p>
<h2 id="buffering">Buffering</h2>
<p><a href="https://nodejs.org/api/stream.html#buffering" target="_blank" rel="noopener noreferrer"><strong>Buffering</strong></a> in Node.js streams refers to the process of storing chunks of data in memory while they are being transferred between the source and the destination.</p>
<p>You can think of it like a <strong>queue of people waiting to enter a room</strong>. The room can only hold a certain number of people at a time, so the <strong>manager</strong> at the front of the queue (<strong>buffer</strong>) helps manage the flow of people in and out of the room.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2024-07-05-understanding-nodejs-streams/queue-of-people.webp" alt="A queue of people can be an analogy for both buffering and backpressure" loading="lazy" decoding="async"/><figcaption>A queue of people can be an analogy for both buffering and backpressure</figcaption></figure>
<p>When data is read from a stream, it is held temporarily in a <strong>buffer</strong> until it can be processed or written to another stream. It is a mechanism used by streams to manage data flow efficiently.</p>
<p>We can help example <strong>buffering</strong> by creating a writable stream with a custom <code>highWaterMark</code>.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const { Writable } = require(&quot;node:stream&quot;);

const myWritable = new Writable({
  highWaterMark: 1024, // Set buffer size to 1KB
  write(chunk, encoding, callback) {
    console.log(&quot;Writing:&quot;, chunk.toString());
    callback();
  },
});</code></pre>
<ol>
<li>Create a writable stream with a 1KB buffer.</li>
<li>Write data to the stream.</li>
<li>If more than 1KB of data is written before it&#39;s processed, the write method will return <code>false</code>, indicating the buffer is full.</li>
</ol>
<p>Here is a more detailed example:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const { Writable } = require(&quot;node:stream&quot;);

const myWritable = new Writable({
  highWaterMark: 1024, // Set buffer size to 1KB
  write(chunk, encoding, callback) {
    console.log(&quot;Writing:&quot;, chunk.toString());
    // Simulate processing delay
    setTimeout(() =&gt; {
      callback();
    }, 100);
  },
});

// Write data to the writable stream
const data = Buffer.from(&quot;This is some data to write to the stream.&quot;);

for (let i = 0; i &lt; 10; i++) {
  const canWrite = myWritable.write(data);

  if (!canWrite) {
    console.log(&quot;Buffer is full, applying backpressure.&quot;);
  } else {
    console.log(&quot;Data written to buffer.&quot;);
  }
}

// End the writable stream
myWritable.end();</code></pre>
<p>In the above examples, when you call the write method of the writable stream, the data is added to the internal buffer. If the buffer is not full, the write method will immediately process the chunk. If the buffer is full, the stream will handle the backpressure by signaling the source to slow down or stop sending data until there&#39;s enough space in the buffer.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2024-07-05-understanding-nodejs-streams/buffering-example.webp" alt="Sequence diagram for our buffering example" loading="lazy" decoding="async"/><figcaption>Sequence diagram for our buffering example</figcaption></figure>
<p>Setting the <code>highWaterMark</code> value does not reject chunks smaller than the specified buffer size. Instead, it determines the maximum amount of data that the internal buffer can hold before applying backpressure. I sometimes think of the <code>highWaterMark</code> as a set of go/stop traffic lights.</p>
<h2 id="backpressure">Backpressure</h2>
<p>We touched briefly above on <strong>backpressure</strong>, but what is it exactly?</p>
<p><strong>Backpressure</strong> is a flow control mechanism. It is used in streams to manage the rate at which data is transmitted from the **source (producer) **to the <strong>destination (consumer).</strong></p>
<p>Let&#39;s continue with our analogy of the <strong>queue of people wanting to enter the room</strong>. When the room reaches its capacity (buffer is full), the manager stops allowing more people to enter. This is akin to the writable stream <strong>applying backpressure</strong> because it cannot handle more data at the moment.</p>
<p>It prevents the <strong>destination</strong> from being overwhelmed by data when it cannot process incoming data as quickly as it is being produced. In the context of Node.js streams, <strong>backpressure</strong> helps ensure efficient and stable data transfer, preventing memory overflow and maintaining application performance.</p>
<p>For another example, here demonstrate handling <strong>backpressure</strong> in a pipe operation:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const fs = require(&quot;node:fs&quot;);

// Create a readable stream from a large file
const readStream = fs.createReadStream(&quot;bigfile.txt&quot;);

// Create a writable stream to an output file
const writeStream = fs.createWriteStream(&quot;output.txt&quot;);

// Pipe the readable stream into the writable stream
readStream.pipe(writeStream);

// Listen for the &#39;drain&#39; event on the writable stream
writeStream.on(&quot;drain&quot;, () =&gt; {
  // This event is emitted when the writable stream buffer is drained
  console.log(&quot;Drained. Resuming read.&quot;);
  // Resume the readable stream to continue reading data
  readStream.resume();
});</code></pre>
<ol>
<li>Create read and write streams.</li>
<li>Pipe data from read to write stream.</li>
<li>If <code>writeStream</code>&#39;s buffer fills up, it emits &#39;<strong>drain</strong>&#39; event.</li>
<li>When &#39;<strong>drain</strong>&#39; occurs, resume reading to manage <strong>backpressure</strong>.</li>
</ol>
<p>Backpressure is managed implicitly by the pipe method, which connects the readable stream (<code>readStream</code>) to the writable stream (<code>writeStream</code>). The pipe method automatically handles the flow control, including backpressure, so you don&#39;t need to manually manage it.</p>
<p>When the <code>drain</code> event is emitted, the <code>writeStream</code> signals that it has processed enough data to make room in its buffer. The <code>readStream</code> then resumes reading data and continues the flow.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2024-07-05-understanding-nodejs-streams/backpressure-example.webp" alt="Sequence diagram for our backpressure example" loading="lazy" decoding="async"/><figcaption>Sequence diagram for our backpressure example</figcaption></figure>
<h2 id="the-two-reading-modes-flowing-and-paused">The two reading modes: Flowing and Paused</h2>
<p>Node.js streams can operate in <a href="https://nodejs.org/api/stream.html#two-reading-modes" target="_blank" rel="noopener noreferrer">two reading modes</a>: <strong>flowing</strong> and <strong>paused</strong>. These modes are separate from <strong>object mode</strong> (that we will touch on later) and determines how data is read from a readable stream.</p>
<ol>
<li>In <strong>flowing mode</strong>, data is read automatically from the stream and emitted as soon as it&#39;s available. This mode is suitable for scenarios where you want to consume data as it arrives, such as real-time data processing or streaming media.</li>
<li>In <strong>paused mode</strong>, data is read manually by calling the <code>read()</code> method. This mode is useful when you need more control over data consumption, such as processing data in batches or pausing and resuming data flow.</li>
</ol>
<p>In the following example, we switch between <strong>flowing</strong> and <strong>paused</strong> modes.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const fs = require(&quot;node:fs&quot;);

const readStream = fs.createReadStream(&quot;sample.txt&quot;);

readStream.on(&quot;data&quot;, (chunk) =&gt; {
  console.log(`Received ${chunk.length} bytes`);
  readStream.pause(); // Switch to paused mode

  setTimeout(() =&gt; {
    readStream.resume(); // Switch back to flowing mode
  }, 1000);
});</code></pre>
<ol>
<li>Create a readable stream in <strong>flowing mode</strong>.</li>
<li>Receive data chunks automatically.</li>
<li>Pause the stream, switching to <strong>paused mode</strong>.</li>
<li>After a delay, resume the stream, switching back to <strong>flowing mode</strong>.</li>
</ol>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2024-07-05-understanding-nodejs-streams/flowing-mode-example.webp" alt="Sequence diagram for our flowing mode example" loading="lazy" decoding="async"/><figcaption>Sequence diagram for our flowing mode example</figcaption></figure>
<p>Here is a more specific example of <strong>flowing mode</strong>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const fs = require(&quot;node:fs&quot;);

const readStream = fs.createReadStream(&quot;bigfile.txt&quot;);
const writeStream = fs.createWriteStream(&quot;output.txt&quot;);

// Pipe the readable stream to the writable stream
readStream.pipe(writeStream);

readStream.on(&quot;data&quot;, (chunk) =&gt; {
  console.log(&quot;Processing data chunk:&quot;, chunk.length);
});</code></pre>
<p>While here is a more specific example of <strong>paused mode</strong>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const fs = require(&quot;node:fs&quot;);

const readStream = fs.createReadStream(&quot;bigfile.txt&quot;);

// Start in paused mode (default)
readStream.on(&quot;readable&quot;, () =&gt; {
  let chunk;
  while (null !== (chunk = readStream.read())) {
    console.log(&quot;Processing data chunk:&quot;, chunk.length);
    // Simulate processing delay
    setTimeout(() =&gt; {
      console.log(&quot;Finished processing chunk&quot;);
    }, 1000);
  }
});</code></pre>
<p>In this scenario, we start in <strong>paused mode</strong> by listening for the <code>readable</code> event. We then manually read data chunks using the <code>read()</code> method and process them one by one.</p>
<h2 id="piping">Piping</h2>
<p><strong>Piping</strong> in Node.js streams refers to <strong>the process of connecting a readable stream to a writable stream</strong>, allowing data to flow automatically from the source (readable stream) to the destination (writable stream). This mechanism simplifies data transfer and handling, making it easy to implement common data processing tasks, such as file copying, HTTP request handling, and more.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const fs = require(&quot;node:fs&quot;);
const { Transform } = require(&quot;node:stream&quot;);

const readStream = fs.createReadStream(&quot;input.txt&quot;);
const writeStream = fs.createWriteStream(&quot;output.txt&quot;);

const upperCaseTransform = new Transform({
  transform(chunk, encoding, callback) {
    this.push(chunk.toString().toUpperCase());
    callback();
  },
});

readStream.pipe(upperCaseTransform).pipe(writeStream);</code></pre>
<ol>
<li>Create read, transform, and write streams.</li>
<li>Pipe data from read stream through transform stream to write stream.</li>
<li>Data flows automatically, with backpressure handled by Node.js.</li>
</ol>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2024-07-05-understanding-nodejs-streams/pipe-example.webp" alt="Sequence diagram for our piping example" loading="lazy" decoding="async"/><figcaption>Sequence diagram for our piping example</figcaption></figure>
<p>A <strong>pipeline</strong> can be created by chaining multiple streams together using the <code>.pipe()</code> method or using the <code>pipeline</code> function from the <code>node:stream</code> module. This allows you to create complex data processing workflows with minimal code and effort.</p>
<p>Here is an example using the <code>.pipe()</code> method to create a pipeline:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const fs = require(&quot;node:fs&quot;);
const zlib = require(&quot;node:zlib&quot;);
const { Transform } = require(&quot;node:stream&quot;);

// Create a transform stream to convert data to uppercase
const upperCaseTransform = new Transform({
  transform(chunk, encoding, callback) {
    this.push(chunk.toString().toUpperCase());
    callback();
  },
});

// Create a readable stream from a source file
const readStream = fs.createReadStream(&quot;source.txt&quot;);

// Create a writable stream to a destination file
const writeStream = fs.createWriteStream(&quot;destination.txt.gz&quot;);

// Create a gzip transform stream
const gzip = zlib.createGzip();

// Pipe the streams together: read -&gt; transform -&gt; gzip -&gt; write
readStream
  .pipe(upperCaseTransform) // Transform to uppercase
  .pipe(gzip) // Compress with gzip
  .pipe(writeStream); // Write to the destination file

// Handle events
writeStream.on(&quot;finish&quot;, () =&gt; {
  console.log(
    &quot;File has been copied, transformed, and compressed successfully.&quot;
  );
});

writeStream.on(&quot;error&quot;, (err) =&gt; {
  console.error(&quot;Error writing to file:&quot;, err);
});

readStream.on(&quot;error&quot;, (err) =&gt; {
  console.error(&quot;Error reading from file:&quot;, err);
});</code></pre>
<p>Here is the alternative using <code>pipeline</code>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const { pipeline } = require(&quot;node:stream&quot;);
const fs = require(&quot;node:fs&quot;);
const zlib = require(&quot;node:zlib&quot;);
const { Transform } = require(&quot;node:stream&quot;);

// Create a transform stream to convert data to uppercase
const upperCaseTransform = new Transform({
  transform(chunk, encoding, callback) {
    this.push(chunk.toString().toUpperCase());
    callback();
  },
});

// Create streams
const readStream = fs.createReadStream(&quot;source.txt&quot;);
const writeStream = fs.createWriteStream(&quot;destination.txt.gz&quot;);
const gzip = zlib.createGzip();

// Use pipeline to handle the streams
pipeline(
  readStream, // Read from the source file
  upperCaseTransform, // Transform to uppercase
  gzip, // Compress with gzip
  writeStream, // Write to the destination file
  (err) =&gt; {
    if (err) {
      console.error(&quot;Pipeline failed:&quot;, err);
    } else {
      console.log(&quot;Pipeline succeeded.&quot;);
    }
  }
);</code></pre>
<h2 id="events">Events</h2>
<p><strong>Events</strong> refer to the various events that streams can emit during their lifecycle. These events allow you to handle different stages and aspects of data processing, such as when data is available, when the stream ends, or when an error occurs.</p>
<p>Under the hood, streams in Node.js can emit events because they are instances of the <a href="https://nodejs.org/en/learn/asynchronous-work/the-nodejs-event-emitter" target="_blank" rel="noopener noreferrer"><strong>EventEmitter</strong></a> class (which I may retroactively touch on more in depth in a future blog post).</p>
<p>Different types of streams emit different events, but we will highlight some of the more common events that you can listen for in Node.js streams.</p>
<h3 id="readable">Readable</h3>
<ol>
<li><strong>data</strong>: Emitted when a chunk of data is available to be read.</li>
<li><strong>end</strong>: Emitted when there is no more data to be read from the stream.</li>
<li><strong>error</strong>: Emitted when an error occurs while reading data.</li>
<li><strong>readable</strong>: Emitted when the stream has data available to be read. This event is used in paused mode to indicate that you can call <code>read()</code> to get more data.</li>
</ol>
<h3 id="writeable">Writeable</h3>
<ol>
<li><strong>drain</strong>: Emitted when the writable stream&#39;s internal buffer is emptied and it is ready to receive more data. This is important for handling backpressure.</li>
<li><strong>finish</strong>: Emitted when the <code>end()</code> method is called and all data has been flushed to the underlying system.</li>
<li><strong>error</strong>: Emitted when an error occurs while writing data.</li>
</ol>
<h3 id="stream-events-in-action">Stream Events in action</h3>
<p>The following is an simple example of hooking to events on a readable stream.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const fs = require(&quot;node:fs&quot;);

const readStream = fs.createReadStream(&quot;events.txt&quot;);

readStream.on(&quot;open&quot;, () =&gt; console.log(&quot;Stream opened&quot;));
readStream.on(&quot;data&quot;, (chunk) =&gt; console.log(`Received ${chunk.length} bytes`));
readStream.on(&quot;end&quot;, () =&gt; console.log(&quot;Stream ended&quot;));
readStream.on(&quot;close&quot;, () =&gt; console.log(&quot;Stream closed&quot;));
readStream.on(&quot;error&quot;, (err) =&gt; console.error(&quot;Error:&quot;, err));</code></pre>
<ol>
<li>Create a readable stream.</li>
<li>Set up event listeners for various stream states.</li>
<li>Events fire asynchronously as the stream operates.</li>
<li>&#39;<strong>open</strong>&#39; event when file is opened, &#39;<strong>data</strong>&#39; for each chunk, &#39;<strong>end</strong>&#39; when all data is read, &#39;<strong>close</strong>&#39; when stream closes.</li>
</ol>
<p>For a more complex example demonstrating more event types, you can refer to the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const fs = require(&quot;node:fs&quot;);
const zlib = require(&quot;node:zlib&quot;);
const { Transform } = require(&quot;node:stream&quot;);

// Create a transform stream to convert data to uppercase
const upperCaseTransform = new Transform({
  transform(chunk, encoding, callback) {
    this.push(chunk.toString().toUpperCase());
    callback();
  },
});

const readStream = fs.createReadStream(&quot;source.txt&quot;);
const writeStream = fs.createWriteStream(&quot;destination.txt.gz&quot;);
const gzip = zlib.createGzip();

// Pipe the streams together
readStream.pipe(upperCaseTransform).pipe(gzip).pipe(writeStream);

// Handle readable stream events
readStream.on(&quot;data&quot;, (chunk) =&gt; {
  console.log(&quot;Reading data:&quot;, chunk.length);
});

readStream.on(&quot;end&quot;, () =&gt; {
  console.log(&quot;Finished reading data.&quot;);
});

readStream.on(&quot;error&quot;, (err) =&gt; {
  console.error(&quot;Read error:&quot;, err);
});

// Handle writable stream events
writeStream.on(&quot;finish&quot;, () =&gt; {
  console.log(&quot;Finished writing data.&quot;);
});

writeStream.on(&quot;drain&quot;, () =&gt; {
  console.log(&quot;Writable stream drained.&quot;);
});

writeStream.on(&quot;error&quot;, (err) =&gt; {
  console.error(&quot;Write error:&quot;, err);
});

// Handle transform stream events
upperCaseTransform.on(&quot;error&quot;, (err) =&gt; {
  console.error(&quot;Transform error:&quot;, err);
});

gzip.on(&quot;error&quot;, (err) =&gt; {
  console.error(&quot;Gzip error:&quot;, err);
});</code></pre>
<p>The above hooks into some events for the different streams in the pipeline. This allows you to monitor the progress of the data processing, handle errors, and respond to various stream states.</p>
<h2 id="object-mode">Object Mode</h2>
<p>All streams created by Node.js APIs operate exclusively with strings or <code>Buffer</code>, <code>TypedArray</code> or <code>DataView</code> objects.</p>
<p><a href="https://nodejs.org/api/stream.html#object-mode" target="_blank" rel="noopener noreferrer"><strong>Object mode</strong></a> however enables streams to work with other types of JavaScript object values.</p>
<p>In the following, we set <code>objectMode</code> to <code>true</code> in order to process JavaScript objects.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const { Transform } = require(&quot;node:stream&quot;);

const objectModeTransform = new Transform({
  objectMode: true,
  transform(chunk, encoding, callback) {
    this.push({ length: chunk.length, content: chunk.toString() });
    callback();
  },
});

process.stdin.pipe(objectModeTransform).on(&quot;data&quot;, (obj) =&gt; {
  console.log(&quot;Received object:&quot;, obj);
});</code></pre>
<ol>
<li>Create a transform stream in object mode.</li>
<li>Pipe stdin (buffer mode) to the transform stream.</li>
<li>Transform <strong>converts Buffer chunks</strong> to JavaScript objects.</li>
<li>Output stream receives and logs JavaScript objects.</li>
</ol>
<p>There are trade-offs for using <code>objectMode</code> as it can impact performance overhead, backpressure handling, buffering constraints and serialization requirements.</p>
<p>Here is another example to more clearly demonstrate the use of <code>objectMode</code> between a readable and writeable stream:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const { Readable, Writable } = require(&quot;node:stream&quot;);

// Create a readable stream in objectMode
const readableStream = new Readable({
  objectMode: true,
  read() {
    this.push({ id: 1, name: &quot;Alice&quot; });
    this.push({ id: 2, name: &quot;Bob&quot; });
    this.push(null); // No more data
  },
});

// Create a writable stream in objectMode
const writableStream = new Writable({
  objectMode: true,
  write(chunk, encoding, callback) {
    console.log(&quot;Received object:&quot;, chunk);
    callback();
  },
});

// Pipe the readable stream to the writable stream
readableStream.pipe(writableStream);</code></pre>
<ol>
<li>Create a readable stream that emits JavaScript objects.</li>
<li>Create a writable stream that receives and logs objects.</li>
<li>Pipe the readable stream to the writable stream.</li>
</ol>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2024-07-05-understanding-nodejs-streams/object-mode-example.webp" alt="Sequence diagram for our object mode example" loading="lazy" decoding="async"/><figcaption>Sequence diagram for our object mode example</figcaption></figure>
<h2 id="conclusion">Conclusion</h2>
<p>Understanding <strong>Node.js streams</strong> is crucial for efficient data handling and manipulation in various applications. This blog post introduced the four fundamental types of streams, <strong>Writable</strong>, <strong>Readable</strong>, <strong>Duplex</strong>, and <strong>Transform</strong>, using relatable analogies to bridge the gap between abstract concepts and practical implementation.</p>
<p>Streams offer numerous benefits, <strong>including memory and time efficiency, composability, backpressure handling, and seamless integration with Node.js&#39;s event-driven architecture</strong>. They enable the creation of powerful, scalable, and flexible data processing pipelines.</p>
<p>By grasping the foundational concepts of streams, such as <strong>buffering</strong>, <strong>backpressure</strong>, <strong>piping</strong>, and <strong>modes of operation</strong>, you can unlock the full potential of Node.js for handling large datasets, network communication, and real-time data processing.</p>
<p>With this solid foundation, you&#39;re now equipped to explore the more complex aspects of Node.js streams and leverage their capabilities to build robust and performant applications.</p>
<p>Future posts will delve deeper into each stream type, providing detailed insights and advanced techniques to further enhance your Node.js applications.</p>
<h2 id="resources-and-further-reading">Resources and further reading</h2>
<ul>
<li><a href="https://nodejs.org/api/stream.html#types-of-streams" target="_blank" rel="noopener noreferrer">Node.js | Stream | Four fundamental stream types</a></li>
<li><a href="https://nodejs.org/api/stream.html#buffering" target="_blank" rel="noopener noreferrer">Node.js | Stream | Buffering in Node.js streams</a></li>
<li><a href="https://nodejs.org/en/learn/asynchronous-work/the-nodejs-event-emitter" target="_blank" rel="noopener noreferrer">Node.js | Asynchronous work | EventEmitter class</a></li>
<li><a href="https://nodejs.org/api/stream.html#two-reading-modes" target="_blank" rel="noopener noreferrer">Node.js | Stream | Two reading modes</a></li>
<li><a href="https://nodejs.org/api/stream.html#object-mode" target="_blank" rel="noopener noreferrer">Node.js | Stream | Object mode</a></li>
<li><a href="https://nodejs.org/api/stream.html#types-of-streams" target="_blank" rel="noopener noreferrer">Node.js | Stream | Types of streams</a></li>
<li><a href="https://nodejs.org/" target="_blank" rel="noopener noreferrer">Node.js homepage</a></li>
</ul>
<p><strong>Disclaimer</strong>: This blog post used AI to generate the images used for the analogy.</p>
<p><em>Photo credit: <a href="https://unsplash.com/@amadejtauses" target="_blank" rel="noopener noreferrer"><code>amadejtauses</code></a></em></p>]]></content>
<category term="nodejs"/>
</entry>
<entry>
<title type="html"><![CDATA[Node.js Buffers Explained]]></title>
<id>https://blog.dennisokeeffe.com/blog/2024-07-04-nodejs-buffers-explained</id>
<link href="https://dennisokeeffe.com/blog/2024-07-04-nodejs-buffers-explained"/>
<published>2024-07-04T00:00:00.000Z</published>
<updated>2024-07-04T00:00:00.000Z</updated>
<summary type="html"><![CDATA[Learn what a Buffer is in Node.js, how to think about it and how it is useful.]]></summary>
<content type="html"><![CDATA[<h2 id="overview">Overview</h2>
<p>Today&#39;s post will walk through <a href="https://nodejs.org/api/buffer.html" target="_blank" rel="noopener noreferrer">Node.js&#39; Buffer object</a>. This will be a prelude to the <a href="https://dennisokeeffe.com/blog/series/nodejs-streams">series on demystifying Node.js streams</a>.</p>
<span id="what-are-nodejs-buffer-objects" class="anchor-alias"><h2 id="what-are-node-js-buffer-objects">What are Node.js Buffer objects?</h2>
<p>A <strong>Buffer</strong> in <a href="https://nodejs.org/" target="_blank" rel="noopener noreferrer">Node.js</a> is a <strong>temporary storage area in memory used to hold raw binary data</strong>. It is a fixed-length sequence of bytes, similar to an array of integers, but corresponds to a raw memory allocation outside <a href="https://v8.dev/" target="_blank" rel="noopener noreferrer">the V8 JavaScript engine</a>.</p>
<p>Buffers are essential for handling raw binary data efficiently in <a href="https://nodejs.org/" target="_blank" rel="noopener noreferrer">Node.js</a>. They serve as the backbone for many operations involving file I/O, networking, and streams. Understanding Buffers is key to mastering <a href="https://nodejs.org/api/stream.html" target="_blank" rel="noopener noreferrer">Node.js streams</a> and dealing with data at a low level.</p>
<span id="what-does-it-mean-to-be-allocated-outside-of-the-v8-engine" class="anchor-alias"><h2 id="what-does-it-mean-to-be-allocated-outside-of-the-v-8-engine">What does it mean to be allocated outside of the V8 engine?</h2>
<p><a href="https://v8.dev/" target="_blank" rel="noopener noreferrer">V8 is the JavaScript engine</a> used by <a href="https://nodejs.org/" target="_blank" rel="noopener noreferrer">Node.js</a>. It is responsible for executing JavaScript code and managing memory allocation for JavaScript objects, such as strings, arrays, and objects. This memory is managed within a structure called the heap, which the V8 engine controls.</p>
<p>Buffers, on the other hand, are allocated in** Node.js&#39;s C++ layer**, which interfaces with the operating system directly. They are designed to handle binary data and are more efficient for certain operations, such as <em>reading files</em> or <em>handling network protocols</em>. To achieve this efficiency, <strong>Buffers allocate memory outside the V8 heap</strong>.</p>
<p>This means:</p>
<ol>
<li><strong>Direct Memory Access</strong>: Buffers provide direct access to memory outside of V8&#39;s managed heap, allowing for faster and more efficient handling of raw binary data. This is especially useful for operations that require manipulation of large amounts of data, such as file I/O and networking.</li>
<li><strong>Fixed-Size Allocation</strong>: When a Buffer is created (as you will see in another section), it allocates a fixed-size block of memory. This block is not subject to the garbage collection process that manages the rest of the JavaScript objects in V8. As a result, Buffers can avoid the overhead associated with garbage collection, leading to better performance in memory-intensive operations.</li>
<li><strong>Native Code Interoperability</strong>: Allocating memory outside the V8 heap allows Node.js to interact more easily with native code (C/C++ libraries) and system-level resources, which often require access to raw binary data.</li>
</ol>
<p>When you create a Buffer, the Node.js runtime <strong>allocates a block of memory from the system&#39;s memory pool</strong>. This is done using methods provided by the operating system, such as <a href="https://www.tutorialspoint.com/c_standard_library/c_function_malloc.htm" target="_blank" rel="noopener noreferrer"><code>malloc</code> in C</a>. The allocated memory is then <strong>managed by Node.js but remains outside the control of the V8 engine</strong>.</p>
<h2 id="how-should-i-think-of-a-buffer">How should I think of a Buffer?</h2>
<p>To mentally visualize a Buffer, you can think of <strong>a water bucket outside of a house</strong>.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2024-07-04-nodejs-buffers-explained/house-bucket.webp" alt="A house with a bucket outside. The house represents the V8 heap, while the bucket represents the Buffer in system memory." loading="lazy" decoding="async"/><figcaption>A house with a bucket outside. The house represents the V8 heap, while the bucket represents the Buffer in system memory.</figcaption></figure>
<p>The <strong>house itself represents the V8 heap</strong>, where JavaScript objects like strings, arrays, and objects live.</p>
<p>The <strong>bucket outside the house represents the Buffer</strong>. It is a separate container for raw binary data that is not subject to the same rules and restrictions as the objects inside the house.</p>
<p>As for using the bucket (Buffer), you can think of a <strong>hose (data source) that can fill the bucket (Buffer) with water (data).</strong></p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2024-07-04-nodejs-buffers-explained/hose-bucket.webp" alt="A hose filling a bucket with water. This represents the binary data held within a Buffer." loading="lazy" decoding="async"/><figcaption>A hose filling a bucket with water. This represents the binary data held within a Buffer.</figcaption></figure>
<p>To start connecting the dots, let&#39;s start walking through some examples of what we can do with Buffers and round out our analogy.</p>
<h2 id="working-with-buffers">Working with Buffers</h2>
<p>To create a Buffer, you can use the <code>Buffer</code> class provided by Node.js. <a href="https://nodejs.org/api/buffer.html#buffer" target="_blank" rel="noopener noreferrer">The buffer documentation</a> recommends explicitly importing the Buffer class, although it is available in the global scope.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const { Buffer } = require(&quot;node:buffer&quot;);

// Create a Buffer of 8 bytes
const buf1 = Buffer.alloc(8); // &lt;Buffer 00 00 00 00 00 00 00 00&gt;

// Create a Buffer from a string
const buf2 = Buffer.from(&quot;Hello, World!&quot;); // &lt;Buffer 48 65 6c 6c 6f 2c 20 57 6f 72 6c 64 21&gt;

// Create a Buffer from an array of integers
const buf3 = Buffer.from([1, 2, 3, 4, 5]); // &lt;Buffer 01 02 03 04 05&gt;</code></pre>
<p>In the above example, if is <code>buf1</code> that we can think of as our empty bucket without water, while <code>buf2</code> and <code>buf3</code> are our buckets that are already filled with water.</p>
<p>In order to fill <code>buf1</code>, we could also make use of the <code>write</code> method:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const buf1 = Buffer.alloc(8); // &lt;Buffer 00 00 00 00 00 00 00 00&gt;

buf1.write(&quot;Hello!!!&quot;); // &lt;Buffer 48 65 6c 6c 6f 21 21 21&gt;</code></pre>
<p>Once a buffer is also created, we can replace &quot;part of the water&quot; directly:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>buf1[7] = 0x65; // &lt;Buffer 48 65 6c 6c 6f 21 21 65&gt;

buf1.toString(); // &quot;Hello!e&quot;</code></pre>
<span id="buffer-and-v8-heap-interaction" class="anchor-alias"><h2 id="buffer-and-v-8-heap-interaction">Buffer and V8 heap interaction</h2>
<p>Note that every time we convert the Buffer back into a JavaScript object data type supported by V8, we are effectively <strong>bring it back into the house (V8 heap)</strong> which is managed by the V8 JavaScript engine.</p>
<p>This process of conversion has some important implications:</p>
<ol>
<li><strong>Performance</strong>: Converting large Buffers to strings or other V8 types can be computationally expensive and may cause memory pressure on V8&#39;s heap.</li>
<li><strong>Memory usage</strong>: The converted data now exists in two places - the original Buffer (outside V8) and the new string or object (inside V8).</li>
<li><strong>Garbage collection</strong>: While the Buffer itself is managed outside of V8, any strings or objects created from it are subject to V8&#39;s garbage collection.</li>
<li><strong>Immutability</strong>: When you convert a Buffer to a string, you get an immutable JavaScript string. Any changes you make to this string will create a new string, not modify the original Buffer.</li>
</ol>
<p>To demonstrate, consider the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const buf = Buffer.from(&quot;Hello, World!&quot;);
// buf is allocated outside V8&#39;s heap

const str = buf.toString();
// str is a new string in V8&#39;s heap

str[0] = &quot;h&quot;; // This doesn&#39;t modify str or buf
console.log(str); // Still &quot;Hello, World!&quot;
const str2 = buf.toString(); // Still &quot;Hello, World!&quot; but as a new string in V8&#39;s heap

buf[0] = 0x68; // This modifies the Buffer directly
const str3 = buf.toString(); // Now &quot;hello, World!&quot; as a new string in V8&#39;s heap</code></pre>
<blockquote>
<p>While looking at the above, I believe it is important to keep comparing what is happening to the analogy of the bucket outside the house.</p>
</blockquote>
<h2 id="hexadecimal-representation">Hexadecimal representation</h2>
<p>For all of the code example above, I have added the <strong>hexadecimal representation</strong> of the Buffer in the comments.</p>
<p>For those unfamiliar with <strong>hexadecimal representation</strong>, each pair of characters in this representation corresponds to a single byte, represented in <strong>hexadecimal (base-16) notation</strong>.</p>
<p>In hexadecimal, each digit can be <strong>0-9 or A-F</strong>, where A-F represent the decimal values 10-15 respectively.</p>
<p>For the byte values in my <code>buf1</code> example, we have <code>48 65 6c 6c 6f 21 21 21</code> where each of these is a byte value in hex.</p>
<p>These hex values in this example correspond to ASCII characters:</p>
<ul>
<li>48 -&gt; &#39;H&#39;</li>
<li>65 -&gt; &#39;e&#39;</li>
<li>6c -&gt; &#39;l&#39;</li>
<li>6c -&gt; &#39;l&#39;</li>
<li>6f -&gt; &#39;o&#39;</li>
<li>21 -&gt; &#39;!&#39;</li>
<li>21 -&gt; &#39;!&#39;</li>
<li>21 -&gt; &#39;!&#39;</li>
</ul>
<p>If we convert this buffer back into a string, it would return to again be &quot;Hello!!!&quot;:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const { Buffer } = require(&quot;node:buffer&quot;);

// Create a Buffer of 8 bytes
const buf1 = Buffer.alloc(8); // &lt;Buffer 00 00 00 00 00 00 00 00&gt;

buf1.write(&quot;Hello!!!&quot;); // &lt;Buffer 48 65 6c 6c 6f 21 21 21&gt;
buf1.toString(); // &quot;Hello!!!&quot;</code></pre>
<p>In fact, instead of using the string <strong>&quot;Hello!!!&quot;</strong> as an argument to <code>write</code> after allocating a buffer, we could even pass in hexadecimal values directly:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const buf1 = Buffer.from([0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x21, 0x21, 0x21]);
console.log(buf1.toString()); // Outputs: Hello!!!

const buf2 = Buffer.from(&quot;48656c6c6f212121&quot;, &quot;hex&quot;);
console.log(buf2.toString()); // Outputs: Hello!!!

const buf3 = Buffer.alloc(8);
[0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x21, 0x21, 0x21].forEach((byte, index) =&gt; {
  buf3.writeUInt8(byte, index);
});
console.log(buf3.toString()); // Outputs: Hello!!!

const buf4 = Buffer.alloc(8);
buf4.write(&quot;\x48\x65\x6c\x6c\x6f\x21\x21\x21&quot;, &quot;hex&quot;);
console.log(buf4.toString()); // Outputs: Hello!!!</code></pre>
<blockquote>
<p>Note that <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Numbers_and_dates#hexadecimal_numbers" target="_blank" rel="noopener noreferrer">hexadecimal representation in JavaScript</a> can be denoted by a leading <code>0x</code>.</p>
</blockquote>
<h2 id="common-operations-with-buffers">Common operations with Buffers</h2>
<h3 id="methods-for-creating-buffers">Methods for creating Buffers</h3>
<table>
<thead>
<tr>
<th>Method</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Buffer.alloc(size)</code></td>
<td>Create a new Buffer of specified size</td>
</tr>
<tr>
<td><code>Buffer.from(array)</code></td>
<td>Create a Buffer from an array of bytes</td>
</tr>
<tr>
<td><code>Buffer.from(string, encoding)</code></td>
<td>Create a Buffer from a string</td>
</tr>
</tbody>
</table>
<h3 id="methods-for-reading-and-writing-from-buffers">Methods for reading and writing from Buffers</h3>
<table>
<thead>
<tr>
<th>Method</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>buf.toString(encoding, start, end)</code></td>
<td>Convert Buffer to string</td>
</tr>
<tr>
<td><code>buf.write(string, offset, length, encoding)</code></td>
<td>Write a string to the Buffer</td>
</tr>
<tr>
<td><code>buf.readUInt8(offset)</code>, <code>buf.readUInt16LE(offset)</code>, etc.</td>
<td>Read integer values</td>
</tr>
<tr>
<td><code>buf.writeUInt8(value, offset)</code>, <code>buf.writeUInt16LE(value, offset)</code>, etc.</td>
<td>Write integer values</td>
</tr>
</tbody>
</table>
<h3 id="buffer-methods-for-comparison-and-search">Buffer methods for comparison and search</h3>
<table>
<thead>
<tr>
<th>Method</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>buf.equals(otherBuffer)</code></td>
<td>Compare two Buffers</td>
</tr>
<tr>
<td><code>buf.indexOf(value[, byteOffset][, encoding])</code></td>
<td>Find the index of a value in the Buffer</td>
</tr>
<tr>
<td><code>buf.includes(value[, byteOffset][, encoding])</code></td>
<td>Check if Buffer includes a value</td>
</tr>
</tbody>
</table>
<h3 id="buffer-methods-for-slicing-and-copying">Buffer methods for slicing and copying</h3>
<table>
<thead>
<tr>
<th>Method</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>buf.slice([start[, end]])</code></td>
<td>Create a new Buffer that references the same memory as the original</td>
</tr>
<tr>
<td><code>buf.copy(target[, targetStart[, sourceStart[, sourceEnd]]])</code></td>
<td>Copy data between Buffers</td>
</tr>
</tbody>
</table>
<h3 id="buffer-methods-for-conversion">Buffer methods for conversion</h3>
<table>
<thead>
<tr>
<th>Method</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>buf.toJSON()</code></td>
<td>Convert Buffer to a JSON representation</td>
</tr>
<tr>
<td><code>Buffer.concat(list[, totalLength])</code></td>
<td>Concatenate a list of Buffers</td>
</tr>
</tbody>
</table>
<h2 id="common-applications-of-buffers">Common applications of Buffers</h2>
<h3 id="file-operations">File operations</h3>
<p>For example, <strong>reading from and writing to files</strong>, especially when dealing with binary data.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const fs = require(&quot;node:fs&quot;);

// Reading a file into a Buffer
const buffer = fs.readFileSync(&quot;example.bin&quot;); // &lt;Buffer ... &gt;

// Writing a Buffer to a file
fs.writeFileSync(&quot;output.bin&quot;, buffer);</code></pre>
<h3 id="network-communications">Network communications</h3>
<p>For example, <strong>handling raw data in network protocols</strong>.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const net = require(&quot;node:net&quot;);

const server = net.createServer((socket) =&gt; {
  socket.on(&quot;data&quot;, (buffer) =&gt; {
    console.log(buffer.toString());
  });
});</code></pre>
<h3 id="cryptography">Cryptography</h3>
<p>This includes things like <strong>hashing, encryption, and decryption</strong>.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const crypto = require(&quot;node:crypto&quot;);

const data = Buffer.from(&quot;Hello, World!&quot;);
const hash = crypto.createHash(&quot;sha256&quot;).update(data).digest(); // &lt;Buffer df fd 60 21 bb 2b d5 b0 af 67 62 90 80 9e c3 a5 31 91 dd 81 c7 f7 0a 4b 28 68 8a 36 21 82 98 6f&gt;</code></pre>
<h3 id="image-processing">Image processing</h3>
<p>We can use Buffers to <strong>manipulate image data</strong>.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const sharp = require(&quot;sharp&quot;);

sharp(&quot;input.jpg&quot;)
  .resize(300, 200)
  .toBuffer((err, buffer) =&gt; {
    // buffer contains the processed image data
  });</code></pre>
<span id="base64-encodingdecoding" class="anchor-alias"><h3 id="base-64-encoding-decoding">Base64 encoding/decoding</h3>
<p>Often used for <strong>data transfer and storage</strong>.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const original = Buffer.from(&quot;Hello, World!&quot;); // &lt;Buffer 48 65 6c 6c 6f 2c 20 57 6f 72 6c 64 21&gt;
const encoded = original.toString(&quot;base64&quot;); // &#39;SGVsbG8sIFdvcmxkIQ==&#39;
const decoded = Buffer.from(encoded, &quot;base64&quot;); // &lt;Buffer 48 65 6c 6c 6f 2c 20 57 6f 72 6c 64 21&gt;
decoded.toString(); // &quot;Hello, World!&quot;</code></pre>
<h3 id="data-streaming">Data streaming</h3>
<p>Efficiently <strong>processing large amounts of data</strong>. We will touch more on this throughout the series.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const fs = require(&quot;fs&quot;);

const readStream = fs.createReadStream(&quot;largefile.txt&quot;);
readStream.on(&quot;data&quot;, (chunk) =&gt; {
  // chunk is a Buffer
  console.log(chunk.length);
});</code></pre>
<h2 id="the-highs-and-lows-of-using-buffers">The highs and lows of using Buffers</h2>
<p>Here a some highs of using <strong>Buffers</strong>:</p>
<ul>
<li><strong>Efficient Binary Data Handling</strong>: Buffers are designed to handle raw binary data efficiently, making them ideal for file I/O, network communication, and other scenarios requiring direct manipulation of binary data.</li>
<li><strong>Direct Memory Allocation</strong>: Buffers allocate memory outside the V8 heap, allowing for more efficient use of memory and avoiding the overhead of garbage collection.</li>
<li><strong>High Performance</strong>: Direct access to memory and efficient handling of binary data result in high performance, especially for operations involving large amounts of data.</li>
<li><strong>Interoperability with Native Code</strong>: Buffers facilitate interoperability with native code (C/C++ libraries) and system-level resources, making it easier to integrate with existing systems and perform low-level operations.</li>
<li><strong>Flexibility in Data Encoding</strong>: Buffers support various data encodings (e.g., UTF-8, ASCII, Base64), making it easy to convert between different formats.</li>
</ul>
<p>In contrast, here are some of the gotchas to look out for:</p>
<ul>
<li><strong>Fixed Size</strong>: Once a Buffer is created, its size cannot be changed. This means you need to allocate the correct amount of memory upfront, which can be difficult to estimate accurately.</li>
<li><strong>Complexity</strong>: Using Buffers requires a decent understanding of binary data manipulation, memory management, and encoding/decoding, which can add complexity to the code.</li>
<li><strong>Potential Security Risks</strong>: Improper handling of Buffers, such as misuse of <code>Buffer.allocUnsafe</code>, can lead to security vulnerabilities.</li>
<li><strong>Less Readable Code</strong>: Code that deals with Buffers can be harder to read and understand compared to using higher-level abstractions, especially for developers not familiar with binary data manipulation.</li>
<li><strong>Buffers do not work with objects directly</strong>: The type must be of type string or an instance of Buffer, ArrayBuffer, or Array or an Array-like Object. This means that you cannot pass an object directly to a Buffer (nor certain other types like booleans).</li>
</ul>
<h2 id="conclusion">Conclusion</h2>
<p><strong>Buffers</strong> are a fundamental component in Node.js for <strong>handling raw binary data efficiently</strong>. By allocating memory outside the <strong>V8 heap</strong>, Buffers provide direct access to memory, enabling high performance for operations involving large data sets, such as file I/O and network communication. Understanding how Buffers work, including their fixed size and manual memory management requirements, is crucial for writing efficient Node.js applications.</p>
<p>In today&#39;s blog post, we&#39;ve covered a the fundamental ideas behind Buffers in Node.js and we gave ourselves an analogy of a bucket outside a house to help us mentally visualize the concepts around Buffers.</p>
<p>While Buffers offer numerous advantages, including <strong>interoperability with native code and flexibility in data encoding</strong>, they also <strong>introduce complexity</strong>. Properly managing buffer allocation, avoiding common pitfalls, and ensuring memory is correctly handled are essential practices for developers working with Buffers.</p>
<p>By mastering Buffers, you&#39;ll be well-prepared to dive deeper into Node.js streams and handle data at a low level, paving the way for building robust and high-performance applications.</p>
<h2 id="resources-and-further-reading">Resources and further reading</h2>
<ul>
<li><a href="https://nodejs.org/api/buffer.html" target="_blank" rel="noopener noreferrer">Node.js&#39; Buffer object documentation</a></li>
<li><a href="https://nodejs.org/api/stream.html" target="_blank" rel="noopener noreferrer">Node.js streams documentation</a></li>
<li><a href="https://v8.dev/" target="_blank" rel="noopener noreferrer">The V8 JavaScript engine</a></li>
<li><a href="https://nodejs.org/" target="_blank" rel="noopener noreferrer">Node.js homepage</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Numbers_and_dates#hexadecimal_numbers" target="_blank" rel="noopener noreferrer">Hexadecimal representation in JavaScript</a></li>
</ul>
<p><strong>Disclaimer</strong>: This blog post used AI to generate the images used for the analogy.</p>
<p><em>Photo credit: <a href="https://unsplash.com/@nervum" target="_blank" rel="noopener noreferrer"><code>nervum</code></a></em></p>]]></content>
<category term="nodejs"/>
</entry>
<entry>
<title type="html"><![CDATA[Dependency Injection with InversifyJS]]></title>
<id>https://blog.dennisokeeffe.com/blog/2024-04-25-dependency-injection-with-inversifyjs</id>
<link href="https://dennisokeeffe.com/blog/2024-04-25-dependency-injection-with-inversifyjs"/>
<published>2024-04-25T00:00:00.000Z</published>
<updated>2024-04-25T00:00:00.000Z</updated>
<summary type="html"><![CDATA[Learn how we can use InversifyJS to help dependency injection with it's inversion of control container.]]></summary>
<content type="html"><![CDATA[<h2 id="overview">Overview</h2>
<p>Today&#39;s post will walk through how we can use InversifyJS to help with dependency injection in our TypeScript projects.</p>
<p>The full code can be found <a href="https://github.com/okeeffed/demo-inversifyjs" target="_blank" rel="noopener noreferrer">here</a>.</p>
<h2 id="prerequisites">Prerequisites</h2>
<p>It is expected that you are familiar with a package manager (like npm or pnpm, the latter of which I will use today) and have Node.js installed on your machine.</p>
<p>You should also be familiar with TypeScript and experimental decorators.</p>
<p>I will be using Node.js v20+ for this demo.</p>
<h2 id="getting-started">Getting started</h2>
<p>Let&#39;s start by creating a new project:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="bash"><code># Create a new project
$ mkdir demo-inversifyjs
$ cd demo-inversifyjs
$ pnpm init

# Install dependencies
# Required for TypeScript
$ pnpm add -D typescript tsx @tsconfig/node20 @types/node

# Required for InversifyJS
$ pnpm add inversify reflect-metadata

# We will create a few config files, a main entry file and two different class files
$ touch tsconfig.json index.ts</code></pre>
<p>Some notes on the above:</p>
<ul>
<li>We install some TypeScript defaults and <code>@tsconfig/node20</code> for our TypeScript configuration.</li>
<li>We install <code>inversify</code> and <code>reflect-metadata</code> for InversifyJS.</li>
<li>We create a <code>tsconfig.json</code> file for our TypeScript config, and an <code>index.ts</code> entry file. We will add more files as required.</li>
</ul>
<p>Next, let&#39;s add some configuration to our <code>tsconfig.json</code>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="json"><code>{
  &quot;extends&quot;: &quot;@tsconfig/node20/tsconfig.json&quot;,
  &quot;compilerOptions&quot;: {
    &quot;lib&quot;: [&quot;es6&quot;, &quot;dom&quot;],
    &quot;types&quot;: [&quot;reflect-metadata&quot;, &quot;node&quot;],
    &quot;experimentalDecorators&quot;: true,
    &quot;emitDecoratorMetadata&quot;: true
  }
}</code></pre>
<p>The extra compiler options added are required for <a href="https://inversify.io/" target="_blank" rel="noopener noreferrer">InversifyJS</a> to work. You will see the use of decorators in the section where we implement the code.</p>
<span id="what-problem-is-inversifyjs-trying-to-solve" class="anchor-alias"><h2 id="what-problem-is-inversify-js-trying-to-solve">What problem is InversifyJS trying to solve</h2>
<p>Directly from the InversifyJS documentation:</p>
<blockquote>
<p>Our goal is to write code that adheres to the <a href="https://en.wikipedia.org/wiki/Dependency_inversion_principle" target="_blank" rel="noopener noreferrer">dependency inversion principle</a>. This means that we should &quot;depend upon Abstractions and do not depend upon concretions&quot;. Let&#39;s start by declaring some interfaces (abstractions).</p>
</blockquote>
<p>Let&#39;s take a simple example of a file reader <em>without InversifyJS</em>. Let&#39;s say we have a <code>FileReader</code> class that reads from a file.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import { readFile } from &quot;node:fs/promises&quot;;

interface IParser {
  parse(data: string): string;
}

interface IFileReader {
  read(filepath: string): Promise&lt;string&gt;;
  parse(data: string): string;
}

class FileReader implements IFileReader {
  private parser: IParser;
  private contents?: string;

  constructor(parser: IParser) {
    this.parser = parser;
  }

  async read(filepath: string) {
    this.contents = await readFile(filepath, &quot;utf-8&quot;);
    return this.contents;
  }

  parse() {
    if (!this.contents) throw new Error(&quot;No contents to parse&quot;);

    return this.parser.parse(this.contents);
  }
}</code></pre>
<p>In the above example, we have a <code>FileReader</code> class that reads from a file. It has a <code>parse</code> method that takes in a string and returns a string.</p>
<p>The <code>FileReader</code> class has a dependency on a <code>parser</code> object that implements the <code>IParser</code> interface.</p>
<p>In our case, we use dependency injection to pass in the <code>parser</code> object to the <code>FileReader</code> class.</p>
<p>In a contrived example where we want one <code>FileReader</code> instance to be able to parse JSON while another simply returns the text, we could use the classes as follows:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import path from &quot;node:path&quot;;

// ... omit existing code

class JsonParser implements IParser {
  parse(data: string) {
    return JSON.parse(data);
  }
}

class FileParser implements IParser {
  parse(data: string) {
    return data;
  }
}

async function main() {
  const jsonParser = new JsonParser();
  const jsonFileReader = new FileReader(jsonParser);

  const fileParser = new FileParser();
  const fileReader = new FileReader(fileParser);

  const jsonFile = await jsonFileReader.read(
    path.resolve(process.cwd(), &quot;./data/example.json&quot;)
  );
  const file = await fileReader.read(
    path.resolve(process.cwd(), &quot;./data/example.txt&quot;)
  );

  console.log(jsonFileReader.parse());
  console.log(fileReader.parse());
}

main();</code></pre>
<p>If we now run the above code (let&#39;s assume it&#39;s in <code>file-reader.ts</code>), we get the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="s"><code># Running the file
$ node file-reader.ts
{ example: &#39;data&#39; }
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</code></pre>
<p>In the above example, we displayed how to inject dependencies into a class. However, this can become cumbersome as the number of dependencies grows.</p>
<p>The full code is as follows:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import { readFile } from &quot;node:fs/promises&quot;;
import path from &quot;node:path&quot;;

interface IParser {
  parse(data: string): string;
}

class JsonParser implements IParser {
  parse(data: string) {
    return JSON.parse(data);
  }
}

class FileParser implements IParser {
  parse(data: string) {
    return data;
  }
}

interface IFileReader {
  read(filepath: string): Promise&lt;string&gt;;
  parse(data: string): string;
}

class FileReader implements IFileReader {
  private parser: IParser;
  private contents?: string;

  constructor(parser: IParser) {
    this.parser = parser;
  }

  async read(filepath: string) {
    this.contents = await readFile(filepath, &quot;utf-8&quot;);
    return this.contents;
  }

  parse() {
    if (!this.contents) throw new Error(&quot;No contents to parse&quot;);

    return this.parser.parse(this.contents);
  }
}

async function main() {
  const jsonParser = new JsonParser();
  const jsonFileReader = new FileReader(jsonParser);

  const fileParser = new FileParser();
  const fileReader = new FileReader(fileParser);

  await jsonFileReader.read(path.resolve(process.cwd(), &quot;./data/example.json&quot;));
  await fileReader.read(path.resolve(process.cwd(), &quot;./data/example.txt&quot;));

  console.log(jsonFileReader.parse());
  console.log(fileReader.parse());
}

main();</code></pre>
<p>InversifyJS helps us manage these dependencies by providing an inversion of control container.</p>
<span id="an-approach-with-inversifyjs" class="anchor-alias"><h2 id="an-approach-with-inversify-js">An approach with InversifyJS</h2>
<p>Let&#39;s refactor the above example to use InversifyJS. Copy the contents of <code>file-reader.ts</code> into another file <code>file-reader-inversify.ts</code>.</p>
<p>InversifyJS needs to use the type as identifiers at runtime. We use symbols as identifiers but you can also use classes and or string literals.</p>
<p>To do this, let&#39;s simply declare variable <code>TYPES</code> for the symbols of our interfaces. We will also add <code>TAGS</code> for our different parsers.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>// ... omitted

// Our existing interfaces
interface IParser {
  parse(data: string): string;
}

interface IFileReader {
  read(filepath: string): Promise&lt;string&gt;;
  parse(data: string): string;
}

// Our new symbols
const TYPES = {
  IParser: Symbol.for(&quot;IParser&quot;),
  // Not entirely necessary for this example, but just to demonstrate how it works.
  IFileReader: Symbol.for(&quot;IFileReader&quot;),
};

const TAGS = {
  JsonParser: &quot;JsonParser&quot;,
  FileParser: &quot;FileParser&quot;,
};

// ... omitted</code></pre>
<p>The next step is to use some decorators to help InversifyJS understand how to inject dependencies.</p>
<p>This is done with the <code>injectable</code> and <code>inject</code> decorators. We also use the <code>named</code> decorator to help with the <code>TAGS</code> we defined earlier.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import { injectable, inject, Container, named } from &quot;inversify&quot;;
import &quot;reflect-metadata&quot;;
import { readFile } from &quot;node:fs/promises&quot;;
import path from &quot;node:path&quot;;

// ... code omitted

// We update our classes
@injectable()
class JsonParser implements IParser {
  parse(data: string) {
    return JSON.parse(data);
  }
}

@injectable()
class FileParser implements IParser {
  parse(data: string) {
    return data;
  }
}

@injectable()
class FileReader implements IFileReader {
  private parser: IParser;
  private contents?: string;

  constructor(@inject(TYPES.IParser) @named(TAGS.FileParser) parser: IParser) {
    this.parser = parser;
  }

  async read(filepath: string) {
    this.contents = await readFile(filepath, &quot;utf-8&quot;);
    return this.contents;
  }

  parse() {
    if (!this.contents) throw new Error(&quot;No contents to parse&quot;);
    return this.parser.parse(this.contents);
  }
}</code></pre>
<p>The <code>@named</code> decorator is used to help InversifyJS understand which parser to inject. We default to the <code>FileParser</code> in this case.</p>
<p>Next, we need to create a container and bind our interfaces to our classes.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>// ... code omitted

// Create the container
const container = new Container();
container
  .bind&lt;IParser&gt;(TYPES.IParser)
  .to(JsonParser)
  .whenTargetNamed(TAGS.JsonParser);
container
  .bind&lt;IParser&gt;(TYPES.IParser)
  .to(FileParser)
  .whenTargetNamed(TAGS.FileParser);
container.bind&lt;IFileReader&gt;(TYPES.IFileReader).to(FileReader);

async function main() {
  // Get the container with the tagged parser
  const jsonFileReader = container.getNamed&lt;IFileReader&gt;(
    TYPES.IFileReader,
    TAGS.JsonParser
  );
  const fileReader = container.getNamed&lt;IFileReader&gt;(
    TYPES.IFileReader,
    TAGS.FileParser
  );

  await jsonFileReader.read(path.resolve(process.cwd(), &quot;./data/example.json&quot;));
  await fileReader.read(path.resolve(process.cwd(), &quot;./data/example.txt&quot;));

  console.log(jsonFileReader.parse());
  console.log(fileReader.parse());
}

main();</code></pre>
<p>In the above code, we configure the container to bind our interfaces to our classes. We also use the <code>whenTargetNamed</code> method to help InversifyJS understand which parser to inject.</p>
<p>Usually, this container configuration would be abstracted into another file (e.g. <code>ioc.ts</code> or something), but for the sake of this example, we have it all in one file.</p>
<p>Finally, we use the <code>container.getNamed</code> method to get the correct instance of the <code>IFileReader</code> class.</p>
<p>Once this is done, we can run the file:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="s"><code># Running the file
$ npx tsx file-reader-inversify.ts
{ example: &#39;data&#39; }
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</code></pre>
<p>As we expected, the output is the same as before.</p>
<p>The final code for the file using InversifyJS is as follows:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>import { injectable, inject, Container, named } from &quot;inversify&quot;;
import &quot;reflect-metadata&quot;;
import { readFile } from &quot;node:fs/promises&quot;;
import path from &quot;node:path&quot;;

interface IParser {
  parse(data: string): string;
}

interface IFileReader {
  read(filepath: string): Promise&lt;string&gt;;
  parse(): string;
}

const TYPES = {
  IParser: Symbol.for(&quot;IParser&quot;),
  IFileReader: Symbol.for(&quot;IFileReader&quot;),
};

const TAGS = {
  JsonParser: &quot;JsonParser&quot;,
  FileParser: &quot;FileParser&quot;,
};

@injectable()
class JsonParser implements IParser {
  parse(data: string) {
    return JSON.parse(data);
  }
}

@injectable()
class FileParser implements IParser {
  parse(data: string) {
    return data;
  }
}

@injectable()
class FileReader implements IFileReader {
  private parser: IParser;
  private contents?: string;

  constructor(@inject(TYPES.IParser) @named(TAGS.FileParser) parser: IParser) {
    this.parser = parser;
  }

  async read(filepath: string) {
    this.contents = await readFile(filepath, &quot;utf-8&quot;);
    return this.contents;
  }

  parse() {
    if (!this.contents) throw new Error(&quot;No contents to parse&quot;);
    return this.parser.parse(this.contents);
  }
}

const container = new Container();
container
  .bind&lt;IParser&gt;(TYPES.IParser)
  .to(JsonParser)
  .whenTargetNamed(TAGS.JsonParser);
container
  .bind&lt;IParser&gt;(TYPES.IParser)
  .to(FileParser)
  .whenTargetNamed(TAGS.FileParser);
container.bind&lt;IFileReader&gt;(TYPES.IFileReader).to(FileReader);

async function main() {
  const jsonFileReader = container.getNamed&lt;IFileReader&gt;(
    TYPES.IFileReader,
    TAGS.JsonParser
  );
  const fileReader = container.getNamed&lt;IFileReader&gt;(
    TYPES.IFileReader,
    TAGS.FileParser
  );

  await jsonFileReader.read(path.resolve(process.cwd(), &quot;./data/example.json&quot;));
  await fileReader.read(path.resolve(process.cwd(), &quot;./data/example.txt&quot;));

  console.log(jsonFileReader.parse());
  console.log(fileReader.parse());
}

main();</code></pre>
<h2 id="conclusion">Conclusion</h2>
<p>In today&#39;s post, we went through a small example on how to use InversifyJS to help with dependency injection in our TypeScript projects. We initially wrote it without InversifyJS to have a comparison.</p>
<p>For myself, this was a review to better understand InversifyJS as it is used heavily within my new company.</p>
<p>In cases where dependencies increase and we need to manage them, InversifyJS can be a great tool to help manage these dependencies. That being said, I am yet to be sold on the approach.</p>
<p>Using <code>getNamed()</code> and tagging seems like it is adding a bit too much magic for my liking. I prefer to be explicit in my code and have the dependencies passed in directly.</p>
<p>There are a ton of extra features that I have not delved into with InversifyJS such as lifecycle management, middleware and more. I would recommend checking out the <a href="https://inversify.io/" target="_blank" rel="noopener noreferrer">InversifyJS documentation</a> for more information, which I will also do myself to get a better understanding of what is possible.</p>
<p>The commit graph is also lacking a little bit these days, but maybe they consider this project feature complete. I am a little wary of treading too far away from the JS/TS mainstream ecosystem, but I am also open to new ideas and ways of doing things.</p>
<p>Find the full code <a href="https://github.com/okeeffed/demo-inversifyjs" target="_blank" rel="noopener noreferrer">here</a>.</p>
<h2 id="resources-and-further-reading">Resources and further reading</h2>
<ul>
<li><a href="https://github.com/okeeffed/demo-inversifyjs" target="_blank" rel="noopener noreferrer">Completed code</a></li>
<li><a href="https://inversify.io/" target="_blank" rel="noopener noreferrer">InversifyJS</a></li>
<li><a href="https://en.wikipedia.org/wiki/Dependency_inversion_principle" target="_blank" rel="noopener noreferrer">Dependency inversion principle</a></li>
</ul>
<p><em>Photo credit: <a href="https://unsplash.com/@boliviainteligente" target="_blank" rel="noopener noreferrer"><code>boliviainteligente</code></a></em></p>]]></content>
<category term="typescript"/><category term="inversifyjs"/>
</entry>
<entry>
<title type="html"><![CDATA[Up and running with Go 1.22's new built-in router]]></title>
<id>https://blog.dennisokeeffe.com/blog/2024-02-18-up-and-running-with-go-1-22s-new-built-in-router</id>
<link href="https://dennisokeeffe.com/blog/2024-02-18-up-and-running-with-go-1-22s-new-built-in-router"/>
<published>2024-02-18T00:00:00.000Z</published>
<updated>2024-02-18T00:00:00.000Z</updated>
<summary type="html"><![CDATA[Go 1.22 comes a brand new built-in router option for more complicated server applications. This is a basic dive in for Golang first timers.]]></summary>
<content type="html"><![CDATA[<span id="getting-started-with-go-122" class="anchor-alias"><h2 id="getting-started-with-go-1-22">Getting started with Go 1.22</h2>
<p>This is a small approach to getting started with Go 1.22. This walkthrough is for MacOS.</p>
<p>The aim is to create a basic &quot;Hello, World&quot; API with the new mux router option from the Go 1.22 release.</p>
<p>The finished code can be found on <a href="https://github.com/okeeffed/go-1-22-mux" target="_blank" rel="noopener noreferrer">GitHub</a>.</p>
<h2 id="install-go">Install Go</h2>
<p>You can install Go from <a href="https://go.dev/dl/" target="_blank" rel="noopener noreferrer">the official website</a>. I used <a href="https://brew.sh/" target="_blank" rel="noopener noreferrer">Homebrew</a> in my case.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="bash"><code># Install with Homebrew
$ brew install go</code></pre>
<p>Once installed, you can confirm the version with <code>go version</code>.</p>
<h2 id="initialize-a-go-project">Initialize a Go project</h2>
<p>You can use the <code>go mod init</code> function in order to initialize a new <code>go.mod</code> file.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="bash"><code># Init the project
$ go mod init github.com/okeeffed/go-1-22-mux</code></pre>
<p><code>go mod</code> is a helper for manager modules. <code>go mod help</code> can display more information about this.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="bash"><code>$ go mod help
Go mod provides access to operations on modules.

Note that support for modules is built into all the go commands,
not just &#39;go mod&#39;. For example, day-to-day adding, removing, upgrading,
and downgrading of dependencies should be done using &#39;go get&#39;.
See &#39;go help modules&#39; for an overview of module functionality.

Usage:

        go mod &lt;command&gt; [arguments]

The commands are:

        download    download modules to local cache
        edit        edit go.mod from tools or scripts
        graph       print module requirement graph
        init        initialize new module in current directory
        tidy        add missing and remove unused modules
        vendor      make vendored copy of dependencies
        verify      verify dependencies have expected content
        why         explain why packages or modules are needed

Use &quot;go help mod &lt;command&gt;&quot; for more information about a command.</code></pre>
<h2 id="configuring-the-makefile">Configuring the Makefile</h2>
<p>A Makefile is a special file used in software development projects, particularly in Unix-like operating systems, to automate the process of compiling and building software. It contains a set of instructions (written in a format called &quot;Makefile syntax&quot;) that define how to build the project and its various components.</p>
<p>Here&#39;s a simple explanation of a Makefile for someone who&#39;s not familiar with it:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="makefile"><code># Makefile for a Go project
# Assumes entry point at project/cmd/server/server.go

# Binary output name
BINARY_NAME=hello_world

# Default make command
all: build

# Build the binary
build:
	@echo &quot;Building...&quot;
	go build -o $(BINARY_NAME) ./cmd/server

# Run the server
run: build
	@echo &quot;Running...&quot;
	./$(BINARY_NAME)

# Test your application
test:
	@echo &quot;Testing...&quot;
	go test ./...

# Clean up binaries
clean:
	@echo &quot;Cleaning...&quot;
	go clean
	rm -f $(BINARY_NAME)

# Help command to display available commands
help:
	@echo &quot;Makefile commands:&quot;
	@echo &quot;all    - Build the application&quot;
	@echo &quot;build  - Build the binary&quot;
	@echo &quot;run    - Build and run the application&quot;
	@echo &quot;test   - Run tests&quot;
	@echo &quot;clean  - Remove binaries&quot;
	@echo &quot;help   - Display this help&quot;

# Mark commands that don&#39;t correspond to files as .PHONY
.PHONY: all build run test clean help</code></pre>
<blockquote>
<p>Warning: Make sure you use the correct indentation.</p>
</blockquote>
<p>The following explains the different parts of this file.</p>
<h3 id="variables">Variables</h3>
<ul>
<li><code>BINARY_NAME</code>: Represents the name of the binary executable.</li>
</ul>
<h3 id="default-target-all">Default Target (<code>all</code>)</h3>
<ul>
<li>When you run <code>make</code> without specifying a target, it executes the <code>build</code> target.</li>
</ul>
<h3 id="build-target">Build Target</h3>
<ul>
<li>Compiles the Go source files and generates the binary executable specified by <code>BINARY_NAME</code>.</li>
</ul>
<h3 id="clean-target">Clean Target</h3>
<ul>
<li>Removes any generated binary executable and cleans up any temporary files created during the build process.</li>
</ul>
<h3 id="test-target">Test Target</h3>
<ul>
<li>Runs all the tests in the project.</li>
</ul>
<h3 id="dependencies-target">Dependencies Target</h3>
<ul>
<li>Installs any dependencies the project might have.</li>
</ul>
<h3 id="run-target">Run Target</h3>
<ul>
<li>Builds and runs the executable.</li>
</ul>
<h3 id="help-target">Help Target</h3>
<ul>
<li>Provides a helpful message listing available targets and their descriptions.</li>
</ul>
<p>To use this Makefile, place it in your Go project directory and run <code>make</code>. You can also use <code>make clean</code> to remove the generated binary, <code>make test</code> to run tests, <code>make deps</code> to install dependencies, and <code>make run</code> to build and run the executable.</p>
<p>This <strong>Makefile</strong> is a little more complex than it needs to be (for this simple project), but understanding it will also help you when you see other Makefiles. It is not just used for Golang applications.</p>
<p>Add the example code above into a <code>Makefile</code>.</p>
<h2 id="setting-up-our-remaining-files-and-folders">Setting up our remaining files and folders</h2>
<p>The <a href="https://github.com/golang-standards/project-layout" target="_blank" rel="noopener noreferrer">Go project layout</a> project on GitHub is a great place to help understand a layout for Go projects. That being, a lot of it can be overkill. We will follow these standards for the sake of today&#39;s example, but please note that this is not necessary for such a small project.</p>
<p>It may help in future if you are build up a large application.</p>
<p>For now, we will use the following folder structure:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="txt"><code>project/
├── cmd/server/          # Application entry points
├── api/v1/              # Route handlers
└── internal/            # Private application and library code
    └── model/           # Domain models
        └── user.go      # User struct and methods</code></pre>
<blockquote>
<p>Please note: this is very much overkill for the size of the project.</p>
</blockquote>
<p>For this, add a few folders:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="s"><code># Make the folders and add the files
$ mkdir -p cmd/server internal/model api/v1
$ touch cmd/server/server.go internal/model/user.go api/v1/goodbye.go api/v1/hello.go</code></pre>
<p>We are ready to add some code.</p>
<h2 id="adding-our-user-model">Adding our User model</h2>
<p>We will be writing the code in this order:</p>
<ol>
<li>Adding our <strong>User</strong> model.</li>
<li>Adding our <strong>GET</strong> routes for <code>/v1/hello</code> and <code>/v1/goodbye</code>.</li>
<li>Adding our <code>main</code> function (using the <code>http.NewServeMux</code> export from Go 1.22).</li>
</ol>
<p>Even though I say <strong>User</strong> model, really it will be a barebones struct to demonstrate using it across files. The actually implementation of it will be a bit rogue and not-for-a-user.</p>
<p>That being said, our <code>internal/model/user.go</code> file will look like this:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="go"><code>package model

// User is a simple model for a user.
type User struct {
	// Name is the name of the user.
	Name string
}</code></pre>
<p>He, we are create a package <code>model</code> that we can use later in our imports as the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="go"><code>import (
	model &quot;github.com/okeeffed/go-1-22-mux/internal/model&quot;
)</code></pre>
<p>Naming the struct with a capital letter (<code>User</code>) makes the struct public and will be importable from other files.</p>
<p>We can then use this in our application.</p>
<h2 id="configuring-our-get-route-handlers">Configuring our GET route handlers</h2>
<p>Inside of <code>api/v1/hello.go</code>, add the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="go"><code>package api

import (
	&quot;net/http&quot;

	model &quot;github.com/okeeffed/go-1-22-mux/internal/model&quot;
)

// HelloHandler is a simple HTTP handler that writes a response.
func HelloHandler(w http.ResponseWriter, r *http.Request) {
	// Create a new user.
	user := model.User{
		Name: &quot;World&quot;,
	}

	// Write a response to the client.
	w.Write([]byte(&quot;Hello, &quot; + user.Name + &quot;!&quot;))
}</code></pre>
<p>Here we are using the <code>http</code> import helping us type our arguments for the response writer and the request, while the <code>model</code> import is used so we can create our new user with the name &quot;World&quot; (just to stick to hello world convention for now).</p>
<blockquote>
<p>In practice, you may see variables denoted with shorthand e.g. <code>user</code> would be <code>u</code>.</p>
</blockquote>
<p>Similar to before, we&#39;ve used our keyword <code>package</code> to define our <code>api</code> package and <code>HelloHandler</code> with a capital to make it public.</p>
<p>We can do something similar for <code>api/v1/goodbye.go</code>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="go"><code>package api

import (
	&quot;net/http&quot;

	model &quot;github.com/okeeffed/go-1-22-mux/internal/model&quot;
)

// HelloHandler is a simple HTTP handler that writes a response.
func GoodbyeHandler(w http.ResponseWriter, r *http.Request) {
	// Create a new user.
	user := model.User{
		Name: &quot;World&quot;,
	}

	// Write a response to the client.
	w.Write([]byte(&quot;Goodbye, &quot; + user.Name + &quot;!&quot;))
}</code></pre>
<h2 id="configuring-our-main-application">Configuring our main application</h2>
<p>As seen in our <strong>Makefile</strong>, our main entry point is <code>cmd/server/server.go</code>.</p>
<p>The convention <code>cmd/&lt;app-name&gt;/&lt;app-name&gt;.go</code> is common across larger projects, although you would normally just see a simple <code>main.go</code> at the root directory for small applications.</p>
<p>Within that file, we will add our server code:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="go"><code>package main

import (
	&quot;net/http&quot;

	v1 &quot;github.com/okeeffed/go-1-22-mux/api/v1&quot;
)

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc(&quot;GET /v1/hello&quot;, v1.HelloHandler)
	mux.HandleFunc(&quot;GET /v1/goodbye&quot;, v1.GoodbyeHandler)

	http.ListenAndServe(&quot;:8080&quot;, mux)
}</code></pre>
<p>In our code, we are using Go 1.22&#39;s <a href="https://pkg.go.dev/net/http@master#NewServeMux" target="_blank" rel="noopener noreferrer">NewServeMux</a> function to serve our routers. We also import <code>v1</code> to use both our <code>HelloHandler</code> and <code>GoodbyeHandler</code> functions that we wrote.</p>
<h2 id="running-our-router">Running our router</h2>
<p>With out Makefile sorted, we can run either <code>make run</code> or simple <code>make</code> to build out our binary <code>hello_world</code>.</p>
<p>For now, run <code>make run</code>.</p>
<p>Once built, our server will be running on port 8080.</p>
<p>To test, we can use <code>curl</code> on another terminal window:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="s"><code># Test /v1/hello
$ curl http://localhost:8080/v1/hello
Hello, World!%
# Test /v1/goodbye
curl http://localhost:8080/v1/goodbye
Goodbye, World!%</code></pre>
<p>Success!</p>
<h2 id="conclusion">Conclusion</h2>
<p>In today&#39;s post, we used the Golang project layout convention to help test drive out the Go 1.22 built-in mux router.</p>
<p>We did this from the process of installing Go on Mac until we had a running server.</p>
<h2 id="resources-and-further-reading">Resources and further reading</h2>
<ul>
<li><a href="https://github.com/okeeffed/go-1-22-mux" target="_blank" rel="noopener noreferrer">Final code</a></li>
<li><a href="https://go.dev/dl/" target="_blank" rel="noopener noreferrer">Download Go</a></li>
<li><a href="https://brew.sh/" target="_blank" rel="noopener noreferrer">Homebrew</a></li>
<li><a href="https://github.com/golang-standards/project-layout" target="_blank" rel="noopener noreferrer">Go project layout</a></li>
<li><a href="https://pkg.go.dev/net/http@master#NewServeMux" target="_blank" rel="noopener noreferrer">NewServeMux | Go Docs</a></li>
</ul>
<p><em>Photo credit: <a href="https://unsplash.com/@shishkovdima" target="_blank" rel="noopener noreferrer"><code>shishkovdima</code></a></em></p>]]></content>
<category term="golang"/>
</entry>
<entry>
<title type="html"><![CDATA[Type-safe usage of React as a prop for flexible components]]></title>
<id>https://blog.dennisokeeffe.com/blog/2024-01-03-typescript-react-as-prop-for-flexible-components</id>
<link href="https://dennisokeeffe.com/blog/2024-01-03-typescript-react-as-prop-for-flexible-components"/>
<published>2024-01-03T00:00:00.000Z</published>
<updated>2024-01-03T00:00:00.000Z</updated>
<summary type="html"><![CDATA[Ever wondered how design systems provide an 'as' prop for their components that you can use to inject or change the component type? Let's look at how we can do this with TypeScript.]]></summary>
<content type="html"><![CDATA[<h2 id="introduction">Introduction</h2>
<p>Design systems like <a href="https://chakra-ui.com/community/recipes/as-prop" target="_blank" rel="noopener noreferrer">Chakra UI</a>, <a href="https://gestalt.pinterest.systems/web/box#Props-as" target="_blank" rel="noopener noreferrer">Gestalt</a>, <a href="https://mui.com/material-ui/api/box/#props" target="_blank" rel="noopener noreferrer">Material UI</a> (as <code>component</code>, not <code>as</code>) and <a href="https://react-bootstrap.github.io/docs/getting-started/introduction/#as-prop-api" target="_blank" rel="noopener noreferrer">React Bootstrap</a> provide ways to override the React component in question with a prop.</p>
<p>For example, image an API like the the following that will render a <code>span</code> instead of a <code>button</code>.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="tsx"><code>&lt;Button as=&quot;span&quot;&gt;I am a span&lt;/Button&gt;</code></pre>
<p>But how does this magic work? There are some articles on popular blog posts such as <a href="https://www.robinwieruch.de/react-as-prop/" target="_blank" rel="noopener noreferrer">robinwieruch</a> and <a href="https://www.developerway.com/posts/react-component-as-prop-the-right-way" target="_blank" rel="noopener noreferrer">Developer Way</a>, but I think most miss the mark on how to implement this in a type-safe way.</p>
<p>Let&#39;s dive deeper into how we can implement this ourselves (with TypeScript).</p>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>Working knowledge of React.</li>
<li>Working knowledge of TypeScript.</li>
</ul>
<h2 id="setting-up-the-vite-project">Setting up the Vite project</h2>
<p>We will use Vite to spin up a quick project to work with.</p>
<p>In the terminal, run the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="bash"><code>pnpm create vite@latest blog-react-ts-as-prop --template react-ts
cd blog-react-ts-as-prop
pnpm install
pnpm dev</code></pre>
<p>At this point, you&#39;re app should be up and running (mine was at <a href="http://localhost:5173/" target="_blank" rel="noopener noreferrer">http://localhost:5173/</a>), so we are ready to move forward!</p>
<span id="understanding-reactelementtype" class="anchor-alias"><h2 id="understanding-react-element-type">Understanding React.ElementType</h2>
<p>As of writing, the definition for this type comes from <a href="https://github.com/DefinitelyTyped/DefinitelyTyped/blob/db81a81898e9e0eaf41c95f29e07e33fedc7a2b2/types/react/index.d.ts#L40" target="_blank" rel="noopener noreferrer">here</a>.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="tsx"><code>type ElementType&lt;
  P = any,
  Tag extends keyof JSX.IntrinsicElements = keyof JSX.IntrinsicElements,
&gt; =
  | { [K in Tag]: P extends JSX.IntrinsicElements[K] ? K : never }[Tag]
  | ComponentType&lt;P&gt;;</code></pre>
<p>The tl;dr on this section is that <code>ElementType</code> is a type that can be either a <code>ComponentType</code> or a <code>keyof JSX.IntrinsicElements</code> where the <code>keyof JSX.InstrinsicElements</code> can be any valid HTML attribute key e.g. <code>p</code> or <code>button</code>.</p>
<p>This is the magic ingredient for an <code>as</code> prop, so let&#39;s implement that now.</p>
<h2 id="implementing-the-as-prop">Implementing the <code>as</code> prop</h2>
<p>Within <code>src/App.tsx</code>, let&#39;s create a <code>Button</code> component that will take an <code>as</code> prop near the top of the file.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="tsx"><code>function Button({
  as,
  children,
}: {
  as: React.ElementType;
  children: React.ReactNode;
}) {
  return React.createElement(as, null, children);
}</code></pre>
<p>Based off what we know, we should be able to pass in any valid HTML attribute key or React component and have it render as that element.</p>
<p>Let&#39;s test this out by rendering a <code>button</code>, a <code>p</code> element and a random component. The entire file will end up looking like this:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="tsx"><code>import React from &quot;react&quot;;
import &quot;./App.css&quot;;

function Button({
  as,
  children,
}: {
  as: React.ElementType;
  children: React.ReactNode;
}) {
  return React.createElement(as, null, children);
}

function ValidDivComponent({ children }: { children: React.ReactNode }) {
  return &lt;div&gt;{children}&lt;/div&gt;;
}

function App() {
  return (
    &lt;&gt;
      &lt;Button as=&quot;p&quot;&gt;p tag&lt;/Button&gt;
      &lt;Button as=&quot;button&quot;&gt;actual button&lt;/Button&gt;
      &lt;Button as=&quot;a&quot;&gt;anchor tag&lt;/Button&gt;
      &lt;Button as={ValidDivComponent}&gt;div from component&lt;/Button&gt;
    &lt;/&gt;
  );
}

export default App;</code></pre>
<p>If you were to open the browser console and check the HTML, you will see the following HTML from the <code>#root</code> div:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="html"><code>&lt;div id=&quot;root&quot;&gt;
  &lt;p&gt;p tag&lt;/p&gt;
  &lt;button&gt;actual button&lt;/button&gt;
  &lt;a&gt;anchor tag&lt;/a&gt;
  &lt;div&gt;div from component&lt;/div&gt;
&lt;/div&gt;</code></pre>
<p>Great! We have a working <code>as</code> prop. But what about TypeScript? Right now, we can tell our button what component to render, but we cannot actually infer the props so that TypeScript can help at the call site.</p>
<h2 id="making-our-types-a-little-more-flexible">Making our types a little more flexible</h2>
<p>At this point, we need to dive into <a href="https://www.typescriptlang.org/docs/handbook/2/generics.html" target="_blank" rel="noopener noreferrer">generics</a> to help us out.</p>
<blockquote>
<p>Side note: I have another <a href="https://dennisokeeffe.com/blog/2023-06-20-advanced-typescript-generics-in-practice">post</a> that also works through generics more in-depth.</p>
</blockquote>
<p>Our problem is this: we want to be able to pass in a component and have TypeScript infer the props for us.</p>
<p>We can solve this problem by abstracting the button type, introducing <code>React.ComponentPropsWithoutRef</code> and making it generic.</p>
<p>Let&#39;s update our <code>Button</code> component to the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="tsx"><code>type ButtonProps&lt;T extends React.ElementType&gt; = T extends React.ElementType
  ? { as: T } &amp; React.ComponentPropsWithoutRef&lt;T&gt;
  : never;

function Button&lt;T extends React.ElementType&gt;({ as, ...props }: ButtonProps&lt;T&gt;) {
  const Component = as;
  return &lt;Component {...props} /&gt;;
}</code></pre>
<p>In the above, we have introduced a generic <code>T</code> that extends <code>React.ElementType</code>. We then use this generic to create a <code>ButtonProps</code> type that will take in a <code>T</code> and return a type that is either the <code>as</code> prop or <code>never</code>.</p>
<p>We then use <code>React.ComponentPropsWithoutRef</code> to infer the props for the <code>T</code> type and then use the <code>as</code> prop to render the component.</p>
<p>Let&#39;s test this out by creating a <code>ValidDivComponent</code> that takes in a <code>name</code> prop and then render it with our <code>Button</code> component.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="tsx"><code>function ValidDivComponent({ name }: { name: string }) {
  return &lt;div&gt;{name}&lt;/div&gt;;
}</code></pre>
<p>Update the entire file code now to the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="tsx"><code>import React from &quot;react&quot;;
import &quot;./App.css&quot;;

type ButtonProps&lt;T extends React.ElementType&gt; = T extends React.ElementType
  ? { as: T } &amp; React.ComponentPropsWithoutRef&lt;T&gt;
  : never;

function Button&lt;T extends React.ElementType&gt;({ as, ...props }: ButtonProps&lt;T&gt;) {
  const Component = as;
  return &lt;Component {...props} /&gt;;
}

function ValidDivComponent({ name }: { name: string }) {
  return &lt;div&gt;{name}&lt;/div&gt;;
}

function App() {
  return (
    &lt;&gt;
      &lt;Button as=&quot;p&quot;&gt;p tag&lt;/Button&gt;
      &lt;Button as=&quot;button&quot; href=&quot;#&quot;&gt;
        actual button
      &lt;/Button&gt;
      &lt;Button as=&quot;a&quot; href=&quot;#&quot;&gt;
        anchor tag
      &lt;/Button&gt;
      &lt;Button as={ValidDivComponent} /&gt;
    &lt;/&gt;
  );
}

export default App;</code></pre>
<p>Once you save, you&#39;ll notice that we now have two TypeScript errors (this is assuming your IDE is set up to use TypeScript).</p>
<ol>
<li>Our <code>&lt;Button as=&quot;button&quot; href=&quot;/&quot;&gt;</code> renders an error because <code>href</code> is not valid.</li>
<li>Our <code>&lt;Button as={ValidDivComponent} /&gt;</code> renders an error because we are missing the <code>name</code> prop.</li>
</ol>
<p>As an added bonus, you&#39;ll notice that our <code>&lt;Button as=&quot;a&quot; href=&quot;/&quot;&gt;anchor tag&lt;/Button&gt;</code> component <strong>does not render</strong> an error as <code>href</code> is a valid prop for an <code>a</code> tag.</p>
<p>Hooray for generics! Now we have a type-safe <code>as</code> prop that we can use to render any component we want.</p>
<p>If we update the <code>App</code> function code, we will be back in business with no errors:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="tsx"><code>function App() {
  return (
    &lt;&gt;
      &lt;Button as=&quot;p&quot;&gt;p tag&lt;/Button&gt;
      &lt;Button as=&quot;button&quot;&gt;actual button&lt;/Button&gt;
      &lt;Button as=&quot;a&quot; href=&quot;#&quot;&gt;
        anchor tag
      &lt;/Button&gt;
      &lt;Button as={ValidDivComponent} name=&quot;div component&quot; /&gt;
    &lt;/&gt;
  );
}</code></pre>
<p>But what happens if we don&#39;t want to accept certain intrinsic elements? Let&#39;s look at how we can do that next.</p>
<h2 id="restricting-the-as-prop">Restricting the <code>as</code> prop</h2>
<p>The TypeScript <a href="https://www.typescriptlang.org/docs/handbook/utility-types.html#extracttype-union" target="_blank" rel="noopener noreferrer">Extract utility</a> enables us to extract common types between a type argument and a union argument.</p>
<p>For example:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="ts"><code>type T0 = Extract&lt;&quot;a&quot; | &quot;b&quot; | &quot;c&quot;, &quot;a&quot; | &quot;f&quot;&gt;;</code></pre>
<p>In the above, <code>T0</code> will be <code>&quot;a&quot;</code> as it is the only common type between the two arguments.</p>
<p>We can use this to restrict the <code>as</code> prop to only accept certain HTML elements.</p>
<p>Let&#39;s first create a new <code>LimitedElementType</code> to allow for anything that is a React component or a <code>button</code>, <code>a</code> or <code>span</code> tag.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="tsx"><code>type LimitedElementType = Extract&lt;
  React.ElementType,
  &quot;button&quot; | &quot;a&quot; | &quot;span&quot; | React.JSXElementConstructor&lt;unknown&gt;
&gt;;</code></pre>
<p>With that in place, let&#39;s update our <code>Button</code> code and props:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="tsx"><code>type ButtonProps&lt;T extends LimitedElementType&gt; = T extends LimitedElementType
  ? { as: T } &amp; React.ComponentPropsWithoutRef&lt;T&gt;
  : never;

function Button&lt;T extends LimitedElementType&gt;({
  as,
  ...props
}: ButtonProps&lt;T&gt;) {
  const Component = as;
  return &lt;Component {...props} /&gt;;
}</code></pre>
<p>At this point, you&#39;ll notice our that we have one new error: <code>&lt;Button as=&quot;p&quot;&gt;p tag&lt;/Button&gt;</code> has the following error:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="text"><code>Type &#39;&quot;p&quot;&#39; is not assignable to type &#39;&quot;a&quot; | &quot;button&quot; | &quot;span&quot; | ComponentClass&lt;any, any&gt; | FunctionComponent&lt;any&gt;&#39;.</code></pre>
<p>This is because we have restricted the <code>as</code> prop to only accept certain types. Perfect!</p>
<p>The last step is to handle a default option of <code>button</code> if no <code>as</code> prop is passed in.</p>
<h2 id="defaulting-to-button">Defaulting to button</h2>
<p>In order to add this part, all we need to do is add some default values across the code.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="tsx"><code>import React from &quot;react&quot;;
import &quot;./App.css&quot;;

type LimitedElementType = Extract&lt;
  React.ElementType,
  &quot;button&quot; | &quot;a&quot; | &quot;span&quot; | React.JSXElementConstructor&lt;unknown&gt;
&gt;;

type ButtonProps&lt;T extends LimitedElementType = &quot;button&quot;&gt; =
  T extends LimitedElementType
    ? { as?: T } &amp; React.ComponentPropsWithoutRef&lt;T&gt;
    : never;

function Button&lt;T extends LimitedElementType = &quot;button&quot;&gt;({
  as = &quot;button&quot;,
  ...props
}: ButtonProps&lt;T&gt;) {
  const Component = as;
  return &lt;Component {...props} /&gt;;
}

function ValidDivComponent({ name }: { name: string }) {
  return &lt;div&gt;{name}&lt;/div&gt;;
}

function App() {
  return (
    &lt;&gt;
      &lt;Button as=&quot;button&quot;&gt;actual button&lt;/Button&gt;
      &lt;Button as=&quot;button&quot;&gt;default button&lt;/Button&gt;
      &lt;Button as=&quot;a&quot; href=&quot;/&quot;&gt;
        anchor tag
      &lt;/Button&gt;
      &lt;Button as={ValidDivComponent} name=&quot;Hello&quot; /&gt;
    &lt;/&gt;
  );
}
export default App;</code></pre>
<p>The first step was to add a default value of <code>&quot;button&quot;</code> to the <code>LimitedElementType</code> type.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="tsx"><code>type ButtonProps&lt;T extends LimitedElementType = &quot;button&quot;&gt; =
  T extends LimitedElementType
    ? { as?: T } &amp; React.ComponentPropsWithoutRef&lt;T&gt;
    : never;</code></pre>
<p>Secondly, we added that to our <code>Button</code> component and also added the default to our <code>as</code> prop.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="tsx"><code>function Button&lt;T extends LimitedElementType = &quot;button&quot;&gt;({
  as = &quot;button&quot;,
  ...props
}: ButtonProps&lt;T&gt;) {
  const Component = as;
  return &lt;Component {...props} /&gt;;
}</code></pre>
<p>Now you can run the code and see that our <code>default button</code> renders as a <code>button</code> element.</p>
<p>Even better, if you add <code>href</code> without an <code>as</code> prop, you will get an error.</p>
<p>Happy days!</p>
<h2 id="conclusion">Conclusion</h2>
<p>In this post, we have looked at how we can implement a type-safe <code>as</code> prop for our React components.</p>
<p>We have also looked at how we can restrict the <code>as</code> prop to only accept certain HTML elements.</p>
<p>I hope you have found this post useful and it helps you in your React TypeScript journey!</p>
<p>You can find the code on this <a href="https://tsplay.dev/mAL0kN" target="_blank" rel="noopener noreferrer">TypeScript Playground</a> or</p>
<h2 id="references-and-further-reading">References and Further Reading</h2>
<ol>
<li><a href="https://chakra-ui.com/community/recipes/as-prop" target="_blank" rel="noopener noreferrer">Chakra UI as prop</a></li>
<li><a href="https://www.typescriptlang.org/docs/handbook/2/generics.html" target="_blank" rel="noopener noreferrer">TypeScript generics</a></li>
<li><a href="https://www.typescriptlang.org/docs/handbook/utility-types.html#extracttype-union" target="_blank" rel="noopener noreferrer">TypeScript Extract utility</a></li>
<li><a href="https://dennisokeeffe.com/blog/2023-06-20-advanced-typescript-generics-in-practice">Advanced TypeScript: Generics in Practice</a></li>
<li><a href="https://github.com/okeeffed/blog-react-ts-as-prop" target="_blank" rel="noopener noreferrer">Blog code</a></li>
<li><a href="https://tsplay.dev/mAL0kN" target="_blank" rel="noopener noreferrer">TypeScript Playground</a></li>
</ol>
<p><em>Photo credit: <a href="https://unsplash.com/@boliviainteligente" target="_blank" rel="noopener noreferrer"><code>boliviainteligente</code></a></em></p>]]></content>
<category term="typescript"/><category term="react"/>
</entry>
<entry>
<title type="html"><![CDATA[Mounting Your Own Cloud Storage With Cloud Mounter and AWS]]></title>
<id>https://blog.dennisokeeffe.com/blog/2023-11-28-mounting-your-own-cloud-storage-with-cloud-mounter-and-aws</id>
<link href="https://dennisokeeffe.com/blog/2023-11-28-mounting-your-own-cloud-storage-with-cloud-mounter-and-aws"/>
<published>2023-11-28T00:00:00.000Z</published>
<updated>2023-11-28T00:00:00.000Z</updated>
<summary type="html"><![CDATA[After another failed hard drive meant losing data, I decided to look into mounting my own cloud storage. Here is how I did it.]]></summary>
<content type="html"><![CDATA[<h2 id="introduction">Introduction</h2>
<p>My partner and I have a few side projects that rely on storage of large video fails. After one too many hard drives failed over time, I decided that we would opt for moving things to cloud storage.</p>
<p>That being said, there were a few issues with doing this using out-of-the-box solutions:</p>
<ol>
<li>Options like iCloud and Google Drive are not cost-effective for the amount of storage we need.</li>
<li>There was a limit to the amount of storage we could get on a single account.</li>
</ol>
<p>That being said, we wanted a way to mount storage directly on our machines like we do with external hard drives. This would allow us to use the storage as if it were a local drive.</p>
<p>I was already trialling out <a href="https://go.setapp.com/invite/6yrbjgdh" target="_blank" rel="noopener noreferrer">SetApp</a> for subscriptions on my Mac, with one such app being <a href="https://cloudmounter.net/" target="_blank" rel="noopener noreferrer">Cloud Mounter</a>, an app which fulfills our need of mounting the S3 bucket as a remote drive.</p>
<p>This post will cover how I set up the S3 bucket using AWS CDK and mounted it using Cloud Mounter.</p>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>Working knowledge of AWS CDK.</li>
<li>Working knowledge of AWS S3.</li>
<li>Working knowledge of TypeScript.</li>
<li>Install Node.js and npm (Node Package Manager). AWS CDK requires Node.js. You can download it from the <a href="https://nodejs.org/" target="_blank" rel="noopener noreferrer">Node.js official website</a>.</li>
<li>If you also want to use CloudMounter, ensure you have a SetApp subscription or have purchased CloudMounter.</li>
</ul>
<p>This guide will walk you through the steps to set up a new application using the AWS Cloud Development Kit (CDK) in TypeScript, then we will add an S3 bucket that we can mount using Cloud Mounter.</p>
<h2 id="installation-steps">Installation Steps</h2>
<h3 id="1-install-aws-cdk">1. Install AWS CDK</h3>
<p>Install the AWS CDK globally using npm:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="bash"><code>npm install -g aws-cdk</code></pre>
<h3 id="2-set-up-aws-credentials">2. Set Up AWS Credentials</h3>
<p>Ensure your AWS credentials are configured:</p>
<ul>
<li>Install the AWS CLI.</li>
<li>Run <code>aws configure</code> and enter your AWS Access Key ID, Secret Access Key, and default region.</li>
</ul>
<blockquote>
<p>For what it&#39;s worth, I use <a href="9designs/aws-vault">aws-vault</a> to store my different AWS account credentials and work from there.</p>
</blockquote>
<h3 id="3-create-a-new-cdk-project">3. Create a New CDK Project</h3>
<p>Create a new CDK project in TypeScript:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="bash"><code># First, create a folder for your project
mkdir mac-cloud-storage-s3-cdk-app
cd mac-cloud-storage-s3-cdk-app

# Initialise the AWS CDK stack
cdk init app --language typescript

# or using npx
npx aws-cdk@latest init app --language typescript</code></pre>
<blockquote>
<p>At the time of writing, I am using <code>aws-cdk@2.111.0</code>.</p>
</blockquote>
<h2 id="development">Development</h2>
<h3 id="1-explore-and-modify-your-cdk-app">1. Explore and Modify Your CDK App</h3>
<ul>
<li>The main file for defining AWS resources is <code>lib/&lt;your-project-name&gt;-stack.ts</code>.</li>
</ul>
<blockquote>
<p>If you followed the same project folder name that I did, you will find the code within <code>lib/mac-cloud-storage-s3-cdk-app-stack.ts</code>.</p>
</blockquote>
<h3 id="2-bootstrap-your-aws-environment-if-necessary">2. Bootstrap Your AWS Environment (if necessary)</h3>
<p>Bootstrap your environment to set up an S3 bucket for storing assets:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="bash"><code>npx cdk bootstrap</code></pre>
<h3 id="3-deploy-your-cdk-app">3. Deploy Your CDK App</h3>
<p>We are not ready quite yet, but you could now deploy your AWS CDK app by running:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="bash"><code>npx cdk deploy</code></pre>
<span id="configuring-the-s3-bucket-and-credentials" class="anchor-alias"><h2 id="configuring-the-s-3-bucket-and-credentials">Configuring the S3 Bucket and credentials</h2>
<p>Update the code in <code>lib/mac-cloud-storage-s3-cdk-app-stack.ts</code> to the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="typescript"><code>import * as cdk from &quot;aws-cdk-lib&quot;;
import { Construct } from &quot;constructs&quot;;
import * as s3 from &quot;aws-cdk-lib/aws-s3&quot;;
import * as iam from &quot;aws-cdk-lib/aws-iam&quot;;

export class MacCloudStorageS3CdkAppStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const bucket = new s3.Bucket(this, &quot;IntelligentTieringBucket&quot;, {
      lifecycleRules: [
        {
          id: &quot;IntelligentTieringRule&quot;,
          enabled: true,
          transitions: [
            {
              storageClass: s3.StorageClass.INTELLIGENT_TIERING,
              transitionAfter: cdk.Duration.days(0), // Transition immediately to Intelligent-Tiering
            },
          ],
        },
      ],
    });

    const user = new iam.User(this, &quot;MyBucketUser&quot;);

    // Policy allowing access to the specific bucket
    const bucketPolicy = new iam.PolicyStatement({
      actions: [&quot;s3:*&quot;],
      resources: [bucket.bucketArn, `${bucket.bucketArn}/*`],
      effect: iam.Effect.ALLOW,
    });

    // Attach the policy to the user
    user.addToPolicy(bucketPolicy);

    // Output the username
    new cdk.CfnOutput(this, &quot;UserNameOutput&quot;, {
      value: user.userName,
      description: &quot;The name of the IAM user&quot;,
    });

    // Output the bucket name
    new cdk.CfnOutput(this, &quot;BucketNameOutput&quot;, {
      value: bucket.bucketName,
      description: &quot;The name of the S3 bucket&quot;,
    });
  }
}</code></pre>
<p>The construct has comments jotted throughout to denote what is happening, but as a summary:</p>
<ol>
<li>We create a new S3 bucket with a lifecycle rule that will transition files to the Intelligent-Tiering storage class immediately.</li>
<li>We create a new IAM user and attach a policy that allows access to the bucket.</li>
<li>We output the username and bucket name to the console.</li>
</ol>
<h2 id="generating-credentials-for-the-created-user">Generating Credentials For The Created User</h2>
<p>In AWS, best practices dictate that you should never display or expose IAM user credentials (like Access Keys) in clear text, especially in your infrastructure code or logs. This is to maintain security and prevent unauthorized access.</p>
<p>However, if you need to create and retrieve IAM user credentials, such as for a newly created user in an automated fashion, you can do this carefully through the AWS Management Console or programmatically using the AWS SDK or CLI. Here&#39;s a general approach:</p>
<h3 id="using-aws-management-console">Using AWS Management Console</h3>
<ol>
<li>
<p><strong>Navigate to IAM</strong>: Log into the AWS Management Console and go to the IAM service.</p>
</li>
<li>
<p><strong>Find the User</strong>: Go to the &#39;Users&#39; section and find the user you created.</p>
</li>
<li>
<p><strong>Create Access Key</strong>:</p>
<ul>
<li>Select the user.</li>
<li>In the &#39;Security credentials&#39; tab, you can create an access key by clicking the &quot;Create access key&quot; button.</li>
<li>This will provide you with the Access Key ID and Secret Access Key. Make sure to download or copy these credentials, as the Secret Access Key won&#39;t be shown again.</li>
</ul>
</li>
</ol>
<h3 id="using-aws-cli-or-sdk">Using AWS CLI or SDK</h3>
<ol>
<li>
<p><strong>Install AWS CLI or SDK</strong>: If not already installed, install the AWS CLI or configure the AWS SDK in your preferred programming language.</p>
</li>
<li>
<p><strong>Use CLI or SDK to Create Access Key</strong>:</p>
<ul>
<li>With the AWS CLI, you can run a command to create a new access key for a user.</li>
<li>The command <code>aws iam create-access-key --user-name &lt;username&gt;</code> will output the Access Key ID and Secret Access Key.</li>
<li>Programmatically, you can use the corresponding function in the AWS SDK of your choice.</li>
</ul>
</li>
</ol>
<h3 id="handling-credentials-securely">Handling Credentials Securely</h3>
<ul>
<li><strong>Never Hardcode Credentials</strong>: Avoid hardcoding credentials in your code or CDK scripts.</li>
<li><strong>Store Securely</strong>: If you must store these credentials, use a secure method like AWS Secrets Manager or another encrypted storage solution.</li>
<li><strong>Use IAM Roles</strong>: For applications running on AWS services (like EC2, Lambda), prefer to use IAM roles for granting permissions. This avoids the need to manage and rotate access keys.</li>
</ul>
<h3 id="cdk-consideration">CDK Consideration</h3>
<p>AWS CDK does not have built-in mechanisms to handle the creation and retrieval of IAM user credentials because of the security implications. You should handle such operations outside of your infrastructure as code, ensuring that any sensitive data is securely managed.</p>
<blockquote>
<p>Follow whichever approach works best, but in my case, I simply used the CLI to generate the credentials and jot them down somewhere safe for the next part.</p>
</blockquote>
<span id="connecting-using-cloudmounter" class="anchor-alias"><h2 id="connecting-using-cloud-mounter">Connecting Using CloudMounter</h2>
<p>The final part is quite trivial.</p>
<p>Open CloudMounter and you will see a screen like the following:</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2023-11-28-mounting-your-own-cloud-storage-with-cloud-mounter-and-aws/1-cloud-mounter.webp" alt="CloudMounter" loading="lazy" decoding="async"/><figcaption>CloudMounter</figcaption></figure>
<p>Click on the <code>+</code> button and select <code>Amazon S3</code> from the list of options. This will lead you to a screen where you can input the bucket name, username, access key and secret key.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2023-11-28-mounting-your-own-cloud-storage-with-cloud-mounter-and-aws/2-cloud-mounter-s3.webp" alt="CloudMounter S3 option" loading="lazy" decoding="async"/><figcaption>CloudMounter S3 option</figcaption></figure>
<p>Once you have filled in the details, click <code>Mount</code> and you will be able to access the bucket as if it were a local drive through Finder.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2023-11-28-mounting-your-own-cloud-storage-with-cloud-mounter-and-aws/3-cloud-mounter-finder.webp" alt="CloudMounter drive on Finder" loading="lazy" decoding="async"/><figcaption>CloudMounter drive on Finder</figcaption></figure>
<p>You now have a connection to the S3 drive that you can use as external storage. Happy days!</p>
<h2 id="final-considerations">Final Considerations</h2>
<ul>
<li>AWS CDK uses AWS CloudFormation to provision resources.</li>
<li>Be aware of AWS costs associated with the resources you deploy.</li>
<li>If you know the expectation or lifecycle of the resources you create, you can set up a deletion policy to automatically delete resources or manually handle transitions instead of the intelligent tiering.</li>
</ul>
<p>If you do not require the stack that we created today, be sure to tear it down with <code>cdk destroy</code>.</p>
<h2 id="references-and-further-reading">References and Further Reading</h2>
<ol>
<li><a href="https://github.com/99designs/aws-vault" target="_blank" rel="noopener noreferrer">99designs/aws-vault</a></li>
<li><a href="https://cloudmounter.net/" target="_blank" rel="noopener noreferrer">CloudMounter</a></li>
<li><a href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_s3-readme.html" target="_blank" rel="noopener noreferrer">AWS CDK S3 documentation</a></li>
<li><a href="https://go.setapp.com/invite/6yrbjgdh" target="_blank" rel="noopener noreferrer">SetApp</a></li>
<li><a href="https://github.com/okeeffed/mac-cloud-storage-s3-cdk-app" target="_blank" rel="noopener noreferrer">Blog code</a></li>
</ol>
<p><em>Photo credit: <a href="https://unsplash.com/@boliviainteligente" target="_blank" rel="noopener noreferrer"><code>boliviainteligente</code></a></em></p>]]></content>
<category term="nodejs"/><category term="typescript"/><category term="aws cdk"/><category term="aws"/>
</entry>
<entry>
<title type="html"><![CDATA[Using Prisma For Migrations And DrizzleORM For Querying]]></title>
<id>https://blog.dennisokeeffe.com/blog/2023-11-29-prisma-migrations-drizzle-orm-querying copy</id>
<link href="https://dennisokeeffe.com/blog/2023-11-29-prisma-migrations-drizzle-orm-querying"/>
<published>2023-11-28T00:00:00.000Z</published>
<updated>2023-11-28T00:00:00.000Z</updated>
<summary type="html"><![CDATA[This post is for you if you are migrating across from a Prisma application or just want to check out the performance difference with DrizzleORM.]]></summary>
<content type="html"><![CDATA[<h2 id="introduction">Introduction</h2>
<p>I love the Prisma developer experience, but some of the performance issues have left things to be desired.</p>
<p>I wanted to experiment a bit more with DrizzleORM and see how it compared to Prisma. I also wanted to see how easy it was to migrate from Prisma to DrizzleORM.</p>
<p>This post will look at the idea of using Prisma for migrations and DrizzleORM for the application after using introspection on the database. I won&#39;t speculate too much on the DX (developer experience) of doing things this way, but it may be a useful experiment that is worth sharing.</p>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>Working knowledge of Prisma.</li>
<li>Working knowledge of DrizzleORM.</li>
<li>Working knowledge of NextJS (we are simply using that as a layer to run the code).</li>
</ul>
<p>This post assumes that you have a working configuration of Postgres set up on your machine.</p>
<span id="setting-up-the-nextjs-project" class="anchor-alias"><h2 id="setting-up-the-next-js-project">Setting up the NextJS project</h2>
<p>In the terminal, run the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="bash"><code>npx create-next-app@latest --ts demo-prisma-migrate-drizzle-introspection</code></pre>
<p>There will be a few prompts. Chose whatever you like here.</p>
<p>Afterwards, change into the directory and install the following dependencies:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="bash"><code>cd demo-prisma-migrate-drizzle-introspection

# For Prisma
pnpm add prisma@latest

# We then initialize Prisma
pnpx prisma init

# For DrizzleORM
pnpm add drizzle-orm
# For Drizzle Kit (used for introspections)
pnpm add -D drizzle-kit

# Install the lib to connect with postgres
pnpm add postgres pg</code></pre>
<h2 id="configuring-prisma">Configuring Prisma</h2>
<p>We are going to update the schema for a basic blog application. We will have an <code>Author</code> and <code>Post</code> model.</p>
<p>Update <code>prisma/schema.prisma</code> to the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="prisma"><code>// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = &quot;prisma-client-js&quot;
}

datasource db {
  provider = &quot;postgresql&quot;
  url      = env(&quot;DATABASE_URL&quot;)
}

// Model for Author
model Author {
  id    String @id @default(cuid())
  name  String
  email String @unique
  posts Post[]
}

// Model for Post
model Post {
  id        String  @id @default(cuid())
  title     String
  content   String?
  published Boolean @default(false)
  authorId  String
  author    Author  @relation(fields: [authorId], references: [id])
}</code></pre>
<p>At this stage, I am assuming you already have a Postgres database set up (as per the prerequisites).</p>
<p>Update your <code>.env</code> file to point to this database:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="bash"><code>DATABASE_URL=&quot;postgresql://prisma:prisma@localhost:5432/demo_prisma_drizzle?schema=public&quot;</code></pre>
<blockquote>
<p>In my case, <code>demo_prisma_drizzle</code> is the name of the local database I have set up and I have set the username and password to <code>prisma</code>.</p>
</blockquote>
<p>Run the migration using the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="bash"><code>pnpx prisma migrate dev</code></pre>
<p>You will be prompted for a name. I chose <code>init</code>.</p>
<p>Once this is run, your database should be in sync with your schema and set up with the <code>Author</code> and <code>Post</code> tables.</p>
<p>Our next step is to run the introspection on the database to generate the DrizzleORM models.</p>
<h2 id="introspection-with-drizzle-kit">Introspection with Drizzle Kit</h2>
<p>We need to add the Drizzle configuration file to the root of our application. Create a file called <code>drizzle.config.ts</code> and add the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="typescript"><code>import { defineConfig } from &quot;drizzle-kit&quot;;

export default defineConfig({
  schema: &quot;./drizzle/schema.ts&quot;,
  driver: &quot;pg&quot;,
  dbCredentials: {
    connectionString: process.env.DATABASE_URL!,
  },
  verbose: true,
  strict: true,
});</code></pre>
<p>Once the configuration is set up, we can run the introspection to generate the models.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="bash"><code>pnpm drizzle-kit introspect:pg</code></pre>
<p>If the output is successful, you will have a new folder <code>drizzle</code> with the models generated within <code>schema.ts</code>.</p>
<h2 id="adding-records-with-drizzle-studio">Adding Records with Drizzle Studio</h2>
<p>We can launch Drizzle Studio to add some records to our database.</p>
<p>That being said, this is a bit of a hack way to do it. NextJS compiler defaults to ES5, which is unsupported. See <a href="https://github.com/drizzle-team/drizzle-orm/issues/803" target="_blank" rel="noopener noreferrer">this issue</a> for more details.</p>
<p>For now, you can either add records however you want or follow my interim solution just to get the records in.</p>
<p>Update <code>tsconfig.json</code> so that <code>&quot;target&quot;: &quot;es5&quot;</code> is changed to <code>&quot;target&quot;: &quot;es6&quot;</code>. You will need to change this after adding the records.</p>
<p>Now running Studio should enable it to launch correctly.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="bash"><code>pnpm drizzle-kit studio</code></pre>
<blockquote>
<p>Please note, at the time of writing, you should be using Node v18 or greater for this to work.</p>
</blockquote>
<p>At this point, you should be able to add some records to the database at <code>https://local.drizzle.studio</code>.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2023-11-29-prisma-migrations-drizzle-orm-querying/1-drizzle-studio.webp" alt="Drizzle Studio" loading="lazy" decoding="async"/><figcaption>Drizzle Studio</figcaption></figure>
<p>For the sake of brevity, I will just add a few authors.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2023-11-29-prisma-migrations-drizzle-orm-querying/2-drizzle-studio-authors.webp" alt="Drizzle Studio > Authors" loading="lazy" decoding="async"/><figcaption>Drizzle Studio &gt; Authors</figcaption></figure>
<p>At this point, we can now update the <code>tsconfig.json</code> back to <code>&quot;target&quot;: &quot;es5&quot;</code>.</p>
<p>Once that is done, let&#39;s just into the NextJS application and query our users with Drizzle.</p>
<h2 id="querying-authors-with-drizzle">Querying Authors With Drizzle</h2>
<p>We are going to create a simple page that will query the authors and display them on the page.</p>
<p>The Drizzle code is straightforward and a full query is as follows:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="tsx"><code>const result = await db.query.author.findMany({
  with: {
    posts: true,
  },
});</code></pre>
<p>We can use this code within the page, get our users and map over them to display them on the page.</p>
<p>For the sake of keeping things simple, we will update <code>src/app/page.tsx</code> in order to display the authors.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="tsx"><code>import * as schema from &quot;../../drizzle/schema&quot;;
import { drizzle } from &quot;drizzle-orm/postgres-js&quot;;
import postgres from &quot;postgres&quot;;

const queryClient = postgres(
  &quot;postgresql://prisma:prisma@localhost:5432/demo_prisma_drizzle?search_path=public&quot;
);
const db = drizzle(queryClient, { schema });

export default async function Home() {
  const result = await db.query.author.findMany();

  return (
    &lt;main className=&quot;flex min-h-screen flex-col items-center justify-between p-24&quot;&gt;
      &lt;h1 className=&quot;text-4xl font-bold&quot;&gt;Authors&lt;/h1&gt;
      &lt;div className=&quot;flex flex-col items-center justify-center space-y-4&quot;&gt;
        {result.map((author) =&gt; (
          &lt;div key={author.id}&gt;
            &lt;h2 className=&quot;text-2xl font-bold&quot;&gt;{author.name}&lt;/h2&gt;
          &lt;/div&gt;
        ))}
      &lt;/div&gt;
    &lt;/main&gt;
  );
}</code></pre>
<p>In production instances, do not inline everything into one page like I have done for the spike. In practice, you will likely want to abstract out the database connection and queries into a separate file and maybe take advantage of server actions.</p>
<blockquote>
<p>Something worth noting is that where Prisma uses <code>?schema=public</code>, Drizzle uses <code>?search_path=public</code>.</p>
</blockquote>
<p>At this point, you should be able to run the application and see the authors displayed on the page.</p>
<h2 id="running-the-application">Running The Application</h2>
<p>To run the application, run the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="bash"><code>pnpm dev</code></pre>
<p>Head to the browser and go to <code>http://localhost:3000</code> (or whatever port is provided in the console).</p>
<p>You should see the authors displayed on the page.</p>
<figure><img src="https://cdn.dennisokeeffe.com/assets/2023-11-29-prisma-migrations-drizzle-orm-querying/3-home.webp" alt="Home page" loading="lazy" decoding="async"/><figcaption>Home page</figcaption></figure>
<h2 id="conclusion">Conclusion</h2>
<p>In this post, we have looked at how we can use Prisma for migrations and DrizzleORM for querying the database using Drizzle Kit introspection.</p>
<h2 id="references-and-further-reading">References and Further Reading</h2>
<ol>
<li><a href="https://orm.drizzle.team/docs/overview" target="_blank" rel="noopener noreferrer">DrizzleORM</a></li>
<li><a href="https://orm.drizzle.team/kit-docs/overview#introspecting-with-db-pull" target="_blank" rel="noopener noreferrer">Drizzle Kit Introspection</a></li>
<li><a href="https://www.prisma.io/" target="_blank" rel="noopener noreferrer">Prisma</a></li>
<li><a href="https://github.com/okeeffed/demo-prisma-migrate-drizzle-introspection" target="_blank" rel="noopener noreferrer">Blog code</a></li>
</ol>
<p><em>Photo credit: <a href="https://unsplash.com/@boliviainteligente" target="_blank" rel="noopener noreferrer"><code>boliviainteligente</code></a></em></p>]]></content>
<category term="nodejs"/><category term="typescript"/><category term="drizzleorm"/><category term="prisma"/><category term="orm"/><category term="postgresql"/>
</entry>
<entry>
<title type="html"><![CDATA[Node.js --prof and --process-prof flags]]></title>
<id>https://blog.dennisokeeffe.com/blog/2023-10-27-nodejs-profile-flag</id>
<link href="https://dennisokeeffe.com/blog/2023-10-27-nodejs-profile-flag"/>
<published>2023-10-27T00:00:00.000Z</published>
<updated>2023-10-27T00:00:00.000Z</updated>
<summary type="html"><![CDATA[Understand the Node.js profile flag by improving an inefficient approach to getting unique elements from an array.]]></summary>
<content type="html"><![CDATA[<h2 id="introduction">Introduction</h2>
<p>Node.js <code>--prof</code> and <code>--process-prof</code> flags allow you to profile your Node.js application to identify performance bottlenecks.</p>
<p>The <code>--prof</code> and <code>--prof-process</code> flags in Node.js provide a streamlined way to profile the performance of a JavaScript program. Here are some scenarios and advantages of using these flags:</p>
<ol>
<li><strong>Performance Analysis</strong>:</li>
</ol>
<ul>
<li>Identify Bottlenecks: Easily find performance bottlenecks in your code by identifying functions or operations that consume the most CPU time.</li>
<li>Optimize Code: By identifying hot spots, you can focus your optimization efforts where they will have the most impact.</li>
</ul>
<ol start="2">
<li><strong>Simplicity</strong>:</li>
</ol>
<ul>
<li>Easy to Use: The <code>--prof</code> and <code>--prof-process</code> flags are straightforward to use, making it easy to generate and process profiling data without a steep learning curve.</li>
<li>Quick Setup: There&#39;s no need for additional setup or external tools; you can start profiling your application with just a couple of commands.</li>
</ul>
<ol start="3">
<li><strong>Low Overhead</strong>:</li>
</ol>
<ul>
<li>Minimal Impact: The <code>--prof</code> flag enables a sampling profiler, which has relatively low overhead compared to instrumenting profilers. This allows for profiling in scenarios where minimizing performance impact is crucial.</li>
</ul>
<ol start="4">
<li><strong>Cross-platform Profiling</strong>:</li>
</ol>
<ul>
<li>Platform Agnostic: The profiling is built into the Node.js runtime, making it a cross-platform solution that works on various operating systems and environments.</li>
</ul>
<ol start="5">
<li><strong>Offline Analysis</strong>:</li>
</ol>
<ul>
<li>Post-Execution Analysis: The <code>--prof-process</code> flag allows for offline processing of profiling data, which can be beneficial for analyzing performance post-execution, sharing profiling data with others, or archiving for later analysis.</li>
</ul>
<ol start="6">
<li><strong>Built-in Tooling</strong>:</li>
</ol>
<ul>
<li>Integrated with V8: The profiling flags leverage the built-in V8 profiler, ensuring tight integration with the JavaScript engine powering Node.js.</li>
</ul>
<ol start="7">
<li><strong>Baseline Profiling</strong>:</li>
</ol>
<ul>
<li>Initial Assessment: It&#39;s a good starting point for performance analysis before possibly moving to more sophisticated profiling tools if necessary.</li>
</ul>
<ol start="8">
<li><strong>Learning and Education</strong>:</li>
</ol>
<ul>
<li>Understanding Performance: It&#39;s a useful tool for learning about the performance characteristics of JavaScript and the Node.js runtime, especially for educational purposes or self-learning.</li>
</ul>
<p>However, while <code>--prof</code> and <code>--prof-process</code> provide valuable insights, they might lack some of the more advanced features, user-friendly interfaces, and detailed visualizations offered by other profiling tools and environments such as Chrome DevTools. Depending on the depth of analysis and the level of detail required, developers might choose to use these flags for quick performance assessments or opt for more advanced profiling tools for deeper analysis.</p>
<p>It is important to note that these flags are not the same as the <code>--inspect</code> flag, which opens a debugging session that allows for more interactive profiling and debugging using DevTools.</p>
<span id="profiling-using---prof-and---prof-process-flags" class="anchor-alias"><h2 id="profiling-using-prof-and-prof-process-flags">Profiling Using <code>--prof</code> and <code>--prof-process</code> Flags</h2>
<p>The <code>--prof</code> flag enables V8&#39;s built-in sampling CPU profile. Here&#39;s how you would use the <code>--prof</code> and <code>--prof-process</code> flags to profile your Node.js application:</p>
<ol>
<li><strong>Enable Profiling</strong>: Run your Node.js script with the <code>--prof</code> flag to generate a log file containing profiling data.</li>
</ol>
<pre class="astro-code night-owl" tabindex="0" data-language="bash"><code>NODE_ENV=production node --prof script.js</code></pre>
<p>This will create a file named <code>isolate-0xnnnnnnnnnnnn-v8.log</code> (the <code>nnnnnnnnnnnn</code> part will be different each time).</p>
<blockquote>
<p>Note: it&#39;s recommended to run your script in production mode to avoid unnecessary overhead from development-related features.</p>
</blockquote>
<ol start="2">
<li><strong>Process the Profiling Data</strong>: Use the <code>--prof-process</code> flag to process the log file and generate a human-readable report.</li>
</ol>
<pre class="astro-code night-owl" tabindex="0" data-language="bash"><code>node --prof-process isolate-0xnnnnnnnnnnnn-v8.log &gt; processed-profile.txt</code></pre>
<p>This will create a file named <code>processed-profile.txt</code> containing the processed profiling data.</p>
<ol start="3">
<li><strong>Analyze the Data</strong>: Open <code>processed-profile.txt</code> and review the profiling data to understand where most of the CPU time is being spent in your script. You&#39;ll see function names, file names, and the number of ticks spent in each function.</li>
</ol>
<pre class="astro-code night-owl" tabindex="0" data-language="plaintext"><code>[Summary]:
   ticks  total  nonlib   name
     100   10.0%   10.5%  JavaScript
      90    9.0%    9.5%  C++
      10    1.0%    1.1%  GC
      20    2.0%          Shared libraries
     780   78.0%          Unaccounted

[Bottom up (heavy) profile]:
  Note: percentage shows a share of a particular caller in the total
  amount of its parent calls.
  Callers occupying less than 1.0% are not shown.

 ticks parent  name
 ^^^^^ ^^^^^^  ^^^^^^^^</code></pre>
<span id="comparison-with---inspect-profiling" class="anchor-alias"><h2 id="comparison-with-inspect-profiling">Comparison with <code>--inspect</code> Profiling</h2>
<ul>
<li>
<p><strong>Type of Profiling</strong>:</p>
<ul>
<li><code>--prof</code> and <code>--prof-process</code>: These flags enable and process sampling CPU profiling.</li>
<li><code>--inspect</code>: This flag opens a debugging session which allows for more interactive profiling and debugging using DevTools.</li>
</ul>
</li>
<li>
<p><strong>Ease of Use</strong>:</p>
<ul>
<li><code>--prof</code> and <code>--prof-process</code>: These flags are straightforward for generating and processing profiling data quickly.</li>
<li><code>--inspect</code>: This may require a bit more setup, but offers a richer, more interactive profiling experience through DevTools.</li>
</ul>
</li>
<li>
<p><strong>Detail Level</strong>:</p>
<ul>
<li><code>--prof</code> and <code>--prof-process</code>: These provide a high-level overview of where time is being spent in your script.</li>
<li><code>--inspect</code>: This provides a more detailed and interactive view, allowing you to drill down into call stacks, view memory allocation, and more.</li>
</ul>
</li>
<li>
<p><strong>Output</strong>:</p>
<ul>
<li><code>--prof</code> and <code>--prof-process</code>: Output is a text-based report.</li>
<li><code>--inspect</code>: Output is viewed in a graphical interface, providing visual insights and allowing for more in-depth analysis.</li>
</ul>
</li>
</ul>
<span id="example-profiling-a-nodejs-application" class="anchor-alias"><h2 id="example-profiling-a-node-js-application">Example: Profiling a Node.js Application</h2>
<p>We are going to use the <code>--prof</code> and <code>--prof-process</code> approach with a de-optimized <code>getUniqueElements</code> function example.</p>
<p>To get started, we will create two files <code>script-slow.js</code> and <code>script-fast.js</code> to compare profile results.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="bash"><code>touch script-slow.js script-fast.js</code></pre>
<h3 id="1-create-your-script">1. <strong>Create Your Script:</strong></h3>
<p>Add the following to the <code>script-slow.js</code> file:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="javascript"><code>function getUniqueElements(arr) {
  let uniqueArr = [];
  for (let i = 0; i &lt; arr.length; i++) {
    let isUnique = true;
    for (let j = 0; j &lt; uniqueArr.length; j++) {
      if (arr[i] === uniqueArr[j]) {
        isUnique = false;
        break;
      }
    }
    if (isUnique) {
      uniqueArr.push(arr[i]);
    }
  }
  return uniqueArr;
}

// Create a large array to amplify the issue
const largeArr = Array(10000)
  .fill()
  .map((_, idx) =&gt; idx % 100);
const uniqueArr = getUniqueElements(largeArr);
console.log(uniqueArr.length); // Output: 100</code></pre>
<h3 id="2-run-the-profiler">2. <strong>Run the Profiler:</strong></h3>
<p>Run the script with the <code>--prof</code> flag to generate a profiling log:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="bash"><code>node --prof script-slow.js</code></pre>
<h3 id="3-process-the-profiling-data">3. <strong>Process the Profiling Data:</strong></h3>
<p>After running the script, you&#39;ll find a log file named <code>isolate-0xnnnnnnnnnnnn-v8.log</code> in your directory. Process this log file using the <code>--prof-process</code> flag:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="bash"><code>node --prof-process isolate-0xnnnnnnnnnnnn-v8.log &gt; processed-profile.txt</code></pre>
<h3 id="4-analyze-the-data">4. <strong>Analyze the Data:</strong></h3>
<p>Open the <code>processed-profile.txt</code> file and look for the <code>getUniqueElements</code> function. You&#39;ll be able to see the number of ticks spent in this function, which represents the amount of time spent executing it.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="plaintext"><code>// ... other parts of the profile

 ticks parent  name
   ^^^^ ^^^^^^  ^^^^^^^^^^^^^^^^^^^^^^^^^^
   ...   ...%   getUniqueElements ...

// ... other parts of the profile</code></pre>
<p>This simplified analysis should give you an idea of the time spent in the <code>getUniqueElements</code> function, indicating a performance bottleneck.</p>
<p>The biggest thing takeaway that we are going to demonstrate for the slow script is the fact that <code>getUniqueElements</code> shows up at all in this profile.</p>
<p>In this de-optimized version of <code>getUniqueElements</code>, the logic for checking uniqueness is implemented in JavaScript within your function. Before elaborating more on this, let&#39;s add the optimized version.</p>
<h2 id="optimized-version">Optimized Version</h2>
<p>Add the following to the <code>script-fast.js</code> file:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="javascript"><code>function getUniqueElements(arr) {
  return [...new Set(arr)];
}

// Create a large array to amplify the issue
const largeArr = Array(10000)
  .fill()
  .map((_, idx) =&gt; idx % 100);
const uniqueArr = getUniqueElements(largeArr);
console.log(uniqueArr.length); // Output: 100</code></pre>
<p>In this code, we are using the <code>Set</code> data structure to get unique elements from the array. This and the array spread are both native JavaScript features, so the logic is implemented in C++ and executed by the JavaScript engine.</p>
<p>Follow a similar process from steps 2 to 4 to profile the optimized version of the script.</p>
<p>Once this is done, you&#39;ll notice something peculiar in the <code>processed-process.txt</code> file - the <code>getUniqueElements</code> function is no longer present in the profiling data.</p>
<p>This is because the logic for getting unique elements is implemented in C++ and executed by the JavaScript engine, so it doesn&#39;t show up in the profiling data.</p>
<p>In fact, JavaScript is 0% of the total ticks in the optimized version of the script:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="plaintext"><code>[Summary]:
   ticks  total  nonlib   name
      0    0.0%    0.0%  JavaScript
    137   68.8%   99.3%  C++
      1    0.5%    0.7%  GC
     61   30.7%          Shared libraries
      1    0.5%          Unaccounted</code></pre>
<p>Compared that to what happened prior:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="plaintext"><code>[Summary]:
   ticks  total  nonlib   name
      1    0.9%    1.2%  JavaScript
     81   73.0%   96.4%  C++
      1    0.9%    1.2%  GC
     27   24.3%          Shared libraries
      2    1.8%          Unaccounted</code></pre>
<h2 id="a-more-detailed-analysis">A More Detailed Analysis</h2>
<p>When you optimize the <code>getUniqueElements</code> function by using the <code>Set</code> object, the bulk of the work is offloaded to the underlying Node.js or V8 engine implementations which are highly optimized. The <code>Set</code> object is part of the ECMAScript standard and its operations are implemented in native code within the V8 engine, which powers Node.js. This native code is optimized for performance and is compiled to machine code, making operations like adding elements to a set or checking for uniqueness very fast.</p>
<p>In the de-optimized version of <code>getUniqueElements</code>, the logic for checking uniqueness is implemented in JavaScript within your function. This logic involves nested loops, which have a time complexity of <code>O(n^2)</code> in the worst case. This is why the function shows up in the profiling data when using the <code>--prof</code> and <code>--prof-process</code> flags - the JavaScript engine spends a significant amount of time executing this function.</p>
<p>On the other hand, in the optimized version of <code>getUniqueElements</code>, the logic for checking uniqueness is handled by the <code>Set</code> object, which likely has a time complexity closer to <code>O(n)</code> or even better for these operations due to its native, optimized implementation. As a result, the <code>getUniqueElements</code> function itself does very little work – it merely creates a new <code>Set</code> object and then creates an array from that set. The heavy lifting is done by native code within the V8 engine, outside of the JavaScript execution environment.</p>
<p>When profiling, the <code>--prof</code> and <code>--prof-process</code> flags are capturing the CPU time spent executing JavaScript code. Since the optimized <code>getUniqueElements</code> function offloads most of the work to native code, the function itself consumes very little CPU time in the JavaScript execution environment, and hence, may not show up in the profiling data or may show up with a significantly reduced number of ticks compared to the de-optimized version.</p>
<h2 id="a-contrived-comparison">A Contrived Comparison</h2>
<p>I used <a href="https://github.com/sharkdp/hyperfine" target="_blank" rel="noopener noreferrer">hyperfine</a> just to run a comparison.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="plaintext"><code>hyperfine --warmup 10 --runs 100 &#39;NODE_ENV=production node ./script-slow&#39; &#39;NODE_ENV=production node ./script-fast&#39;
Benchmark 1: NODE_ENV=production node ./script-slow
  Time (mean ± σ):      84.0 ms ±  42.8 ms    [User: 51.8 ms, System: 13.8 ms]
  Range (min … max):    54.5 ms … 343.4 ms    100 runs

  Warning: Statistical outliers were detected. Consider re-running this benchmark on a quiet system without any interferences from other programs. It might help to use the &#39;--warmup&#39; or &#39;--prepare&#39; options.

Benchmark 2: NODE_ENV=production node ./script-fast
  Time (mean ± σ):      71.7 ms ±  18.9 ms    [User: 47.2 ms, System: 13.3 ms]
  Range (min … max):    49.7 ms … 238.2 ms    100 runs

  Warning: Statistical outliers were detected. Consider re-running this benchmark on a quiet system without any interferences from other programs. It might help to use the &#39;--warmup&#39; or &#39;--prepare&#39; options.

Summary
  NODE_ENV=production node ./script-fast ran
    1.17 ± 0.67 times faster than NODE_ENV=production node ./script-slow</code></pre>
<p>Take this with a grain of salt. Our implementation would only make significant impacts over a large number of iterations. This is just to show that the optimized version is faster.</p>
<h2 id="conclusion">Conclusion</h2>
<p>In summary, the <code>--prof</code> and <code>--prof-process</code> flags provide a quick and easy way to identify performance bottlenecks, while <code>--inspect</code> provides a more interactive and detailed profiling experience.</p>
<p>We did this using two versions of a <code>getUniqueElements</code> function, one that was de-optimized and one that was optimized. The de-optimized version showed up in the profiling data due to its implementation details, while the optimized version did not.</p>
<p>At the very least, it is a good reminder that we should opt to use the built-in features of JavaScript and Node.js where possible, as they are likely expected to be more performant than our own implementations.</p>
<p>For a more real-world scenario, you may want to consider exploring other profiling tools or libraries that provide a more detailed and visual representation of the profiling data, such as the Chrome DevTools, <code>node-inspect</code>, or others. Depending on your needs and the level of detail required, you may choose one approach over the other.</p>
<h2 id="references-and-further-reading">References and Further Reading</h2>
<ol>
<li><a href="https://nodejs.org/en/docs/guides/simple-profiling" target="_blank" rel="noopener noreferrer">Node.js simple profiling</a></li>
<li><a href="https://github.com/sharkdp/hyperfine" target="_blank" rel="noopener noreferrer">hyperfine</a></li>
</ol>
<p><em>Photo credit: <a href="https://unsplash.com/@dynastywind" target="_blank" rel="noopener noreferrer"><code>dynastywind</code></a></em></p>]]></content>
<category term="nodejs"/><category term="javascript"/>
</entry>
<entry>
<title type="html"><![CDATA[Explaining CJS vs AMD vs UMD vs ESM]]></title>
<id>https://blog.dennisokeeffe.com/blog/2023-10-26-explaining-cjs-vs-amd-vs-umd-vs-esm</id>
<link href="https://dennisokeeffe.com/blog/2023-10-26-explaining-cjs-vs-amd-vs-umd-vs-esm"/>
<published>2023-10-26T00:00:00.000Z</published>
<updated>2023-10-26T00:00:00.000Z</updated>
<summary type="html"><![CDATA[One of the JavaScript ecosystems biggest pain points. What are they and why do library maintainers hurt inside when they have to support them all?]]></summary>
<content type="html"><![CDATA[<h2 id="introduction">Introduction</h2>
<p>Something that I think we can all agree on though is our mutual hurt for the module system. Since ES6 (or even before...), the pain has mainly been felt by library maintainers, with the majority of pain being abstracted behind bundlers and transpilers.</p>
<p>However, as the ecosystem has continued to grow (along with ways to support these module systems), it is no longer just library maintainers that feel the pain. It is now also developers who are trying to understand the differences between each module system, how to use them and wtf is happening when you see that dreaded <a href="https://nodejs.org/api/errors.html#err_require_esm" target="_blank" rel="noopener noreferrer">ERR_REQUIRE_ESM</a> message and attempt padding it over with a <code>&quot;type&quot;: &quot;module&quot;</code> file extension in <code>package.json</code> to only find out it does not work and you do not truly understand why.</p>
<p>If you are yet to truly understand their differences, then do not despair: this post is for you (and me when I inevitably forget how it works again).</p>
<p>We will explore the module types, follow that up with understanding how <code>package.json</code> and file extensions affect Node.js and then finish on how bundlers have abstracted away the pain for us mere non-library maintainers for so long.</p>
<h2 id="what-are-cjs-amd-umd-and-esm">What Are CJS, AMD, UMD, and ESM?</h2>
<p>CommonJS (CJS), Asynchronous Module Definition (AMD), Universal Module Definition (UMD) and ES Modules (ESM) are all module systems in JavaScript, allowing developers to organize and structure their code better.</p>
<p>Let&#39;s break down each, then demonstrate how to use them in Node.js and with (and without) bundlers like Webpack and Rollup.</p>
<span id="cjs-commonjs" class="anchor-alias"><h2 id="cjs-common-js">CJS (CommonJS)</h2>
<ul>
<li>Introduced for use in <strong>Node.js</strong>.</li>
<li>Synchronous <code>require</code> and <code>module.exports</code> for importing/exporting.</li>
<li>Mainly for server-side.</li>
</ul>
<p>For a CJS example:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>const moduleA = require(&quot;moduleA&quot;);
module.exports = someFunction;</code></pre>
<h2 id="amd-asynchronous-module-definition">AMD (Asynchronous Module Definition)</h2>
<ul>
<li>For the browser, designed to load modules asynchronously.</li>
<li>Uses <code>define</code> and <code>require</code>.</li>
</ul>
<p>For an AMD example:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>define([&quot;moduleA&quot;, &quot;moduleB&quot;], function (moduleA, moduleB) {
  return someFunction;
});</code></pre>
<h2 id="umd-universal-module-definition">UMD (Universal Module Definition)</h2>
<ul>
<li>Tries to unify CJS and AMD, making modules work in both client and server.</li>
<li>Checks the environment and then decides which system to use.</li>
</ul>
<p>For a UMD example:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>(function (root, factory) {
  if (typeof define === &quot;function&quot; &amp;&amp; define.amd) {
    define([&quot;moduleA&quot;], factory);
  } else if (typeof exports === &quot;object&quot;) {
    module.exports = factory(require(&quot;moduleA&quot;));
  }
})(this, function (moduleA) {
  return someFunction;
});</code></pre>
<span id="esm-ecmascript-modules" class="anchor-alias"><h2 id="esm-ecma-script-modules">ESM (ECMAScript Modules)</h2>
<ul>
<li>Native JavaScript module system introduced in ES6.</li>
<li>Uses <code>import</code> and <code>export</code> keywords.</li>
<li>Static module structure, meaning you can statically analyze the imports and exports without running the code.</li>
</ul>
<p>For an ESM example:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>import moduleA from &quot;./moduleA&quot;;
export const someFunction = () =&gt; {};</code></pre>
<h2 id="important-differences-between-each">Important Differences Between Each</h2>
<ul>
<li><strong>Synchronous vs. Asynchronous</strong>: CJS is synchronous while AMD is asynchronous. UMD can handle both.</li>
<li><strong>Environment</strong>: CJS is mainly for Node.js, AMD is for browsers, UMD is universal, and ESM is the new standard for both browser and server.</li>
<li><strong>Syntax</strong>: Different keywords and approaches (e.g., <code>require</code> vs. <code>import</code>).</li>
<li><strong>Static vs. Dynamic</strong>: ESM is static (better for tree shaking and optimization), while others like CJS are dynamic.</li>
</ul>
<p>With the growing adoption of ES6 and beyond, <strong>ESM</strong> is becoming the most commonly used module system, especially with build tools and bundlers like Webpack and Rollup which can handle various module systems and transpile them to a chosen format.</p>
<span id="nodejs-using-cjs-or-esm" class="anchor-alias"><h2 id="node-js-using-cjs-or-esm">Node.js: Using CJS or ESM</h2>
<p>As you may have grokked from above, CJS comes from Node.js, and both UMD and ESM since have been approaches to unify the browser and server.</p>
<p>With the introduction of ESM in ES6, Node.js has been working on supporting it natively.</p>
<p>There are few ways to tell Node.js which module system to use. In Node.js, the module system (CommonJS vs. ESM) can be determined by either the file extension or by specifying the module type in the <code>package.json</code> file.</p>
<h3 id="1-by-file-extension">1. By File Extension</h3>
<ul>
<li><code>.mjs</code>: By using the <code>.mjs</code> extension, you tell Node.js to treat the file as an ES module, regardless of other settings.</li>
<li><code>.cjs</code>: This extension forces Node.js to treat the file as a CommonJS module.</li>
</ul>
<span id="2-specifying-type-within-packagejson" class="anchor-alias"><h3 id="2-specifying-type-within-package-json">2. Specifying &quot;type&quot; within package.json</h3>
<ul>
<li><code>&quot;type&quot;: &quot;module&quot;</code>: By adding this line to your <code>package.json</code>, you inform Node.js to treat <code>.js</code> files in your project as ESM.</li>
</ul>
<p>For example:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="json"><code>{
  &quot;type&quot;: &quot;module&quot;
}</code></pre>
<ul>
<li>If you don&#39;t specify the <code>&quot;type&quot;: &quot;module&quot;</code> entry (or if it&#39;s set to <code>&quot;commonjs&quot;</code>), then <code>.js</code> files are treated as CommonJS modules by default.</li>
</ul>
<h3 id="3-dynamic-import">3. Dynamic Import</h3>
<ul>
<li>Regardless of the module system in place, you can always use dynamic imports in Node.js to load ESM:</li>
</ul>
<p>This can be done as follows:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="javascript"><code>import(&quot;path/to/module.mjs&quot;).then((module) =&gt; {
  console.log(module);
});</code></pre>
<blockquote>
<p>Important note: If you&#39;re using <code>&quot;type&quot;: &quot;module&quot;</code> in your <code>package.json</code>, but still need to include a CommonJS file, you can use the <code>.cjs</code> extension for that specific file.</p>
</blockquote>
<h2 id="using-bundlers-like-webpack-and-rollup">Using Bundlers Like Webpack and Rollup</h2>
<p>When you&#39;re using bundlers like <a href="https://webpack.js.org/" target="_blank" rel="noopener noreferrer">Webpack</a> or <a href="https://rollupjs.org/introduction/" target="_blank" rel="noopener noreferrer">Rollup</a>, you generally don&#39;t need to specify the module type via file extension or <code>package.json</code> for the following reasons:</p>
<ol>
<li><strong>Bundlers Handle Module Resolution</strong>: Tools like Webpack and Rollup have their own mechanisms to resolve and bundle modules. They can handle a mix of module types (CJS, ESM, AMD, etc.) and transpile them into a format of your choice.</li>
<li><strong>Configuration Files</strong>: These bundlers come with configuration files (<code>webpack.config.js</code> for Webpack and <code>rollup.config.js</code> for Rollup) where you define how modules are resolved, loaded, and bundled. You have extensive control over the bundling process, including specifying loaders, plugins, and output formats.</li>
<li><strong>Transpilers</strong>: Often, bundlers are used in tandem with transpilers like Babel. With Babel, you can use the latest ECMAScript features and transpile them down to a version compatible with your target environments. This also allows you to write in ESM and then compile to CJS or another format if needed.</li>
</ol>
<p>Let&#39;s create a simple example where we have an ES module and want to bundle it using Webpack and Rollup to output it in a different format (e.g., CommonJS).</p>
<h3 id="directory-structure">Directory Structure:</h3>
<pre class="astro-code night-owl" tabindex="0" data-language="s"><code>src/
  |- index.js</code></pre>
<p>In <code>index.js</code>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="javascript"><code>export const greet = () =&gt; {
  console.log(&quot;Hello from the module!&quot;);
};</code></pre>
<p>In our example above, the code is written in ESM. We want to bundle it using Webpack + Babel and Rollup + Babel and output it in CommonJS format to demonstrate how bundlers can handle module resolution and transpilation.</p>
<h3 id="example-webpack">Example: Webpack</h3>
<p>For a working Webpack example, we first need to install the necessary dependencies for Webpack and Babel and create a configuration file.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="bash"><code># Install Necessary Dependencies
npm install --save-dev webpack webpack-cli babel-loader @babel/core @babel/preset-env

# Create a `webpack.config.js` in the root
touch webpack.config.js</code></pre>
<p>Inside of <code>webpack.config.js</code>:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="javascript"><code>const path = require(&quot;path&quot;);

module.exports = {
  entry: &quot;./src/index.js&quot;,
  // This tells Webpack to output the bundle in CommonJS format into the `dist` folder as `bundle.js`
  output: {
    filename: &quot;bundle.js&quot;,
    path: path.resolve(__dirname, &quot;dist&quot;),
    libraryTarget: &quot;commonjs2&quot;,
  },
  module: {
    rules: [
      {
        // This targets all `.js` files in the project
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: &quot;babel-loader&quot;,
          options: {
            presets: [&quot;@babel/preset-env&quot;],
          },
        },
      },
    ],
  },
};</code></pre>
<p>Finally, you can run the bundler:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="bash"><code>npx webpack</code></pre>
<p>You&#39;ll get a <code>bundle.js</code> in the <code>dist</code> folder, which is in CommonJS format.</p>
<h3 id="example-rollup">Example: Rollup</h3>
<p>Like the previous Webpack example, we first need to install the necessary dependencies for Rollup and Babel and create a configuration file.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="bash"><code># Install Necessary Dependencies
npm install --save-dev rollup @rollup/plugin-node-resolve @rollup/plugin-babel @babel/core @babel/preset-env
# Create a `rollup.config.js` in the root
touch rollup.config.js</code></pre>
<p>Inside of <code>rollup.config.js</code>, we can put the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="javascript"><code>import babel from &quot;@rollup/plugin-babel&quot;;
import resolve from &quot;@rollup/plugin-node-resolve&quot;;

export default {
  input: &quot;src/index.js&quot;,
  output: {
    file: &quot;dist/bundle.js&quot;,
    format: &quot;cjs&quot;,
  },
  plugins: [
    resolve(),
    babel({ babelHelpers: &quot;bundled&quot;, presets: [&quot;@babel/preset-env&quot;] }),
  ],
};</code></pre>
<p>Finally, you can run the bundler:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="bash"><code>npx rollup -c</code></pre>
<p>Again, you&#39;ll get a <code>bundle.js</code> in the <code>dist</code> folder, which is in CommonJS format.</p>
<p>In both examples, we&#39;re using Babel to transpile our code. The configurations above will take the ES module from <code>src/index.js</code>, transpile it using Babel to make it compatible with older JavaScript versions, and bundle it in CommonJS format in the <code>dist</code> folder.</p>
<p>This example specifically demonstrated valid ESM code in a <code>.js</code> file and outputting it to valid CJS in another <code>.js</code> file, but you can use the same approach to bundle other module types (e.g., AMD, UMD, etc.) and output them in a format of your choice.</p>
<h3 id="a-worthy-mention-about-bundlers">A Worthy Mention About Bundlers</h3>
<p>However, there are a few things to keep in mind:</p>
<ul>
<li>If you&#39;re developing a library or a package that you want to publish to npm, and you want it to be usable both in Node.js and in the browser, you might still consider specifying module types or providing both CJS and ESM builds.</li>
<li>Even if you&#39;re using a bundler, your Node.js specific code (like server-side code or scripts) that isn&#39;t being bundled might still require you to specify module types based on how you intend to use modules.</li>
</ul>
<p>While using a bundler, the need to specify module type via file extension or <code>package.json</code> becomes less critical for the bundled output. However, depending on your project&#39;s structure and target environments, there might still be scenarios where you&#39;d specify module types.</p>
<p>This may be why, if you&#39;ve been using more recent versions of Node, you&#39;ve seen <code>.cjs</code> or <code>.mjs</code> file extensions (particularly for configuration files), you may have been confused about how they fit into the process when there is still a bundler targets other files for transpiling (such as <code>.ts</code>, <code>.tsx</code>, or more confusingly, other <code>.js</code> files).</p>
<h2 id="conclusion">Conclusion</h2>
<p>In this post, we covered the different module systems in JavaScript, how to use them in Node.js, and how bundlers like Webpack and Rollup can handle them.</p>
<p>It&#39;s not a fire blanket to douse the fire (the ecosystem is much bigger than this), but it will cover you up at night and help you sleep a little easier.</p>
<p>As Node.js continues to evolve, it&#39;s always a good idea to consult the official documentation to understand the current behavior and best practices regarding modules.</p>
<p>Finally, while not covered, understanding this will also help you as alternative JavaScript runtimes such as <a href="https://deno.com/" target="_blank" rel="noopener noreferrer">Deno</a> and <a href="https://bun.sh/" target="_blank" rel="noopener noreferrer">Bun</a> implement the solutions around the module system and continue their march forward in this space.</p>
<h2 id="references-and-further-reading">References and Further Reading</h2>
<ol>
<li><a href="https://nodejs.org/api/modules.html" target="_blank" rel="noopener noreferrer">Node.js modules documentation</a></li>
<li><a href="https://webpack.js.org/" target="_blank" rel="noopener noreferrer">Webpack</a></li>
<li><a href="https://rollupjs.org/introduction/" target="_blank" rel="noopener noreferrer">Rollup</a></li>
<li><a href="https://babeljs.io/" target="_blank" rel="noopener noreferrer">Babel</a></li>
<li><a href="https://deno.com/" target="_blank" rel="noopener noreferrer">Deno</a></li>
<li><a href="https://bun.sh/" target="_blank" rel="noopener noreferrer">Bun</a></li>
</ol>
<p><em>Photo credit: <a href="https://unsplash.com/@pawel_czerwinski" target="_blank" rel="noopener noreferrer"><code>pawel_czerwinski</code></a></em></p>]]></content>
<category term="bun"/><category term="nodejs"/><category term="typescript"/><category term="javascript"/>
</entry>
<entry>
<title type="html"><![CDATA[Mastering API Testing with Supertest, Express.js, and Jest]]></title>
<id>https://blog.dennisokeeffe.com/blog/2023-10-27-testing-express-apps-with-jest-and-supertest</id>
<link href="https://dennisokeeffe.com/blog/2023-10-27-testing-express-apps-with-jest-and-supertest"/>
<published>2023-10-26T00:00:00.000Z</published>
<updated>2023-10-26T00:00:00.000Z</updated>
<summary type="html"><![CDATA[A walkthrough on setting up a basic Express.js application and writing tests using Supertest for robust APIs.]]></summary>
<content type="html"><![CDATA[<h2 id="introduction">Introduction</h2>
<p><a href="https://github.com/ladjs/supertest" target="_blank" rel="noopener noreferrer">Supertest</a> is a highly efficient and flexible testing library designed for testing HTTP assertions.</p>
<p>Working hand in hand with frameworks like <a href="https://expressjs.com/" target="_blank" rel="noopener noreferrer">Express.js</a>, Supertest makes it easy to write assertions for your APIs, ensuring they respond as expected. Coupled with Jest, a delightful JavaScript Testing Framework with a focus on simplicity, you can ensure that your APIs are robust and reliable. In this tutorial, we will dive into setting up a simple Express.js application and write tests using Supertest and Jest.</p>
<p>In this blog post, we will set up a basic Express app and write tests using Supertest and <a href="https://jestjs.io/docs/getting-started" target="_blank" rel="noopener noreferrer">Jest</a>.</p>
<p>At first, the tests will be straight forward on validating the response status. Then, we will dive into asserting the response body and its values.</p>
<p>All code from today can be found remotely on <a href="https://github.com/okeeffed/demo-express-jest-supertest" target="_blank" rel="noopener noreferrer">this repo</a>.</p>
<h2 id="setting-up-the-repository">Setting Up The Repository</h2>
<p>Before we dive into writing our application and tests, we need to set up our repository. Here are the steps to do that:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="bash"><code># Create a new directory for your project
mkdir demo-express-jest-supertest
cd demo-express-jest-supertest

# Initialize a new Node.js project
pnpm init

# Install necessary dependencies
pnpm i express
pnpm i -D jest supertest</code></pre>
<p>Once our repository is set up, we can start writing our application and tests.</p>
<h2 id="crafting-the-demo-express-app">Crafting The Demo Express App</h2>
<p>Let&#39;s build a simple Express.js application to demonstrate how we can test it using Supertest and Jest.</p>
<p>Firstly, create a new file <code>app.js</code> at the root of the project directory using <code>touch app.js</code> and add the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>// app.js

// 1. Require express
const express = require(&quot;express&quot;);
// 2. Create the new express app instance for our API
const app = express();

// 3. Define the route for the GET /greet endpoint
app.get(&quot;/greet&quot;, (req, res) =&gt; {
  const name = req.query.name || &quot;World&quot;;
  res.json({ message: `Hello, ${name}!` });
});

// 4. Export the app for testing later
module.exports = app;</code></pre>
<p>In the above code, we add the following:</p>
<ol>
<li>We require the <code>express</code> module.</li>
<li>We create a new instance of the Express app.</li>
<li>We define a route for the <code>GET /greet</code> endpoint that returns a JSON response with a <code>message</code> property.</li>
<li>We export the app instance for testing later.</li>
</ol>
<p>This will set up our baseline Express app, but it does not start the server. If we want to do that, we need to create a new file <code>server.js</code> at the root of the project directory using <code>touch server.js</code> and add the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="js"><code>// 1. Import our app from the app.js file
const app = require(&quot;./app&quot;);

// 2. Start the server on port 3000
app.listen(3000, () =&gt; {
  console.log(&quot;Server is running on port 3000&quot;);
});</code></pre>
<p>At this point, we could run our app using <code>node server.js</code> on the terminal and test it using a tool like <code>curl</code> or Postman.</p>
<p>To confirm, we can run <code>curl http://localhost:3000/greet</code> on the terminal and get the following response:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="bash"><code>curl http://localhost:3000/greet
# {&quot;message&quot;:&quot;Hello, World!&quot;}</code></pre>
<p>Running <code>curl http://localhost:3000/greet?name=John</code> on the terminal will return the following response:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="bash"><code>curl http://localhost:3000/greet?name=John
# {&quot;message&quot;:&quot;Hello, John!&quot;}</code></pre>
<p>At this point, we are ready to write our first tests. You can stop running the server in the first terminal using <code>Ctrl + C</code> and move onto the next section.</p>
<h2 id="writing-tests-with-supertest-and-jest">Writing Tests with Supertest and Jest</h2>
<p>Now that we have our Express app ready, it&#39;s time to write some tests to assert the response and value of the body.</p>
<p>First, create a new file <code>app.test.js</code> at the root of the project directory using <code>touch app.test.js</code> and add the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="javascript"><code>// app.test.js
const request = require(&quot;supertest&quot;);
const app = require(&quot;./app&quot;);

describe(&quot;GET /greet&quot;, () =&gt; {
  it(&quot;should greet the world when no name is provided&quot;, async () =&gt; {
    const res = await request(app)
      .get(&quot;/greet&quot;)
      .expect(&quot;Content-Type&quot;, /json/)
      .expect(200);

    expect(res.body.message).toBe(&quot;Hello, World!&quot;);
  });

  it(&quot;should greet the user when a name is provided&quot;, async () =&gt; {
    const res = await request(app)
      .get(&quot;/greet?name=John&quot;)
      .expect(&quot;Content-Type&quot;, /json/)
      .expect(200);

    expect(res.body.message).toBe(&quot;Hello, John!&quot;);
  });
});</code></pre>
<p>In the code above, we are using Supertest to send HTTP requests to our app and Jest to write and run the test assertions.</p>
<p>The <code>expect</code> method is used to check the response status and headers, while Jest&#39;s <code>expect</code> function is used to assert the value of the response body.</p>
<h2 id="running-the-tests">Running The Tests</h2>
<p>To run the tests, let&#39;s jump into the <code>package.json</code> file that was initialized with <code>pnpm init</code> and update the <code>scripts &gt; test</code> property to the following:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="json"><code>{
  &quot;scripts&quot;: {
    &quot;test&quot;: &quot;jest&quot;
  }
}</code></pre>
<p>You can now run <code>pnpm</code> test to run the tests. You should see the following output:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="s"><code>$ pnpm test

&gt; demo-express-jest-supertest@1.0.0 test /Users/dennisokeeffe/code/projects/demo-express-jest-supertest
&gt; jest

Determining test su
 PASS  ./app.test.js
  GET /greet
    ✓ should greet the world when no name is provided (45 ms)
    ✓ should greet the user when a name is provided (14 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total</code></pre>
<p>Our tests are passing, happy days!</p>
<h2 id="conclusion">Conclusion</h2>
<p>This setup provides a solid foundation to further explore and understand the capabilities of Supertest, Express.js, and Jest in the realm of API testing. As you delve deeper, you&#39;ll discover the power and flexibility this combination offers to ensure the reliability and correctness of your APIs.</p>
<p>We demonstrated how to set up a basic Express.js application and write tests using Supertest and Jest. We started with simple tests to assert the response status and then moved onto asserting the response body and its values.</p>
<p>Supertest can be used for far more things than I demonstrated in today&#39;s post. It is also great for asserting headers, cookies, and more. I encourage you to check out the <a href="https://github.com/ladjs/supertest" target="_blank" rel="noopener noreferrer">Supertest documentation</a>.</p>
<h2 id="references-and-further-reading">References and Further Reading</h2>
<ol>
<li><a href="https://github.com/ladjs/supertest" target="_blank" rel="noopener noreferrer">Supertest documentation</a></li>
<li><a href="https://jestjs.io/docs/getting-started" target="_blank" rel="noopener noreferrer">Jest - Getting Started</a></li>
<li><a href="https://expressjs.com" target="_blank" rel="noopener noreferrer">Express.js</a></li>
<li><a href="https://github.com/okeeffed/demo-express-jest-supertest" target="_blank" rel="noopener noreferrer">Blog code</a></li>
</ol>
<p><em>Photo credit: <a href="https://unsplash.com/@pecarrolo" target="_blank" rel="noopener noreferrer"><code>pecarrolo</code></a></em></p>]]></content>
<category term="nodejs"/><category term="api"/><category term="expressjs"/><category term="javascript"/>
</entry>
<entry>
<title type="html"><![CDATA[Diving Into Typescript's Type System: Type Operators]]></title>
<id>https://blog.dennisokeeffe.com/blog/2023-06-26-diving-into-typescripts-type-system-type-operators</id>
<link href="https://dennisokeeffe.com/blog/2023-06-26-diving-into-typescripts-type-system-type-operators"/>
<published>2023-06-26T00:00:00.000Z</published>
<updated>2023-06-26T00:00:00.000Z</updated>
<summary type="html"><![CDATA[Join me as we dive deep into TypeScript's type operators, including keyof, index types, and index signatures. I'll be guiding you through practical examples that you can follow along with in the TypeScript Playground. Master these powerful tools with me to boost your TypeScript's code reliability and predictability with an advanced type system.]]></summary>
<content type="html"><![CDATA[<h2 id="introduction">Introduction</h2>
<p>TypeScript, a statically typed superset of JavaScript, introduces several advanced type operators that can significantly improve your code&#39;s reliability and predictability. Among these powerful tools, <code>keyof</code>, index types, and index signatures stand out for their versatility and utility. In this post, we will deep-dive into these type operators, demonstrating their usage with practical examples that you can try out in the <a href="https://www.typescriptlang.org/play/" target="_blank" rel="noopener noreferrer">TypeScript Playground</a>.</p>
<span id="typescript-keyof" class="anchor-alias"><h2 id="type-script-keyof">TypeScript keyof</h2>
<p>The <code>keyof</code> operator in TypeScript can be used to create a type representing the property names of another type.</p>
<p>Here&#39;s a simple illustration:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="typescript"><code>type Person = {
  name: string;
  age: number;
};

type PersonKeys = keyof Person; // &quot;name&quot; | &quot;age&quot;</code></pre>
<p>In the above example, <code>PersonKeys</code> is a type that represents the keys of <code>Person</code> - i.e., <code>name</code> and <code>age</code>.</p>
<p>Now, let&#39;s leverage the <code>keyof</code> operator in a function:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="typescript"><code>function getProperty&lt;T, K extends keyof T&gt;(obj: T, key: K): T[K] {
  return obj[key];
}

const person: Person = {
  name: &quot;John&quot;,
  age: 30,
};

console.log(getProperty(person, &quot;name&quot;)); // &quot;John&quot;</code></pre>
<p>In the <code>getProperty</code> function, <code>T</code> is the type of object, and <code>K</code> extends <code>keyof T</code>, meaning it has to be a key of the object type <code>T</code>. The function returns a property of the object of type <code>T[K]</code>.</p>
<span id="typescript-index-types" class="anchor-alias"><h2 id="type-script-index-types">TypeScript Index Types</h2>
<p>Index types are a powerful feature of TypeScript that allow you to create types dynamically based on other types.</p>
<p>Here&#39;s a basic example:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="typescript"><code>type Person = {
  name: string;
  age: number;
};

type ValueOf&lt;T&gt; = T[keyof T];

type PersonValues = ValueOf&lt;Person&gt;; // string | number</code></pre>
<p>In this example, <code>ValueOf&lt;T&gt;</code> represents the type of values that the properties of <code>T</code> can have. In this case, <code>PersonValues</code> is a type representing any value a <code>Person</code> can have - i.e., <code>string</code> or <code>number</code>.</p>
<p>A practical example involving index types might look like this:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="typescript"><code>function getValues&lt;T&gt;(obj: T): ValueOf&lt;T&gt;[] {
  return Object.values(obj);
}

const person: Person = {
  name: &quot;John&quot;,
  age: 30,
};

console.log(getValues(person)); // [&quot;John&quot;, 30]</code></pre>
<p>In the function <code>getValues</code>, it returns an array of values of type <code>ValueOf&lt;T&gt;[]</code>, which represents an array of any values that the properties of <code>T</code> can have.</p>
<span id="typescript-index-signatures" class="anchor-alias"><h2 id="type-script-index-signatures">TypeScript Index Signatures</h2>
<p>TypeScript index signatures allow you to declare types for object properties with unknown names.</p>
<p>Here&#39;s a simple example:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="typescript"><code>interface StringDictionary {
  [index: string]: string;
}

const dict: StringDictionary = {
  hello: &quot;world&quot;,
  ts: &quot;TypeScript&quot;,
};</code></pre>
<p>In the above example, <code>StringDictionary</code> is an interface with an index signature that indicates that when a <code>StringDictionary</code> is indexed with a string, it will return a string.</p>
<p>A more complex, practical use-case might involve manipulating a data structure:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="typescript"><code>interface RecordKeeper&lt;T&gt; {
  [id: string]: T;
}

const record: RecordKeeper&lt;Person&gt; = {};

function addRecord(
  id: string,
  person: Person,
  record: RecordKeeper&lt;Person&gt;
): void {
  record[id] = person;
}

addRecord(&quot;1&quot;, { name: &quot;John&quot;, age: 30 }, record);

console.log(record); // { &quot;1&quot;: { name: &quot;John&quot;, age: 30 } }</code></pre>
<p>In this example, <code>RecordKeeper&lt;T&gt;</code> is an interface with an index signature. It is a generic type, allowing any type to be stored in the record. The <code>addRecord</code> function adds a <code>Person</code> to the record with a particular ID.</p>
<h2 id="summary">Summary</h2>
<p>In TypeScript, <code>keyof</code>, index types, and index signatures are powerful tools that allow you to interact with the structure of types, offering a degree of dynamism and precision to your type definitions. These type operators provide ways to enhance your TypeScript projects, making them more type-safe and robust.</p>
<p>Remember, it&#39;s crucial to understand your tools to make the most out of them. So, take the time to explore these concepts, experiment with them on the TypeScript Playground, and watch your TypeScript proficiency grow.</p>
<h2 id="references">References</h2>
<ul>
<li><a href="https://www.typescriptlang.org/docs/" target="_blank" rel="noopener noreferrer">TypeScript official documentation</a> - A comprehensive guide to TypeScript&#39;s features.</li>
<li><a href="https://www.typescriptlang.org/play/" target="_blank" rel="noopener noreferrer">TypeScript Playground</a> - A handy online sandbox to experiment with TypeScript code.</li>
<li><a href="https://basarat.gitbook.io/typescript/" target="_blank" rel="noopener noreferrer">TypeScript Deep Dive</a> - An in-depth look at TypeScript with extensive practical examples.</li>
</ul>
<p>Always remember: a TypeScript developer who masters their tools, masters their code. Happy coding!</p>
<p>This post is blog number 12 of 20 for my <a href="https://dennisokeeffe.com/blog/series/20-days-of-type-script">series on intermediate-to-advance TypeScript tips</a>.</p>
<p><em>Photo credit: <a href="https://unsplash.com/@pontebernardo" target="_blank" rel="noopener noreferrer"><code>pontebernardo</code></a></em></p>]]></content>
<category term="typescript"/>
</entry>
<entry>
<title type="html"><![CDATA[Typescript And Promises: Conquering Asynchronous Programming]]></title>
<id>https://blog.dennisokeeffe.com/blog/2023-06-25-typescript-and-promises-conquering-asynchronous-programming</id>
<link href="https://dennisokeeffe.com/blog/2023-06-25-typescript-and-promises-conquering-asynchronous-programming"/>
<published>2023-06-25T00:00:00.000Z</published>
<updated>2023-06-25T00:00:00.000Z</updated>
<summary type="html"><![CDATA[See how TypeScript is used with async promises.]]></summary>
<content type="html"><![CDATA[<p>This post will is 11 of 20 for my <a href="https://dennisokeeffe.com/blog/series/20-days-of-type-script">series on intermediate-to-advance TypeScript tips</a>.</p>
<p>All tips can be run on the <a href="https://www.typescriptlang.org/play" target="_blank" rel="noopener noreferrer">TypeScript Playground</a>.</p>
<h2 id="introduction">Introduction</h2>
<p>TypeScript, a statically typed superset of JavaScript, has brought a new level of robustness to web development. One of the areas where TypeScript shines is in handling asynchronous programming, particularly with Promises. This blog post will explore how TypeScript and Promises work together to conquer asynchronous programming, providing you with practical examples that you can try out on the TypeScript Playground.</p>
<h2 id="example-1-basic-promise">Example 1: Basic Promise</h2>
<pre class="astro-code night-owl" tabindex="0" data-language="typescript"><code>let promise = new Promise&lt;string&gt;((resolve, reject) =&gt; {
  setTimeout(() =&gt; {
    resolve(&quot;Promise resolved!&quot;);
  }, 2000);
});

promise.then((value) =&gt; {
  console.log(value);
});</code></pre>
<p>This is a basic example of a Promise in TypeScript. A Promise is an object that represents the eventual completion or failure of an asynchronous operation. In this case, we create a new Promise that resolves with a string after 2 seconds. The <code>then</code> method is used to specify what should happen when the Promise is resolved.</p>
<h2 id="example-2-promise-with-error-handling">Example 2: Promise with Error Handling</h2>
<pre class="astro-code night-owl" tabindex="0" data-language="typescript"><code>let promise = new Promise&lt;string&gt;((resolve, reject) =&gt; {
  setTimeout(() =&gt; {
    reject(&quot;Promise rejected!&quot;);
  }, 2000);
});

promise
  .then((value) =&gt; {
    console.log(value);
  })
  .catch((error: unknown) =&gt; {
    console.error(error);

    // We can type narrow to handle different error types
    if (error instanceof Error) {
      console.log(error.message); // we now know this is of type `Error` with the message property
    }
  });</code></pre>
<p>This example is similar to the previous one, but with one key difference: instead of resolving the Promise, we reject it. This simulates an error occurring during the asynchronous operation. We use the <code>catch</code> method to specify what should happen if the Promise is rejected.</p>
<p>In the example, I an also using type narrowing to handle different error types. This is a powerful feature of TypeScript that allows you to write code that is more robust and easier to maintain. See more on type narrowing in my blog post <a href="https://dennisokeeffe.com/blog/2023-06-16-ensuring-type-safety-with-type-guards">here</a>.</p>
<blockquote>
<p>With errors, it is better to set the type to <code>unknown</code> and use narrowing to handle the different types of errors that can occur.</p>
</blockquote>
<h2 id="example-3-promise-chaining">Example 3: Promise Chaining</h2>
<pre class="astro-code night-owl" tabindex="0" data-language="typescript"><code>let promise1 = new Promise&lt;string&gt;((resolve, reject) =&gt; {
  setTimeout(() =&gt; {
    resolve(&quot;Promise 1 resolved!&quot;);
  }, 2000);
});

let promise2 = promise1.then((value) =&gt; {
  console.log(value);
  return &quot;Promise 2 resolved!&quot;;
});

promise2.then((value) =&gt; {
  console.log(value);
});</code></pre>
<p>This example demonstrates Promise chaining. After the first Promise (<code>promise1</code>) is resolved, its result is passed to the next Promise in the chain (<code>promise2</code>). This allows you to perform multiple asynchronous operations in a specific order.</p>
<span id="example-4-promiseall" class="anchor-alias"><h2 id="example-4-promise-all">Example 4: Promise.all</h2>
<pre class="astro-code night-owl" tabindex="0" data-language="typescript"><code>let promise1 = new Promise&lt;string&gt;((resolve, reject) =&gt; {
  setTimeout(() =&gt; {
    resolve(&quot;Promise 1 resolved!&quot;);
  }, 2000);
});

let promise2 = new Promise&lt;number&gt;((resolve, reject) =&gt; {
  setTimeout(() =&gt; {
    resolve(2);
  }, 3000);
});

Promise.all([promise1, promise2]).then((values) =&gt; {
  // `values` is type [string, number]
  console.log(values);
});</code></pre>
<p>This example demonstrates the use of <code>Promise.all</code>. This method takes an array of Promises and returns a new Promise that resolves when all of the input Promises have resolved. The result is an array of the values that the input Promises were resolved with.</p>
<p>It understands the return type as provided by the generic, so we can use the values as we would expect (in this case it is <code>[string, number]</code>).</p>
<h2 id="summary">Summary</h2>
<p>Promises are a powerful tool for handling asynchronous operations in TypeScript. They allow you to write code that is easier to understand and reason about, especially when dealing with complex asynchronous workflows. Whether you&#39;re fetching data from an API, reading a file from disk, or waiting for a timer to finish, Promises can help you manage these operations in a robust and efficient manner.</p>
<h2 id="references">References</h2>
<ol>
<li><a href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-1-7.html#promises" target="_blank" rel="noopener noreferrer">TypeScript Handbook: Promises</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise" target="_blank" rel="noopener noreferrer">MDN Web Docs: Promise</a></li>
<li><a href="https://www.typescriptlang.org/play" target="_blank" rel="noopener noreferrer">TypeScript Playground</a></li>
</ol>
<p><em>Photo credit: <a href="https://unsplash.com/@adamj_miller" target="_blank" rel="noopener noreferrer"><code>adamj_miller</code></a></em></p>]]></content>
<category term="typescript"/>
</entry>
<entry>
<title type="html"><![CDATA[Typescript And Functional Programming]]></title>
<id>https://blog.dennisokeeffe.com/blog/2023-06-24-typescript-and-functional-programming</id>
<link href="https://dennisokeeffe.com/blog/2023-06-24-typescript-and-functional-programming"/>
<published>2023-06-24T00:00:00.000Z</published>
<updated>2023-06-24T00:00:00.000Z</updated>
<summary type="html"><![CDATA[Implement your own map function with TypeScript.]]></summary>
<content type="html"><![CDATA[<p>This post will is 10 of 20 for my <a href="https://dennisokeeffe.com/blog/series/20-days-of-type-script">series on intermediate-to-advance TypeScript tips</a>.</p>
<p>All tips can be run on the <a href="https://www.typescriptlang.org/play" target="_blank" rel="noopener noreferrer">TypeScript Playground</a>.</p>
<h2 id="introduction">Introduction</h2>
<p>Functional programming is a programming paradigm that treats computation as the evaluation of mathematical functions and avoids changing state and mutable data. In the world of JavaScript and TypeScript, functional programming concepts have become increasingly popular due to their ability to make code more readable, maintainable, and testable.</p>
<p>One of the key concepts in functional programming is the use of higher-order functions. A higher-order function is a function that takes one or more functions as arguments, returns a function, or both. This post will explore how to leverage higher-order functions in TypeScript.</p>
<h2 id="practical-example-using-map-function">Practical Example: Using <code>map</code> Function</h2>
<p>Let&#39;s start with a simple example of a higher-order function that you&#39;ve likely used before: the <code>map</code> function. Here&#39;s how you might use it in TypeScript:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="typescript"><code>let numbers = [1, 2, 3, 4, 5];

let squares = numbers.map((num) =&gt; num * num);

console.log(squares); // Output: [1, 4, 9, 16, 25]</code></pre>
<p>In this example, <code>map</code> is a higher-order function that takes a function as an argument. This function is applied to each element in the <code>numbers</code> array, and a new array is returned.</p>
<h2 id="expanding-the-example-creating-a-custom-map-function">Expanding the Example: Creating a Custom <code>map</code> Function</h2>
<p>Now, let&#39;s create our own version of the <code>map</code> function. This will help us understand how higher-order functions work under the hood.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="typescript"><code>function map&lt;T, U&gt;(array: T[], transform: (value: T) =&gt; U): U[] {
  let result: U[] = [];
  for (let value of array) {
    result.push(transform(value));
  }
  return result;
}

let numbers = [1, 2, 3, 4, 5];

let squares = map(numbers, (num) =&gt; num * num);

console.log(squares); // Output: [1, 4, 9, 16, 25]</code></pre>
<p>In this example, we&#39;ve defined a <code>map</code> function that takes an array and a transform function as arguments. The transform function is applied to each element in the array, and the results are collected into a new array.</p>
<h2 id="complex-example-using-reduce-to-implement-map">Complex Example: Using <code>reduce</code> to Implement <code>map</code></h2>
<p>Now, let&#39;s take things a step further by implementing our <code>map</code> function using another higher-order function: <code>reduce</code>.</p>
<pre class="astro-code night-owl" tabindex="0" data-language="typescript"><code>function map&lt;T, U&gt;(array: T[], transform: (value: T) =&gt; U): U[] {
  return array.reduce((result, value) =&gt; {
    result.push(transform(value));
    return result;
  }, [] as U[]);
}

let numbers = [1, 2, 3, 4, 5];

let squares = map(numbers, (num) =&gt; num * num);

console.log(squares); // Output: [1, 4, 9, 16, 25]</code></pre>
<p>In this example, <code>reduce</code> is a higher-order function that takes a function (the reducer) and an initial value as arguments. The reducer function is called for each element in the array, with the result of the previous call passed as the first argument and the current element passed as the second argument.</p>
<h2 id="summary">Summary</h2>
<p>Higher-order functions are a powerful tool in functional programming, and TypeScript&#39;s static typing features make them even more powerful. By using higher-order functions, we can write code that is more reusable, testable, and readable.</p>
<p>In this post, we&#39;ve explored how to use and implement higher-order functions in TypeScript. We started with a simple example using the <code>map</code> function, then created our own version of <code>map</code>, and finally implemented `</p>
<p><code>map</code> using <code>reduce</code>. Each step of the way, we leveraged the power of higher-order functions to make our code more declarative and less imperative.</p>
<h2 id="references">References</h2>
<ol>
<li><a href="https://www.typescriptlang.org/docs/" target="_blank" rel="noopener noreferrer">TypeScript Documentation</a></li>
<li><a href="https://maryrosecook.com/blog/post/a-practical-introduction-to-functional-programming" target="_blank" rel="noopener noreferrer">Functional Programming in JavaScript</a></li>
<li><a href="https://www.typescriptlang.org/play" target="_blank" rel="noopener noreferrer">TypeScript Playground</a></li>
</ol>
<p><em>Photo credit: <a href="https://unsplash.com/@fonsheijnsbroek_amsterdam_photos" target="_blank" rel="noopener noreferrer"><code>fonsheijnsbroek_amsterdam_photos</code></a></em></p>]]></content>
<category term="typescript"/>
</entry>
<entry>
<title type="html"><![CDATA[Advanced Union And Intersection Types In Typescript]]></title>
<id>https://blog.dennisokeeffe.com/blog/2023-06-23-advanced-union-and-intersection-types-in-typescript</id>
<link href="https://dennisokeeffe.com/blog/2023-06-23-advanced-union-and-intersection-types-in-typescript"/>
<published>2023-06-23T00:00:00.000Z</published>
<updated>2023-06-23T00:00:00.000Z</updated>
<summary type="html"><![CDATA[A practical explanation of how union and intersection types work.]]></summary>
<content type="html"><![CDATA[<p>This post will is 9 of 20 for my <a href="https://dennisokeeffe.com/blog/series/20-days-of-type-script">series on intermediate-to-advance TypeScript tips</a>.</p>
<h2 id="introduction">Introduction</h2>
<p>In TypeScript, one of the powerful features that allow us to create complex types are Union and Intersection types. These types provide us with the ability to create new types from existing ones, leading to more flexible and reusable code. This post will delve into the practical use of these advanced types, with examples that you can try out in the <a href="https://www.typescriptlang.org/play" target="_blank" rel="noopener noreferrer">TypeScript Playground</a>. For a more detailed explanation, you can refer to the TypeScript Handbook&#39;s section on <a href="https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html" target="_blank" rel="noopener noreferrer">Unions and Intersection Types</a>.</p>
<h2 id="union-types">Union Types</h2>
<p>Union types are a way of declaring a type that could be one of several types. We use the vertical bar (|) to separate each type.</p>
<p>Here&#39;s an example of a function that uses a union type:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="typescript"><code>function padLeft(value: string, padding: string | number) {
  // ...
}</code></pre>
<p>In this function, the <code>padding</code> parameter can be either a string or a number. This is a simple yet powerful way to handle multiple types in a single function. You can try this out in the TypeScript Playground.</p>
<h2 id="intersection-types">Intersection Types</h2>
<p>Intersection types are another powerful feature in TypeScript. They allow you to combine multiple types into one, resulting in a type that has all the properties of the combined types.</p>
<p>Here&#39;s an example of intersection types:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="typescript"><code>interface ErrorHandling {
  success: boolean;
  error?: { message: string };
}

interface ArtworksData {
  artworks: { title: string }[];
}

type ArtworksResponse = ArtworksData &amp; ErrorHandling;</code></pre>
<p>Here, <code>ArtworksResponse</code> is an intersection type that combines <code>ArtworksData</code> and <code>ErrorHandling</code>. An object of type <code>ArtworksResponse</code> will have all the properties of <code>ArtworksData</code> and <code>ErrorHandling</code>.</p>
<p>Now, let&#39;s create a function that takes an <code>ArtworksResponse</code> object and processes it:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="typescript"><code>function handleArtworksResponse(response: ArtworksResponse) {
  if (!response.success) {
    console.error(response.error?.message);
    return;
  }

  console.log(response.artworks);
}</code></pre>
<p>In the <code>handleArtworksResponse</code> function, we first check if the <code>response</code> was successful. If not, we log the error message and return early. If the <code>response</code> was successful, we log the artworks.</p>
<p>Here&#39;s an example of how you might call this function:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="typescript"><code>const response: ArtworksResponse = {
  success: true,
  artworks: [{ title: &quot;Mona Lisa&quot; }, { title: &quot;The Starry Night&quot; }],
};

handleArtworksResponse(response);</code></pre>
<p>In this example, <code>response</code> is an object that satisfies the <code>ArtworksResponse</code> type, so we can pass it to <code>handleArtworksResponse</code> without any issues. If you run this code in the TypeScript Playground, you should see the array of artworks logged to the console.</p>
<p>This example demonstrates how intersection types can be used to create complex types that ensure our functions are used correctly.</p>
<h2 id="representing-network-state-example">Representing Network State example</h2>
<p>Let&#39;s dive into a more complex example using both union and intersection types.</p>
<p>Consider a network request scenario where we have different states for our request - it could be in a loading state, a success state, or a failed state. Each of these states has different properties associated with them. Here&#39;s how we might model this:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="typescript"><code>type NetworkLoadingState = {
  state: &quot;loading&quot;;
};

type NetworkFailedState = {
  state: &quot;failed&quot;;
  code: number;
};

type NetworkSuccessState = {
  state: &quot;success&quot;;
  response: {
    title: string;
    duration: number;
    summary: string;
  };
};

type NetworkState =
  | NetworkLoadingState
  | NetworkFailedState
  | NetworkSuccessState;</code></pre>
<p>In this example, <code>NetworkState</code> is a union of <code>NetworkLoadingState</code>, <code>NetworkFailedState</code>, and <code>NetworkSuccessState</code>. Each of these types has a <code>state</code> property that can be used to determine the current state of the network request.</p>
<p>Now, let&#39;s create a function that takes a <code>NetworkState</code> object and processes it:</p>
<pre class="astro-code night-owl" tabindex="0" data-language="typescript"><code>function networkStatus(state: NetworkState): string {
  switch (state.state) {
    case &quot;loading&quot;:
      return &quot;Downloading...&quot;;
    case &quot;failed&quot;:
      return `Error ${state.code} downloading`;
    case &quot;success&quot;:
      return `Downloaded ${state.response.title} - ${state.response.summary}`;
  }
}</code></pre>
<p>In the <code>networkStatus</code> function, we use a switch statement to handle each possible state. The TypeScript compiler can infer the type of <code>state</code> in each case clause. This is known as &quot;discriminant union types&quot;, a pattern where TypeScript can narrow down the type based on a certain property (in this case, <code>state</code>).</p>
<p>This example demonstrates how union and intersection types can be used together to create complex and robust type definitions. You can try this out in the TypeScript Playground.</p>
<h2 id="summary">Summary</h2>
<p>Union and Intersection types are powerful tools in TypeScript that allow us to create complex types from existing ones. Union types let us declare a type that could be one of several types, while Intersection types allow us to combine multiple types into one. These features make our code more flexible and reusable, and they are essential for writing complex TypeScript applications.</p>
<h2 id="references">References</h2>
<ol>
<li><a href="https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html" target="_blank" rel="noopener noreferrer">TypeScript Handbook: Unions and Intersection Types</a></li>
</ol>
<p><em>Photo credit: <a href="https://unsplash.com/@fynngeerdsen" target="_blank" rel="noopener noreferrer"><code>fynngeerdsen</code></a></em></p>]]></content>
<category term="typescript"/>
</entry>
</feed>