<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Kamranicus]]></title><description><![CDATA[Indie web dev, educator, speaker, and Chief Gamer at Keep Track of My Games]]></description><link>https://kamranicus.com/</link><image><url>https://kamranicus.com/favicon.png</url><title>Kamranicus</title><link>https://kamranicus.com/</link></image><generator>Ghost 6.26</generator><lastBuildDate>Wed, 08 Apr 2026 10:30:25 GMT</lastBuildDate><atom:link href="https://kamranicus.com/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Building a Raspberry Pi 3/4/5 Baby Monitor]]></title><description><![CDATA[Use a Raspberry Pi and a couple cheap components to create an HD livestream that you can view on any device within your home.]]></description><link>https://kamranicus.com/building-a-raspberry-pi-3-baby-monitor/</link><guid isPermaLink="false">60593d5ba8d4cc003baa8653</guid><category><![CDATA[Raspberry Pi]]></category><dc:creator><![CDATA[Kamran Ayub]]></dc:creator><pubDate>Fri, 22 Nov 2024 15:00:00 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1552283576-3ea3519bf12e?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDI0fHxyYXNwYmVycnklMjBwaXxlbnwwfHx8fDE2MTY0NjE1MjM&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<div class="kg-card kg-button-card kg-align-left"><a href="#fastcomments-widget" class="kg-btn kg-btn-accent">Jump to Comments Section</a></div><img src="https://images.unsplash.com/photo-1552283576-3ea3519bf12e?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDI0fHxyYXNwYmVycnklMjBwaXxlbnwwfHx8fDE2MTY0NjE1MjM&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=2000" alt="Building a Raspberry Pi 3/4/5 Baby Monitor"><p>After looking at reviews for baby monitors, a few things were clear:</p><ol><li>Manufacturers aren&apos;t putting good cameras in these monitors</li><li>Some have night vision, some don&apos;t and the quality usually sucks.</li><li>If the monitor can be viewed remotely, it sometimes runs insecure RF wireless and <a href="https://arstechnica.com/security/2015/09/9-baby-monitors-wide-open-to-hacks-that-expose-users-most-private-moments/?ref=kamranicus.com">can be snooped on or intercepted</a></li><li>Good monitors aren&apos;t actually baby monitors, they&apos;re just nice cameras (Nest Cam) and cost $300+</li></ol><p>I knew there had to be a better, more affordable way to keep tabs on our little guy. A <a href="https://raspberrypi.org/?ref=kamranicus.com">Raspberry Pi</a> was a perfect solution--it has a camera module with 1080p resolution, it has USB ports for extra peripherals, it&apos;s portable, and it&apos;s (relatively) cheap &#x2013; although in 2024, even a Raspberry Pi 5 kit costs $200. The good news is, this guide works down to a Raspberry Pi 3 or even a Pi Zero!</p><p>Check out the resulting image quality:</p><h3 id="720p-low-light">720p, low light</h3><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2021/03/image-15.png" class="kg-image" alt="Building a Raspberry Pi 3/4/5 Baby Monitor" loading="lazy" width="1288" height="726" srcset="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w600/2021/03/image-15.png 600w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w1000/2021/03/image-15.png 1000w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2021/03/image-15.png 1288w" sizes="(min-width: 1200px) 1200px"><figcaption><span style="white-space: pre-wrap;">Image quality, low light</span></figcaption></figure><h3 id="720p-high-light">720p, high light</h3><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2021/03/image-16.png" class="kg-image" alt="Building a Raspberry Pi 3/4/5 Baby Monitor" loading="lazy" width="1286" height="724" srcset="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w600/2021/03/image-16.png 600w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w1000/2021/03/image-16.png 1000w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2021/03/image-16.png 1286w" sizes="(min-width: 1200px) 1200px"><figcaption><span style="white-space: pre-wrap;">Image quality, daylight</span></figcaption></figure><p>It&apos;s possible to reach 1080p with the Pi camera, it&apos;s up to you--I found 720p to be perfectly acceptable but 1080p caused the image to &quot;zoom in&quot; a bit too much. If I figure that out, I&apos;ll update the guide accordingly.</p><div class="kg-card kg-header-card kg-v2 kg-width-full kg-content-wide " style="background-color: #000000;" data-background-color="#000000">
            
            <div class="kg-header-card-content">
                
                <div class="kg-header-card-text kg-align-center">
                    <h2 id="who-this-guide-is-for" class="kg-header-card-heading" style="color: #FFFFFF;" data-text-color="#FFFFFF"><span style="white-space: pre-wrap;">Who This Guide is For</span></h2>
                    <p id="this-guide-is-for-raspberry-pi-enthusiasts-makers-and-diyers-who-prefer-a-cheaper-privacycentric-camera-under-their-control-that-can-be-more-secure-than-an-iot-camera-ive-tried-not-to-assume-you-know-anything-about-linux" class="kg-header-card-subheading" style="color: #FFFFFF;" data-text-color="#FFFFFF"><span style="white-space: pre-wrap;">This guide is for Raspberry Pi enthusiasts, makers, and DIYers who prefer a cheaper, privacy-centric camera under their control that </span><i><em class="italic" style="white-space: pre-wrap;">can be</em></i><span style="white-space: pre-wrap;"> more secure than an IoT camera. I&apos;ve tried not to assume you know anything about Linux.</span></p>
                    
                </div>
            </div>
        </div><p>I am a developer by trade, but I will try to keep this guide as simple as possible. You are presumably ready to get your hands (a little) dirty if you have a Pi, but if you&apos;re like me, Linux is not your forte.</p><p>The other nice thing about using a Raspberry Pi is that it&apos;s easy to use! There are many guides and tutorials available for the device.</p><p>If you get stuck, the comments at the bottom provide a forum to help each other &#x1F91D;</p><h2 id="what-this-guide-covers"><strong>What This Guide Covers</strong></h2><p>This guide will show you how to build your own DIY baby monitor using a Raspberry Pi 3/4/5. </p><p>The image and sound will be way better than any low- to mid-level monitors (and cheaper!). Additionally, this will give you a lot of flexibility to add other functionality such as a speaker, etc. </p><p>Since the Raspberry Pi is under your control, you can choose to expose it only within your home network or securely over the Internet--no weird Internet of Things or cloud &quot;phone home&quot; nonsense or possible vulnerabilities that allow people to view your baby. </p><p>It will be as secure as your home network, and if you choose to expose it publicly, you can use encryption and strong passwords to secure it (this guide does not cover that).</p><div class="kg-card kg-header-card kg-v2 kg-width-full kg-content-wide " style="background-color: #000000;" data-background-color="#000000">
            
            <div class="kg-header-card-content">
                
                <div class="kg-header-card-text kg-align-center">
                    <h2 id="in-this-guide" class="kg-header-card-heading" style="color: #FFFFFF;" data-text-color="#FFFFFF"><b><strong style="white-space: pre-wrap;">In This Guide</strong></b></h2>
                    
                    <a href="#fastcomments-widget" class="kg-header-card-button " style="background-color: #ffffff;color: #000000;" data-button-color="#ffffff" data-button-text-color="#000000">Jump to Comments Section</a>
                </div>
            </div>
        </div><p><strong>Before You Begin</strong></p><ul><li><a href="https://kamranicus.com/guides/raspberry-pi-3-baby-monitor#updates">Updates</a></li><li><a href="https://kamranicus.com/guides/raspberry-pi-3-baby-monitor#the-hardware">The Hardware</a></li><li><a href="https://kamranicus.com/guides/raspberry-pi-3-baby-monitor#nightvision-support">Nightvision Support</a></li><li><a href="https://kamranicus.com/guides/raspberry-pi-3-baby-monitor#programmable-led-light">Programmable LED Light</a></li></ul><p><strong>How To</strong></p><ul><li><a href="https://kamranicus.com/guides/raspberry-pi-3-baby-monitor#video-walkthrough">YouTube Video Walkthrough</a></li><li><a href="https://kamranicus.com/guides/raspberry-pi-3-baby-monitor#installing-the-pi-camera">Installing the Pi Camera</a></li><li><a href="https://kamranicus.com/guides/raspberry-pi-3-baby-monitor#configuring-the-pi">Configuring the Pi</a></li><li><a href="https://kamranicus.com/guides/raspberry-pi-3-baby-monitor#configuring-raspbian">Configuring Raspbian</a></li><li><a href="https://kamranicus.com/guides/raspberry-pi-3-baby-monitor#installing-picam">Installing Picam</a></li><li><a href="https://kamranicus.com/guides/raspberry-pi-3-baby-monitor#running-picam-at-startup">Running Picam at Startup</a></li><li><a href="https://kamranicus.com/guides/raspberry-pi-3-baby-monitor#installing-nginx">Installing nginx</a></li><li><a href="https://kamranicus.com/guides/raspberry-pi-3-baby-monitor#mounting-the-monitor">Mounting the Monitor</a></li><li><a href="https://kamranicus.com/guides/raspberry-pi-3-baby-monitor#viewing-on-a-device">Viewing on a Device</a></li><li><a href="https://kamranicus.com/guides/raspberry-pi-3-baby-monitor#optionally-adding-a-web-player">(Optionally) Adding a Web Player</a></li><li><a href="https://kamranicus.com/guides/raspberry-pi-3-baby-monitor#optionally-exposing-over-the-internet">(Optionally) Exposing over the Internet</a></li></ul><div class="kg-card kg-header-card kg-v2 kg-width-full kg-content-wide " style="background-color: #000000;" data-background-color="#000000">
            
            <div class="kg-header-card-content">
                
                <div class="kg-header-card-text kg-align-center">
                    <h2 id="before-you-begin" class="kg-header-card-heading" style="color: #FFFFFF;" data-text-color="#FFFFFF"><span style="white-space: pre-wrap;">Before You Begin</span></h2>
                    
                    <a href="#how-to" class="kg-header-card-button " style="background-color: #ffffff;color: #000000;" data-button-color="#ffffff" data-button-text-color="#000000">Jump to How To</a>
                </div>
            </div>
        </div><h2 id="updates"><strong>Updates</strong></h2><h3 id="2024-11-22-%E2%80%93-raspberry-pi-os-and-picam-2-compatibility"><strong>2024-11-22 &#x2013; Raspberry Pi OS and Picam 2 compatibility</strong></h3><p>I recently wiped my Raspberry Pi 3 and am setting it up as a self-contained video recording and editing system for my son who is now 7 years old (yes, the one in the picture)!</p><p>I was able to follow my own guide again to get everything set up again, but several commands no longer worked and needed tweaks. I also added some more screenshots and clarified a few steps.</p><p><em>Using the Built-in rpicamvid</em></p><p>I show you how to use and configure <a href="https://github.com/iizukanao/picam?ref=kamranicus.com" rel="noreferrer">Picam</a> for video streaming, but a commenter said they found this guide on using built-in <code>rpicamvid</code> a bit easier! That is another option for you.</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://peppe8o.com/remote-video-streaming-from-raspberry-pi-camera/?ref=kamranicus.com"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Remote video streaming from Raspberry PI camera</div><div class="kg-bookmark-description">Using Raspivid and NetCat in headless Raspberry PI (OS Lite) for remote video streaming and get video from a local network PC with VLC</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/icon/cropped-Logo2D_512x512_2-180x180.png" alt="Building a Raspberry Pi 3/4/5 Baby Monitor"><span class="kg-bookmark-author">peppe8o
Raspberry PI, Arduino and Electronics made simple</span><span class="kg-bookmark-publisher">peppe8o</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/thumbnail/foto-Giuseppe-Cassibba.jpg" alt="Building a Raspberry Pi 3/4/5 Baby Monitor" onerror="this.style.display = &apos;none&apos;"></div></a></figure><p><em>Price Increases on Canakit</em></p><p>I updated the Amazon links to the latest Canakit starter kits and was surprised that the price had doubled since 2019. You may want to shop around, but I think Canakit is worth it.</p><p>With cams as low as $50 now, this is definitely more of a <em>hobby project</em> than it was back then&#x2014;you&apos;ll probably spend more money and way more time on it, so if all you&apos;re looking for is a good baby cam, maybe a <a href="https://www.babylist.com/gp/nanit-nanit-x-babylist-ultimate-bundle/22379/1511430?ref=kamranicus.com" rel="noreferrer">$280 Nanit</a> makes sense now.</p><p>The rest of the hardware has mostly fallen in price.</p><h3 id="2020-08-25"><strong>2020-08-25</strong></h3><ul><li>Command typo fixes by <a href="https://github.com/cowley05?ref=kamranicus.com">cowley05</a> (<a href="https://github.com/kamranayub/kamranayub.github.io/pull/27?ref=kamranicus.com">#27</a>)</li></ul><h3 id="2019-11-13"><strong>2019-11-13</strong></h3><ul><li>Add video walkthrough pitch</li><li>The Kinobo mini USB mic is discontinued so I replaced it with a well-reviewed <a href="https://amzn.to/2QnHJPb?ref=kamranicus.com">Samson GO portable mic</a></li><li>Add clarification for installing the right Binary distribution</li><li>Add clarification for <code>picam-viewer</code> installation</li></ul><h3 id="2019-06-08"><strong>2019-06-08</strong></h3><ul><li>Updated Binary section to reflect instructions for Raspbian Buster</li></ul><h3 id="2018-01-11"><strong>2018-01-11</strong></h3><ul><li>Fixed picam extraction command typo because the archive contains the original filename as the top-level folder</li></ul><h3 id="2017-10-25"><strong>2017-10-25</strong></h3><ul><li>Thanks to <a href="https://kamranicus.com/guides/raspberry-pi-3-baby-monitor#comment-3574356701">Andrew</a> for pointing out some differences between different Raspbian versions! I updated the Picam installation step.</li><li>I updated my Picam arguments to be what I&apos;m using now after some testing. The args I&apos;m using seem pretty stable now.</li></ul><h3 id="2017-10-02"><strong>2017-10-02</strong></h3><ul><li>I missed the fact that the <code>/run/shm</code> directories are blown away on Pi restarts. The startup script now ensures they exist (should fix slow feeds).</li><li>I updated my picam-viewer web player to use Video.js now which should support more devices.</li><li>The comments have reported that the Pi Zero works well for this!</li></ul><h2 id="the-hardware"><strong>The Hardware</strong></h2><p>Here&apos;s what you&apos;ll need (I&apos;ve marked &#x1F476; with what the guide is using):</p><ul><li><a href="https://amzn.to/3Z3CQfO?ref=kamranicus.com" rel="noreferrer">Raspberry Pi 5 Canakit</a> - $170<ul><li><strong>OR</strong> <a href="https://amzn.to/4eHO6Ef?ref=kamranicus.com" rel="noreferrer">Raspberry Pi 4 Canakit</a> - $150</li><li>&#x1F476; <strong>OR</strong> <a href="https://amzn.to/3CHnWEv?ref=kamranicus.com" rel="noreferrer">Raspberry Pi 3 Canakit</a> - $100</li><li><strong>&#x1F476; OR</strong> <a href="https://www.amazon.com/gp/product/B071L2ZQZX/ref=as_li_tl?ie=UTF8&amp;camp=1789&amp;creative=9325&amp;creativeASIN=B071L2ZQZX&amp;linkCode=as2&amp;tag=kamranicus-20&amp;linkId=11a90708c6058c931d343f5a34243809&amp;ref=kamranicus.com">Raspberry Pi Zero W Canakit</a> - $25</li></ul></li><li>&#x1F476; <a href="https://www.amazon.com/gp/product/B01ER2SKFS/ref=as_li_tl?ie=UTF8&amp;tag=kamranicus-20&amp;camp=1789&amp;creative=9325&amp;linkCode=as2&amp;creativeASIN=B01ER2SKFS&amp;linkId=c4b2ac954d25538e792e193ff088faac&amp;ref=kamranicus.com">Raspberry Pi Camera Module v2</a> - $13</li><li>&#x1F476; <a href="https://www.amazon.com/gp/product/B010Q57T02/ref=as_li_tl?ie=UTF8&amp;tag=kamranicus-20&amp;camp=1789&amp;creative=9325&amp;linkCode=as2&amp;creativeASIN=B010Q57T02&amp;linkId=6ad18aebc7e3dd06daf87ef2f40910a4&amp;ref=kamranicus.com">MicroSD card</a> (8GB or higher) - $8<ul><li><strong>NOTE:</strong> Links to each Canakit above include SD card!</li></ul></li><li><a href="https://amzn.to/2QnHJPb?ref=kamranicus.com">Samson GO USB omni/condenser microphone</a> - $30<ul><li>Optional if you only want video and no audio captured</li></ul></li><li>&#x1F476; <a href="https://amzn.to/40UqMju?ref=kamranicus.com" rel="noreferrer">USB reading Lamp</a> (optional)<ul><li>You just want enough light for the camera</li></ul></li><li>&#x1F476; <a href="https://amzn.to/3Z4MezA?ref=kamranicus.com" rel="noreferrer">Gooseneck phone mount</a> (optional)<ul><li>Makes pointing at the right location easier</li></ul></li><li>HDMI cable (temporary)</li><li>USB keyboard/mouse (temporary)</li><li>Laptop to configure the Pi</li></ul><p>You can snag all the hardware needed for ~$200 off Amazon. I went with a 32GB SD card but that&apos;s just me, you could configure this to store all videos/photos on a network device or cloud account.</p><p>The gooseneck LED light is optional--I want to be able to have the monitor have a night-light attached vs. having to have a separate ambient light at night.</p><p>For mounting the Pi, it&apos;s up to you. I need to mount it to the bassinet/crib and a clamp-based gooseneck mount is perfect for me (at least until the little guy starts moving around and grabbing things!). You may want to wall-mount it, use cardboard, etc. Use whatever works for you!</p><h2 id="nightvision-support"><strong>Nightvision Support</strong></h2><p>Originally I had planned to use the Pi NoIR camera module because it can see infrared light. However, since infrared light is still light (just not visible) I didn&apos;t really want to take a chance of accidentally &quot;shining&quot; it straight onto my son&apos;s face. <a href="http://www.intersil.com/content/dam/Intersil/documents/an17/an1737.pdf?ref=kamranicus.com">There is no real evidence</a> to suggest that infrared baby monitors are dangerous to infants--but concentrated IR is still not good to have directed at you. The fact is that nightvision baby monitors probably use a very low light level and it&apos;s <em>probably</em> not dangerous at all.</p><p>If you want to add nightvision to this monitor, simply replace the normal camera module above with the <a href="https://www.amazon.com/gp/product/B01ER2SMHY/ref=as_li_tl?ie=UTF8&amp;tag=kamranicus-20&amp;camp=1789&amp;creative=9325&amp;linkCode=as2&amp;creativeASIN=B01ER2SMHY&amp;linkId=0f96de503803ea64bfb9b0f854cc9fe0&amp;ref=kamranicus.com">Pi NoIR Camera module</a>. You will then need to find a suitable infrared light to use at night.</p><p>I have opted to use a simple flexible USB light that I can shine specifically where it makes sense and won&apos;t be in the baby&apos;s line of sight. It has an on/off switch which comes in handy for my wife at night to simply lean over and switch on too.</p><h2 id="programmable-led-light"><strong>Programmable LED Light</strong></h2><p>While I opted for a simple on/off nightlight, you could potentially go crazy with a <a href="https://www.blinkstick.com/products/blinkstick-nano?ref=kamranicus.com#mini-shop">BlinkStick Nano USB light</a> that is programmable, so you could have it automatically turn on in low-light conditions, program light shows, or what have you.</p><div class="kg-card kg-header-card kg-v2 kg-width-full kg-content-wide " style="background-color: #000000;" data-background-color="#000000">
            
            <div class="kg-header-card-content">
                
                <div class="kg-header-card-text kg-align-center">
                    <h2 id="how-to" class="kg-header-card-heading" style="color: #FFFFFF;" data-text-color="#FFFFFF"><span style="white-space: pre-wrap;">How To</span></h2>
                    
                    
                </div>
            </div>
        </div><h2 id="installing-the-pi-camera"><strong>Installing the Pi Camera</strong></h2><p>To install the camera on the Pi, firmly insert the end of the cable with the silver pins facing <em>toward</em> the HDMI port.</p><p>I picked the Canakit because the case allows you to attach the camera on the inside, allowing an integrated camera.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2021/03/image-17.png" class="kg-image" alt="Building a Raspberry Pi 3/4/5 Baby Monitor" loading="lazy" width="2000" height="1125" srcset="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w600/2021/03/image-17.png 600w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w1000/2021/03/image-17.png 1000w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w1600/2021/03/image-17.png 1600w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w2400/2021/03/image-17.png 2400w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Canakit camera case</span></figcaption></figure><h2 id="configuring-the-pi"><strong>Configuring the Pi</strong></h2><p>First things first, we need to configure our Raspberry Pi. The <a href="https://www.raspberrypi.com/documentation/computers/getting-started.html?ref=kamranicus.com#install-an-operating-system" rel="noreferrer">software installation guide</a> on the Raspberry Pi documentation site is perfect and easy to understand.</p><p>I used the Raspberry Pi OS imager:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2024/11/image-21.png" class="kg-image" alt="Building a Raspberry Pi 3/4/5 Baby Monitor" loading="lazy" width="1788" height="1352" srcset="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w600/2024/11/image-21.png 600w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w1000/2024/11/image-21.png 1000w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w1600/2024/11/image-21.png 1600w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2024/11/image-21.png 1788w" sizes="(min-width: 720px) 720px"></figure><h3 id="raspberry-pi-imager-settings">Raspberry Pi Imager Settings</h3><p>You will see a screen like this:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2024/11/image-22.png" class="kg-image" alt="Building a Raspberry Pi 3/4/5 Baby Monitor" loading="lazy" width="1854" height="1330" srcset="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w600/2024/11/image-22.png 600w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w1000/2024/11/image-22.png 1000w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w1600/2024/11/image-22.png 1600w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2024/11/image-22.png 1854w" sizes="(min-width: 720px) 720px"></figure><p>Click &quot;Edit Settings&quot; so you can configure some stuff upfront:</p><ul><li><strong>Wi-Fi:</strong> You must configure this to connect to your Wi-Fi. You can do it once you boot into the OS, but you might as well do it now.</li><li><strong>Hostname:</strong> I used <code>babypi</code> but the default is <code>raspberrypi</code></li><li><strong>Username/password:</strong> Good to set for security.</li><li><strong>Timezone:</strong> When you boot up the OS, it should be set automatically, but you can also set it upfront.</li></ul><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">&#x1F468;&#x200D;&#x1F4BB;</div><div class="kg-callout-text"><b><strong style="white-space: pre-wrap;">Advanced Users</strong></b>: You can also set up SSH here. This will allow you to use the Pi without the user interface enabled; you just need to know the IP address or hostname. You can disable the GUI after booting up the OS and going into <b><strong style="white-space: pre-wrap;">Menu -&gt; Preferences -&gt; Raspberry Pi OS Configuration</strong></b>.</div></div><p>Follow all the steps until your SD card has the Raspberry Pi OS.</p><p>Connect the Pi via HDMI to a monitor or TV. Connect a mouse/keyboard to the Pi for configuring. Connect an Ethernet cable unless you want to use Wi-Fi during the guide.</p><p>Boot up your Pi (i.e. plug it in) and it should boot into the OS.</p><h2 id="updating-raspberry-pi-os"><strong>Updating Raspberry Pi OS</strong></h2><p>I&apos;m sorry I have to delve into the command-line but because Raspberry Pi is a Linux device, most of the power user features are only available through there. Hey, presumably you&apos;re reading this because you want a power-user baby monitor! We have to get our hands a little dirty.</p><p>Launch the Terminal app on the Pi (it&apos;s in the menu bar at the top).</p><p>Run the following command:</p><pre><code>sudo apt-get update &amp;&amp; sudo apt full-upgrade
</code></pre><p>When the prompt changes to a changelog, press <code>Ctrl-C</code> to close the wall of text, then <code>Enter</code> to continue.</p><p>This takes awhile! This updates Raspberry Pi OS to the latest updates including security patches. You want to run this every so often to keep the operating system up-to-date. You can also <a href="https://www.raspberrypi.com/documentation/computers/os.html?ref=kamranicus.com#update-software" rel="noreferrer">follow the official documentation here</a>.</p><p>There are more technical guides on <a href="https://wiki.debian.org/UnattendedUpgrades?ref=kamranicus.com">automatic updating</a> so you don&apos;t need to login to keep it up-to-date.</p><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">&#x1F449;</div><div class="kg-callout-text">If using the OS user interface, there is an &quot;Update&quot; option in the top menu bar but that didn&apos;t work for me. Using the Terminal was the most reliable way to update.</div></div><h3 id="configure-ipv6-optional"><strong>Configure IPv6 (Optional)</strong></h3><p>The Raspberry Pi can be pinged and connected to via the hostname <code>raspberrypi.local</code> or whatever name you chose in the imager (plus <code>.local</code> at the end). </p><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">&#x1F449;</div><div class="kg-callout-text"><b><strong style="white-space: pre-wrap;">2025 Update: </strong></b>When I updated to Raspberry Pi OS, I had no trouble accessing the Raspberry Pi from my MacBook or Google Pixel with Chrome/VLC and the <code spellcheck="false" style="white-space: pre-wrap;">babypi.local</code> hostname.<br><br>I did not verify whether you still need to update IPv6 in the latest OS, so maybe this isn&apos;t applicable anymore. I am leaving it in for reference just in case.</div></div><p>However, if you use the IP address, it defaults to a long one (IPv6). Web browsers can have trouble connecting to the Pi with this enabled, so we need to disable IPv6 hostname resolution.</p><pre><code>sudo nano /etc/avahi/avahi-daemon.conf
</code></pre><p>On the line <code>use-ipv6=yes</code> change <code>yes</code> to <code>no</code>. Press <code>Ctrl-O</code> to save (hit Enter), then <code>Ctrl-X</code> to exit.</p><p>Now reboot! <code>sudo reboot</code></p><p>You should be able to ping your Pi from another machine:</p><pre><code>ping babypi.local
ping -4 babypi.local
</code></pre><p>The second command forces the ping to return the IPv4 address. On Windows you may need the <a href="https://support.apple.com/kb/DL999?locale=en_US&amp;ref=kamranicus.com">Bonjour service installed</a> if you don&apos;t have iTunes installed.</p><p>Some devices (my Android phone for example) still won&apos;t be able to use <code>babypi.local</code> so you&apos;ll still need the IP address of your Pi, which you can see with <code>ip addr show</code>.</p><blockquote><strong>Note:</strong> I had some issues on Windows trying to access <a href="http://babypi.local/?ref=kamranicus.com">http://babypi.local</a> in my browser. When I pinged my Pi, I got a private address space back (198.105.x.x) instead of the local IP. I changed my router&apos;s DNS to Google&apos;s public DNS servers and <em>for some reason</em> that worked. I think CenturyLink (my ISP) was somehow interfering since it would always redirect to some proprietary search results page. However, with the knowledge I have of networking and DNS, this still makes no sense to me!</blockquote><h3 id></h3><h2 id="installing-picam"><strong>Installing Picam</strong></h2><p>Because we want to stream <em>both</em> audio and video from the camera and USB microphone, we need to use a software project called <a href="https://github.com/iizukanao/picam?ref=kamranicus.com">Picam</a>.</p><p>Make sure to run <em>all commands</em> from the Pi user directory, i.e. your prompt should look like:</p><pre><code class="language-sh">pi@raspberrypi:~ $
</code></pre><p>The <code>~</code> denotes your user&apos;s home directory.</p><p>Run the following command in the terminal to install some Picam dependencies:</p><pre><code class="language-sh">sudo apt-get install libharfbuzz0b libfontconfig libepoxy0
</code></pre><p>Then this entire thing (if you are using SSH, just copy/paste otherwise have fun!):</p><pre><code class="language-sh">cat &gt; make_dirs.sh &lt;&lt;&apos;EOF&apos;
#!/bin/bash
DEST_DIR=~/picam
SHM_DIR=/run/shm

mkdir -p $SHM_DIR/rec
mkdir -p $SHM_DIR/hooks
mkdir -p $SHM_DIR/state
mkdir -p $DEST_DIR/archive

ln -sfn $DEST_DIR/archive $SHM_DIR/rec/archive
ln -sfn $SHM_DIR/rec $DEST_DIR/rec
ln -sfn $SHM_DIR/hooks $DEST_DIR/hooks
ln -sfn $SHM_DIR/state $DEST_DIR/state
EOF</code></pre><p>Then the following, to execute the script we just made and create the required directories:</p><pre><code class="language-sh">chmod +x make_dirs.sh
./make_dirs.sh
</code></pre><h3 id="downloading-the-right-version"><strong>Downloading the right version</strong></h3><p><strong>Raspberry Pi OS</strong> <strong>and Picam Binary Release (1.4.7+, 2.x)</strong></p><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">&#x1F449;</div><div class="kg-callout-text">The latest Picam 2.x releases work on the latest Raspberry Pi OS version, and it&apos;s what I&apos;m using in 2024.</div></div><p>Use the <a href="https://github.com/iizukanao/picam/releases?ref=kamranicus.com" rel="noreferrer">latest version release</a> (2.0.12 as of Dec 2024):</p><pre><code>wget https://github.com/iizukanao/picam/releases/download/v2.0.12/picam-2.0.12-`uname -m`.tar.gz -O picam-2.0.12.tar.gz
tar zxvf picam-2.0.12.tar.gz
cp picam-2.0.12/picam ~/picam/</code></pre><p>Replace <code>2.0.12</code> with the latest version string if applicable.</p><p>This script:</p><ul><li>Downloads the file to the current directory as <code>picam-{version}.tar.gz</code></li><li>Extracts the archive to the <code>picam-{version}</code> folder</li><li>Copies the extracted <em>sub-folder</em> <code>picam-{version}/picam</code> to a top-level <code>~/picam</code> folder in your HOME directory</li></ul><p><strong>Jessie or Stretch (Picam 1.4.6)</strong></p><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">&#x1F449;</div><div class="kg-callout-text"><b><strong style="white-space: pre-wrap;">If you are using Jessie or Stetch on your Pi, you need the right binary file</strong></b>! Thanks to <a href="https://kamranicus.com/guides/raspberry-pi-3-baby-monitor#comment-3574356701">Andrew</a> for pointing this out in the comments.<br><br>This is only applicable to Picam v1.4.6</div></div><ul><li>For Jessie: <a href="https://github.com/iizukanao/picam/releases/download/v1.4.6/picam-1.4.6-binary-jessie.tar.xz?ref=kamranicus.com">https://github.com/iizukanao/picam/releases/download/v1.4.6/picam-1.4.6-binary-jessie.tar.xz</a></li><li>For Stretch: <a href="https://github.com/iizukanao/picam/releases/download/v1.4.6/picam-1.4.6-binary-stretch.tar.xz?ref=kamranicus.com">https://github.com/iizukanao/picam/releases/download/v1.4.6/picam-1.4.6-binary-stretch.tar.xz</a></li></ul><p>Now we install Picam directly using the link above:</p><pre><code>wget &lt;URL ABOVE&gt; -O picam.tar.gz
tar xvf picam.tar.gz
cp picam/picam ~/picam/
</code></pre><h3 id="configuring-picam"><strong>Configuring Picam</strong></h3><p>We now have Picam installed! It&apos;s time to get a stream working.</p><p>To configure Picam to use our microphone, we need to know what its &quot;device ID&quot; is. Run the following to list any USB recording devices:</p><pre><code>arecord -l
</code></pre><p>The name will be Card # and Device #, so for example, here is mine:</p><pre><code>**** List of CAPTURE Hardware Devices ****
card 1: Device [USB Audio Device], device 0: USB Audio [USB Audio]
  Subdevices: 1/1
  Subdevice #0: subdevice #0
</code></pre><p>You see &quot;card 1&quot; and &quot;device 0&quot; so my microphone ID will be <code>hw:1,0</code>.</p><p>Now, let&apos;s make a script we can run to start Picam. <strong>Replace &quot;hw:1,0&quot; with your device ID above if it&apos;s different.</strong></p><p>First, create a new <code>run_picam.sh</code> bash script file and mark it as executable:</p><pre><code>touch run_picam.sh
chmod +x run_picam.sh</code></pre><p>Now, use the <code>nano</code> editor to edit the script:</p><pre><code class="language-sh">nano run_picam.sh</code></pre><p>The nano editor is very straightforward to use. Ctrl-O to save, Ctrl-X to exit.</p><p>Copy and paste or write this script:</p><pre><code class="language-sh">#!/bin/sh

# !!! Change to your user home directory!
PI_USER=/home/&lt;your-username&gt;

sudo $PI_USER/make_dirs.sh

sudo $PI_USER/picam/picam \
  -o /run/shm/hls \
  --time --alsadev hw:1,0 \
  &gt; /var/log/picam.log 2&gt;&amp;1
</code></pre><div class="kg-card kg-callout-card kg-callout-card-yellow"><div class="kg-callout-emoji">&#x1F449;</div><div class="kg-callout-text"><b><strong style="white-space: pre-wrap;">Important: </strong></b>Change <code spellcheck="false" style="white-space: pre-wrap;">&lt;your-username&gt;</code> to your Raspberry Pi OS username.<br><br>My path for example is <code spellcheck="false" style="white-space: pre-wrap;">/home/kamran</code></div></div><p>Because Picam writes to some system directories, it is running using <code>sudo</code> although I am positive with the correct permissions, you wouldn&apos;t need to.</p><p>The script starts Picam with an HLS stream output to the RAM drive (fast) and uses our microphone as the recording device. It also writes out the current timestamp in the bottom corner of the stream. There are a ton of other options, too, including subtitles, etc. The default video resolution is 720p, which is fine enough for a baby cam <a href="https://github.com/iizukanao/picam?ref=kamranicus.com">ton of other options</a>.</p><p>The script also writes the Picam output to <code>/var/log/picam.log</code> for debugging purposes.</p><p>Test the script by typing <code>sudo run_picam.sh</code>. You will not see any output because it is outputting to the log file.</p><p>Open a new Terminal tab (<strong>File -&gt; New Tab</strong>) and type in the following:</p><pre><code class="language-sh">cat /var/log/picam.log</code></pre><p> You should see:</p><pre><code>kamran@babypi:~ $ cat /var/log/picam.log
created state dir: ./state
created hooks dir: ./hooks
created directory: ./rec
created directory: ./rec/tmp
created directory: ./rec/archive
created HLS output directory: /run/shm/hls
configuring devices
capturing started
</code></pre><p>You can quit the <code>run_picam.sh</code> script by hitting Ctrl-C to exit.</p><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">&#x1F449;</div><div class="kg-callout-text">You can also run the <code spellcheck="false" style="white-space: pre-wrap;">top</code> command to verify <code spellcheck="false" style="white-space: pre-wrap;">picam</code> is running.</div></div><h2 id="running-picam-at-startup"><strong>Running Picam at startup</strong></h2><p><a href="http://www.stuffaboutcode.com/2012/06/raspberry-pi-run-program-at-start-up.html?ref=kamranicus.com">There&apos;s a great post</a> on how to run a script at startup. We want Picam to run right away when the Pi boots up, so let&apos;s do that!</p><p>Create the script:</p><pre><code>sudo nano /etc/init.d/picam
</code></pre><p>Then copy/paste the following:</p><pre><code>#! /bin/sh
# /etc/init.d/picam

### BEGIN INIT INFO
# Provides:          picam
# Required-Start:    $remote_fs $syslog $network
# Required-Stop:     $remote_fs $syslog $network
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Simple script to start Picam at boot
# Description:       A simple script to start Picam at boot
### END INIT INFO

# If you want a command to always run, put it here

# Carry out specific functions when asked to by the system
case &quot;$1&quot; in
  start)
    echo &quot;Starting Picam&quot;
    # run application you want to start
    sudo /home/pi/run_picam.sh
    ;;
  stop)
    echo &quot;Stopping Picam&quot;
    # kill application you want to stop
    killall picam
    ;;
  *)
    echo &quot;Usage: /etc/init.d/picam {start|stop}&quot;
    exit 1
    ;;
esac

exit 0
</code></pre><p>This is an initialization script that will execute our <code>run_picam.sh</code> script we just made.</p><p>Mark it as executable:</p><pre><code>sudo chmod +x /etc/init.d/picam
</code></pre><p>Now, let&apos;s test it.</p><pre><code>sudo /etc/init.d/picam start
</code></pre><p>You should see the Picam output. Picam is now running in the background! You can stop it manually like this:</p><pre><code>sudo /etc/init.d/picam stop
</code></pre><p>Now let&apos;s register it on startup:</p><pre><code>sudo update-rc.d picam defaults
</code></pre><p>All set. We can test it out by rebooting the Pi and verifying the Picam process is running.</p><pre><code>sudo reboot
</code></pre><p>Once the Pi is booted, type <code>top</code> to see running processes. You should see Picam at the top.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2021/03/top.png" class="kg-image" alt="Building a Raspberry Pi 3/4/5 Baby Monitor" loading="lazy" width="979" height="605" srcset="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w600/2021/03/top.png 600w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2021/03/top.png 979w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Linux &quot;top&quot; command running</span></figcaption></figure><h2 id="installing-nginx"><strong>Installing nginx</strong></h2><p>We&apos;ve set up Picam to output a stream to a file but we need to expose that stream so we can connect to it from our devices or web browsers. &quot;nginx&quot; is a web server that can do that for us.</p><p>Let&apos;s install nginx:</p><pre><code>sudo apt-get install nginx
</code></pre><p>Now let&apos;s expose the RAM stream as a directory:</p><pre><code>sudo nano /etc/nginx/sites-available/default
</code></pre><p>In the editor, scroll down to right before <code>location / {</code> and add a new block above it:</p><pre><code>location /hls/ {
    root /run/shm;
}
</code></pre><p>This adds a new /hls URL that points to the /run/shm/hls directory Picam is outputting the stream to.</p><p>Restart nginx to allow the changes to take effect:</p><pre><code>sudo /etc/init.d/nginx restart
</code></pre><p>This starts the web server. Now you can browse (on the Pi or other device) to the root of the site and view the nginx welcome page.</p><p>For example, type in <code>http://babypi.local/</code> to see the default nginx homepage:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2024/11/image-23.png" class="kg-image" alt="Building a Raspberry Pi 3/4/5 Baby Monitor" loading="lazy" width="540" height="212"></figure><p>If you type in <code>http://babypi.local/hls/index.m3u8</code> the browser should download or display a video file. We&apos;ll get to how to view the video in a few minutes!</p><h2 id="mounting-the-monitor"><strong>Mounting the monitor</strong></h2><p>Now that we&apos;ve configured the baby Pi, we need to put it near the baby! As I mentioned at the start, I am just using a simple LED light to help when its dark. With a gooseneck mount, we can also position the monitor in whatever way we need that provides the best view of the crib. What&apos;s nice is that you can fit everything you need onto a single Pi, so it can travel with you.</p><figure class="kg-card kg-gallery-card kg-width-wide kg-card-hascaption"><div class="kg-gallery-container"><div class="kg-gallery-row"><div class="kg-gallery-image"><img src="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2021/03/mount-1-1.jpg" width="1836" height="1716" loading="lazy" alt="Building a Raspberry Pi 3/4/5 Baby Monitor" srcset="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w600/2021/03/mount-1-1.jpg 600w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w1000/2021/03/mount-1-1.jpg 1000w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w1600/2021/03/mount-1-1.jpg 1600w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2021/03/mount-1-1.jpg 1836w" sizes="(min-width: 720px) 720px"></div><div class="kg-gallery-image"><img src="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2021/03/mount-2.jpg" width="1836" height="1716" loading="lazy" alt="Building a Raspberry Pi 3/4/5 Baby Monitor" srcset="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w600/2021/03/mount-2.jpg 600w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w1000/2021/03/mount-2.jpg 1000w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w1600/2021/03/mount-2.jpg 1600w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2021/03/mount-2.jpg 1836w" sizes="(min-width: 720px) 720px"></div></div></div><figcaption><p><span style="white-space: pre-wrap;">Different angles of the mount</span></p></figcaption></figure><p>It looks a bit like an angler fish but it does the job!</p><h2 id="viewing-on-a-device"><strong>Viewing on a device</strong></h2><p>We&apos;re officially all set up! That&apos;s all that is needed to use our baby monitor.</p><p>In the first part where you configured the Pi, you choose a hostname (or didn&apos;t). You can now access the stream URL at <code>http://babypi.local/hls/index.m3u8</code>. You can open that up in any compatible software to view the livestream.</p><h3 id="vlc-player"><strong>VLC Player</strong></h3><p>On a PC, Mac, Android, or iOS, I recommend <a href="https://www.videolan.org/vlc/index.html?ref=kamranicus.com">VLC Media Player</a>.</p><ol><li>Install VLC</li><li>Open VLC</li><li>Go to File</li><li>Open Network Stream</li><li>Paste in the URL</li></ol><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2021/03/vlc.png" class="kg-image" alt="Building a Raspberry Pi 3/4/5 Baby Monitor" loading="lazy" width="529" height="432"><figcaption><span style="white-space: pre-wrap;">VLC screenshot of using mDNS URL</span></figcaption></figure><p>The steps are similar for Android and iOS, just choose &quot;Stream&quot; and type in the same URL.</p><p>You should now see a live stream with video and audio!</p><h2 id="optionally-adding-a-web-player"><strong>(Optionally) Adding a web player</strong></h2><p>VLC is a nice player but sometimes it might be nice to just hit the Pi directly in a web browser (open a new tab on your computer, etc.).</p><p>We can optionally add a stream player to our web server. Follow the steps below:</p><pre><code>cd /var/www/html
sudo git clone https://github.com/kamranayub/picam-viewer.git .
</code></pre><blockquote><strong>IMPORTANT:</strong> Ensure you keep the period (<code>.</code>) at the end of that command! It will download the <em>contents</em> of the Git repository into the current one.</blockquote><p>This will bring down a bare metal web player I put together. Feel free to edit it as you see fit. If you changed the URL to where the HLS stream is at on the server, you will need to edit <em>config.json</em>.</p><p>Now, open a browser and visit your Raspberry Pi (e.g. <a href="http://babypi.local/?ref=kamranicus.com">http://babypi.local/</a>):</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2021/03/browser.png" class="kg-image" alt="Building a Raspberry Pi 3/4/5 Baby Monitor" loading="lazy" width="812" height="566" srcset="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w600/2021/03/browser.png 600w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2021/03/browser.png 812w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Browsing the babypi.local URL</span></figcaption></figure><blockquote><strong>Note:</strong> The viewer is very basic right now, I plan to add some functionality to change the exposure, resolution, white balance, and mute settings for Picam as well. Some Raspberry Pi configuration might be nice too such as rebooting. You can contribute back at <a href="https://github.com/kamranayub/picam-viewer?ref=kamranicus.com">https://github.com/kamranayub/picam-viewer</a></blockquote><h2 id="optionally-exposing-over-the-internet"><strong>(Optionally) Exposing over the Internet</strong></h2><p>I have chosen not to expose my Pi to the internet, instead it sits securely on my network behind my router and is not accessible via the Internet. If you choose to expose it remotely, you will need to do the following to ensure it&apos;s secure:</p><ol><li><a href="https://www.wikihow.com/Set-Up-Port-Forwarding-on-a-Router?ref=kamranicus.com">Set up Port Forwarding</a> on your router to forward some random port to port 80 on the Pi</li><li>Enable HLS encryption <a href="https://github.com/iizukanao/picam?ref=kamranicus.com#enabling-encryption">which is in the Picam documentation</a>.</li><li><a href="https://www.nginx.com/blog/nginx-https-101-ssl-basics-getting-started/?ref=kamranicus.com">Configure nginx to use HTTPS</a> (and force it) and an SSL certificate (preferably a signed one) for the website</li><li><a href="https://www.digitalocean.com/community/tutorials/how-to-set-up-http-authentication-with-nginx-on-ubuntu-12-10?ref=kamranicus.com">Configure nginx to prompt for a username/password</a> (Basic Authentication) which is secure as long as its over SSL</li><li>Probably <a href="http://raspi.tv/2012/how-to-set-up-keys-and-disable-password-login-for-ssh-on-your-raspberry-pi?ref=kamranicus.com">disable password authentication</a> in SSH and switch to SSH keys</li></ol><p>These steps are outside the scope of this guide but there are many tutorials online for Nginx, port forwarding, and SSL that you can follow to achieve this. If someone wants to write it up, I will happily add it to this guide! Just remember as soon as you allow your Pi to be exposed, you need to make sure it&apos;s the server is secure and encrypted.</p>]]></content:encoded></item><item><title><![CDATA[Why I Bought An Old Ass MacBook]]></title><description><![CDATA[I share the strategies and process I follow to make big purchases that has saved me hundreds of thousands of dollars]]></description><link>https://kamranicus.com/skill-of-spending/</link><guid isPermaLink="false">6734d6efabd34e000109a103</guid><category><![CDATA[Personal Finance]]></category><dc:creator><![CDATA[Kamran Ayub]]></dc:creator><pubDate>Wed, 13 Nov 2024 22:14:46 GMT</pubDate><media:content url="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2024/11/skill-of-spending.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2024/11/skill-of-spending.jpg" alt="Why I Bought An Old Ass MacBook"><p>This week my MacBook Pro 2011 SSD drive died, and it can&apos;t be restored using the built-in Disk Utilities (spent a couple hours trying). My options were to... </p><ol><li>Buy a new SSD and reinstall everything (which requires using workarounds for Sonoma)</li><li>Buy a new (to me) MacBook Pro</li></ol><p>Option 1 would certainly save me money but <em>I already did that</em> initially. I got the Macbook for free initially from a friend. I bought new RAM + an SSD, and installed Sonoma <a href="https://dortania.github.io/OpenCore-Legacy-Patcher/?ref=kamranicus.com" rel="noreferrer">using OpenCore Legacy Patcher</a>. It <em>worked</em> but it was still painfully slow. Plus, you&apos;re living on the edge with an unsupported MacOS version.</p><p>I have been thinking about a new laptop for awhile to replace an old Lenovo Yoga. It fell on its face trying to spin up a local Docker environment. </p><p>I decided this was a good time to think about a new work laptop where I can do <em>most</em> of the things I do on my PC but on my laptop when I&apos;m out and about.</p><p>Fast forward and I ended up purchasing a refurbished MacBook Pro M1 Max 14&quot;:</p><figure class="kg-card kg-bookmark-card kg-card-hascaption"><a class="kg-bookmark-container" href="https://support.apple.com/en-us/111902?ref=kamranicus.com"><div class="kg-bookmark-content"><div class="kg-bookmark-title">MacBook Pro (14-inch, 2021) - Technical Specifications - Apple Support</div><div class="kg-bookmark-description"></div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/icon/favicon.ico" alt="Why I Bought An Old Ass MacBook"><span class="kg-bookmark-author">Apple Support</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/thumbnail/111902_mbp14-silver2.png" alt="Why I Bought An Old Ass MacBook" onerror="this.style.display = &apos;none&apos;"></div></a><figcaption><p><span style="white-space: pre-wrap;">My new Old Glory</span></p></figcaption></figure><p>The first reaction you probably have is: </p><blockquote><em>WHY?!</em> Why not buy the latest M4?</blockquote><p>It comes down to the way I make any kind of big purchasing decision and I wanted to share the process with you because it has saved me loads of money over the years.</p><hr><h2 id="spending-is-a-skill">Spending is a skill</h2><p>Last year in 2023, Pete aka Mr. Money Mustache who was my gateway drug to learning about financial independence, <a href="https://www.biggerpockets.com/articles/money-377?ref=kamranicus.com" rel="noreferrer">was on the BiggerPockets podcast</a> (highly recommend) and he said something very important:</p><blockquote>Spending less money is not a deprivation, it is a skill.</blockquote><p>In fact, it&apos;s literally the first thing he says.</p>
<!--kg-card-begin: html-->
<iframe frameborder="0" height="482" scrolling="no" src="https://playlist.megaphone.fm/?e=BIGPOC3617714372" width="100%"></iframe>
<!--kg-card-end: html-->
<p>I encourage you to listen because this is the essence of how to reach financial independence. It is not solely about earning more. Without a skill of spending, you can earn more and be no better off (or worse off) than someone who earns less but has a high skill of spending.</p><p>Examples:</p><ul><li>Instead of paying $200/mo for cell plan from Verizon, you pay $30/mo with Mint</li><li>Instead of paying $44,000 with a car loan for a brand new minivan, you look at ConsumerReports and use <a href="https://www.autolist.com/?ref=kamranicus.com" rel="noreferrer">Autolist</a> to pay $18,000 in cash for a used one with 44,000 miles</li><li>Instead of paying $750,000 for a new home in a nice area, you pay $305,000 for an older home in a nice area (with more square footage)</li><li>Instead of paying $10,601 to install new HVAC using the contractor you&apos;ve always used, you shop around save $1,000 by using a different one who ends up providing better service anyway</li><li>Instead of paying $2,836 to visit Texas with a family of 5, you use travel rewards and pay $43.74</li><li>Instead of paying $2,328 for a new MacBook Pro M4, you pay $1,350 for an M1 Max with 2.5X the amount of RAM and more GPU power</li></ul><p>In case it&apos;s not obvious, these are <em>actual numbers </em>from the big purchase decisions I&apos;ve made in the past few years.</p><p>The savings from these decisions alone are in the tens of thousands in the short-term, and hundreds of thousands in the long-term.</p><p>And was it a deprivation? Not at all. We fucking love our 2015 Honda Odyssey. My HVAC is boring and keeps us comfortable. We stay under the 2GB of data per month for our phones. We visited family in Texas and had a blast.</p><p>If this were D&amp;D, I&apos;d probably put <strong>Spending</strong> as a skill under <strong>Wisdom</strong>:</p><blockquote>Wisdom reflects how attuned you are to the world around you and represents perceptiveness and intuition.<br><br>-- <a href="https://5thsrd.org/rules/abilities/wisdom/?ref=kamranicus.com" rel="noreferrer">D&amp;D 5e</a></blockquote><p>Perhaps you could argue it falls under Intelligence &#x2013; but I would say the <em>skill</em> is in the planning, which requires situational awareness. </p><p>Cool, now that we&apos;ve established you need a skill of spending to achieve financial freedom, what does it <em>mean</em> exactly?</p><h2 id="my-process-for-big-purchases">My process for big purchases</h2><p>Let me share what I did today for my MacBook purchase because it&apos;s basically the same strategy I used for everything else.</p><h3 id="start-with-the-end-in-mind">Start with the End in Mind</h3><p>A solid strategy for most things is to <em>start with the end in mind</em>. </p><p>In other words, work backwards.</p><p>There are probably thousands of options to choose from, so which ones are you going to narrow it down to? This is part of strategy &#x2013; deciding what you&apos;re going to say No to.</p><ol><li>What do you even want? What are you going to do with it?</li><li>What are you absolutely <em>not</em> going to compromise on?</li><li>What are you willing to compromise on?</li></ol><p>My answers here:</p><ol><li>I need a laptop for business work. I plan to do coding with .NET and Node.js, as well as video editing/recording. I would love to be able to write native iOS apps.</li><li>At least 16GB of RAM and 1TB of storage (because I&apos;ll use it). I also want an NVMe hard drive. I want something durable and well-built to last a long time. Minimum 1080p resolution.</li><li>I don&apos;t give a flying fuck about battery, audio, GPU, or the camera. I&apos;m nearly always by an outlet, I use headphones, and I&apos;m not playing games (and GPU cores on M chips are perfectly fine for video editing). I&apos;d prefer a high screen resolution but it&apos;s not a dealbreaker.</li></ol><p>This is an important part and skip it at your own peril &#x2013; if you cannot get clear about what you want, you&apos;re not going to make an optimized purchase decision.</p><p>Also, <strong>be honest with yourself.</strong> </p><p>Do you really <em>need </em>64GB of RAM or can you live with 16GB? This comes down to the details of how you use the <em>thing</em> or <em>experience </em>you&apos;re buying. Don&apos;t confuse &apos;wants&apos; with &apos;needs.&apos; I don&apos;t <em>need </em>more than 16GB but I <em>do </em>need more than 8GB based on my historical usage. Decide what&apos;s &quot;nice-to-have&quot; vs. &quot;must-have.&quot;</p><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">&#x1F4DC;</div><div class="kg-callout-text"><b><strong style="white-space: pre-wrap;">Exercise: </strong></b>For each spec you want note down <i><em class="italic" style="white-space: pre-wrap;">why</em></i> you want it specifically. Now, do you <i><em class="italic" style="white-space: pre-wrap;">actually </em></i>need it? Be honest. Think of a compromise you&apos;re okay with if you don&apos;t really need it.<br><br><i><em class="italic" style="white-space: pre-wrap;">Example 1</em></i><br><br>Need: I need first-class seats.<br>Why: I want more legroom. I want luxury. I hardly ever fly and I&apos;m a solo traveler.<br>Compromise: I&apos;ll buy groceries and stay at a cheaper Airbnb instead of an expensive hotel and eating out everyday.<br><br><i><em class="italic" style="white-space: pre-wrap;">Example 2</em></i><br><br>Need: I need a new car.<br>Why: I&apos;m worried a used one won&apos;t last that long.<br>Compromise: I&apos;ll buy a lightly used car that has less than 50,000 miles and buy an extended warranty for peace-of-mind.</div></div><p>There is no value judgement I&apos;m making on you here &#x2013; if you can find a compromise that saves you money somewhere else, and you get what you want, that&apos;s a win. The important part is that you&apos;re being intentional and have thought about the options.</p><p>Speaking of options...</p><h3 id="narrowing-down-the-options">Narrowing down the options</h3><p>If you start with the criteria laid out previously, you now have a baseline search:</p><ul><li>1TB of storage</li><li>16GB of RAM</li><li>Durable and well-built</li><li>Portable</li><li>Native iOS app support</li></ul><p>The obvious criteria here is supporting iOS development. If I want to make courses on .NET MAUI or port Excalibur games to iOS, I need a Mac. Since I need something portable, it&apos;s going to be a laptop as I already have a powerful PC. </p><p>As part of the initial research, I thought I might go with a Windows laptop but that was when my old MacBook was working. I thought about MacBook but figured it may be too expensive. However, now that it died, that changes the situation, and it meant I pretty much exclusively needed to look at Mac.</p><ul><li>Since I&apos;m going portable, that means it&apos;s a MacBook Pro or MacBook Air. </li><li>Since we have a minimum of 1TB/16GB of RAM, that throws out Air.</li></ul><p>Now we can ask the question: what are <em>all</em> possible MacBook Pro configurations that satisfy this criteria?</p><p>Obviously, I didn&apos;t want one from 2011 &#x2013; that makes no sense. But I also wasn&apos;t sure if I wanted the <em>latest </em>M4. Is it really that much of a difference?</p><p>Before M-series silicon, MacBooks used Intel chipsets. From the research I did, I knew that I probably wanted the M-series chipsets because they represented a massive leap in performance.</p><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">&#x1F4A1;</div><div class="kg-callout-text"><b><strong style="white-space: pre-wrap;">Tip: </strong></b>Check the <i><em class="italic" style="white-space: pre-wrap;">earliest</em></i> model in the <i><em class="italic" style="white-space: pre-wrap;">current</em></i> generation. Often there are only incremental improvements from the first version, yet much higher price premiums. But the &quot;major version&quot; update between generations could be signficant.</div></div><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2024/11/image-3.png" class="kg-image" alt="Why I Bought An Old Ass MacBook" loading="lazy" width="1262" height="971" srcset="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w600/2024/11/image-3.png 600w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w1000/2024/11/image-3.png 1000w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2024/11/image-3.png 1262w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Wikipedia is the changelog of the world</span></figcaption></figure><p>Since the M-series is the latest gen of MacBook, I decided I&apos;d stack the versions against each other to compare.</p><h3 id="make-a-spreadsheet">Make a spreadsheet</h3><p>I laid out the core specs I cared about in a spreadsheet, then checked each Apple M generation chip for benchmarks. I wanted to see what <em>value</em> the M4 brings to the table vs. the other chips.</p><p>The M-series usually has two variants: Pro and Max. The Max is the top-of-the-line trim which usually supports higher specs.</p><p>Here&apos;s what I came up with:</p><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2024/11/image-2.png" class="kg-image" alt="Why I Bought An Old Ass MacBook" loading="lazy" width="1336" height="124" srcset="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w600/2024/11/image-2.png 600w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w1000/2024/11/image-2.png 1000w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2024/11/image-2.png 1336w" sizes="(min-width: 1200px) 1200px"><figcaption><span style="white-space: pre-wrap;">You know it&apos;s serious when we break out the spreadsheet.</span></figcaption></figure><p>Notice how my columns reflect what I value the most. I was OK with a 16&quot; screen but actually preferred the 14&quot; since I&apos;m used to an ultrabook (and it ends up nearly always being cheaper anyway).</p><p>For the M1, in order to exceed 16GB of RAM, I also had to look at the M1 Max, which turns out to be pretty important as you&apos;ll see in a second.</p><p>We are almost to the min-maxing phase, but we&apos;re missing what we want to minimize: <strong>price</strong>.</p><h3 id="figure-out-your-budget">Figure out your budget</h3><p>What are you willing to spend? I can&apos;t answer that for you. I find it helpful to define a ceiling for myself.</p><blockquote>I won&apos;t spend more than $2,000 on a new laptop.</blockquote><p>And sometimes, a floor can be useful:</p><blockquote>I don&apos;t want anything less than a $600 laptop because it&apos;s junk.</blockquote><p>I&apos;d be careful with floors because you&apos;ll be <em>surprised</em> how often you&apos;ll find a premium product for a low price. But they <em>can</em> make sense especially when price dictates quality; like when I was shopping for HVAC contractors. I would <strong>not</strong> advise going with the lowest bid for anything you work on in your home.</p><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">&#x1F4A1;</div><div class="kg-callout-text">Don&apos;t confuse the sticker price with the out-of-pocket cost you&apos;ll pay. Treat them separately. As you&apos;ll see below, the final price can end up being far lower than the sticker price.</div></div><h3 id="employ-price-shopping-strategies">Employ price shopping strategies</h3><p>There are so many tactics here but I&apos;ll share the overall strategies:</p><ul><li>Buy used/refurb instead of new</li><li>Discount initial price with coupons/promos</li><li>Credit purchase with credit card rewards points</li><li>Check for credit card bonus offers</li><li>Check employee discounts</li><li>Check price history</li><li>Buy through rewards portals</li></ul><p>I&apos;ll share how I employed some of these strategies.</p><p><strong>Seasonal and membership discounts</strong></p><p>Since it&apos;s around Black Friday, it made sense to check out local retailers for doorbuster deals. Best Buy also offers Geek Squad refurbished items. I&apos;m a Costco member, so it made sense to check there too.</p><p>I also got turned onto <a href="https://backmarket.com/?ref=kamranicus.com" rel="noreferrer">Backmarket</a> from a repair shop YouTube channel. Shops repair and certify refurb tech and resell it on Backmarket. There are some nice perks you get above Facebook Marketplace or Craigslist:</p><ul><li>Sold by actual shops</li><li>Repair or replacement if it doesn&apos;t work within a year</li><li>Option to extend by another year that includes two repairs/replacements <em>and</em> a full refund</li><li>Laptops must have <a href="https://help.backmarket.com/hc/en-us/articles/360026656634-What-condition-will-my-device-be-in?ref=kamranicus.com" rel="noreferrer">at least 85% battery life</a> left</li></ul><p>Finally, since my wife is an educator, I checked out Apple&apos;s educator discount: not much it turns out ($100 off) &#x1F44E;.</p><p><strong>Checking rewards points and bonus offers</strong></p><p>For my credit card rewards, my Chase Ink Business card had $106 I could use to refund a purchase (that&apos;s a 1X refund, but Chase points are worth 25% more if used for travel). </p><p>Chase <em>also</em> had an offer where points were worth the 25% extra if used to buy Apple products. This means, instead of buying from Best Buy brand new, it would make more sense to buy straight from Apple <em>through</em> the Chase rewards portal.</p><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">&#x1F4A1;</div><div class="kg-callout-text"><b><strong style="white-space: pre-wrap;">Using Welcome Bonuses:</strong></b> If you don&apos;t have a good rewards card, you might want to get one. You can use the welcome bonus to pay for stuff too. After we opened our Chase cards, we accumulated over 250,000 points which we used to pay for travel. <br><br>Aaron Hurd came to Minnedemo this year and he has good advice on his site <a href="https://www.cardsandpoints.com/?ref=kamranicus.com" rel="noreferrer">CardsAndPoints.com</a> if you need a list of cards for welcome bonuses or specific retail shopping (like Amazon, Walmart, or Target).</div></div><p><strong>Choosing the shop(s)</strong></p><p>The shops you end up going with may be influenced by your urgency (need it today vs. can wait a week), your affiliations, or your values (I don&apos;t buy on Amazon if there&apos;s an alternative, even if it&apos;s more expensive).</p><p>After some browsing around, I quickly decided that if I bought new, I&apos;d use my Chase business rewards portal, and if I bought refurbished, I&apos;d go with Backmarket &#x2013; Costco and Best Buy didn&apos;t have deals for the specs I wanted.</p><p>With the price sites picked out, it&apos;s time to figure out which configurations are in stock, what the specs are, and how much you&apos;ll expect to pay.</p><h3 id="min-maxing-specs">Min-maxing specs</h3><p>If you want to save money, you <strong>will</strong> trade off with something else. Convenience. Efficiency. Quality. Luxury. The exercise you did upfront in deciding what to compromise on will lead to what you naturally value.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2024/11/image.png" class="kg-image" alt="Why I Bought An Old Ass MacBook" loading="lazy" width="1920" height="1080" srcset="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w600/2024/11/image.png 600w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w1000/2024/11/image.png 1000w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w1600/2024/11/image.png 1600w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2024/11/image.png 1920w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">A part-time job. Credit: </span><a href="https://www.gamerguides.com/diablo-iv/guide/stats-and-key-terms/overviews/best-stats-for-each-character-in-diablo-4?ref=kamranicus.com" rel="noreferrer"><span style="white-space: pre-wrap;">GamerGuides</span></a></figcaption></figure><p>For this strategy, think of RPGs. Are you a min-maxer? I kind of am and kind of not. I look for the value, the Goldilocks zone. I don&apos;t want to spend <em>so</em> much time tweaking 1% values. I look for the 20-80% wins, and then don&apos;t sweat the small stuff. </p><p>A $10 savings is not going to do much, but $500? That&apos;s significant. <a href="https://www.simplypsychology.org/pareto-principle.html?ref=kamranicus.com" rel="noreferrer">Stick to the Pareto principle</a>.</p><p>Now that we have the specs laid out (which we want to maximize), and where to find the prices (which we want to minimize), we can spend some time to min-max and find that sweet spot.</p><p>In the end, I narrowed it down to several specific in-stock configurations:</p><figure class="kg-card kg-image-card kg-width-full"><img src="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2024/11/image-4.png" class="kg-image" alt="Why I Bought An Old Ass MacBook" loading="lazy" width="2000" height="108" srcset="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w600/2024/11/image-4.png 600w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w1000/2024/11/image-4.png 1000w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w1600/2024/11/image-4.png 1600w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2024/11/image-4.png 2333w"></figure><p>There are plenty of things to talk about here:</p><p><strong>Figure out the unit price</strong></p><p>When I bought my home, I paid about $150/sqft. I used price per square foot as the measuring stick to compare other homes. This is a unit price and it applies in pretty much any shopping context.</p><p>The lowest unit price doesn&apos;t automatically mean it&apos;s the best decision &#x2013; notice how I didn&apos;t highlight the cheapest option (the M1 Pro) at all. When I compare 16GB of RAM to 64GB of RAM (and the slightly higher score), the higher RAM wins out against the lower price <em>and</em> the 16&quot; loses against the 14&quot; in my book.</p><p>But it <em>does</em> provide a nice reference value to compare &#x2013; is this option in the ballpark, or is it a poor deal?</p><p><strong>Compare actual performance</strong></p><p>It&apos;s not like I plotted each home we went to and noted down energy efficiency ratings to compare against (although I would if it were important to my decision). But some purchasing decisions have clear measures of performance &#x2013; like CPU benchmarks.</p><p>What I wanted to know was: how much better is each successive M-series chip?</p><p>You see my columns for &quot;vs. M1&quot;, &quot;vs. M2&quot; etc. that I filled in.</p><p>Sometimes you&apos;ll be surprised by the results, like that the difference between a previous-gen M3 Pro and the current M4 Pro is really low. To me, it made no sense to buy a refurb M3 MacBook when I could buy a brand new one factory-sealed for <em>essentially the same price</em> (and with more RAM). That took the M3 out of the running for me.</p><p>Similarly, I was <em>surprised</em> at how much value the M1 Max was compared to the M4:</p><ul><li>A 20% performance differential is a lot, but was it worth the extra $1000?</li><li>The M1 Max had 2.5x the amount of RAM, which usually translates to massive premiums for MacBooks.</li><li>The M1 Max and M4 Pro have the same amount of CPU cores, but the M1 has <em>twice </em>as much GPU cores.</li><li>The M1 Max has retina display of 3024x1964</li></ul><p>Couple these with some facts:</p><ul><li>You can&apos;t expand RAM on a M-series MacBook </li><li>You can always expand storage externally</li><li>MacBooks are built to last</li><li>The latest MacOS Sequoia is compatible with the M1</li></ul><p>In my mind, there&apos;s absolutely <em>no question</em> here &#x2013; the M1 Max was a clear winner in terms of value on the dollar.</p><p><strong>Include discounts, rewards, and taxes</strong></p><p>I added everything to my cart to calculate sales tax and I used the aforementioned rewards points or took discounts into account.</p><p>In fact, after I did a coupon search, I got an additional $25 off the order. &#x1F389;</p><p>All these need to be added to understand the total picture.</p><h3 id="consider-the-downsides-and-risk-mitigation">Consider the downsides and risk mitigation</h3><p>That&apos;s not to say I didn&apos;t consider the downsides to getting the M1 Max:</p><ul><li>It&apos;s the earliest of the M-series generation, so it will be the first to not be compatible with a later MacOS version</li><li>I&apos;m buying it in Fair condition so it may not look pretty</li><li>I&apos;m buying it refurbished, so it already has a past life</li><li>It won&apos;t be serviced under AppleCare</li></ul><p>But I can mitigate these risks:</p><ul><li>I&apos;m comfortable with DIY repair. If it comes down to using OpenCore Patcher again I know I can do it.</li><li>BackMarket offers a 1-year warranty but it can be extended another year with full refund support.</li><li>I actually would <em>prefer</em> to recycle and breathe new life into old hardware vs. giving Apple money directly.</li></ul><p>So you see, sometimes a downside for one person is actually an upside.</p><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">&#x1F4A1;</div><div class="kg-callout-text">Don&apos;t forget to think about <b><strong style="white-space: pre-wrap;">Total Cost of Ownership</strong></b>. Will cutting a corner now actually cost you <i><em class="italic" style="white-space: pre-wrap;">more</em></i> in the long run? A 24-month warranty may cost me $168 but it would save me over $1000 if the laptop failed during that time.<br><br>When I did my HVAC purchase, I projected equipment maintenance cost and potential efficiency savings on my electric bills for 5Y, 10Y and 15Y timelines. This made sure I researched which contractor provided the best long-term warranty coverage (which also tells you how much they&apos;ll stand by their own work).</div></div><h3 id="dont-wait-too-long">Don&apos;t wait too long</h3><p>The whole point of having this clear strategy is to make decisions quickly and not get stuck in <em>analysis paralysis</em>.</p><p>I started looking at MacBooks at 9am and purchased the M1 Max at 10:41am.</p><p>Now granted, I&apos;ve used these strategies a lot in the past, so I kinda have a feel for it but each decision is unique. I took about 2 weeks to research and finally find a contractor for my HVAC. It took us a month or so to buy our house (in a buyer&apos;s market). </p><p>But, if the potential upside is huge, spend the time you need. It takes months of planning and prepping for our big international trips where we save $1000&apos;s.</p><p>What you don&apos;t want to do is take <em>so long</em> that you end up missing out on a good value. Who knows how long that M1 Max would have stayed in stock on a refurb marketplace &#x2013; my guess is not that long. Then again, HVAC contractors aren&apos;t going anywhere anytime soon.</p><p>Like in most things, there&apos;s <em>some</em> kind of Goldilocks zone. One rule of thumb that can be helpful is the &quot;72-hour rule.&quot; I first heard it from Jillian Johnsrud but it&apos;s pretty simple: wait 72 hours before hitting that buy button. By the end of that 72 hours, if you still feel like you <em>need</em> the thing, buy it. But many times, you&apos;ll find you don&apos;t actually need it (or in my case, completely forget about it).</p><h2 id="the-skill-of-spending-is-about-being-intentional">The skill of spending is about being intentional</h2><p>I bought an &quot;old&quot; MacBook simply because it ended up being more value for the amount of money I was willing to spend and I figured it would last me another 10 years. (Also, it ain&apos;t <em>that </em>old.)</p><p>All of this is just about being <em>intentional</em>.</p><p>There are many forces at work that want you to spend money:</p><ul><li>Social pressure and status</li><li>Time urgency</li><li>Marketing</li><li>Fear (perceived or real) and anxiety</li><li>Stress</li><li>Late stage capitalism</li></ul><p>Most of that is outside your control &#x2013; you can&apos;t exactly <em>stop</em> yourself from being on the receiving end of this sexy-ass M4 marketing page.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2024/11/image-5.png" class="kg-image" alt="Why I Bought An Old Ass MacBook" loading="lazy" width="1490" height="1294" srcset="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w600/2024/11/image-5.png 600w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w1000/2024/11/image-5.png 1000w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2024/11/image-5.png 1490w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">All I&apos;ve ever wanted was to use my MacBook while surrounded by the beauty of the night sky</span></figcaption></figure><p>But you do retain control over your spending &#x2013; you get to decide, before you hit the buy button, whether this is something you need and whether this is the price you pay for it.</p><h3 id="like-any-skill-it-can-be-honed">Like any skill, it can be honed</h3><p>I may have started out using a process similar to this when I was building custom PCs when I was 12 &#x2013; but it&apos;s been refined over the years and a lot of the strategies like rewards points were only added in the last few years. It&apos;s ever-evolving and it&apos;s not like you <em>stop</em> spending (not for long, anyway!).</p><p>Wherever you are, whether you&apos;re an impulse buyer, or take forever to make decisions, there&apos;s always room to improve your spending skills.</p><p>The best place to start I think is by refining your approach to <em>big</em> purchases &#x2013; home, transportation, food. Once you nail that down, you will automatically start applying these strategies in smaller ways to daily purchases.</p><p>I share this slide in my talk because after getting intentional, it made a massive impact on my finances &#x2013; leading to hundreds of thousands of dollars saved in the long-term:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2024/11/image-6.png" class="kg-image" alt="Why I Bought An Old Ass MacBook" loading="lazy" width="1274" height="716" srcset="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w600/2024/11/image-6.png 600w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w1000/2024/11/image-6.png 1000w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2024/11/image-6.png 1274w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Consider the opportunity cost of saving those extra dollars</span></figcaption></figure><p>All of this adds up &#x2013; because when you save that money, it gives it a chance to be invested and grow. In case you need a framework for that, I&apos;ve got you covered:</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://kamranicus.com/money"><div class="kg-bookmark-content"><div class="kg-bookmark-title">F U Money for Developers</div><div class="kg-bookmark-description">How developers can build a seven-figure net worth and achieve financial freedom with financial design patterns</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/icon/Square-Logo@3x.png" alt="Why I Bought An Old Ass MacBook"><span class="kg-bookmark-author">Kamranicus</span><span class="kg-bookmark-publisher">Kamran Ayub</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/thumbnail/esi-framework.png" alt="Why I Bought An Old Ass MacBook" onerror="this.style.display = &apos;none&apos;"></div></a></figure>]]></content:encoded></item><item><title><![CDATA[Monarch Money 3-Month Review]]></title><description><![CDATA[I switched from Mint to Monarch Money 3 months ago so I thought I'd share how it's going.]]></description><link>https://kamranicus.com/monarch-money-review/</link><guid isPermaLink="false">65ce37f54727c5000158f0a1</guid><category><![CDATA[Personal Finance]]></category><dc:creator><![CDATA[Kamran Ayub]]></dc:creator><pubDate>Thu, 15 Feb 2024 17:39:01 GMT</pubDate><media:content url="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2024/02/Where-Does-My-Money-Go-1.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2024/02/Where-Does-My-Money-Go-1.jpg" alt="Monarch Money 3-Month Review"><p>One of the things I like about using personal finance apps is the insight you get into your spending. What gets measured gets improved, so if you need to improve your finances you best start gathering data.</p><p>You&apos;ve maybe heard me mention using Mint in the past &#x2013; but <a href="https://www.forbes.com/advisor/banking/mint-disappearing-what-to-do/?ref=kamranicus.com" rel="noreferrer">Mint is dead</a>. There are a number of alternatives and the smart ones have new marketing landing pages that break down how they compare.</p><p>I can save you the trouble and tell you that I&apos;ve been happily paying for <a href="https://www.monarchmoney.com/referral/v14fqyerhq?ref=kamranicus.com" rel="noreferrer">Monarch Money</a> since November (that&apos;s my referral code!).</p><figure class="kg-card kg-bookmark-card kg-card-hascaption"><a class="kg-bookmark-container" href="https://www.monarchmoney.com/compare/mint-alternative?c=MINT50&amp;ref=kamranicus.com"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Monarch vs. Mint: Why Monarch is the best Mint alternative</div><div class="kg-bookmark-description">With Mint shutting down, learn how to switch to Monarch, the best Mint alternative. Use our Mint import tools to bring all of your financial history to Monarch in minutes. Join thousands of happy ex-Mint users that have made the switch.</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://www.monarchmoney.com/favicon.ico" alt="Monarch Money 3-Month Review"><span class="kg-bookmark-author">Monarch</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://www.monarchmoney.com/social/monarch.jpg" alt="Monarch Money 3-Month Review"></div></a><figcaption><p><span style="white-space: pre-wrap;">Use </span><b><strong style="white-space: pre-wrap;">MINT50 </strong></b><span style="white-space: pre-wrap;">to get 50% off your first year</span></p></figcaption></figure><p>Monarch made it easy to transfer over my account, including a custom open source browser extension that would vacuum up all my transaction history to import.</p><h3 id="here-are-some-other-things-i-love-about-monarch">Here are some other things I love about Monarch:</h3><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2024/02/Screenshot_20240215-104754.png" class="kg-image" alt="Monarch Money 3-Month Review" loading="lazy" width="864" height="1171" srcset="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w600/2024/02/Screenshot_20240215-104754.png 600w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2024/02/Screenshot_20240215-104754.png 864w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">The app makes it easy to quickly check in on your finances.</span></figcaption></figure><p><strong>Quick progress check-ins</strong>. Anytime you open the app, it provides a calendar at the top where it highlights the days since you last reviewed your finances. You tap &quot;Start Review&quot; and it walks through your key vitals step by step &#x2013; cash in/out, net worth increase, debt breakdown, etc. It makes checking in much easier. This is not a feature in the web-based app, I&apos;ve only seen it in the mobile app but that makes sense &#x2013; on my phone, I&apos;m mostly interested in quick check-ins but the app is also fully-featured.</p><hr><figure class="kg-card kg-gallery-card kg-width-wide kg-card-hascaption"><div class="kg-gallery-container"><div class="kg-gallery-row"><div class="kg-gallery-image"><img src="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2024/02/Spending-by-category-1.png" width="1584" height="828" loading="lazy" alt="Monarch Money 3-Month Review" srcset="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w600/2024/02/Spending-by-category-1.png 600w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w1000/2024/02/Spending-by-category-1.png 1000w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2024/02/Spending-by-category-1.png 1584w" sizes="(min-width: 720px) 720px"></div><div class="kg-gallery-image"><img src="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2024/02/Where-Does-My-Money-Go_--1-.png" width="1584" height="3508" loading="lazy" alt="Monarch Money 3-Month Review" srcset="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w600/2024/02/Where-Does-My-Money-Go_--1-.png 600w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w1000/2024/02/Where-Does-My-Money-Go_--1-.png 1000w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2024/02/Where-Does-My-Money-Go_--1-.png 1584w" sizes="(min-width: 720px) 720px"></div></div></div><figcaption><p><span style="white-space: pre-wrap;">Kids are expensive...</span></p></figcaption></figure><p><strong>Lots of reports.</strong> As featured above, Monarch is continually adding new ways to visualize and get insights into your spending. The Sankey diagram is awesome and for most visualizations, you can share them without numbers (so you can see how kids are more expensive than our house).</p><hr><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2024/02/image-1.png" class="kg-image" alt="Monarch Money 3-Month Review" loading="lazy" width="1509" height="819" srcset="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w600/2024/02/image-1.png 600w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w1000/2024/02/image-1.png 1000w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2024/02/image-1.png 1509w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Advanced rules to auto-categorize transactions &#x1F916;</span></figcaption></figure><p><strong>Advanced rule engine</strong>. Mint had custom categories but Monarch goes a step farther, allowing you to dynamically match transactions. It was always annoying in Mint how I always had to manually categorize daycare expenses because it just says &quot;CHECK&quot; yet the amounts always were the same. With Monarch, I set up a rule to do it &#x1F389;</p><hr><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2024/02/image-2.png" class="kg-image" alt="Monarch Money 3-Month Review" loading="lazy" width="1602" height="159" srcset="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w600/2024/02/image-2.png 600w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w1000/2024/02/image-2.png 1000w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w1600/2024/02/image-2.png 1600w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2024/02/image-2.png 1602w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Target Runs &#x1F3C3;&#x200D;&#x2642;&#xFE0F; are just a fact of life.</span></figcaption></figure><p><strong>Better recurring detection.</strong> Mint could do <em>some</em> recurring detection but Monarch is loads better. It will use &quot;estimated&quot; amounts and you can always edit or remove the ones it detects. But it&apos;ll learn over time. You can view everything in a calendar view or list.</p><hr><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2024/02/image-3.png" class="kg-image" alt="Monarch Money 3-Month Review" loading="lazy" width="976" height="739" srcset="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w600/2024/02/image-3.png 600w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2024/02/image-3.png 976w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">This is the last one. &#x1F972;</span></figcaption></figure><p><strong>Flexible goals.</strong> I always hated how in Mint you basically could only set a goal to reduce credit card debt so they could sell you cards. In Monarch, you can make a goal for anything. Take a summer off. Buy materials for the shedquarters. Whatever you want, and you can assign more advanced rules for the accounts or transactions it uses.</p><hr><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2024/02/image-4.png" class="kg-image" alt="Monarch Money 3-Month Review" loading="lazy" width="763" height="170" srcset="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w600/2024/02/image-4.png 600w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2024/02/image-4.png 763w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">No more password sharing.</span></figcaption></figure><p><strong>Share with your partner</strong>. For Mint, my wife never really used it on her phone and I had to use my login for her. With Monarch, she has her own login and can get customized alerts/reports. Household sharing is just built in because... that&apos;s how you manage finances. Financial advisors can even be invited separately (with fewer permissions).</p><hr><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2024/02/image-7.png" class="kg-image" alt="Monarch Money 3-Month Review" loading="lazy" width="1008" height="717" srcset="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w600/2024/02/image-7.png 600w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w1000/2024/02/image-7.png 1000w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2024/02/image-7.png 1008w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">You can submit your own ideas</span></figcaption></figure><p><strong>Public roadmap</strong>. The Monarch team is pretty transparent in what&apos;s coming and you can see the work as it progresses. This is a great way to have your say in what gets built.</p><p><strong>Support that cares.</strong> Unlike Mint, with Monarch I can contact support and they&apos;ll <em>actually </em>pay attention. Over the past couple of months, they&apos;ve been inundated with 20-30X the amount of support due to the Great Mint Migration and they STILL replied to me pretty quickly despite all that.</p><h3 id="it-isnt-perfect-but-its-close-enough">It isn&apos;t perfect but it&apos;s close enough</h3><p>Compared to Mint, the experience has been <strong>much better.</strong> It&apos;s not 100% perfect though.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2024/02/image-6.png" class="kg-image" alt="Monarch Money 3-Month Review" loading="lazy" width="997" height="105" srcset="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/size/w600/2024/02/image-6.png 600w, https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2024/02/image-6.png 997w" sizes="(min-width: 720px) 720px"></figure><p><strong>Accounts that don&apos;t sync.</strong> I&apos;ve had issues syncing a couple of accounts like my wife&apos;s 457b. However, I&apos;ve made do by having the apps on my phone and occasionally updating the balances in Monarch when I do my reviews. Other accounts will work if I manually go through the auth flow once a month.</p><div class="kg-card kg-callout-card kg-callout-card-green"><div class="kg-callout-emoji">&#x1F4E3;</div><div class="kg-callout-text"><b><strong style="white-space: pre-wrap;">Update (April 2024):</strong></b> My 401k is now syncing. They added the ability to try different providers for Alight, so it looks like MX works (and Plaid/Fincity did not). I&apos;m a happy camper! <br><br>In addition, I got my Optum Bank HSA working as well. Now we only have one account (Empower/MNDCP) that we have to manually update.</div></div><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2024/02/image-5.png" class="kg-image" alt="Monarch Money 3-Month Review" loading="lazy" width="253" height="129"><figcaption><span style="white-space: pre-wrap;">I swear it&apos;s higher!</span></figcaption></figure><p><strong>Can&apos;t customize savings rate</strong>. In the financial independence space,<a href="https://earlyretirementnow.com/2017/04/05/savings-rate/?ref=kamranicus.com" rel="noreferrer"> there&apos;s varying calculations for savings rate</a> that can include investments or mortgage principle payments. Monarch calculates a savings rate, but it&apos;s <em>cash only</em> and doesn&apos;t incorporate some other measures like I had done with my own little Reach FI app.</p><p><strong>No financial modeling</strong>. This is not a feature I&apos;d really <em>expect</em> with Monarch but it would be nice. Personal Capital (now Empower) has a great retirement calculator and a way to simulate different future scenarios based on your net worth and history. For now, you&apos;ll just need two different tools and that&apos;s probably fair. Monarch is focused on the here and now, not the future. They are adding more and more investment-related features though like transaction importing.</p><p>If I&apos;ve convinced you to try it out, here&apos;s my referral link again and I think you can even still use the <strong>MINT50</strong> promo code to get your 50% off.</p><div class="kg-card kg-button-card kg-align-center"><a href="https://www.monarchmoney.com/referral/v14fqyerhq?ref=kamranicus.com" class="kg-btn kg-btn-accent">Try Monarch Money</a></div>]]></content:encoded></item><item><title><![CDATA[Monitor Exports from Packages in Monorepos]]></title><description><![CDATA[Using Jest inline snapshots and a codeowners file can allow teams to be notified when their public exports change within a large monorepo project]]></description><link>https://kamranicus.com/monitor-exports-from-node-js-packages-in-monorepos/</link><guid isPermaLink="false">61fe874340af2f003b188947</guid><category><![CDATA[JavaScript]]></category><dc:creator><![CDATA[Kamran Ayub]]></dc:creator><pubDate>Sat, 05 Feb 2022 16:09:53 GMT</pubDate><media:content url="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2022/02/carbon--1-.png" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2022/02/carbon--1-.png" alt="Monitor Exports from Packages in Monorepos"><p>When you maintain an npm package you are maintaining a public API surface that <strong>will change over time</strong>. </p><p>This is a <em>chore </em>but if you&apos;re working on a large project with many teams of developers, who all own different packages, and you have to publish all those packages regularly, well... then it becomes a <strong>problem</strong>. Downstream consumers will raise their Slack pitchforks if you&apos;re not careful! </p><figure class="kg-card kg-image-card"><img src="https://media.tenor.com/images/29533547f82bf83b8e76e2a340fb71a9/tenor.gif" class="kg-image" alt="Monitor Exports from Packages in Monorepos" loading="lazy" width="320" height="240"></figure><p>How do you track and monitor the changes to this public-facing API in a large project like a monorepo?</p><p>We use a basic approach is a combination of codeownership and convention-based module tests using Jest inline snapshots which I explain below.</p><blockquote>One of my colleagues came up with this idea, <a href="https://twitter.com/AndersDJohnson?ref=kamranicus.com">thanks Anders</a>! &#x1F64F;</blockquote><h2 id="whats-exported-from-a-package">What&apos;s exported from a package?</h2><p>In JavaScript (or TypeScript) projects, we use the <code>export</code> keyword to expose an API in the module index, you&apos;ve likely seen this:</p><pre><code class="language-js">export * from &apos;./wood-cutting&apos;;
export * from &apos;./camping&apos;;
export { useFishingContext, useFishingHook } from &apos;./fishing&apos;;</code></pre><p>There are 2 things to note here:</p><ol><li>We have <strong>no idea</strong> what is being exposed from <code>wood-cutting</code> and <code>camping</code> modules. We have to dive into and follow the export chain to figure that out.</li><li>We <strong>have a good idea</strong> of what is being exposed from <code>fishing</code></li></ol><p>One approach when building a consumer-facing package like this is to use <em>explicit</em> named exports in the index module (we will set aside the fact you can deep import, something which we&apos;ve disabled through eslint in our projects).</p><h2 id="export-star-is-a-double-edged-sword">Export star is a double-edged sword</h2><p>It&apos;s very common for the internals of a package to use <code>export *</code> for exposing folder-based modules and for namespacing. This is nice because it makes it much easier to maintain <em>inside</em> the package.</p><p>But when it&apos;s used to expose things <em>at the top level</em> it introduces maintenance complexity &#x2013; because we don&apos;t know <em>what&apos;s</em> exposed to the public.</p><p>You could enforce <strong>always</strong> being explicit in the root module and that can work. However, it could also be burdensome when a module has 100s of exports (should it be split? That&apos;s a different discussion).</p><p><strong>Even so,</strong> we still have a problem on Big Teams.</p><h2 id="mo-monorepo-mo-problems">Mo&apos; monorepo, mo&apos; problems</h2><p>We have 80+ packages in our monorepo. All packages get released <strong>at once</strong> weekly. Packages are owned by multiple teams and we have 80+ engineers working in this repository.</p><p>What we&apos;ve seen is the following problems can happen over time:</p><ol><li>A new API is added but someone forgot to export it</li><li>A new API is added and it <em>automatically</em> gets made into a public API even though it was supposed to be internal</li><li>An API was <em>removed </em>and is no longer exported</li><li>An API was <em>changed</em> and its <em>behavior </em>changed</li></ol><p>The fourth issue should be handled through tests and socializing the fact that downstream consumers will be affected. We use deprecation notices and migrations so folks make that process easier which updates the CHANGELOG and can run/record commands with versioning.</p><p>So let&apos;s focus on the first three and a quick process we implemented to help track changes to public module exports.</p><h2 id="the-module-exports-test">The module exports test</h2><p>You will be disappointed by how simple this ends up being &#x1F605;</p><p>We use <a href="https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners?ref=kamranicus.com">GitHub CODEOWNERS</a> to tag a special &quot;custodial&quot; group when certain things change in the monorepo: think <code>package.json</code> dependencies, <code>yarn.lock</code> files, and now, <em>module export tests</em>.</p><p>A module export test is a regular test file that imports the surface area of a module and tracks a snapshot of the public-facing interface over time. Combined with a <code>CODEOWNERS</code> file, this means teams will be notified if a contributor ends up changing the public API surface by adding an API, removing an API, or renaming an API so it can be reviewed for whether it requires a migration or deprecation notice.</p><p>Here&apos;s an example that should illustrate the idea:</p><pre><code class="language-js">/* packages/survival/__tests__/module.test.mjs */

import * as moduleExports from &apos;..&apos;

describe(&quot;module exports&quot;, () =&gt; {
    
  it(&quot;should not change unless reviewed by owners and custodians&quot;, () =&gt; {
      
    expect(moduleExports).toMatchInlineSnapshot(`
Object {
  &quot;collectFirewood&quot;: [Function],
  &quot;cutWood&quot;: [Function],
  &quot;getCampingSite&quot;: [Function],
  &quot;useFishingContext&quot;: [Function],
  &quot;useFishingHook&quot;: [Function],
}
`);
  })
})</code></pre><p>And the corresponding <code>CODEOWNERS</code> entry for some of our conventions:</p><pre><code># Custodial Ownership
module.test.mjs       @custodians
yarn.lock             @custodians
package.json          @custodians

# Team Ownership
/packages/survival/   @survivors</code></pre><p>You can check out <a href="https://github.com/kamranayub/example-jest-module-exports-test?ref=kamranicus.com">a tiny sample repo</a> to play with it.</p><p>This was a quick and easy way for us to help track package changes and notify the owners if someone outside their team managed to change the public API.</p><h2 id="this-wont-solve-everything">This won&apos;t solve everything</h2><p>It&apos;s important to mention that <em>this doesn&apos;t help with signature changes</em>.</p><p>We use TypeScript so we could take the same approach against the root <code>index.d.ts</code> that gets generated for dists but that would likely have to be done separate from Jest. We&apos;ve talked about centralizing the module tests in one place which <em>consumed</em> the dist packages instead of being <em>within</em> the package. This would allow us to do more kinds of tests/checks.</p><p>This is just one small example of interesting scaling issues monorepos present which is why investment in tooling is critical. </p><p>What pains have you run into using monorepos or on large repositories? What have you done to alleviate that pain?</p>]]></content:encoded></item><item><title><![CDATA[Overriding Specific Property Types Using Mapped and Conditional Types in TypeScript]]></title><description><![CDATA[How can you easily transform types to change specific properties without hardcoding tons of property key names?]]></description><link>https://kamranicus.com/typescript-overriding-specific-property-types-mapped-conditional/</link><guid isPermaLink="false">61d45c5c5a4e2f003b0e3bf5</guid><category><![CDATA[TypeScript]]></category><dc:creator><![CDATA[Kamran Ayub]]></dc:creator><pubDate>Tue, 04 Jan 2022 17:30:00 GMT</pubDate><media:content url="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2022/01/2022-01-04-typescript-specific-overrides-1.png" medium="image"/><content:encoded><![CDATA[<img src="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2022/01/2022-01-04-typescript-specific-overrides-1.png" alt="Overriding Specific Property Types Using Mapped and Conditional Types in TypeScript"><p>Let&apos;s say you have the following <code>DbUser</code> type:</p><pre><code class="language-typescript">interface DbUser {
  id: ObjectId;
  employer_id: ObjectId;
  
  name: string;
  age: number;
}</code></pre><p>What would you do if you wanted to create a <em>new</em> type <code>UiUser</code> that was the <strong>same </strong>but with<strong> </strong>the <code>ObjectId</code> type annotations changed to <code>string</code>?</p><h2 id="overriding-specific-property-type-annotations-of-another-type">Overriding specific property type annotations of another type</h2><p>Here is what my proposed solution looks like:</p><pre><code class="language-typescript">interface DbUser {
  id: ObjectId;
  employer_id: ObjectId;
  
  name: string;
  age: number;
}

type MapObjectIdToString&lt;PropType&gt; = 
  PropType extends ObjectId ? string : PropType;

type MapDbObject&lt;T&gt; = {
  [PropertyKey in keyof T]:
    MapObjectIdToString&lt;T[PropertyKey]&gt;;
}

type UiUser = MapDbObject&lt;DbUser&gt;;</code></pre><p>The <code>MapDbObject&lt;T&gt;</code> helper will take the type <code>T</code> and convert any of its properties with a type annotation <code>ObjectId</code> and replace it with <code>string</code>. This scales to handle any number of properties and only requires a single type alias per usage.</p><p>This question was inspired by a colleague at work asking about sharing types between a TypeScript app with a MongoDB backend.</p><p>How did we arrive at this approach and how does work exactly?</p><h2 id="ways-that-dont-work">Ways That Don&apos;t Work</h2><p>There were a few ways we tried that didn&apos;t work.</p><h3 id="overriding-an-interface-with-extends">Overriding an interface with extends</h3><p>Could you &quot;override&quot; a property by extending the interface?</p><pre><code class="language-typescript">interface DbUser {
  id: ObjectId;
  employer_id: ObjectId;
  
  name: string;
  age: number;
}

interface UiUser extends DbUser {
  id: string;
  employer_id: string;
}</code></pre><p>Unfortunately, interfaces cannot redefine the type of an inherited (or ambient) property:</p><pre><code>Interface &apos;UiUser&apos; incorrectly extends interface &apos;DbUser&apos;.
  Types of property &apos;id&apos; are incompatible.
    Type &apos;string&apos; is not assignable to type &apos;ObjectId&apos;.(2430)</code></pre><h3 id="overriding-interfaces-using-intersection">Overriding interfaces using intersection</h3><p>What about trying to intersect two interfaces? Would putting the second type last &quot;override&quot; the first?</p><pre><code class="language-typescript">
interface DbUser {
  id: ObjectId;
  employer_id: ObjectId;
  
  name: string;
  age: number;
}

interface IdAsString {
  id: string;
  employer_id: string;
}

type UiUser = DbUser &amp; IdAsString;

var foo: UiUser = { id: &quot;abc&quot; }
</code></pre><p>It <em>almost</em> works except that <code>id</code> is unioned so the resulting type is <code>string | ObjectId</code> &#x1F926;&#x200D;&#x2642;&#xFE0F;</p><p>However, we are getting closer. We can use type aliases to redefine or re-map property types in pretty powerful ways.</p><h2 id="overriding-properties-using-the-omit-helper">Overriding properties using the Omit helper</h2><p>Initially my colleague tried a combination of using the <a href="https://www.typescriptlang.org/docs/handbook/utility-types.html?ref=kamranicus.com#omittype-keys">Omit</a> helper and <a href="https://www.typescriptlang.org/docs/handbook/2/objects.html?ref=kamranicus.com#intersection-types">intersection types</a>:</p><pre><code class="language-typescript">type UiUser = Omit&lt;DbUser, &apos;id&apos; | &apos;employer_id&apos;&gt; &amp; {
  id: string;
  employer_id: string;
}</code></pre><p>For simple cases, <em>this works fine. </em>The problem is that this <strong>won&apos;t scale</strong> with multiple <code>ObjectId</code> properties since they&apos;d have to enumerate them in the <code>Omit</code> helper and intersecting object. It would create a maintenance nightmare.</p><p>Since we <em>want to scale</em> we can introduce some more language features of TypeScript. &#x1F64C;</p><h2 id="using-mapped-and-conditional-typing-to-transform-types">Using mapped and conditional typing to transform types</h2><p>I love TypeScript because there are features in the language that let us achieve really granular business rules like this.</p><p>The two features I leaned on for this were <a href="https://www.typescriptlang.org/docs/handbook/2/mapped-types.html?ref=kamranicus.com">Mapped types</a> and <a href="https://www.typescriptlang.org/docs/handbook/2/conditional-types.html?ref=kamranicus.com">Conditional types</a>.</p><h3 id="mapped-types">Mapped Types</h3><p>This is a &quot;<a href="https://www.typescriptlang.org/docs/handbook/2/mapped-types.html?ref=kamranicus.com">mapped type</a>&quot; in TypeScript. It follows a pattern of using a &quot;property key&quot; index expression:</p><pre><code class="language-typescript">type MapDbObject&lt;T&gt; = {
 [&lt;PropertyKey&gt; in keyof &lt;SourceType&gt;]: &lt;ReturnTypeExpression&gt;
}</code></pre><p>An indexing expression is similar to what indexing looks like for JavaScript objects like <code>user[&quot;name&quot;]</code>. We&apos;d normally write that as <code>user.name</code> but sometimes we have a variable that represents the property to retrieve like <code>user[nameField]</code>.</p><p>In a TypeScript mapped type, the expression inside the brackets tells the compiler two things: <em>what to label the matching <code>PropertyKey</code> </em>and <em>what to assign the value of the match</em> (in <code>keyof T</code>). The <code>in</code> keyword essentially makes this an iterable expression (like the JavaScript expression <code>&quot;name&quot; in user</code>). <code><a href="https://www.typescriptlang.org/docs/handbook/2/keyof-types.html?ref=kamranicus.com">keyOf</a></code> is an operator that can be used in any type expression.</p><p>So to translate the left-hand bracket expression:</p><blockquote>Match all property key names in the type <code>T</code> and assign them to the type alias <code>Property</code> </blockquote><p>Imagine substituting the type and seeing the list of properties enumerated by the compiler with the expression <code>keyOf User</code>:</p><ul><li><code>id</code></li><li><code>employer_id</code></li><li><code>name</code></li><li><code>age</code></li></ul><p>If you imagine the intermediate steps, this is what is substituted in that left hand expression:</p><pre><code class="language-typescript">type MapDbObject&lt;T&gt; = {
 [Property in keyof T]: &lt;ReturnTypeExpression&gt;
}

type UiUser = MapDbObject&lt;User&gt;;

// Intermediate step for left-hand substitution:
UiUser {
  id: &lt;type?&gt;;
  employer_id: &lt;type?&gt;;
  
  name: &lt;type?&gt;;
  age: &lt;type?&gt;;
}</code></pre><p>I hope this sort of makes sense. Next, the right-hand side of the map expression will tell the compiler what <code>&lt;type?&gt;</code> is supposed to be.</p><p>This means if we provided <code>T[Property]</code> as the right-hand expression we would be redefining the <code>DbUser</code> type verbatim:</p><pre><code>// if passing the same exact type (Property)
type MapDbObject&lt;T&gt; = {
  [Property in keyof T]: T[Property]
}

type UiUser = MapDbObject&lt;DbUser&gt;;

// it would map to the equivalent type

UiUser {
  [id]: DbUser[&quot;id&quot;];
  [employer_id]: DbUser[&quot;employer_id&quot;];
  [name]: DbUser[&quot;name&quot;];
  [age]: DbUser[&quot;age&quot;];
}

UiUser { 
  id: ObjectId;
  employer_id: ObjectId;
  
  name: string;
  age: number;
}</code></pre><p>The <code>T[Property]</code> expression in TypeScript returns the <em>type</em> of that property e.g. <code>DbUser[&quot;id&quot;]</code> returns the (type) value <code>ObjectId</code>.</p><p>This is close but obviously not what we want. At this point, we want to say &quot;return <code>string</code> if <code>T[Property]</code> is <code>ObjectId</code>&quot;.</p><p>To accomplish that, we can use conditional typing.</p><h3 id="conditional-typing">Conditional Typing</h3><p>Let&apos;s add a <a href="https://www.typescriptlang.org/docs/handbook/2/conditional-types.html?ref=kamranicus.com">conditional type</a> expression on the right-hand side of the mapping expression:</p><pre><code class="language-typescript">type MapDbObject&lt;T&gt; = {
  [Property in keyof T]: 
    T[Property] extends ObjectId ? string : T[Property];
}</code></pre><p>I hope this feels a bit familar and maybe you can intuit what this does.</p><p>It says:</p><blockquote>If the property type <code>T[Property]</code> extends the type <code>ObjectId</code><br>Then substitute the property type as <code>string</code><br>Else use the original <code>T[Property]</code> type</blockquote><p>So once again, the intermediate compiler steps might look like this:</p><pre><code class="language-typescript">type MapDbObject&lt;T&gt; = {
  [Property in keyof T]: 
    T[Property] extends ObjectId ? string : T[Property];
}

type UiUser = MapDbObject&lt;DbUser&gt;;

// intermediate steps, substituting T[Property]

UiUser { 
  id: ObjectId extends ObjectId ? string : ObjectId;
  employer_id: ObjectId extends ObjectId ? string : ObjectId;
  name: string extends ObjectId ? string : string;
  age: number extends ObjectId ? string : number;
}

UiUser { 
  id: string;
  employer_id: string;
  name: string;
  age: number;
}</code></pre><p>Et voila! We have now overridden <em>just</em> the <code>ObjectId</code>-based properties <em><code>id</code> and </em><code>employee_id</code> without affecting <code>name</code> or <code>age</code>.</p><h2 id="refactoring-for-readability">Refactoring for readability</h2><p>I love TypeScript but it gets a bad rap for readability. I understand this but there <em>are</em> ways to make type expressions more readable and it&apos;s the same as in regular programming: extract expressions to named <em>things</em>.</p><p>Since the right-hand expression is a bit unwieldy and not very descriptive I opted to extract it into its own named alias to make it clearer and simplify the usage:</p><pre><code class="language-typescript">type MapObjectIdToString&lt;T&gt; = T extends ObjectId ? string : T;

type MapDbObject&lt;T&gt; = {
  [Property in keyof T]: MapObjectIdToString&lt;T[Property]&gt;;
}</code></pre><p>The <code>MapObjectIdToString</code> takes the type to inspect <code>T</code> and performs the same conditional expression. At the consumption site, we are passing the type of the property <code>T[Property]</code> which we were doing previously but now we only need to provide that once.</p><p>At this point we could be <strong>really</strong> verbose and clear and rename the type aliases to be specific to the domain they deal with (i.e. MongoDB entities in the real-life example):</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">import { ObjectId } from &apos;mongodb&apos;;

type MapObjectIdToString&lt;PropType&gt; = 
  PropType extends ObjectId ? string : SourceType;

type SerializedMongoEntity&lt;TMongoEntity&gt; = {
  [PropertyKey in keyof TMongoEntity]:
    MapObjectIdToString&lt;TMongoEntity[PropertyKey]&gt;;
}</code></pre><figcaption>A bit more verbose but clearer?</figcaption></figure><p>I will let you determine how verbose you&apos;d prefer to be! I like things to be <em>really clear</em> about their intended usage &#x1F601;</p><h2 id="let-the-types-flow">Let the types flow</h2><p>Using generic, mapped, and conditional types are definitely more complicated but I hope you can see how <em>the types flow</em> now through that generic substitution and why these features make TypeScript incredibly powerful. Using them together allows you to be <em>more specific</em> while leaving the rest of the object alone.</p><p>You can find a working example of this snippet on the <a href="https://www.typescriptlang.org/play??ref=kamranicus.com#code/PQKhAIEFwYwGwIYGcngE4FMAOmkYHYAuChAlgPb7jkBm4hAFhuAEIDKA8gHLgcBGAKwwxCASQAm9AJ5YMAOnAhgAWABQ44YkyxEKXoOFjJAbzXhz4UCDMXF4AMKYSzBOHwYA7vqEiJ4UvhIxPgwGDYWEAACWAhoCAC24MYAFEFoAQDmAD74AK7xfBhoWfw+RgCUAL7+kvYIVIXgrgBMACzgfFKEzEwAHuBpmQA04ACMzR1dzHwBsVIDhOn4GdRoTeBc+YVocuHmSnswlGm5IuRoyaTiAPwAXAtLK1luW0Xgz6WGEuUA3HtW4AAKkxwBkCEUSBQqGR4sxaPQGKRUJ9fJIAkF6qFFCpVLYwe44mRKIDSLD7nkCkU-riLADRHRFrlmDAEDAQYwehh+oNluhsLgCMQiVR4SijNi9hiyDBYKymAAJLlsRaZO4dcjkOAYerU2xWPZ2Rza7qoereL6SGhociJVx9B6ZPk4DB4IiQyjUOhmsUSXY0iLgaKxBJJHkZap9ZWPWBObrrH2W6228AxFAYNFUNqTON9MN+2x2SKYQi5NBUYwJ6rF0vQkEwWPp82og0Hf0LSEy+vGjAAMSTit6UcyyUjKuW9zD5XuCd1tLALYcDdNVAT4CtNvWeCO+EkfGQjYp2xGHlIjARzFwhE956b4oAXkVyI3yLlCAoAKp4S3nWA2mLpJBjlWAZzjIXkOVvX0FyDOJEmMQ8imqGEXCoAJujBNYELWTBnVdMCVlcLDry3ShxCQfNbCiasyySSs+RLGiIK7ZxJATFscVsKVSE7Bs+xtElYWSZDyVeNAp0g8RZ3MfU20NJgYAAa1QUgvXAAA3BA4CZfxTXUzSrg6QCVwMZtZOsMzA2o8s+A1LV6irDAGOhNBtJUm8NK05gkXWDyDL4IyJJGKy100vBqA5NATzwCiIg4iwuJlJEADV9PES5xAnMcnheSk1g+EyKnuGzNW1fApMsecLPsP9YhdG8MAAR1yfTCHmeFGG81cTzPAADcgItEAARHqYv2QN-xDCsCokLIw2qfqmDQIaJP8QJgixQhyF-eJ-xcDIEHRN9oOC4xirs-AkJBXBcjgK94SOHbYkdQgPC2hMAHIkHYvZGuauAkGSBaiiG6dpskZ5JyK2zSvKmSCwgABxcFCRcMYJk6OMDLDcBci-VaJM+6CJrgrCkNJOEsGFTSU2DWFujWTS4HIE9wK21NlKoVwSJ3Dp90kZCMR20bCxOub6JrG9xmzLzd1mNB5jzb62wS0FkecITybVLDxJYXIaBoKl-kq+HwAAJUcmtUAg-EIWFcBxGccBklZGBS0d3IsHoLaIO58RygREgEW85aPGQVWCRY4WqItmjjEG5xLuYG3CShe3nCVvFHIEl1iB25JxPj7pYeNyizZj2tmFXAyw5adoMc5bksqdAU3WFY7y9DLKHKcm8s3r8B7WxnCW6FKEo7i8xNoHIdlnzzLHmpSo1DUNCihoVlmAAWUoDJyE-N5TBpAB9K5QbKCRZ3wBIMHnzJF+X1RWtkcB5SkcQUfEAAeQEAD5wAAXiSDYAA2gABWtLINArV8YKQwFIeEgIAC69xkiAjARAoorVEHgC5N0HcyIwbgGuA6Xk9w0HgPIJArBvw1BL1UGoJ+zBX7vxYvvNYgDmEf0-tvZYe88BoB-tSIAA">TypeScript Playground</a>.</p><hr><p>I am calling this a <em>proposed solution </em>because TypeScript always surprises me with its power. Do you have another way to achieve the same result? I&apos;d love to hear it.</p><h2 id="appendix-how-do-you-handle-nested-types">Appendix: How do you handle nested types?</h2><p>As a follow-up question, Saravanan asked how to handle nested objects. The answer lies in <strong>recursive types</strong>.</p><p>Here&apos;s a link to the answer!</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://stackoverflow.com/a/73827971/109458?ref=kamranicus.com"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Typescript - convert type of mongoose ObjectId to string when sharing between backend and frontend</div><div class="kg-bookmark-description">I am trying to create few types based on mongo schema models and sharing the same with front end in mono repo.Example type:import { Types } from &amp;quot;mongoose&amp;quot;; export interface Profile {</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://cdn.sstatic.net/Sites/stackoverflow/Img/apple-touch-icon.png?v=c78bd457575a" alt="Overriding Specific Property Types Using Mapped and Conditional Types in TypeScript"><span class="kg-bookmark-author">Stack Overflow</span><span class="kg-bookmark-publisher">Saravanan S</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://cdn.sstatic.net/Sites/stackoverflow/Img/apple-touch-icon@2.png?v=73d79a89bded" alt="Overriding Specific Property Types Using Mapped and Conditional Types in TypeScript"></div></a></figure><p></p>]]></content:encoded></item><item><title><![CDATA[Tricking TypeScript Into Typing Untyped JavaScript Modules]]></title><description><![CDATA[<p>You&apos;re trying to import a JS module from a package in a TypeScript file like this:</p><pre><code class="language-ts">// file-of-sadness.ts

import { Query } from &apos;react-query/lib/core/query&apos;;</code></pre><p>This is an internal JS module with no <code>.d.ts</code> files that declares types. TypeScript has no idea what to do.</p>]]></description><link>https://kamranicus.com/tricking-typescript-into-typing-untyped-javascript-modules/</link><guid isPermaLink="false">60770e2a21ac26003eba8b04</guid><category><![CDATA[TypeScript]]></category><dc:creator><![CDATA[Kamran Ayub]]></dc:creator><pubDate>Thu, 15 Apr 2021 13:00:00 GMT</pubDate><content:encoded><![CDATA[<p>You&apos;re trying to import a JS module from a package in a TypeScript file like this:</p><pre><code class="language-ts">// file-of-sadness.ts

import { Query } from &apos;react-query/lib/core/query&apos;;</code></pre><p>This is an internal JS module with no <code>.d.ts</code> files that declares types. TypeScript has no idea what to do. It&apos;s freaking out. &#x1F974;</p><blockquote>Could not find a declaration file for module &apos;react-query/lib/core/query&apos;. &apos;/project/node_modules/react-query/lib/core/query.js&apos; implicitly has an &apos;any&apos; type.<br><br>Try `npm i --save-dev @types/react-query` if it exists or add a new declaration (.d.ts) file containing `declare module &apos;react-query/lib/core/query&apos;;`ts(701</blockquote><p>You&apos;ve swerved off the Happy Path and headed straight for the muddy mountainside cliffs and you&apos;re about to have a Bad Day.</p><p>We can steer away from fiery doom by trying what TypeScript suggests and create a new declaration file next to our existing file:</p><pre><code class="language-typescript">// react-query-extended.d.ts

declare module &apos;react-query/lib/core/query&apos; {
  
}
</code></pre><p>Awesome, no errors so far. But what do we put in it?!</p><p>If there is a type already available from the declarations that ship with a package what you can do is <strong>re-export</strong> them for the untyped module which will effectively let TypeScript know what typings it should expect to see for the untyped JS module.</p><p>Otherwise, you can manually provide the right typings for whatever it is you&apos;re trying to import.</p><p>In this case, React Query <em>does</em> export a <code>Query</code> type but the <strong>actual implementation</strong> is <em>not</em> exported. &#x1F631; So that&apos;s why the cliff is coming up fast and our lives are flashing in front of us.</p><p><strong>Tricking TS is dangerous territory</strong> only because the package is no longer the source of truth, <em>you are</em> and you could be wrong. The right way to do this is to have the package export what you need, if possible. But hey, sometimes we need to live on the edge a little! This is React Query v2 and that&apos;s Legacy now so there&apos;s no recourse for me!</p><pre><code class="language-typescript">declare module &apos;react-query/lib/core/query&apos; {
  import { Query } from &apos;react-query&apos;;
  
  export { Query };
}</code></pre><p>By using <code>import</code> and <code>export</code> within the declaration, we are implicitly using ES modules and now TypeScript understands what we&apos;re trying to import from the JS module.</p><p>We live to see another day and manage to get back onto the Happy Path (for now), phew. &#x1F605;</p>]]></content:encoded></item><item><title><![CDATA[Building a TypeDoc-powered Gatsby Documentation Site]]></title><description><![CDATA[<p>I help maintain the <a href="https://excaliburjs.com/?ref=kamranicus.com">Excalibur.js</a> web-based game engine. Excalibur was written from the ground up in TypeScript. Luckily, early on we started to adopt a tool called <a href="https://typedoc.org/?ref=kamranicus.com">Typedoc</a> which could generate a rich API documentation site for TypeScript-based projects.</p><h2 id="linking-to-api-symbols">Linking to API Symbols</h2><p>TypeDoc has a compelling feature to</p>]]></description><link>https://kamranicus.com/building-excalibur-docs-typedoc-gatsby/</link><guid isPermaLink="false">6054aab49ccda4004bae2ef7</guid><category><![CDATA[Open Source]]></category><category><![CDATA[TypeScript]]></category><dc:creator><![CDATA[Kamran Ayub]]></dc:creator><pubDate>Sat, 16 Jan 2021 04:50:00 GMT</pubDate><content:encoded><![CDATA[<p>I help maintain the <a href="https://excaliburjs.com/?ref=kamranicus.com">Excalibur.js</a> web-based game engine. Excalibur was written from the ground up in TypeScript. Luckily, early on we started to adopt a tool called <a href="https://typedoc.org/?ref=kamranicus.com">Typedoc</a> which could generate a rich API documentation site for TypeScript-based projects.</p><h2 id="linking-to-api-symbols">Linking to API Symbols</h2><p>TypeDoc has a compelling feature to <a href="http://typedoc.org/guides/doccomments/?ref=kamranicus.com#symbol-references">link to API symbols</a> using a <code>[[symbolName]]</code> or <code>{@link symbolName}</code> syntax. This was awesome because we could write &quot;user-facing&quot; documentation and easily create maintainable links to the raw API symbols.</p><p>We separated user-facing documentation into separate <code>.md</code> files and this approach had been working well for us in the past years but there were several downsides:</p><ul><li>The &quot;user&quot; documentation was locked into the TypeDoc site and theme</li><li>While TypeDoc was customizable, it was still hard to do more complex things like custom components (think MDX which is React + Markdown)</li><li>It wasn&apos;t cohesive--we had a separate main site and you&apos;d expect to find docs there.</li></ul><p>We needed to do something different and <em>ideally</em> have these user docs hosted within the main site <strong>but still maintain the ability to easily link to API docs</strong>.</p><h2 id="migrating-to-gatsbyjs">Migrating to Gatsby.js</h2><p>Awhile back, I converted the site to be statically generated using <a href="https://gatsbyjs.org/?ref=kamranicus.com">Gatsby.js</a> and this has proved to be a good decision. Gatsby allows us to customize all the aspects of the site including the way we generate documentation.</p><p>Gatsby uses a GraphQL-based sourcing architecture where you can add any kind of &quot;source&quot; of data--this could be Wordpress, the GitHub API, or basically <em>any</em> external piece of data you wanted. These source plugins take the data from one place and <em>transform</em> it into GraphQL nodes that Gatsby can understand and make available to your pages statically. This makes Gatsby incredibly versatile and customizable using a consistent architecture.</p><p>Gatsby also has the idea of <em>transformers</em>. Transformers take input, usually an Abstract Syntax Tree (AST) and run it through processors that make changes. For example, there&apos;s <a>gatsby-transformer-remark</a> that takes Markdown AST and parses it using Remark.</p><p>Using a combination of <em>sources</em> and <em>transformers</em> you can basically transform one source of data into whatever you want as the output.</p><h2 id="creating-a-typedoc-gatsby-source">Creating a TypeDoc Gatsby Source</h2><p>When we migrated our user documentation, we didn&apos;t want to lose the ability to link to the API symbols using the convenient <code>[[symbol]]</code> syntax. In order to maintain that, we needed a way to source the TypeDoc JSON (or AST) into our Gatsby site.</p><p>For that purpose, I made an npm package <a href="https://npmjs.com/package/gatsby-source-typedoc?ref=kamranicus.com">gatsby-source-typedoc</a>. This will allow you to run TypeDoc against a TypeScript project and it will take the generated structure and store it as a GraphQL node for querying within your Gatsby app.</p><p>For example, here&apos;s what <a href="https://github.com/excaliburjs/excaliburjs.github.io/blob/site/gatsby-config.js?ref=kamranicus.com#L10">Excalibur&apos;s Gatsby config looks like</a>:</p><pre><code class="language-js">plugins: [
  {
    resolve: &apos;gatsby-source-typedoc&apos;,
    options: {
      src: [
        `${__dirname}/ex/edge/src/engine/index.ts`,
        `${__dirname}/ex/edge/src/engine/globals.d.ts`,
        `${__dirname}/ex/edge/src/engine/files.d.ts`,
        `${__dirname}/ex/edge/src/engine/excalibur.d.ts`,
      ],
      typedoc: {=
        excludePrivate: true,
        tsconfig: `${__dirname}/ex/edge/src/engine/tsconfig.json`,
      },
    },
  },
]
</code></pre><p>This will then allow you to query for the TypeDoc JSON content in a Gatsby page:</p><pre><code class="language-js">export const pageQuery = graphql`
  typedoc(typedocId: { eq: &quot;default&quot; }) {
    internal {
      content
    }
  }
`

export default function MyPage({ data: { typedoc } }) {
	const typedocContent = JSON.parse(typedoc?.internal.content);
	
	// do something with that data...
}
</code></pre><p>With this source package, it is enough to where you could build a custom Gatsby-based TypeDoc site since you now have complete access to the entire TypeDoc structure for your project. We didn&apos;t need to go that far, since we are happy with the TypeDoc default theme we use.</p><p>But we&apos;re still missing something important: parsing those special TypeDoc <code>[[symbol]]</code> links in our MDX-based documentation.</p><h2 id="parsing-typedoc-symbol-links-in-markdown">Parsing TypeDoc Symbol Links in Markdown</h2><p>We write our user documentation using MDX, which is Markdown and React. For example, here&apos;s one snippet of document from the <a href="https://excaliburjs.com/docs/intro?ref=kamranicus.com">Introduction page</a>:</p><pre><code class="language-md">To create a new game, create a new instance of [[Engine]] and pass in
the configuration ([[EngineOptions]]). Excalibur only supports a single
instance of a game at a time, so it is safe to use globally.
You can then call [[Engine.start|start]] which starts the game and optionally accepts
a [[Loader]] which you can use to [load assets](/docs/assets) like sprites and sounds.
</code></pre><p>Notice how we have multiple symbol links denoted by the <code>[[ ]]</code> syntax, including some with aliases like <code>[[Engine.start|start]]</code>.</p><p>If you run this through a Markdown parser, the only link that gets transformed is the &quot;load assets&quot; link because by default, Markdown has no idea what the <code>[[ ]]</code> syntax is! Somehow, we need to take the GraphQL TypeDoc source node(s) we generated and then run our Markdown through a <em>transformer</em> to convert these links to Markdown links.</p><p>To accomplish this, I released two packages: <a href="https://www.npmjs.com/package/remark-typedoc-symbol-links?ref=kamranicus.com">remark-typedoc-symbol-links</a> and <a href="https://www.npmjs.com/package/gatsby-remark-typedoc-symbol-links?ref=kamranicus.com">gatsby-remark-typedoc-symbol-links</a>.</p><p>Here is how this works in Gatsby, which was a completely new learning experience for me:</p><ul><li>Gatsby loads the MDX file</li><li>Gatsby then runs it through <code>gatsby-plugin-mdx</code></li><li><code>gatsby-plugin-mdx</code> runs the Markdown through Remark, a Markdown parser</li><li>Remark supports plugins, that can take the Markdown AST (mdast) and modify it</li><li>Gatsby supports special Gatsby Transformer Remark plugins which have access to <em>both</em> the Gatsby API <strong>and</strong> the Markdown AST</li><li>This pipeline uses <a href="https://github.com/unifiedjs/unified?ref=kamranicus.com">unified.js</a> as the underlying API at the lowest level</li></ul><p>So, what I needed to do was to make a Gatsby Remark Transformer plug-in. Since Gatsby just delegates down to Remark, I was able to split this up into two packages, just in case someone wanted to use the Typedoc symbol transformer outside Gatsby. The symbol transformer only needs one additional piece of input: the TypeDoc AST.</p><p>The meat of it all is <a href="https://github.com/kamranayub/remark-typedoc-symbol-links/blob/master/src/index.ts?ref=kamranicus.com">within the Remark transformer</a> where I traverse the AST and parse out the tokens that make up the symbol links and perform the lookups to build the generated URLs. It could definitely use some optimizations! &#x1F605;</p><h2 id="putting-it-all-together">Putting it all together</h2><p>To hook this all up is a matter of configuring the plugins to work with each other, which is documented on the <a href="https://www.npmjs.com/package/gatsby-remark-typedoc-symbol-links?ref=kamranicus.com">README of gatsby-remark-typedoc-symbol-links</a>. The approach differs if using with the Gatsby MDX plugin or Gatsby Remark transformer but they essentially do the same thing: pass the generated TypeDoc AST to the symbol link plugin.</p><p>The end result is links to TypeDoc symbol pages in the MDX-based documentation:</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2021/03/image-1.png" class="kg-image" alt loading="lazy" width="869" height="194"></figure><p>&#x1F389; Huzzah!</p><h2 id="want-to-make-your-own-gatsby-plugins">Want to make your own Gatsby plugins?</h2><p>It was a learning experience for me to understand the way plugins were authored as there are a lot of moving parts and I was unfamiliar with the tools for working with AST and Remark. There is &quot;documentation&quot; in this sense of basic examples but I had to dive through other plugin source code to really understand how to put it all together.</p><p>I have just started production on a Pluralsight course for authoring Gatsby plugins where I hope to show you how you can do this in a step-by-step fashion for different use cases (it&apos;s a &quot;playbook&quot;-style course, just the essentials to get it done!).</p><p>If you&apos;re interested <a href="https://kamranicus.com/#/portal">follow me</a> to keep updated on progress as I produce the course!</p>]]></content:encoded></item><item><title><![CDATA[Hacktoberfest:  Can  I  ask  a  question?]]></title><description><![CDATA[<p>I have a friend who is doing their first open source contribution through <a href="https://hacktoberfest.digitalocean.com/?ref=kamranicus.com">Hacktoberfest</a> this year, which is a yearly event to encourage open source participation.</p><p>In their email, they asked:</p><blockquote>Is there any etiquette or expectation for how much coding help one can ask for when taking up an</blockquote>]]></description><link>https://kamranicus.com/hacktoberfest-can-i-ask-a-question/</link><guid isPermaLink="false">60557b25a8d4cc003baa83eb</guid><category><![CDATA[Open Source]]></category><dc:creator><![CDATA[Kamran Ayub]]></dc:creator><pubDate>Thu, 08 Oct 2020 13:00:00 GMT</pubDate><content:encoded><![CDATA[<p>I have a friend who is doing their first open source contribution through <a href="https://hacktoberfest.digitalocean.com/?ref=kamranicus.com">Hacktoberfest</a> this year, which is a yearly event to encourage open source participation.</p><p>In their email, they asked:</p><blockquote>Is there any etiquette or expectation for how much coding help one can ask for when taking up an issue and contributing?</blockquote><p>What a good question! Imagine yourself feeling pretty psyched to take on a Hacktoberfest issue and when you start to dig in to understand the codebase, you start to feel intimidated.</p><p>Is it OK to ask for help? Would that be rude to the maintainers?</p><p>The short answers are <strong>YES</strong> and <strong>NO</strong> (<em>hopefully</em>). Here is my longer answer!</p><h2 id="hacktoberfest-projects-expect-newcomers">Hacktoberfest projects expect newcomers</h2><p>There was a little bit of drama this year because historically projects did not necessarily opt into Hacktoberfest and were being spammed with low-effort PRs (or outright useless PRs). However, the <a href="https://hacktoberfest.digitalocean.com/hacktoberfest-update?ref=kamranicus.com">rules have changed</a> and now repositories can opt-in in several ways:</p><p>They can have a <code>hacktoberfest</code> topic in their GitHub project, like this:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2021/03/image-2.png" class="kg-image" alt loading="lazy" width="310" height="275"></figure><p>Or, maintainers can mark accepted PRs with the <code>hacktoberfest-accepted</code>, like this:</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2021/03/image-3.png" class="kg-image" alt loading="lazy" width="348" height="38"></figure><p>If one of these is true, the project is considered opted-in to Hacktoberfest.</p><p>My expectation as a contributor would be that if a project opted into Hacktoberfest, especially with the <strong>topic</strong> approach, they are <em>expecting</em> to help you with making quality contributions.</p><p>Now since we at <a href="https://github.com/excaliburjs/Excalibur?ref=kamranicus.com">Excalibur</a> have always opted-in each year, I can tell you that <strong>we expect people to ask questions!</strong> We try to triage and choose potential Hacktoberfest issues ahead of time (heh, in &quot;Preptember&quot;) but we know that people will likely have questions.</p><p>After all, first-time contributors can&apos;t be expected to have answers to everything, of course you will need some help along the way. This is especially true for anything beyond mere typos.</p><h2 id="but-will-my-question-be-welcomed">But will my question be welcomed?</h2><p>Now this is getting at a different issue, unrelated to Hacktoberfest specifically and more related to toxicity in the open source world. And let&apos;s be real: open source can be toxic.</p><p>Here&apos;s what I recommend to help avoid a toxic project until it&apos;s too late: &quot;scout&quot; a potential project beforehand. Does it have a Code of Conduct? Do the maintainers seem to follow it? How do they react to external contributors asking questions? Are they helpful? How do they review their own code?</p><p>Doing some prep work on your part beforehand I think will help having to ask this question in the first place.</p><h2 id="where-should-i-ask">Where should I ask?</h2><p>Start with the CONTRIBUTING document of the project or the CODE_OF_CONDUCT document. An actively maintained project that works with external contributors will typically lay out the expectations for issues and pull requests and may tell you where the best place to get help is. With the recent <a href="https://github.blog/2020-05-06-new-from-satellite-2020-github-codespaces-github-discussions-securing-code-in-private-repositories-and-more/?ref=kamranicus.com">introduction of GitHub Discussions</a>, that may also be the first place to look if the project is opted-into the beta.</p><figure class="kg-card kg-image-card"><img src="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2021/03/image-4.png" class="kg-image" alt loading="lazy" width="1242" height="525"></figure><p>GitHub Discussions is slowly being made available on a opt-in basis</p><p>If there&apos;s no clear place to start, I recommend first asking within issues, assuming there&apos;s a Hacktoberfest issue you&apos;re taking. This is also where you&apos;d volunteer to take the issue (like, &quot;&#x1F64B;&#x200D;&#x2642;&#xFE0F; Hey, I&apos;d love to try and take this on!&quot;).</p><p>At that point, maintainers should see your interest and expect that you&apos;ll have questions as you get farther into it. If you have questions right away, like about the design, proposal, or a general question, the issue is where I&apos;d ask.</p><p>If you have a draft pull request (and I always recommend starting with a draft!) then that is where I&apos;d start asking questions about the approach, feedback on your current solution, etc. A draft pull request is like your writing drafts -- do you send them to your friends to read or an editor? Of course! So get feedback early in the process.</p><h2 id="time-to-explore-the-cave-dont-go-it-alone">Time to explore the cave, don&apos;t go it alone!</h2><p>I like to think of exploring a codebase like exploring a cave system (or &quot;spelunking&quot; if you&apos;re a fancypants). You reveal one area, then reveal the next and that might have any number of offshoot paths or trails back to previous areas, etc. You can get lost in a codebase just as you would a cave system.</p><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://storage.ghost.io/c/07/bf/07bff1af-b313-422e-a6dd-29c14b2842f2/content/images/2021/03/image-5.png" class="kg-image" alt loading="lazy" width="1920" height="1282"><figcaption>Photo by <a href="https://unsplash.com/@sortino?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Joshua Sortino</a> on <a href="https://unsplash.com/wallpapers/nature/cave?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></figcaption></figure><p>A maintainer is the cave guide. Or maybe just someone who&apos;s really been in and out of the cave a ton. Or maybe <strong>really, really</strong> likes this one area of the cave and knows it blindfolded. The metaphor sort of falls apart because who made the cave, anyway? Nevermind. &#x1F92B;</p><p>When you ask questions while you are working on a codebase (of any size), it&apos;s like you are wandering into the cave and <em>then</em> asking for the maintainer to fill in parts of the cave map, throw you some new flashlight batteries, and tell you, &quot;This is the safe path through. Avoid this one, though!&quot;</p><p>Being a newcomer though brings fresh perspective. You might find parts of the cave no one has been through in awhile that could use some dusting. Maybe there&apos;s a new pathway the cave has opened up that could be faster than an existing pathway that&apos;s well-trodden. You get the picture -- when you ask questions, you may inspire new and better solutions to existing problems maintainers haven&apos;t considered.</p><p>It&apos;s definitely fun to explore a cave on your own at first but it won&apos;t be long until your flashlight dies. Ask for help and recharge those batteries! It results in a more agreeable outcome for everyone. Huzzah.</p><h2 id="what-to-avoid">What to avoid</h2><p>I can&apos;t speak for all maintainers but I think it&apos;s safe to say that generally maintainers welcome quality contributions (of any kind!).</p><p>When you don&apos;t ask questions you may end up going down a road that could lead to maintainbility or design issues you haven&apos;t foreseen. Remember: this might be your first (and only) time in the cave, it&apos;s better to go in prepared!</p><h2 id="always-be-asking">Always Be Asking</h2><p>I&apos;d be <strong>surprised</strong> if you didn&apos;t have a question about a new codebase. Actually, it would be unbelievable. Of course you will.</p><p>I get it, though. You might be listening to that voice in your head, &quot;You can&apos;t ask a question like this, it&apos;s a silly question, I will look like a fool, and what will they think of me?&quot; <strong>Don&apos;t listen!</strong></p><p>Listen, <strong>it is not a sign of weakness to ask questions, it is how we learn.</strong> Doesn&apos;t matter if you&apos;re brand new to open source, new in your role, or a Principal Engineer -- we ask questions to understand better so <strong>Always Be Asking</strong>. If anyone (person, company, etc.) expresses to you that asking a question is some sign of weakness, run the other way.</p><p>If this is your first time contributing to open source, first, how exciting! Second, I lay out everything I just mentioned in more detail in my course on <a href="https://bit.ly/PSContributingToOpenSource?ref=kamranicus.com">Contributing to an Open Source Project on GitHub</a>. It&apos;s an A-to-Z guide geared toward new open source contributors with practical scenarios, workflows, and a guided process on choosing the right project for you to work on.</p>]]></content:encoded></item><item><title><![CDATA[Fixing JavaScript heap out of memory with WebdriverIO]]></title><description><![CDATA[<p>Throughout <a href="https://bit.ly/KamranOnPluralsight?ref=kamranicus.com">working on a course</a> you usually run into bugs and issues that throw you for a loop for awhile. </p><p>In this case, it was doubly frustrating because I had previously set up <a href="https://webdriver.io/?ref=kamranicus.com">webdriverio</a> to run in my continuous integration environment (GitHub Actions) and <em>it was working fine</em>. Until it</p>]]></description><link>https://kamranicus.com/fixing-javascript-heap-out-of-memory-with-webdriverio/</link><guid isPermaLink="false">6055f232a8d4cc003baa8456</guid><category><![CDATA[Troubleshooting]]></category><category><![CDATA[Open Source]]></category><dc:creator><![CDATA[Kamran Ayub]]></dc:creator><pubDate>Wed, 29 Jul 2020 13:50:00 GMT</pubDate><content:encoded><![CDATA[<p>Throughout <a href="https://bit.ly/KamranOnPluralsight?ref=kamranicus.com">working on a course</a> you usually run into bugs and issues that throw you for a loop for awhile. </p><p>In this case, it was doubly frustrating because I had previously set up <a href="https://webdriver.io/?ref=kamranicus.com">webdriverio</a> to run in my continuous integration environment (GitHub Actions) and <em>it was working fine</em>. Until it stopped working.</p><p>I kept getting this error:</p><pre><code>&lt;--- Last few GCs ---&gt;

[168:0x5ba0970]   105925 ms: Mark-sweep (reduce) 2046.6 (2050.9) -&gt; 2045.7 (2050.9) MB, 588.4 / 0.0 ms  (average mu = 0.053, current mu = 0.021) allocation failure scavenge might not succeed

[0-0] 
&lt;--- JS stacktrace ---&gt;

[0-0] FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
[0-0]  1: 0xa3ac30 node::Abort() [/usr/local/bin/node]
[0-0]  2: 0x98a45d node::FatalError(char const*, char const*) [/usr/local/bin/node]
[0-0]  3: 0xbae25e v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [/usr/local/bin/node]
[0-0]  4: 0xbae5d7 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [/usr/local/bin/node]
[0-0]  5: 0xd56125  [/usr/local/bin/node]
[0-0]  6: 0xd56acb v8::internal::Heap::RecomputeLimits(v8::internal::GarbageCollector) [/usr/local/bin/node]
[0-0]  7: 0xd6481c v8::internal::Heap::PerformGarbageCollection(v8::internal::GarbageCollector, v8::GCCallbackFlags) [/usr/local/bin/node]
[0-0]  8: 0xd65684 v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [/usr/local/bin/node]
[0-0]  9: 0xd680fc v8::internal::Heap::AllocateRawWithRetryOrFailSlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [/usr/local/bin/node]
[0-0] 10: 0xd2f3aa v8::internal::Factory::AllocateRaw(int, v8::internal::AllocationType, v8::internal::AllocationAlignment) [/usr/local/bin/node]
[0-0] 11: 0xd29254 v8::internal::FactoryBase&lt;v8::internal::Factory&gt;::AllocateRawWithImmortalMap(int, v8::internal::AllocationType, v8::internal::Map, v8::internal::AllocationAlignment) [/usr/local/bin/node]
[0-0] 12: 0xd2a789 v8::internal::FactoryBase&lt;v8::internal::Factory&gt;::NewStruct(v8::internal::InstanceType, v8::internal::AllocationType) [/usr/local/bin/node]
[0-0] 13: 0xd36be6 v8::internal::Factory::NewStackTraceFrame(v8::internal::Handle&lt;v8::internal::FrameArray&gt;, int) [/usr/local/bin/node]
[0-0] 14: 0xc28a98  [/usr/local/bin/node]
[0-0] 15: 0xc2fb66 v8::internal::Builtin_CallSitePrototypeToString(int, unsigned long*, v8::internal::Isolate*) [/usr/local/bin/node]
[0-0] 16: 0x13f5159  [/usr/local/bin/node]
</code></pre><p>What was <strong>weird</strong> is that the tests all passed!</p><p>It did not <a href="https://gregtyler.co.uk/blog/fatal-error-allocation-failed-error-with-webdriverio-and-cucumber?ref=kamranicus.com">turn out to be a Selenium Grid issue</a>, as I wasn&apos;t using that but it turned out to be another simple fix (i.e. nothing to do with memory size).</p><p><strong>I was missing the <code>@wdio/sync</code> package from my package dependencies after I split out my e2e tests into a separate folder.</strong></p><p>Once I added <code>@wdio/sync</code> back, things worked.</p><pre><code class="language-diff">&quot;dependencies&quot;: {
  &quot;@wdio/cli&quot;: &quot;6.1.24&quot;,
  &quot;@wdio/local-runner&quot;: &quot;6.1.24&quot;,
  &quot;@wdio/mocha-framework&quot;: &quot;6.1.19&quot;,
  &quot;@wdio/spec-reporter&quot;: &quot;6.1.23&quot;,
+ &quot;@wdio/sync&quot;: &quot;6.1.14&quot;
}
</code></pre><p>The telltale sign was that the tests were taking 1 minute when before I checked the logs and the tests used to run in 4 seconds. <strong>Ding, ding, ding!</strong> It must not wait properly for the commands without the sync package and uses up more and more memory.</p><p>Hope this helps anyone in a similar situation!</p>]]></content:encoded></item><item><title><![CDATA[Controlling Browser Permissions in Cypress End-to-End Tests]]></title><description><![CDATA[<p>I am excited to release a new open source package <a href="https://npmjs.com/package/cypress-browser-permissions?ref=kamranicus.com">cypress-browser-permissions</a>. &#x1F389; You can view it on GitHub at <a href="https://github.com/kamranayub/cypress-browser-permissions?ref=kamranicus.com">kamranayub/cypress-browser-permissions</a>.</p><p>This package solves a real need when testing more sophisticated applications when using <a href="https://cypress.io/?ref=kamranicus.com">Cypress</a>, the end-to-end testing framework. It helps control the permission level of various browser features such</p>]]></description><link>https://kamranicus.com/cypress-browser-permissions/</link><guid isPermaLink="false">605c1a11334bec003b6465c6</guid><category><![CDATA[Open Source]]></category><category><![CDATA[Cypress]]></category><category><![CDATA[Testing]]></category><dc:creator><![CDATA[Kamran Ayub]]></dc:creator><pubDate>Fri, 17 Jul 2020 02:00:00 GMT</pubDate><media:content url="https://kamranicus.ghost.io/content/images/2021/03/87799786-42456780-c813-11ea-9e84-9a4ff71b72c8.png" medium="image"/><content:encoded><![CDATA[<img src="https://kamranicus.ghost.io/content/images/2021/03/87799786-42456780-c813-11ea-9e84-9a4ff71b72c8.png" alt="Controlling Browser Permissions in Cypress End-to-End Tests"><p>I am excited to release a new open source package <a href="https://npmjs.com/package/cypress-browser-permissions?ref=kamranicus.com">cypress-browser-permissions</a>. &#x1F389; You can view it on GitHub at <a href="https://github.com/kamranayub/cypress-browser-permissions?ref=kamranicus.com">kamranayub/cypress-browser-permissions</a>.</p><p>This package solves a real need when testing more sophisticated applications when using <a href="https://cypress.io/?ref=kamranicus.com">Cypress</a>, the end-to-end testing framework. It helps control the permission level of various browser features such as:</p><ul><li>Desktop Notifications</li><li>Geolocation</li><li>Images</li><li>Camera</li><li>Microphone</li><li>etc.</li></ul><figure class="kg-card kg-image-card"><img src="https://kamranicus.ghost.io/content/images/2021/03/87500464-2169f000-c622-11ea-8dbb-a480a6f137ac.png" class="kg-image" alt="Controlling Browser Permissions in Cypress End-to-End Tests" loading="lazy"></figure><h2 id="how-to-use-it">How to Use It</h2><p>To get started, you&apos;ll need to install the package and you&apos;ll need Cypress installed already.</p><pre><code class="language-bash">npm i cypress cypress-browser-permissions --save-dev
</code></pre><p>If this is your first time installing Cypress, you&apos;ll need to run it once to generate a project structure:</p><pre><code class="language-bash">npx cypress open
</code></pre><p>Then, you need to initialize the plugin to hook it into Cypress&apos; plugin pipeline. In <code>cypress/plugins/index.js</code>, modify it as follows:</p><pre><code class="language-diff">+ const { cypressBrowserPermissionsPlugin } = require(&apos;cypress-browser-permissions&apos;)

/**
 * @type {Cypress.PluginConfig}
 */
module.exports = (on, config) =&gt; {
  // `on` is used to hook into various events Cypress emits
  // `config` is the resolved Cypress config
+ config = cypressBrowserPermissionsPlugin(on, config);
+ return config;
};
</code></pre><p>Now you will have the ability to control various permissions for Chrome, Edge, and Firefox using <a href="https://docs.cypress.io/guides/guides/environment-variables.html?ref=kamranicus.com">Cypress environment variables</a>.</p><p>For example, if you want to just set permissions for your project you can do so in <code>cypress.json</code>:</p><pre><code class="language-json">{
  &quot;env&quot;: {
    &quot;browserPermissions&quot;: {
      &quot;notifications&quot;: &quot;allow&quot;,
      &quot;geolocation&quot;: &quot;allow&quot;
    }
  }
}
</code></pre><p>The plugin will read the permission settings and apply them when launching the browser. It will also reset between launches since modifying the browser profile is persisted across sessions.</p><p>You can read more about <a href="https://github.com/kamranayub/cypress-browser-permissions?ref=kamranicus.com">supported permissions and values in the README</a>.</p><h3 id="writing-an-end-to-end-notification-test">Writing an End-to-End Notification Test</h3><p>So let&apos;s try it out! Once I finish my <strong>Testing Progressive Web Apps</strong> <a href="http://bit.ly/KamranOnPluralsight?ref=kamranicus.com">Pluralsight course</a>, it will come with an open source sample app. In the meantime, we can write a basic test to see if permissions are working. This same test is included in the repo.</p><p>First, we have an HTML file that uses <code>window.Notification</code> to display a desktop notification:</p><p><strong>cypress/html/notification.html</strong></p><pre><code class="language-html">&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
    &lt;title&gt;Cypress Notification Test&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;script type=&quot;text/javascript&quot;&gt;
        const n = new window.Notification(&apos;test&apos;, { body: &apos;This is a test!&apos; })
        n.addEventListener(&apos;show&apos;, (e) =&gt; {
            window.__CypressNotificationShown = e;
        })
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre><p>You can learn more about how the <a href="https://developer.mozilla.org/en-US/docs/Web/API/notification?ref=kamranicus.com">Notification API</a> works but what we are doing is immediately triggering a notification. Once the browser shows the toast, it triggers the <code>show</code> event on the <code>Notification</code> instance. Since Cypress is awesome and we can hook directly into the <code>window</code> object, we set a callback value globally that we can then inspect/wait for in our test.</p><p>If you have a blank Cypress project you do not even need a server as Cypress will automatically host the root of the project when there is no other configuration.</p><p>Save the <code>notification.html</code> file under <code>cypress/html</code> and then we can visit that page in the test.</p><p>We can create a test suite in <code>cypress/integration</code>:</p><p><strong>cypress/integration/notification.test.js</strong></p><pre><code class="language-js">import { isPermissionAllowed } from &apos;cypress-browser-permissions&apos;;

describe(&quot;notifications&quot;, () =&gt; {
    it(&quot;should be enabled&quot;, () =&gt; {
        expect(isPermissionAllowed(&quot;notifications&quot;)).to.be.true;
    })

    // Only test notification showing in &quot;headed&quot; browsers, which also
    // works in CI :tada:
    Cypress.browser.isHeaded &amp;&amp; it(&quot;should display desktop notification&quot;, () =&gt; {
    
        // Visit the page we created previously
        cy.visit(&apos;/cypress/html/notification.html&apos;)
        
        // Wait for the window callback to populate with the event data
        cy.window().its(&apos;__CypressNotificationShown&apos;).should(&apos;exist&apos;);
    })
})
</code></pre><p>Now we can run our tests:</p><pre><code class="language-bash">npx cypress open
</code></pre><p>That&apos;s all! If <code>browserPermissions.notifications</code> is set to <code>allow</code> then our test should pass:</p><figure class="kg-card kg-image-card"><img src="https://kamranicus.ghost.io/content/images/2021/03/87737665-3620c200-c7a1-11ea-8429-73bd40c99bed.png" class="kg-image" alt="Controlling Browser Permissions in Cypress End-to-End Tests" loading="lazy"></figure><p>And a notification will be shown!</p><figure class="kg-card kg-image-card"><img src="https://kamranicus.ghost.io/content/images/2021/03/87737706-52bcfa00-c7a1-11ea-893e-9f6f8dec1e2e.png" class="kg-image" alt="Controlling Browser Permissions in Cypress End-to-End Tests" loading="lazy"></figure><h3 id="how-it-works">How It Works</h3><p>In Cypress, <a href="https://docs.cypress.io/api/plugins/browser-launch-api.html?ref=kamranicus.com#Modify-browser-launch-arguments-preferences-and-extensions">you have control over the launch preferences for browsers</a>, so the magic lies in <em>what preferences to pass to each browser.</em></p><p>This topic is not heavily documented <a href="https://github.com/cypress-io/cypress/issues/2671?ref=kamranicus.com">as evidenced by this open issue in the Cypress repo</a> I came across while researching this. It has been open since 2018 with no one mentioning the ability to control launch preferences.</p><p>Thanks to BrowserStack for <a href="https://www.browserstack.com/automate/handle-popups-alerts-prompts-in-automated-tests?ref=kamranicus.com">documenting some of these permissions</a> as well as these StackOverflow posts:</p><ul><li><a href="https://stackoverflow.com/questions/55435198/selenium-python-allow-firefox-notifications?ref=kamranicus.com">Selenium + Python Allow Firefox Notifications</a></li><li><a href="https://stackoverflow.com/questions/48007699/how-to-allow-or-deny-notification-geo-location-microphone-camera-pop-up?ref=kamranicus.com">How to allow or deny notification geo-location microphone camera pop up</a></li></ul><p>I was able to piece together the information needed to tackle this with a Cypress plugin. Since each browser family uses different preferences, I thought it would be best to abstract it.</p><h3 id="whats-next">What&apos;s Next?</h3><p>My hope is that this package is <em>actually short-lived</em> and the Cypress team can incorporate these permission settings into the core of the product, since it&apos;s such an important feature especially when testing new, modern APIs.</p><p>There will be a <strong>full sample</strong> of using Cypress with this plugin (as well as other black magicks such as bypassing service workers and more!) in my <em>Testing Progressive Web Apps</em> course soon on <a href="https://bit.ly/KamranOnPluralsight?ref=kamranicus.com">Pluralsight</a>. It should be released in August, you can follow me there to get notified when it releases. The sample app will be open source on GitHub so you&apos;ll be able to reference it &#x1F44D;</p>]]></content:encoded></item><item><title><![CDATA[Storyflow: Using Storybook to Build a Better... Game Engine?]]></title><description><![CDATA[<p>Do you use <a href="https://storybook.js.org/?ref=kamranicus.com">Storybook</a>? Do you use it to test game engines? Didn&apos;t think so! But we do for <a href="https://excaliburjs.com/?ref=kamranicus.com">Excalibur.js</a> and I presented how and why we did it. The genesis for the talk came from a workflow I&apos;ve been using recently that you can</p>]]></description><link>https://kamranicus.com/storybook-for-game-engines/</link><guid isPermaLink="false">605c1a0f334bec003b6465b1</guid><category><![CDATA[Talks]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Storybook]]></category><dc:creator><![CDATA[Kamran Ayub]]></dc:creator><pubDate>Tue, 26 May 2020 13:30:00 GMT</pubDate><content:encoded><![CDATA[<p>Do you use <a href="https://storybook.js.org/?ref=kamranicus.com">Storybook</a>? Do you use it to test game engines? Didn&apos;t think so! But we do for <a href="https://excaliburjs.com/?ref=kamranicus.com">Excalibur.js</a> and I presented how and why we did it. The genesis for the talk came from a workflow I&apos;ve been using recently that you can implement in your own projects which I call &quot;Storyflow.&quot;</p><h3 id="watch-the-talk">Watch the Talk</h3><p>I gave this talk at MN Dev Con on May 4 and again at React Minneapolis on May 21:</p><figure class="kg-card kg-embed-card"><iframe width="560" height="315" src="https://www.youtube.com/embed/biSFvQmMJsc" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></figure><h3 id="what-is-storyflow">What is Storyflow?</h3><p>The Storybook workflow (ahem, <em>Storyflow</em>) we follow puts Storybook stories at the center of our workflow. The concept is simple in practice: write your unit and functional tests <em>against Storybook</em> instead of importing directly from component files like other workflows.</p><figure class="kg-card kg-image-card"><img src="https://kamranicus.ghost.io/content/images/2021/03/2020-05-21-storyflow.png" class="kg-image" alt="Storyflow: write tests against Storybook" loading="lazy"></figure><p>The &quot;normal&quot; component-based workflow goes like:</p><ol><li>Write my component in <code>MyComponent.js</code></li><li>Write my unit test, <code>MyComponent.test.js</code> and import <code>MyComponent</code></li><li>Write a functional test, against my running app, which may test one aspect of <code>MyComponent</code> in use or maybe a few different behaviors/states</li></ol><p>But with <a href="https://storybook.js.org/?ref=kamranicus.com">Storybook</a> the workflow instead flips the script and centers your testing on isolated components through stories:</p><ol><li>Write my component in <code>MyComponent.js</code></li><li>Write my Storybook story <code>DefaultState</code> in <code>MyComponent.stories.js</code> and import <code>MyComponent</code></li><li>Write my unit test, <code>MyComponent.test.js</code> <strong>and import <code>DefaultState</code> from stories</strong></li><li>Write a functional test against <code>DefaultState</code> story in Storybook</li></ol><p>It&apos;s comes down to a <em>slight</em> shift in thinking. Without Storybook, normally you&apos;d be putting your component into different states <em>within</em> your unit tests. <em>With</em> Storybook, you&apos;re already showcasing your component in different states, which makes your unit tests a lot trimmer. Finally, you can add UI testing on top of it to ensure your component works in the browser (without having to manually verify in Storybook).</p><p>There are 3 major benefits we&apos;ve seen from this:</p><ol><li>One source of truth for all our tests (stories)</li><li>Incentivizes writing more stories</li><li>Promotes more testability</li></ol><h3 id="one-source-of-truth">One source of truth</h3><p>Since your unit and functional tests are run against stories, Storybook becomes the source of truth for any tests. Contrast that to developing without stories, where each test could render components in different states and the only way to know would be to examine each test. Having a published Storybook as a static site makes things a lot more discoverable.</p><h3 id="incentivizes-writing-more-stories">Incentivizes writing more stories</h3><p>Since tests are written <em>against</em> stories, in order to write more tests... you&apos;ll need to write more stories. Having more stories means better documentation (even if it&apos;s just code!).</p><h3 id="promotes-more-testability">Promotes more testability</h3><p>In order to write a solid Storybook story, it has to run in isolation. That means that you will likely lift up more heavy concerns like data fetching, state management, and other stuff higher and have more atomic reusable components. We actually still write stories against our &quot;container&quot; components but it requires a lot more mocking using <a href="https://storybook.js.org/docs/basics/writing-stories/?ref=kamranicus.com#decorators">Storybook decorators</a>.</p><h2 id="example">Example</h2><p>If you&apos;re curious to see this in action, I have a <a href="https://github.com/kamranayub/example-storyflow?ref=kamranicus.com">GitHub repository</a> set up you can clone and run and I showcase this workflow within the video above.</p><p>You can view a working demo on CodeSandbox using the repository!</p><figure class="kg-card kg-embed-card"><iframe src="https://codesandbox.io/embed/github/kamranayub/example-storyflow/tree/master/?fontsize=14&amp;hidenavigation=1&amp;module=%2Fsrc%2FLoginForm.stories.js&amp;theme=dark" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" title="storybook-testing-example" allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts allow-autoplay"></iframe></figure><p>Let me know in the comments if you&apos;ve used this workflow and how it&apos;s been working for you!</p>]]></content:encoded></item><item><title><![CDATA[Role of Indexing in RavenDB vs. MongoDB and PostgreSQL]]></title><description><![CDATA[<p>I recently was engaged to write an article on how RavenDB indexing compares to MongoDB and PostgreSQL. I had a lot of fun writing it because I finally got to dive deeper into how MongoDB and PostgreSQL query engines work under the covers. I was actually surprised to be honest,</p>]]></description><link>https://kamranicus.com/ravendb-indexing-mongodb-postgresql/</link><guid isPermaLink="false">605c1a0f334bec003b6465b9</guid><category><![CDATA[Articles]]></category><category><![CDATA[RavenDB]]></category><category><![CDATA[MongoDB]]></category><category><![CDATA[NoSQL]]></category><dc:creator><![CDATA[Kamran Ayub]]></dc:creator><pubDate>Mon, 25 May 2020 15:00:00 GMT</pubDate><content:encoded><![CDATA[<p>I recently was engaged to write an article on how RavenDB indexing compares to MongoDB and PostgreSQL. I had a lot of fun writing it because I finally got to dive deeper into how MongoDB and PostgreSQL query engines work under the covers. I was actually surprised to be honest, I thought that MongoDB was more similar to RavenDB but it ended up being quite different.</p><p>Read more about <a href="https://ravendb.net/articles/nosql-document-database-indexing?ref=kamranicus.com">the role indexing plays in RavenDB vs. MongoDB and PostgreSQL</a></p><p>If this interests you be sure to check out my <a href="http://bit.ly/PSRavenDB4?ref=kamranicus.com">Getting Started with RavenDB 4</a> course on Pluralsight.</p>]]></content:encoded></item><item><title><![CDATA[Frontend Masters Workshop: React Native Recap]]></title><description><![CDATA[<p>Today I attended the local Frontend Masters <a href="https://frontendmasters.com/workshops/react-native-v2/?ref=kamranicus.com">in-person workshop</a> for React Native by <a href="https://twitter.com/kadikraman?ref=kamranicus.com">Kadi Kraman</a>. First, I have to point out her last name is my first name with a couple letters swapped. Too funny. Next, it was great! &#x1F389; I had a lot of fun.</p><p>I&apos;ve <a href="https://github.com/kamranayub/sample-react-native-workshop-expo-ts?ref=kamranicus.com">posted</a></p>]]></description><link>https://kamranicus.com/frontend-masters-react-native/</link><guid isPermaLink="false">605c1a0d334bec003b6465a3</guid><category><![CDATA[React Native]]></category><category><![CDATA[React]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[Workshops]]></category><dc:creator><![CDATA[Kamran Ayub]]></dc:creator><pubDate>Sat, 07 Mar 2020 02:20:00 GMT</pubDate><content:encoded><![CDATA[<p>Today I attended the local Frontend Masters <a href="https://frontendmasters.com/workshops/react-native-v2/?ref=kamranicus.com">in-person workshop</a> for React Native by <a href="https://twitter.com/kadikraman?ref=kamranicus.com">Kadi Kraman</a>. First, I have to point out her last name is my first name with a couple letters swapped. Too funny. Next, it was great! &#x1F389; I had a lot of fun.</p><p>I&apos;ve <a href="https://github.com/kamranayub/sample-react-native-workshop-expo-ts?ref=kamranicus.com">posted my lab repository</a> on GitHub for anyone else to peruse, notably I followed along in TypeScript as a way to better explore the APIs and see how the TypeScript experience was.</p><h3 id="the-workshop">The workshop</h3><p>Kadi works at Formidable Labs as an engineer and she specializes in React Native applications. Her workshop was focused on getting started with React Native and we built a &quot;color theme&quot; app that allows you to view color schemes and add a new one. Since I use React daily, it definitely felt familiar but Kadi went over a lot of the foundational concepts just in case attendees weren&apos;t as familiar.</p><h3 id="what-i-took-away">What I took away</h3><p>Using React day-to-day I feel like I know it pretty well but I had never sat down to actually look into React Native. What I appreciate about workshops like this is the ability to take dedicated time and go end-to-end with something, with an instructor who knows their subject and can help you not waste time.</p><p>As far as the <em>motivation</em> to learn it, first, I&apos;m always interested in learning new things. I&apos;m a lifelong learner. But second, I was interested in comparing it to my experience building native Windows Phone apps <a href="https://kamranicus.com/projects">back in the day</a> as well as some passing familiarity with <a href="https://ionicframework.com/?ref=kamranicus.com">Ionic</a>, <a href="https://dotnet.microsoft.com/apps/xamarin?ref=kamranicus.com">Xamarin</a> and <a href="https://cordova.apache.org/?ref=kamranicus.com">Cordova</a> apps.</p><p>I was <em>very impressed</em> by how quick it was to run an app using the Expo CLI. I had the app running on my phone within a few minutes. That&apos;s awesome! Installing traditional React Native was definitely more of a chore, even when I had Xcode installed previously.</p><p>The workshop covered a lot of topics, enough to build out simple applications and I especially liked the familiar <code>fetch</code> examples without having to bring in complicated dependencies. In other words, coding in React Native was very familiar for a traditional React developer. I felt like the workshop taught me enough where I could fumble around and build out the app more without feeling like I was overwhelmed.</p><p>I think I would attend an Advanced React Native workshop in the future because of course my mind went to all sorts of questions that were beyond the scope of what time we had:</p><ol><li>How do you change the UI in response to the running platform? e.g. show the &quot;Floating Action Button&quot; in Android vs. a button in iOS</li><li>How hard <em>is it</em> to add a native module?</li><li>How do you handle device storage? That was a huge pain for me in Windows Phone (multithreading + I/O, ugh).</li><li>How do you write tests? React Native Testing Library and Detox were mentioned but we didn&apos;t have time to cover them</li><li>How do you bring in a design system that can work well cross-platform? I was glad she mentioned <code>styled-components</code> is supported and I also see <a href="https://github.com/callstack/react-native-paper?ref=kamranicus.com">Paper</a> seems well-established.</li></ol><p>So Marc, if you&apos;re reading this, invite Kadi again next year for some Advanced React Native &#x1F60E;</p><h3 id="any-improvements-or-feedback">Any improvements or feedback?</h3><p>I appreciated Kadi&apos;s attention to realism -- she did go ahead and run through the installation steps as a new developer would and there <em>were</em> roadblocks she ran into that she showed how to resolve. At the same time, it <em>would</em> have been nice to get to testing in a one-day workshop and the installation process took time away from other subjects so it&apos;s a balancing act there. I could see this being a two-day workshop in the future, to cover animations, testing, storage, and deployment on day two.</p><p>The exercises were just right I think -- they were helpful in reinforcing what we learned and were at just the right spots. I enjoyed the stretch goals!</p><p>I love the trend of instructors providing a full workshop site (the <a href="https://www.learnpython.dev/?ref=kamranicus.com">Python Fundamentals workshop</a> was also like this) and I actually really liked how Kadi simply presented from the site itself versus slides. It meant we could follow along, read ahead, or work at our own pace if we needed to without missing anything. Everything was in one place. It was easy to copy code samples and click links for further reading. Her pace was perfect when going through the demos. It was at the right level for someone with previous React experience but I could see by the hook section where it could lose folks who haven&apos;t done a lot of hooks-based development. There were a lot of questions pertaining to hook semantics that came up! I can relate since even I get tripped up by them.</p><h3 id="would-i-use-react-native">Would I use React Native?</h3><p>I was keenly interested in what it might look like to build a React Native app for <a href="https://keeptrackofmygames.com/?ref=kamranicus.com">Keep Track of My Games</a> and after taking the workshop, I feel more confident it could work out well. I can&apos;t say I&apos;m 100% on board yet though since it&apos;s still true that when you need to get down to the nitty-gritty, you might be writing Objective-C/Swift or Java/Kotlin and <em>that&apos;s</em> what sends shivers down my back. I love learning! <em>But I also have limited time!</em></p><p>The biggest question going into the workshop was, <strong>how much could I try to share between my React web code and React Native code</strong>? I think the answer is, not much, unless I changed my web code to use something like <a href="https://github.com/necolas/react-native-web?ref=kamranicus.com">React Native for Web</a>. What I <em>could</em> share are logic modules though but you couldn&apos;t share 3rd party React modules unless they <em>also</em> targeted React Native.</p><p>One thing to consider about Xamarin in particular is that when using <a href="https://docs.microsoft.com/en-us/xamarin/xamarin-forms/?ref=kamranicus.com">Xamarin.Forms</a> it&apos;s C# and .NET up and down <em>the entire stack including the UI</em>. In talking with my friend who has done a lot of Xamarin development you don&apos;t get to ignore XCode or Android Studio but at the same time you don&apos;t need to go and learn 2-4 languages to write native modules.</p><p>At the end of the day, it&apos;ll probably come down to how complicated I think the KTOMG app would get (I&apos;d say, not that complicated...). I think overall the fact is <em>building native mobile apps is hard</em>. Overall the React Native developer experience felt really productive to build out this app. That&apos;s an important factor for me, with limited time to spend wrestling with cross-platform tooling.</p><p>The closest parallel I can draw for mobile development was my experience building my Gatsby-based <a href="https://reachfi.app/?ref=kamranicus.com">savings tracker app</a>. Since that is a Progressive Web App (PWA) using traditional React and Material UI, I <em>loved</em> the experience. I had a fully-working app in a weekend. But the <a href="https://www.simicart.com/blog/pwa-app-stores/?ref=kamranicus.com">deployment of PWAs to app stores</a> is still in a sub-optimal state. Honestly, I hope to rewrite the KTOMG frontend to be a PWA anyway and maybe by the time I do that, PWAs will be first-class citizens on mobile platforms and my decision will be made for me. Always bet on the web... right?</p>]]></content:encoded></item><item><title><![CDATA[Throttling Outgoing Requests in Node.js and .NET Core]]></title><description><![CDATA[<p>I have published two articles recently on a problem I was running into working on <a href="https://keeptrackofmygames.com/?ref=kamranicus.com">Keep Track of My Games</a>. In order to sync user&apos;s Steam collections, I have to call the <a href="https://developer.valvesoftware.com/wiki/Steam_Web_API?ref=kamranicus.com">Steam Web API</a>.</p><p>The Steam Web API implements &quot;rate limiting&quot; meaning that if you</p>]]></description><link>https://kamranicus.com/throttling-outgoing-requests-nodejs-dotnet-core/</link><guid isPermaLink="false">605c1a0d334bec003b646597</guid><category><![CDATA[Programming]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[.NET]]></category><category><![CDATA[C#]]></category><category><![CDATA[JavaScript]]></category><dc:creator><![CDATA[Kamran Ayub]]></dc:creator><pubDate>Thu, 05 Mar 2020 14:00:00 GMT</pubDate><content:encoded><![CDATA[<p>I have published two articles recently on a problem I was running into working on <a href="https://keeptrackofmygames.com/?ref=kamranicus.com">Keep Track of My Games</a>. In order to sync user&apos;s Steam collections, I have to call the <a href="https://developer.valvesoftware.com/wiki/Steam_Web_API?ref=kamranicus.com">Steam Web API</a>.</p><p>The Steam Web API implements &quot;rate limiting&quot; meaning that if you call it too many times too quickly it returns a <code>HTTP 429 Too Many Requests</code> response. According to <a href="https://steamcommunity.com/dev/apiterms?ref=kamranicus.com">the terms</a> the rate limit is 100,000 requests per day, which is pretty generous. But if you&apos;re thinking of syncing 2000 users every 15 minutes, that puts you <strong>two times</strong> over the limit! So you need a throttling mechanism to defer processing once you reach the limit. In most scenarios like this, public APIs will return some useful HTTP headers that let you know what your current request count is but in this case, the Steam API does no such thing (it&apos;s a bit dated).</p><p>There are a few ways to rate limit or <em>throttle</em> outgoing requests to an API like this but most approaches don&apos;t work with <strong>clustering</strong> meaning multiple isolated clients. Approaches like using <a href="https://codeburst.io/throttling-concurrent-outgoing-http-requests-in-net-core-404b5acd987b?ref=kamranicus.com">slim semaphore</a> or <a href="https://www.npmjs.com/package/limiter?ref=kamranicus.com">limiter</a> don&apos;t cut it because those only work <strong>in-memory</strong>. We need a backing store to coordinate counting requests across a cluster. <a href="https://npmjs.com/package/bottleneck?ref=kamranicus.com">Bottleneck</a> is one npm package that supports this but it can only use Redis. Since I don&apos;t use Redis (and I&apos;m using C#) that wasn&apos;t an option for me. Instead I turned to RavenDB for the solution and it&apos;s been working out well!</p><p>I wrote up two guides on achieving this using RavenDB, <a href="https://www.codeproject.com/Articles/5260137/Throttling-Outgoing-HTTP-Requests-in-a-Distributed?ref=kamranicus.com">one for .NET</a> and <a href="https://www.codeproject.com/Articles/5260913/Throttling-Outgoing-Requests-in-Node-js?ref=kamranicus.com">one for Node.js</a>, so if you&apos;re curious how I solved the problem then check them out!</p>]]></content:encoded></item><item><title><![CDATA[Catching Errors with External Commands in PowerShell and Azure DevOps]]></title><description><![CDATA[<p>Here&apos;s a quick tip for a problem I ran into within my Azure DevOps pipeline. I have a job task that executes a PowerShell script (Inline) and that script invokes a <code>git push</code> command to an Azure Kudu Git endpoint that deploys my site:</p><pre><code>git push -u kudu</code></pre>]]></description><link>https://kamranicus.com/azure-devops-remote-command-failure/</link><guid isPermaLink="false">605c1a0c334bec003b64658e</guid><category><![CDATA[PowerShell]]></category><category><![CDATA[DevOps]]></category><category><![CDATA[Azure]]></category><dc:creator><![CDATA[Kamran Ayub]]></dc:creator><pubDate>Thu, 05 Mar 2020 01:00:00 GMT</pubDate><content:encoded><![CDATA[<p>Here&apos;s a quick tip for a problem I ran into within my Azure DevOps pipeline. I have a job task that executes a PowerShell script (Inline) and that script invokes a <code>git push</code> command to an Azure Kudu Git endpoint that deploys my site:</p><pre><code>git push -u kudu HEAD:$env:GITBRANCH
</code></pre><p>I have multiple stages in my pipeline where <code>GITBRANCH</code> environment variable gets set to <code>dev</code>, <code>master</code>, etc. depending on the target environment I&apos;m deploying to.</p><p>The Git command will print this kind of output within the task log:</p><pre><code>remote: Updating branch &apos;master&apos;.
remote: Updating submodules.
remote: Preparing deployment for commit id &apos;xxx&apos;.
remote: Running custom deployment command...
remote: Running deployment command...
remote: Handling .NET Web Application deployment.
</code></pre><p>The problem becomes that even when the remote build <strong>fails</strong>, the <code>git push</code> command technically executes successfully with exit code 0 so PowerShell&apos;s <code>$LASTEXITCODE</code> check doesn&apos;t fail.</p><pre><code>##[debug]$LASTEXITCODE: 0
##[debug]Exit code: 0
</code></pre><p>Since the command executed successfully, Azure does not fail the pipeline and I don&apos;t get notified when my builds actually fail.</p><p>To fix this, we need to somehow capture the output (but still preserve the logs) and check for a specific error string in the output to manually fail the pipeline.</p><p>We&apos;re looking for a log message like this in the output:</p><pre><code>remote: An error has occurred during web site deployment.
remote:
remote: Error - Changes committed to remote repository but deployment to website failed.
</code></pre><p>If the string <code>An error has occurred during web site deployment</code> is present in the command output, we can fail the build.</p><p>So in our PowerShell script, we can take advantage of a neat cmdlet called <a href="tee-object">Tee-Object</a> which I had never heard of, inspired by <a href="so-capture-output">this StackOverflow answer</a>.</p><pre><code class="language-powershell">cmd /c &quot;git push&quot; &apos;2&gt;&amp;1&apos; | Tee-Object -Variable pushOutput

if ($null -ne ($pushOutput | ? { $_ -match &apos;An error has occurred during web site deployment&apos; })) {
  Write-Error &apos;Build failed&apos;
} else {
  Write-Verbose &apos;Build succeeded&apos;
}
</code></pre><p>What <code>Tee-Object</code> does is redirect output to <em>two places</em> (like a T, get it? &#x1F43A;) so we get our output both logged <em>and</em> stored in a variable (a PowerShell string array).</p><p>What we can then do is operate against our variable <code>$pushOutput</code> and match any lines that contain our target string. If there&apos;s a match, it will <em>not equal</em> <code>$null</code> so the condition will pass and we can write an error.</p><blockquote><strong>&#x26A0; HUGE ISSUE ALERT:</strong> Normally you would not need to include the <code>cmd /c</code> and <code>&apos;2&gt;&amp;1&apos;</code> portions of the script to execute the <code>git push</code> but because life is unfair, you can actually run into <a href="gh-tee-object-issue">a specific edge case</a> with git commands where <code>Tee-Object</code> does <strong>not</strong> create a variable when all the output directed to it is stderr output (which is unintuitively the case with a <code>git push</code> to Azure Kudu). It took me several hours of trial and error with different commands to stumble upon <a href="so-tee-not-working">this StackOverflow answer</a> that mentioned the edge case. By using <code>cmd /c</code> and then utilizing its redirection with <code>2&gt;&amp;1</code> it turns it <a href="so-capture-output">all into stderr into stdout</a> for PowerShell&apos;s consumption.</blockquote><p>This will properly fail the Azure pipeline now if our remote build fails, hurray! &#x1F929;</p><p>Hopefully this helps the one person running into the same issues I did!</p><h3 id="links">Links</h3><ul><li><a href="https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/tee-object?ref=kamranicus.com">Microsoft Docs: PowerShell - Tee-Object</a></li><li><a href="https://stackoverflow.com/questions/47523222/tee-object-variable-not-sending-to-stdout?ref=kamranicus.com">StackOverflow: Tee-Object variable not sending to stdout</a></li><li><a href="https://stackoverflow.com/a/35980675/109458?ref=kamranicus.com">StackOverflow: How do I capture the output into a variable from an external process in PowerShell?</a></li><li><a href="https://github.com/PowerShell/PowerShell/issues/5560?ref=kamranicus.com">GitHub PowerShell/PowerShell Issue #5560: Tee-Object should clear the -Variable target variable if no success-stream input is received</a></li></ul>]]></content:encoded></item></channel></rss>