<?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"><channel><title><![CDATA[Learning to Actually Code]]></title><description><![CDATA[In this blog I write about my journey learning what is actually involved in developing robust, maintainable software and writing clean code. I share interesting]]></description><link>https://code-after-degree.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sat, 19 Sep 2026 05:56:20 GMT</lastBuildDate><atom:link href="https://code-after-degree.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Why is Redis so Fast despite being (mostly) single-threaded?]]></title><description><![CDATA[What is Redis?
Redis is one of the most popular and versatile data-stores today, favored for its speed and simplicity. Redis offers features that resemble common data structures, such as strings, list]]></description><link>https://code-after-degree.hashnode.dev/why-is-redis-so-fast-despite-being-mostly-single-threaded</link><guid isPermaLink="true">https://code-after-degree.hashnode.dev/why-is-redis-so-fast-despite-being-mostly-single-threaded</guid><category><![CDATA[Redis]]></category><category><![CDATA[redis-cache]]></category><category><![CDATA[software]]></category><category><![CDATA[software development]]></category><dc:creator><![CDATA[Benjamin Inglis]]></dc:creator><pubDate>Wed, 02 Sep 2026 19:56:09 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/68c19865376514b2a5927314/8a4fa080-df5a-4d1f-a232-dfb77fe4317c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>What is Redis?</h1>
<p>Redis is one of the most popular and versatile data-stores today, favored for its speed and simplicity. Redis offers features that resemble common data structures, such as strings, lists, hashes, sets, sorted sets, streams, and even geospatial indexes. Redis works by storing key-value pairs, such that a key names one value, and the value can be one of the aforementioned structures. Think of it like a directory, where you use a name (the key) to locate information about the person (the value).</p>
<p>For example, you could use a Redis hash to store user data by using a <code>user_id</code> key, then the value would be an object with hash fields mapped to values:</p>
<pre><code class="language-json">"user_123" : {
    "name": "Bob", 
    "age": 40, 
    "email": "bob@gmail.com"
  }
</code></pre>
<h1>What makes Redis so Fast?</h1>
<h2>Leveraging Physical Attributes</h2>
<p>While a variety of factors contribute to Redis’s speedy performance, the first one to understand is in-memory storage. All data in Redis lives in RAM; disk is used only for persisting data, never to serve a read. Think of RAM as your desk: limited space, but everything on it is within arm's reach. Meanwhile, disk storage is the filing room down the hall – far more capacity, but retrieving an item means getting up and walking.</p>
<p>To help quantify this advantage, DRAM access is ~100 ns, NVMe SSD is ~20-100 μs, and spinning disk (HDD) 5-10 ms. That makes DRAM ~200-1000x faster vs. NVMe and ~50,000-100,000x compared to HDD.</p>
<p>While tempting to conclude that Redis outperforms disk-backed databases like Postgres simply because it reads from RAM while the latter reads from disk, this doesn’t reflect how modern databases operate. Indeed, databases contain their own buffer pools (RAM managed directly by the DB) that store recently-accessed pages. Similarly, the <a href="https://en.wikipedia.org/wiki/Kernel_(operating_system)">kernel</a> itself retains recently-read file blocks in RAM automatically, which the database can utilize. Taken together, even disk-backed databases do not primarily read from disk.</p>
<p>The real difference is the overhead required to <em>support</em> a disk representation, which is costly even when the disk is never read. A conventional database must parse and plan the query, consult a buffer manager to locate and pin the page, evaluate <a href="https://www.geeksforgeeks.org/dbms/what-is-multi-version-concurrency-control-mvcc-in-dbms/">MVCC visibility</a>, and deserialize the tuple from its on-disk format. <a href="https://dl.acm.org/doi/10.1145/1376616.1376713">Harizopoulos et al</a>. (2008) benchmarked a conventional database with its data already fully cached in memory, and found that buffer management, latching, locking, and recovery consumed the large majority of instructions, leaving only a small fraction as useful query work. Redis has no on-disk format to translate from, so the structure it queries is the structure in memory.</p>
<p>Thus, our earlier analogy needs an amendment. A conventional database <em>does</em> keep frequently accessed pages on the desk; however, those pages are copies from the filing room, and the office enforces rules about them. Each time you want one, someone has to work out which file you actually need, look up where its copy is being held, and confirm that the version you have is the one you're supposed to read. By contrast, Redis's data is not a copy of anything. There is no filing room, no index, and no checkout procedure. What you see on the desk is the original, and only one.</p>
<img src="https://cdn.hashnode.com/uploads/covers/68c19865376514b2a5927314/6696048e-ea00-4c4f-9521-d3b4851c6c08.png" alt="" style="display:block;margin:0 auto" />

<p>While operating purely from memory contributes to Redis's speed, that alone does not explain what sets it apart; Redis also makes the most of low-level data structures.</p>
<h2>Efficient Data Storage</h2>
<p>Redis <a href="https://redis.io/docs/latest/operate/oss_and_stack/management/optimization/memory-optimization/">stores data incredibly efficiently</a>, with small values being encoded in compact memory formats: listpack (which replaced ziplist in Redis 7.0), intset, and quicklist, that improve CPU cache locality. Hashes and sets are automatically converted to hash tables, lists become quicklists, and sorted sets become skiplists when exceeding the configured max size (<a href="https://redis.io/docs/latest/commands/object-encoding/">Object Encoding docs</a>).</p>
<p>To understand the advantage of these compact memory formats, we first must explore some computer architecture. Random access memory (RAM) is a massive array of bytes. Each byte has a number representing its position in the array, which also serves as the address. When the CPU runs a load instruction, it requests a small number of bytes at a specific address (eight, in the case of a pointer). However, the smallest unit that can be transferred from memory to the CPU is a 64-byte cache line. Memory is pre-divided into 64-byte blocks at fixed boundaries (0-63, 64-127, 128-191, …), so whichever block the address falls into is the one you get, along with everything else inside it.</p>
<p>Between CPU and RAM there are several levels of smaller and faster cache: L1 on the core, L2 beside it, and L3 shared across the chip. Each level is larger and slower than the last – ~1 ns for L1, a few nanoseconds for L2, ~15 ns for L3, and ~100 ns for main RAM. Indeed, a single trip to RAM amounts to about a hundred L1 hits, meaning that even once data is in memory where it sits can influence performance.</p>
<img src="https://cdn.hashnode.com/uploads/covers/68c19865376514b2a5927314/cacca702-678d-4be4-a7aa-847869676991.png" alt="" style="display:block;margin:0 auto" />

<p>Now consider what happens when Redis reads a field from each structure. With a compact memory format such as a listpack, data is stored contiguously, meaning each field exists immediately after the previous. The first access costs a full trip to RAM, returning 64 bytes (most of the structure for a small collection). Since the scan moves forward through memory predictably, the CPU’s prefetcher proactively pulls in subsequent cache lines. Once the bytes are in L1, comparing each field costs ~1 ns, so twenty comparisons add ~20 ns.</p>
<p>On the other hand, a hash table stores its bucket array, entries, its key strings, and its value as separate allocations scattered across the heap. Each hop must be completed before the next address is available, making the four resulting trips to RAM dependent and non-overlapping. Thus, at small sizes, the O(N) scan outperforms the O(1) lookup.</p>
<p>However, the four trips involved in the hash table fetch are constant – they do not grow with the number of fields whereas the listpack’s scan does. After a certain size, the scan cost exceeds the cost of the RAM trips, and the structure stops fitting in the cache, making the comparisons themselves more expensive. Redis resolves this by automatically converting the compact structures into hash tables once they grow past the configured threshold.</p>
<img src="https://cdn.hashnode.com/uploads/covers/68c19865376514b2a5927314/5bfa07c2-0bcd-4069-bb52-08441a76443f.png" alt="" style="display:block;margin:0 auto" />

<p>In addition to clever utilization of data structures, architectural decisions further contribute to Redis's notable performance.</p>
<h2>Single Threaded Architecture</h2>
<p>Redis employs single-threaded command execution, avoiding the complexity and overhead associated with shared-state multithreaded systems: context switching, thread scheduling, lock contention, and even cache lines moving between cores. This design is viable because CPU is rarely the bottleneck for Redis, usually it is memory or network (<a href="https://redis.io/docs/latest/develop/get-started/faq/">FAQ</a>). Since typical Redis commands are very small, adding the cost of coordination (which is roughly the same regardless of the operation size) becomes a bad tradeoff.</p>
<p>Of note, Redis is not strictly a single-threaded process – modern versions use threads for a variety of supporting functions, such as closing files, cleaning memory, or reading/writing to client sockets. Threaded I/O, introduced in Redis 6, parallelizes socket reads, writes and protocol parsing (<a href="https://github.com/redis/redis/blob/unstable/redis.conf"><code>redis.conf</code></a>, THREADED I/O). However, <em>only</em> the main thread executes commands that touch the global keyspace, such as lookups, mutations, and triggering expiry/eviction to name a few. This architecture has the added benefit of preserving atomicity, since only a single thread is modifying Redis’s in-memory data at a given time.</p>
<p>What enables Redis to handle thousands of clients with a single thread is <a href="https://redis.io/docs/latest/develop/reference/clients/">multiplexing and non-blocking I/O.</a> Every client connection is a socket, and the kernel holds a receive buffer for each one.</p>
<p>Redis sets these sockets to non-blocking, so read and write operations return immediately if nothing is ready rather than blocking the thread. It then registers all of them with an event notification interface such as <a href="https://en.wikipedia.org/wiki/Epoll"><code>epoll</code></a>, asking to be notified when one of them has something ready for it, then sleeps. A socket is <em>ready</em> when a <code>read</code> or <code>write</code> wouldn't halt execution: readable means that at least one byte has arrived, writeable means there's room in socket's send buffer. Because the kernel is the one putting arriving bytes into the buffers, it can add the socket to a ready list at that moment. When data arrives for one of those sockets, the kernel wakes Redis, which checks the short list of ready descriptors, runs the handler registered for each, and goes back to sleep. As a result, the thread never blocks on any individual client – it only pauses when no client has anything ready.</p>
<p>A helpful analogy is a restaurant with forty tables (client connections) and a single waiter (the Redis thread). If the waiter were to approach the table and wait until everyone was ready to order, this prevents the waiter from being able to attend other tables while they deliberate. Non-blocking here translates to the waiter asking first if a table is ready, and if not, he leaves. The issue is that the waiter now has to continuously walk the floor to determine which tables are ready, checking the whole room each lap, regardless of anyone actually is ready (this corresponds to <a href="https://en.wikipedia.org/wiki/Select_(Unix)"><code>select</code></a>/<a href="https://en.wikipedia.org/wiki/Poll_(Unix)"><code>poll</code></a>). Meanwhile, <code>epoll</code> is akin to giving each table a call button that allows them to notify the waiter when they are ready. This allows the waiter to attend tables that are guaranteed to be ready without constantly checking all the tables.</p>
<img src="https://cdn.hashnode.com/uploads/covers/68c19865376514b2a5927314/31e6dd92-60c8-4b35-9e1a-3b59c0ea4791.png" alt="" style="display:block;margin:0 auto" />

<p>It's worth noting an important drawback that this design introduces: a single long-running command will block all clients. The documentation highlights commands that involve many elements, such as <code>SORT</code>, <code>LREM</code>, <code>SUNION</code>. To troubleshoot this, the <a href="https://redis.io/docs/latest/operate/oss_and_stack/management/optimization/latency/">Diagnosing latency issues</a> documentation provides a comprehensive guide.</p>
<p>Multiplexing and non-blocking I/O are part of the networking story, but two more factors facilitate Redis's network I/O efficiency.</p>
<h2>Optimizations for Network I/O</h2>
<p>Redis is further optimized for handling network I/O by using a custom protocol called <a href="https://redis.io/docs/latest/develop/reference/protocol-spec/">Redis Serialization Protocol</a> (RESP) and <a href="https://redis.io/docs/latest/operate/oss_and_stack/management/optimization/benchmarks/">pipelining</a>. RESP offers a simple implementation, fast parsing, and human readable commands, enabling Redis to quickly parse commands with minimal CPU cycles. This performance benefit is achieved by using prefixed lengths, thereby removing the need to scan for special characters or quote/escape the payload. Meanwhile, pipelining enables multiple commands to be sent with a single <code>write</code> operation by the client. The client can skip reading replies and continue to send commands to the query buffer. Redis then drains the query buffer and executes each command as it’s parsed, appending each reply to the client’s output buffer for the replies to go out together before Redis sleeps. This process reduces the latency by decreasing the total number of network round trips, and increases throughput by minimizing socket I/O, condensing multiple <code>read()</code> and <code>write()</code> syscalls into fewer (note that a read caps at 16KB, so a large pipeline may still require multiple syscalls).</p>
<img src="https://cdn.hashnode.com/uploads/covers/68c19865376514b2a5927314/03a5e25e-a1bf-43c3-bd34-5cec1b1222b1.png" alt="" style="display:block;margin:0 auto" />

<h1>Conclusion</h1>
<p>There is no one thing that can take credit for Redis's performance. In reality it is the culmination of a deep understanding for how computers store and access memory combined with architectural decisions that maximize resource utilization based on Redis's use case. The fact that Redis operates entirely in-memory facilitates incredibly fast access and avoids costly operations involved in maintaining multiple data representations. Memory is further optimized by dynamically switching between compact memory formats and hash tables, which guarantees the best possible performance when working with smaller data structures. Moreover, as Redis is not bound by CPU and the operations are typically small, keeping all command execution on a single thread avoids unnecessary costs associated with shared-state, multi-threaded applications. Finally, networking optimizations such as a lightweight, custom communication protocol and pipelining minimize wasted CPU cycles without impacting the functionality.</p>
]]></content:encoded></item><item><title><![CDATA[Developing as a Developer:
Code Smells – Object-Orientation Abusers]]></title><description><![CDATA[The category of Object-Orientation Abusers encompass smells where the code fails in it's implementation of object-oriented design principles and best practices. This includes but is not limited to usi]]></description><link>https://code-after-degree.hashnode.dev/developing-as-a-developer-code-smells-object-orientation-abusers</link><guid isPermaLink="true">https://code-after-degree.hashnode.dev/developing-as-a-developer-code-smells-object-orientation-abusers</guid><dc:creator><![CDATA[Benjamin Inglis]]></dc:creator><pubDate>Fri, 17 Jul 2026 15:59:13 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/68c19865376514b2a5927314/0d588406-6534-4aec-b71b-06671d78e683.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The category of Object-Orientation Abusers encompass smells where the code fails in it's implementation of object-oriented design principles and best practices. This includes but is not limited to using conditionals or type codes instead of subclasses, failing to leverage inheritance, or inappropriately scoping data (misuse of encapsulation) (Mäntylä &amp; Lassenius, 2006).</p>
<p>Keep in mind that the refactoring solutions presented are not unique to the code smells presented and likely also appeared (or will appear) in other posts in this series. To me this illustrates the beauty of Fowler's Refactoring book, since he wrote it in a way that his solutions were reusable and the code smells simply reference them. However, for the sake of clarity and the ability for each article to be read as a standalone piece, I will likely reuse some prose and examples. I would also note that the problems the same refactor solves may be distinct, and understanding how the same pattern can apply to multiple code smells will strengthen your mental model and understanding of how to write "clean" code.</p>
<h3>Links to other posts in this series:</h3>
<p><a href="https://code-after-degree.hashnode.dev/developing-as-a-developer-code-smells-bloaters">Bloaters</a></p>
<h1>Principles of Object-Oriented Programming</h1>
<p>To frame this discussion of what violates the best practices of object-oriented programming (OOP) we first must understand the core principles that guide us.</p>
<h2>Inheritance</h2>
<p>Inheritance is the property that allows us to derive new classes by extending existing ones, thereby enabling code reuse. My favorite definition for inheritance comes from Deborah Armstrong's Quarks of Object Oriented Development (2006) as</p>
<blockquote>
<p>a mechanism that allows the data and behavior of one class to be included in or used as the basis for another class</p>
</blockquote>
<p>Consider an example where we have two classes representing animals:</p>
<pre><code class="language-python">class Dog:
    def __init__(self, name):
        self.name = name

    def walk(self):
        print("Walks on four legs")

    def bark(self):
        print("woof!")

class Cat:
    def __init__(self, name):
        self.name = name
    
    def walk(self):
        print("Walks on four legs")

    def meow(self):
        print("meow!")
</code></pre>
<p>We can simplify the above code by moving the shared properties into a parent class called <code>Animal</code> that <code>Dog</code> and <code>Cat</code> both inherit from:</p>
<pre><code class="language-python">class Animal:
    def __init__(self, name):
        self.name = name
    
    def walk(self):
        print("Walks on four legs")

class Dog(Animal):
    def bark(self):
        print("woof!")

class Cat(Animal):
    def meow(self):
        print("meow!")
</code></pre>
<p>This facilitates code reuse as we can easily create new animals, improves maintainability since changes to the <code>Animal</code> superclass only live in one location, and provides a built-in delegation mechanism such that subclasses can implement their own behaviors.</p>
<h2>Encapsulation</h2>
<p>The idea behind encapsulation is controlling what is accessible from outside an object. In other words, a clear division between an object's private internal state and its contract with (methods exposed to other objects/clients). In his 1986 paper <em>Encapsulation and inheritance in object-oriented programming</em>, Snyder posits that one should minimize the exposure of a module's implementation details to its clients to allow for changes made within the module without affecting the clients. This is particularly relevant to <em>states</em> within an object.</p>
<p>Imagine we introduce an <code>energy</code> state, indicating how energized the animal feels and some new methods that interact with it. Observe what happens when the state is left public (exposed in the interface):</p>
<pre><code class="language-python">class Animal:
    def __init__(self, name):
        self.name = name
        self.energy = 100     # public — anyone can set it to anything

    def walk(self):
        if self.energy &lt; 10:
            print(f"{self.name} is too tired to walk")
            return
        self.energy -= 10
        print(f"{self.name} walks on four legs")

    def feed(self, amount):
        self.energy = min(100, self.energy + amount)

cat = Cat("Whiskers")
cat.energy = 5000    # nonsense, but nothing prevents it
cat.walk()           # energy is now 4990
cat.energy = -20     # the cat now has negative energy??
</code></pre>
<p>To prevent this from occurring we <em>encapsulate</em> <code>energy</code> (the Python convention is to prefix with <code>_</code> ) so that it can only be accessed within the class. Then we create and expose special method for the public interface:</p>
<pre><code class="language-python">class Animal:
    def __init__(self, name):
        self.name = name
        self._energy = 100

    # ... walk and feed implementation

    @property
    def energy(self):
        return self._energy
</code></pre>
<p>Note that the combination of the <code>@property</code> decorator with no setter is what truly blocks assignment– the leading underscore is just convention.</p>
<p>We can even change the derivation of <code>energy</code> within the class without affecting the way the client interacts with the object at all:</p>
<pre><code class="language-python">class Animal:
    def __init__(self, name):
        self.name = name
        self._activity = 0

    @property
    def energy(self):
        return max(0, 100 - self._activity) # now derived
</code></pre>
<h2>Polymorphism</h2>
<p>Subtype polymorphism is what enables a method to use a uniform interface regardless of the specific type. This is best illustrated with an example.</p>
<p>Consider a variation of the example we've been working with using a generic <code>speak</code> method that is concretely implemented by the subclasses:</p>
<pre><code class="language-python">class Animal:
    def __init__(self, name):
        self.name = name
    
    def walk(self):
        print("Walks on four legs")
    
    def speak(self):
        raise NotImplementedError 

class Dog(Animal):
    def speak(self):
        print("woof!")

class Cat(Animal):
    def speak(self):
        print("meow!")
</code></pre>
<p>The <code>speak</code> method can now be called on any <code>Animal</code>. The client does not know or care about specific details as those are handled within the subclasses. This relates back to the <a href="https://en.wikipedia.org/wiki/Liskov_substitution_principle">Liskov substitution principle</a>, which posits that an object of the superclass (<code>Animal</code>) can be replaced by an object of the subclass (<code>Dog</code> or <code>Cat</code>) without breaking the program.</p>
<h2>Abstraction</h2>
<p>Abstraction is the mechanism that ties everything together and allows us to provide simplified models (classes) for complex realities to suppress irrelevant details. In this regard, a class can be thought of as an <em>abstraction</em> of an object, such that the class <em>encapsulates</em> data and behavior, and <em>inheritance</em> is what allows the encapsulated data and behavior to be based on an existing class (Armstrong, 2006). Finally, <em>polymorphism</em> enables different objects to respond to the same invocations and respond appropriately.</p>
<h1>Repeated Switches</h1>
<p>This is another way of saying <code>if/else</code> blocks that appear in more than one location in the system. The primary concern is that changes or additions made to any one switch statement must occur in every instance of the repeated switch.</p>
<p>What makes this smell an Object-Orientation Abuser is that the repeated switch approach neglects the elegant solution of polymorphism offered by object-oriented design, in which we significantly improve the readability and organization of the code.</p>
<h2>Refactoring Solutions</h2>
<h3>Replace Conditional with Polymorphism</h3>
<p>This technique is actually quite similar to the <a href="https://code-after-degree.hashnode.dev/developing-as-a-developer-strategy-pattern">Strategy Pattern</a> (which I previously wrote about). The goal is to eliminate large conditional branches by replacing them with classes and polymorphism. Consider the logic we use to assign a dough, which involves adding a different flour for each distinct type:</p>
<pre><code class="language-python">def make_dough(dough_variant): 
    if variant == "regular": 
        add_flour() 
    if variant == "gluten-free": 
        add_almond_flour() 
    # New variant: whole wheat 
    if variant == "whole wheat": 
        add_whole_wheat_flour()
</code></pre>
<p>Instead of repeating the conditional or decomposing it, we instead convert it to it's own type, letting each concrete implementation handle the implementation details:</p>
<pre><code class="language-python">class Dough:
    def add_flour(self):
        # Implemented by specific types

class RegularDough(Dough):
    def add_flour(self):
        add_flour()

class GlutenFreeDough(Dough):
    def add_flour(self):
        add_almond_flour()

class WholeWheatDough(Dough):
    def add_flour(self):
        add_whole_wheat_flour()

def make_dough(dough):
    activate_yeast()
    add_olive_oil()
    dough.add_flour()
    # ... rest of steps
</code></pre>
<p>Now instead of adding a conditional branch whenever we need to add a new variant, we can simply add a child to the <code>Dough</code> class, then define the <code>add_flour</code> method.</p>
<h1>Temporary Field</h1>
<p>The temporary field describes a field that is only set <em>sometimes</em>, adding ambiguity to the object or function containing it. The solution involves a sequence of three strategies that build upon each other.</p>
<p>Consider the following example of a <code>PizzaOrder</code> class:</p>
<pre><code class="language-python">class PizzaOrder:
    def __init__(self, items, is_delivery):
        self.items = items
        self.is_delivery = is_delivery
        self.delivery_address = None
        self.delivery_fee = None     
        self.driver_tip = None        

    def total(self):
        subtotal = sum(item.price for item in self.items)
        if self.is_delivery:
            return subtotal + self.delivery_fee + self.driver_tip
        return subtotal
</code></pre>
<p>The primary issue here is that three of the variables (<code>delivery_address</code>, <code>delivery_fee</code>, and <code>driver_tip</code>) are dead weight when the order is not a delivery.</p>
<h2>Refactoring Solution</h2>
<h3>Extract Class</h3>
<p>First, these inconsistent variables need a dedicated place to live, such as a dedicated <code>Delivery</code> class:</p>
<pre><code class="language-python">class Delivery:
    def __init__(self, address, fee, tip):
        self.address = address
        self.fee = fee
        self.tip = tip
</code></pre>
<h3>Move Function</h3>
<p>Next, the behavior related to those fields (computing the subtotal) is moved to a method within the new class:</p>
<pre><code class="language-python">class Delivery:    
    # ... init    
    def cost(self):
        return self.fee + self.tip
</code></pre>
<p>This allows us to refactor the original <code>PizzaOrder</code> <code>total</code> method to the following:</p>
<pre><code class="language-python">class PizzaOrder(self):
    def total(self):
        subtotal = sum(item.price for item in self.items)
        if self.delivery:
            return subtotal + self.delivery.cost()
        return subtotal
</code></pre>
<p>The combination of these two changes enforce the single-responsibility principle since the knowledge of the delivery cost lives with the delivery data instead of bleeding into the order, and improve encapsulation by exposing a single <code>cost</code> method to handle the implementation details.</p>
<h3>Introduce Special Case</h3>
<p>Fowler proposes one additional step, which is to create a new class to handle the conditional. Introduce Special Case involves creating a class to capture common behaviors to replace special-case checks, and is commonly referred to as the Null Object pattern because null is frequently the value involved in special-case processing (Fowler, p.289). In the <code>PizzaOrder</code> scenario, we would introduce a special class <code>NoDelivery</code> to handle the case when <code>delivery</code> is null:</p>
<pre><code class="language-python">class NoDelivery:                  # the special case
    def cost(self):
        return 0
</code></pre>
<p>Then, instead of our guard checking if <code>self.delivery</code> is truthy, we can replace the variable assignment with <code>self.delivery = delivery or NoDelivery()</code>, which will guarantee that <code>self.delivery.cost()</code> is always callable.</p>
<h1>Alternative Classes with Different Interfaces</h1>
<p>This smell involves two or more classes that essentially do the same job but have different method names or signatures, thereby preventing client code from using them interchangeably despite being conceptually the same. Take the following example where two similar classes are used to notify customers that their pizza is on the way using a <code>notify_customer</code> method:</p>
<pre><code class="language-python">class EmailNotifier:
    def send_email(self, address, subject, body):
        print(f"Emailing {address}: {subject}")

class SmsNotifier:
    def dispatch_text(self, phone_number, message):
        print(f"Texting {phone_number}: {message}")

def notify_customer(notifier, customer):
    if isinstance(notifier, EmailNotifier):
        notifier.send_email(customer.email, "Pizza Update", "On its way!")
    elif isinstance(notifier, SmsNotifier):
        notifier.dispatch_text(customer.phone, "Pizza on its way!")
</code></pre>
<h2>Refactoring Solutions</h2>
<h3>Change Function Declaration</h3>
<p>This refactoring technique encompasses both renaming the method itself and modifying the parameters (Fowler, p.125).</p>
<p>The first step is to align the method names (we'll use <code>notify</code>). Next, look at both the declarations and match up the signatures:</p>
<pre><code class="language-python">class EmailNotifier:
    def notify(self, recipient, message):
        print(f"Emailing {recipient}: {message}")

class SmsNotifier:
    def notify(self, recipient, message):
        print(f"Texting {recipient}: {message}")
</code></pre>
<p>Now that both classes contain the same general interface, the client no longer cares which type it receives– it can invoke them interchangeably!</p>
<pre><code class="language-python">def notify_customer(notifier, customer):
    notifier.notify(customer.contact, "Pizza on its way!")
</code></pre>
<h3>Move Function</h3>
<p>Move function comes into play when the alternative classes have somewhat different capabilities (but the core functionality remains the same). For example, let's say the <code>EmailNotifier</code> implementation had a <code>record_sent</code> method to log each notification to the company's audit log:</p>
<pre><code class="language-python">class EmailNotifier:
    def notify(self, recipient, message):
        print(f"Emailing {recipient}: {message}")
        self.record_sent(recipient, message)

    def record_sent(self, recipient, message):
        audit_log.append((recipient, message))

class SmsNotifier:
    def notify(self, recipient, message):
        print(f"Texting {recipient}: {message}")
        # no logging here — the caller has to do it

def notify_customer(notifier, customer):
    notifier.notify(customer.contact, "Pizza on its way!")
    if isinstance(notifier, SmsNotifier):
        audit_log.append((customer.contact, "Pizza on its way!"))
</code></pre>
<p>We can move the logging behavior into the <code>SmsNotifier</code> to ensure consistency between the classes, keeping the client code lean:</p>
<pre><code class="language-python">class EmailNotifier:
    def notify(self, recipient, message):
        print(f"Emailing {recipient}: {message}")
        self.record_sent(recipient, message)

    def record_sent(self, recipient, message):
        audit_log.append((recipient, message))

class SmsNotifier:
    def notify(self, recipient, message):
        print(f"Texting {recipient}: {message}")
        self.record_sent(recipient, message)

    def record_sent(self, recipient, message):
        audit_log.append((recipient, message))

def notify_customer(notifier, customer):
    notifier.notify(customer.contact, "Pizza on its way!")
</code></pre>
<h3>Extract Superclass</h3>
<p>While not necessary, taking this a step further to make the code more extensible in the future might involve pulling the shared <code>notify</code> interface into one <code>Notifier</code> superclass so that additional notifiers can be added in the future.</p>
<h1>Refused Bequest</h1>
<p>Refused Bequest describes a two ways in which a subclass can violate the inheritance contract (Fowler, p.84):</p>
<ol>
<li>Subclass only uses some of the methods and/or fields inherited from its parent.</li>
</ol>
<pre><code class="language-python">class Enemy:
    def __init__(self, name, hp):
        self.name = name
        self.hp = hp

    def take_damage(self, amount):
        self.hp -= amount
    
    def move(self):
        print(f"{self.name} moves toward you")

    def flee(self):
        print(f"{self.name} runs away")

class Boss(Enemy):
    def take_damage(self, amount):
        self.hp -= amount
    
    def move(self):
        print(f"{self.name} moves toward you")
    # a Boss never flees, so it just ignores the inherited flee()
</code></pre>
<ol>
<li>Subclass that reuses behavior but fails to support the interface of the superclass. This occurs when the subclass inherits a method that it cannot actually fulfill (meaning it throws an error or returns something nonsensical).</li>
</ol>
<pre><code class="language-python">class Enemy:
    # ... init and take_damage from before
    def move(self):
        print(f"{self.name} moves toward you")

    def flee(self):
        print(f"{self.name} runs away")

class Turret(Enemy):
    def move(self):
        raise NotImplementedError("turrets can't move")

    def flee(self):
        raise NotImplementedError("turrets can't flee") 
</code></pre>
<p>While Fowler considers the former case a trivial issue often not worth cleaning up, he emphasizes the severity of the latter violation. This smell also represents a violation of the Liskov Substitution Principle and the promise of inheritance as we can no longer guarantee that any subclass of <code>Enemy</code> can be substitued without breaking the program.</p>
<h2>Refactoring Solutions</h2>
<h3>Push Down Method / Push Down Field</h3>
<p>To resolve the tension of the mild case, the solution involves moving the unused methods and fields out of the parent and down into the siblings that actually use them. Say, taking <code>flee</code> out of <code>Enemy</code> and wiring it into the subclasses that actually use it, preventing the <code>Boss</code> subclass from ever receiving it.</p>
<h3>Replace Subclass with Delegate</h3>
<p>Rather than break the inheritance contract, the fix is to hold a reference to the superclass, then extract the parts it wants.</p>
<pre><code class="language-python">class Turret:
    def __init__(self, name, hp):
        self._enemy = Enemy(name, hp)
    
    def take_damage(self, amount):
        self._enemy.take_damage(amount)

    def attack(self):
        print(f"{self._enemy.name} fires from a fixed position")

    # omitted move() and flee()
</code></pre>
<p>The tradeoff here is that we are forwarding the boilerplate code (<code>take_damage</code> and any other methods we delegate) from the <code>Enemy</code> class in order to achieve a valid contract.</p>
<h3>Replace Superclass with Delegate</h3>
<p>This mis-inheritance also crops up on the parent's side when we drag an entire interface into a subclass only to use a portion of it. Fowler provides the example of making a stack a subclass of list, causing all of the list operations to be forwarded to stack despite most not applying. The solution is to make a new stack class with the list a property that we can delegate operations to (Fowler, p.399). Continuing with the video game example, imagine we have a <code>Sprite</code> class that we use for rendering drawings. We might logically attempt to make <code>Enemy</code> a subclass of <code>Sprite</code> in order to inherit the <code>draw</code> method:</p>
<pre><code class="language-python">class Sprite:
    def __init__(self, image):
        self.image = image
        self.x = 0
        self.y = 0

    def draw(self):
        print(f"drawing {self.image} at ({self.x}, {self.y})")

    def set_position(self, x, y):
        self.x, self.y = x, y


class Enemy(Sprite):   # inherits Sprite to reuse drawing
    def __init__(self, name, hp, image):
        super().__init__(image)
        self.name = name
        self.hp = hp
</code></pre>
<p>The issue is that an <code>Enemy</code> is not truly a kind of <code>Sprite</code>, it simply uses one to render on the screen. Instead, we let <code>Enemy</code> hold a reference to a <code>Sprite</code>, delegating the behavior we need (with the caveat of forwarding boilerplate from before):</p>
<pre><code class="language-python">class Enemy:
    def __init__(self, name, hp, image):
        self.name = name
        self.hp = hp
        self._sprite = Sprite(image)      

    def draw(self):
        self._sprite.draw()   # delegate the rendering 

    def move_to(self, x, y):
        self._sprite.set_position(x, y)
</code></pre>
<h1>Final Thoughts</h1>
<p>This category of code smells emphasizes how the core principles of OOP (inheritance, encapsulation, polymorphism, abstraction) operate in tandem to reduce waste and enforce a robust, maintainable codebase. One thing that I took away is the utility of Extract Class, not only in how it addresses encapsulation and the single-responsibility principle, but how it facilitates a shift in how I view code. I can now recognize when I'm "cross-contaminating" or overloading a class or function, and can more proactively refactor before it becomes a significant lift.</p>
<p>Additionally, the Refused Bequest smell got me thinking about contracts between objects in a way I hadn't considered before. While you can write code that compiles and runs despite violating an inheritance contract, it plants seeds for a future errors and forces the developer (or future reader) to perform more work trying to unravel why something broke. Further, the contract that is being broken is one that the compiler cannot see. While the compiler can enforce a method signature (e.g., invalid types, wrong number of arguments), the <em>behavioral</em> contracts (what a method promises to do) are invisible to it. Indeed, this is dangerous because it <em>defers</em> the cost and separates the failure point from the location of the code.</p>
<p><strong>References:</strong></p>
<ol>
<li><p>Fowler, M. (2018). <em>Refactoring: Improving the design of existing code</em> (2nd ed.). Addison-Wesley Professional</p>
</li>
<li><p>Mäntylä, M.V., Lassenius, C. Subjective evaluation of software evolvability using code smells: An empirical study. <em>Empir Software Eng</em> <strong>11</strong>, 395–431 (2006). <a href="https://doi.org/10.1007/s10664-006-9002-8"><strong>https://doi.org/10.1007/s10664-006-9002-8</strong></a></p>
</li>
<li><p>Black, A. Object-oriented programming: Some history, and challenges for the next fifty years. <em>Information and Computation</em> <strong>231</strong>, 3-20 (2013). <a href="https://doi.org/10.1016/j.ic.2013.08.002">https://doi.org/10.1016/j.ic.2013.08.002</a></p>
</li>
<li><p>Snyder, A. Encapsulation and Inheritance in Object-Oriented Programming Languages. (1986). <a href="https://doi.org/10.1145/960112.28702">https://dl.acm.org/doi/10.1145/960112.28702</a></p>
</li>
<li><p>Armstrong, D. The Quarks of Object-Oriented Development. (2006). <a href="https://doi.org/10.1145/1113034.1113040">https://doi.org/10.1145/1113034.1113040</a></p>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[Developing as a Developer:
Code Smells - Bloaters]]></title><description><![CDATA[In this installment of the Developing as a Developer series I will be exploring the code smells belonging to the category known as Bloaters, which Mäntylä and Lassenius (2006) define as "something tha]]></description><link>https://code-after-degree.hashnode.dev/developing-as-a-developer-code-smells-bloaters</link><guid isPermaLink="true">https://code-after-degree.hashnode.dev/developing-as-a-developer-code-smells-bloaters</guid><dc:creator><![CDATA[Benjamin Inglis]]></dc:creator><pubDate>Tue, 23 Jun 2026 20:40:44 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/68c19865376514b2a5927314/962eec65-ad9f-4da5-9845-8802ca284b0a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this installment of the Developing as a Developer series I will be exploring the code smells belonging to the category known as Bloaters, which Mäntylä and Lassenius (2006) define as "something that has grown so large that it cannot be effectively handled."</p>
<h3>Links to other posts in this series:</h3>
<p><a href="https://code-after-degree.hashnode.dev/developing-as-a-developer-code-smells-object-orientation-abusers">Object-Orientation Abusers</a></p>
<h1>Long Function</h1>
<p>This code smell arises when a method or function is simply too large. The basis for this smell is that the longer a function is, the more difficult it is to reason about. Since modern languages have mostly eliminated overhead costs associated with in-process calls, there is significant value in breaking a large function up into smaller functions. Furthermore, the key to easily understandable small functions is deliberate and descriptive naming, which serves to reduce the burden placed on the reader caused by switching context to see what the function does. A good rule of thumb is to write a function whenever you feel the need to comment something, such that the function contains the code that you wanted to comment and is named after the intention (Fowler, 2018, p.73).</p>
<h2>Refactoring Solutions</h2>
<h3>Extract Function</h3>
<p>This is the first strategy to reach for when trying to shorten a function. The goal is to take a fragment of code that shares a similar purpose, and then extract it into its own aptly named function.</p>
<p>For example, let's say you wanted to make a pizza, which involves 3 main steps:</p>
<ol>
<li><p>Make the dough (regular or gluten-free)</p>
</li>
<li><p>Assemble the pizza</p>
</li>
<li><p>Bake the pizza</p>
</li>
</ol>
<p>We could combine it all into one function as follows (apologies if I missed some steps if there are any pizza experts here 🥲):</p>
<pre><code class="language-python">def make_pizza(dough_variant):
    # Make the dough
    activate_yeast()
    add_olive_oil()
    if dough_variant == "regular":
        add_flour()
    if dough_variant == "gluten-free":
        add_almond_flour()   
    knead_dough()
    rise_dough()
    toss_dough()
    
    assemble_pizza()
    
    bake_pizza()
</code></pre>
<p>We could extract all the steps for making the dough into its own function that we call within the <code>make_pizza(variant)</code> method:</p>
<pre><code class="language-python">def make_dough(variant):
    activate_yeast()
    add_olive_oil()
    if variant == "regular":
        add_flour()
    if variant == "gluten-free":
        add_almond_flour()        
    knead_dough()
    rise_dough()
    toss_dough()

def make_pizza(dough_variant):
    make_dough(dough_variant)
    
    assemble_pizza()
    
    bake_pizza()
</code></pre>
<p>Encapsulating the irrelevant implementation details makes it much easier to reason about what the <code>make_pizza(variant)</code> function is doing. We also now have the added benefit of reusability, such that the <code>make_dough(variant)</code> function can be called elsewhere if we were to extend the functionality (e.g., different types of pizza that use the same dough). The caveat with this technique is that the names must be clear and descriptive to minimize the effort spent discerning the purpose of a code fragment (Fowler, 2018, p.107).</p>
<p>In some cases there may be many parameters and temporary variables that make it difficult to extract a function as the result would require forwarding all that context to the new method. Fowler provides a number of options for dealing with these based on the specific obstacle.</p>
<h3>Replace Temp with Query</h3>
<p>Let's expand the first example to have <code>make_pizza</code> use a temporary variable <code>bake_temp</code> that it passes to the <code>bake_pizza</code> method based on the desired "crispiness" (another new parameter) of the pizza:</p>
<pre><code class="language-python">
def make_pizza(dough_variant, crispiness):
    make_dough(dough_variant)
    if crispiness == "well-done":
        bake_temp = 500
    if crispiness == "regular":
        bake_temp = 450

    assemble_pizza()
    
    bake_pizza(bake_temp)
</code></pre>
<p>This is a problem because now anytime we want to extract something that requires that context, we have to duplicate the logic. Instead, we turn it into a query (a simple method) that contains the logic and returns the value we need.</p>
<pre><code class="language-python">def bake_temp_for(crispiness):
    if crispiness == "well-done":
        return 500
    if crispiness == "regular":
        return 450
    

def make_pizza(dough_variant, crispiness):
    make_dough(dough_variant)
    assemble_pizza()
    
    bake_pizza(bake_temp_for(crispiness))
</code></pre>
<h3>Introduce Parameter Object</h3>
<p>Fowler introduces the idea of a data clump as a groups of items that consistently appear together (Fowler, 2018, p.140). In the pizza example, as the <code>make_pizza</code> method grows more realistic (in the sense that it accounts for all the factors that actually matter for making a pizza), we end up having to pass more and more data to our extracted helper functions. We actually can see this happening as we extend the functionality to account for things like <code>crispiness</code>. What would happen if we were to introduce a few more factors, such as <code>size</code>, <code>sauce</code>, and <code>toppings</code>? We would definitely need to consider the size when adding sauce and toppings because it determines the amount used. Similarly, the <code>size</code> determines the quantity of ingredients that go into making our dough, and how long the pizza bakes for.</p>
<pre><code class="language-python">def make_pizza(dough_variant, crispiness, size, sauce, toppings):
    make_dough(dough_variant, size)
    assemble_pizza(size, sauce, toppings)
    bake_pizza(bake_temp_for(crispiness), size)
</code></pre>
<p>You can already see how this is becoming tedious. Instead, what if we were to bundle all the features into a single <code>order</code> object:</p>
<pre><code class="language-python">class PizzaOrder:
    def __init__(self, dough_variant, crispiness, size, sauce, toppings):
        self._dough_variant = dough_variant
        self._crispiness = crispiness
        # ... You get the point

def make_pizza(order):
    make_dough(order)
    assemble_pizza(order)
    bake_pizza(order)
</code></pre>
<p>We now have a much more concise <code>make_pizza</code> function that only takes in a single argument, from which we can extract the properties we need in each helper function. Fowler argues for passing the whole record (what he calls "Preserve the Whole Object") rather than deriving select values for a few reasons (Fowler, 2018, p.318):</p>
<ol>
<li><p>Protects against future changes. For example, what happens when we need to extract more values, such a <code>cheese</code> variable from the <code>PizzaOrder</code></p>
</li>
<li><p>It reduces the size of the parameter list, which makes the function more comprehensible.</p>
</li>
<li><p>If there are many functions called with the parts, that logic can be moved to the object so it can be invoked by the helper functions.</p>
</li>
</ol>
<h3>Replace Function with Command</h3>
<p>If none of the above strategies successfully resolve the interdependent local variables and parameters, Fowler recommends switching to what he calls the heavy artillery: Replace Function with Command (Fowler, 2018, p.73).</p>
<p>In this strategy, you convert the entire function into an object, where the temporary variables become fields and the sub-steps become methods that share state. Consider the following <code>PizzaMaker</code> class to replace the <code>make_pizza</code> function:</p>
<pre><code class="language-python">class PizzaMaker:
    def __init__(self, order):
        self.order = order

    def make(self):
        self._make_dough()
        self._assemble()
        self._bake()

    def _make_dough(self):
        # Steps to make the dough
    def _assemble(self):
        # Steps to assemble the pizza
    def _bake(self):
        # Steps to bake the pizza
</code></pre>
<p>While this is not a perfect example because it's particularly long or complicated, hopefully you can see the goal of breaking down the function into methods and fields contained within a class. We can leverage the properties provided by classes such as shared state to create methods that manipulate parameters, eliminating the need to pass around variables from the main function to helpers.</p>
<p>The strategies above aim to resolve excessively large functions by extracting code fragments into logical groupings and clearing obstacles that prevent us from doing that cleanly. The next few strategies provide ways to remove conditionals and loops, which are often good candidates for extraction.</p>
<h3>Decompose Conditional</h3>
<p>The essence of this technique is to delegate the specifics of conditional logic into its own clearly named function that makes the calling code easier to read and reason about. Imagine our hypothetical pizza shop is running a promotion where a large pizza is half off from 2-3 pm (a pizza happy hour, if you will).</p>
<pre><code class="language-python">def calculate_pizza_cost(order):
    if (2pm ≤ datetime.now() ≤ 3pm): # Pseudocode
        return order.pizza.cost * 0.5
    else:
        return order.pizza.cost
    
        
</code></pre>
<p>Instead, we can delegate this logic to a new clearly named function that gives us tells us which rate we should use. We can even decompose the legs so that the evaluation reads more clearly:</p>
<pre><code class="language-python">def is_pizza_happy_hour():
    if (2pm ≤ datetime.now() ≤ 3pm): # Pseudocode
        return True
    else:
        return False

def happy_hour_cost(order):
    return order.pizza.cost * 0.5

def regular_cost(order)
    return order.pizza.cost

def calculate_pizza_cost(order):
    if is_pizza_happy_hour():
        return happy_hour_cost(order)
    else:
        return regular_cost(order
    
</code></pre>
<p>This allows the reader to quickly grasp the purpose of the <code>calculate_pizza_cost</code> function without getting caught up trying to trace the branching logic of the conditional.</p>
<h3>Replace Conditional with Polymorphism</h3>
<p>This strategy is actually quite similar to the <a href="https://code-after-degree.hashnode.dev/design-patterns-in-practice-strategy">Strategy Pattern</a> (which I previously wrote about). The goal is to eliminate large conditional branches by replacing them with classes and polymorphism. Consider the logic we use to assign a dough, which involves adding a different flour for each distinct type:</p>
<pre><code class="language-python">def make_dough(dough_variant):
    if variant == "regular":
        add_flour()
    if variant == "gluten-free":
        add_almond_flour()
    # New variant: whole wheat
    if variant == "whole wheat":
        add_whole_wheat_flour()
</code></pre>
<p>Instead of repeating the conditional or decomposing it, we instead convert it to it's own type, letting each concrete implementation handle the implementation details:</p>
<pre><code class="language-python">class Dough:
    def add_flour(self):
        # Implemented by specific types

class RegularDough(Dough):
    def add_flour(self):
        add_flour()

class GlutenFreeDough(Dough):
    def add_flour(self):
        add_almond_flour()

class WholeWheatDough(Dough):
    def add_flour(self):
        add_whole_wheat_flour()

def make_dough(dough):
    activate_yeast()
    add_olive_oil()
    dough.add_flour()
    # ... rest of steps
</code></pre>
<p>Now instead of adding a conditional branch whenever we need to add a new variant, we can simply add a child to the <code>Dough</code> class, then define the <code>add_flour</code> method.</p>
<h3>Split Loop</h3>
<p>Fowler posits that loops should be extracted into their own methods, and further separated into distinct functions (Fowler, 2018, p.227). He gives the example of a loop that updates two variables on each iteration (stepping away from the pizza example because it doesn't make much sense here):</p>
<pre><code class="language-python">people = [person_a, person_b, person_c, ...]
average_age = 0
total_salary = 0
for person in people:
    average_age += person.age
    total_salary += person.salary

average_age = average_age / len(people)

# Applying the Split Loop refactor, this would become:
total_salary = 0
for person in people:
    total_salary += person.salary

average_age = 0
for person in people:
    average_age += person.age

average_age = average_age / len(people)
</code></pre>
<p>While it does slightly increase the amount of code written and force us to execute the loop twice, it removes the ambiguity of <em>what</em> the loop is actually doing, which allows us to clearly describe each one and place it into it's own method that can replace the code in the larger function. Fowler emphasizes the importance of separating refactoring from optimization, noting that we can easily recombine the loops if it turns out to be a bottleneck in the future (Fowler, 2018, p.228).</p>
<pre><code class="language-python">def total_salary():
    total_salary = 0
    for person in people:
        total_salary += person.salary
    return total_salary

def average_age():
    average_age = 0
    for person in people:
        average_age += person.age
    return average_age
</code></pre>
<h1>Long Parameter List</h1>
<p>First, let's understand the purpose of a parameter list:</p>
<blockquote>
<p>"The parameter list to a function should summarize the points of variability of the function, indicating the primary ways in which that function may behave differently" (Fowler, 2018, p.324).</p>
</blockquote>
<p>This smell appears when a function requires a large amount of data and requires an extensive parameter list to be passed in. In such cases, it becomes challenging to reason about the key ways a function might differ.</p>
<h2>Refactoring Solutions</h2>
<h3>Replace Parameter with Query</h3>
<p>Reach for this strategy when an argument passed into the function can be <em>easily</em> derived by the function itself. The idea is to shift the responsibility of determining the value from the calling code to the function itself. Fowler notes that this technique should not be used if removing the parameter adds an unwanted dependency to the function (i.e., an element that the function should be ignorant of). The ideal case is when the value you want to remove from the function arguments is retrieved by querying another parameter in the list.</p>
<p>Consider the following <code>Order</code> class</p>
<pre><code class="language-python">class Order:
    def __init__(self, quantity, item_price):
        self._quantity = quantity
        self._item_price = item_price
        self._discount_level = 2 if quantity &gt; 100 else 1
    
    def final_price(self):
        base_price = self._quantity * self._item_price
        return discounted_price(base_price, self._discount_level)

    def discounted_price(base_price, discount_level):
        if discount_level == 1:
            return base_price * 0.95
        elif discount_level == 2:
            return base_price * 0.9
        
</code></pre>
<p>Rather than pass in <code>self._discount_level</code> to the method, we can remove it from the parameter list and simply derive it in the function:</p>
<pre><code class="language-python">    def discounted_price(base_price):
        if self._discount_level == 1:
            return base_price * 0.95
        elif self._discount_level == 2:
            return base_price * 0.9
</code></pre>
<h3>Preserve Whole Object</h3>
<p>In the event where you are passing several individual properties of an object as arguments, consider passing the whole entity instead, then extracting only what you need. Take the example below, where we pass in each parameter from our order individually:</p>
<pre><code class="language-python">def make_pizza(toppings, sauce, dough_variant, crispiness, cheese):
    make_dough(dough_variant)
    # ... you get the point by now


order = Order(
    toppings = ['pepperoni', 'bell peppers'],
    sauce = 'regular', 
    dough_variant = 'regular',
    crispiness = 'well-done',
    cheese =  'fresh mozzarella'
)

make_pizza(order.toppings, order.sauce, order.dough_variant, order.crispiness, order.cheese)
</code></pre>
<p>Alternatively, we simply pass in the whole object, then extract what we need:</p>
<pre><code class="language-python">def make_pizza(order):
    make_dough(order.dough_variant)
    # ... you get the point by now


order = Order(
    toppings = ['pepperoni', 'bell peppers'],
    sauce = 'regular', 
    dough_variant = 'regular',
    crispiness = 'well-done',
    cheese =  'fresh mozzarella'
)

make_pizza(order)
</code></pre>
<h3>Introduce Parameter Object</h3>
<p>If this looks familiar... that's because it is. It is in fact the same exact strategy recommended for reducing long functions, and is what would enable us to perform the refactor shown above.</p>
<h3>Remove Flag Argument</h3>
<p>A flag argument refers to an argument the caller passes to indicate which behavior to execute. Fowler avoids flag arguments as they make it harder to understand what function calls are available and how to call them (Fowler, 2018, p.315). To illustrate the problem, consider the the following line of code: <code>make_pizza(order, True)</code></p>
<p>What is <code>True</code> doing here? There's no way to know what it's doing without opening the <code>make_pizza</code> and reading through the body– it silently switches the behavior of the function but tells the reader nothing.</p>
<p>Consider the following example where we use an <code>is_rush</code> flag:</p>
<pre><code class="language-python">def make_pizza(order, is_rush):
    make_dough(order)
    assemble_pizza(order)
    if is_rush:
        express_bake(order)
    else:
        standard_bake(order)
</code></pre>
<p>In this case, the flag is more descriptive, but the reader still has to open the <code>make_pizza</code> method and read through it to understand what behavior change is actually taking place. Instead, we remove the flag argument, replacing it with explicit, intention-revealing functions:</p>
<pre><code class="language-python">def make_rush_pizza(order):
    make_dough(order)
    assemble_pizza(order)
    express_bake(order)

def make_regular_pizza(order):
    make_dough(order)
    assemble_pizza(order)
    standard_bake(order)
</code></pre>
<p>And if you wanted to eliminate the duplicate body:</p>
<pre><code class="language-python">def make_rush_pizza(order):
    _make_pizza(order, express_bake)

def make_regular_pizza(order):
    _make_pizza(order, standard_bake)

def _make_pizza(order, bake_step):
    make_dough(order)
    assemble_pizza(order)
    bake_step(order)
</code></pre>
<p>Note that <code>bake_step</code> is not a flag being decoded, but a <em>function</em> that is passed from the public entry points (<code>make_rush_pizza</code> and <code>make_regular_pizza</code>)</p>
<p>While there is a time and place for flag arguments, such as avoiding writing explicit functions for every combination of flag values, they serve as an indicator that a function is doing too much.</p>
<h3>Combine Functions into Class</h3>
<p>This technique works well when multiple functions share several parameter values. The idea is to create a class containing the common fields. Consider the following example where multiple steps involved in making the pizza share some fields:</p>
<pre><code class="language-python">def make_dough(size, dietary_restrictions, dough_variant):
    # Logic for making the dough

def assemble_pizza(size, dietary_restrictions, toppings, sauce, cheese):
    # Logic for assembling the pizza

def bake_pizza(size, dietary_restrictions, crispiness):
    # Logic for baking the pizza
</code></pre>
<p>Noticing that all three of these functions share two of the same parameters, we create a <code>PizzaMaker</code> class that encapsulates the common data (i.e., make the shared arguments fields):</p>
<pre><code class="language-python">class PizzaMaker:
    def __init__(self, size, dietary_restrictions):
        self.size = size
        self.dietary_restrictions = dietary_restrictions

    def make_dough(self, dough_variant):
    # uses self.size, self.dietary_restrictions, and dough_variant

    def assemble_pizza(self, toppings, sauce, cheese):
    # uses self.size, self.dietary_restrictions, and the rest

    def bake_pizza(self, crispiness):
    # uses self.size, self.dietary_restrictions, and crispiness
</code></pre>
<p>Now observe how this simplifies the client code:</p>
<pre><code class="language-python"># Before
make_dough(size, dietary_restrictions, dough_variant)
assemble_pizza(size, dietary_restrictions, toppings, sauce, cheese)
bake_pizza(size, dietary_restrictions, crispiness)

# After
maker = PizzaMaker(size, dietary_restrictions)
maker.make_dough(dough_variant)
maker.assemble_pizza(toppings, sauce, cheese)
maker.bake_pizza(crispiness)
</code></pre>
<p>This solution falls into a Long Parameter List fix since the shared parameters were the bloat, and moving them into fields shrinks all the signatures simultaneously. It also sets us up for later refactoring, since any helper that is defined inside the class can freely use <code>self.size</code> and <code>self.dietary_restrictions</code>. If you were paying attention, you might point out that we could further refactor this by combining all shared fields into a single <code>Order</code> object that is initialized in the <code>PizzaMaker</code> class, from which the helper functions can derive what they need.</p>
<p>Additionally, while this resembles the solution to Replace Function with Command from the previous section, note that they are solutions answer to solve different problems. In the original example, we created a class to break down an excessively large function into an object so that the internal methods could share state. In the present example, we used a class to encapsulate shared data and reduce the size of the parameter lists and simplify the calling code.</p>
<h1>Large Class</h1>
<p>Classes that attempt to do too much, and often contain too many fields</p>
<h2>Refactoring Solutions</h2>
<h3>Extract Class</h3>
<p>This technique involves bundling related variables into their own components. To demonstrate, imagine our <code>Order</code> object from before contained information about both the pizza and the individual who placed the order.</p>
<pre><code class="language-python">class Order:
    def __init__(toppings, dough_variant, sauce, ..., customer_name, customer_phone_number, customer_address):
        self.toppings = toppings
        self.customer_name = customer_name        
        # ...
        
</code></pre>
<p>At this point we can take the customer information and bundle it into a <code>Customer</code> class, then pass the entire object into the <code>Order</code> class:</p>
<pre><code class="language-python">class Customer:
    def __init__(name, phone_number, address):
        self.name = name
        self.phone_number = phone_number
        self.address = address

class Order:
    def __init__(toppings, dough_variant, sauce, ..., customer)
        self.toppings = toppings
        self.customer = customer
</code></pre>
<h3>Extract Superclass</h3>
<p>This refactoring strategy leverages the power of inheritance to consolidate similarities among multiple classes into a superclass. Imagine you are creating creatures for a video game, such as "werewolf" and "undead"</p>
<pre><code class="language-python">class Werewolf: 
    def __init__(self, name, hp, mp, strength):
        self.name = name
        self.hp = hp
        self.mp = mp
        self.strength = strength

    def beast_claw(self):
        return f"{self.name} attacked for {self.strength} damage" 

class Undead:
    def __init__(self, name, hp, mp, intelligence):
        self.name = name
        self.hp = hp
        self.mp = mp
        self.strength = strength

    def rot_breath(self):
        return f"{self.name} attacked for {self.intelligence} damage"

    
</code></pre>
<p>We can combine these into a common superclass which extracts the fields and methods that are shared and leaves what differs to the subclasses. To do this, we apply what Fowler (2018) describes as Pull Up Field (p.353) and Pull Up Method (p.350). In this example, the methods used to attack have different names and bodies, but represent the same intent. With an additional helper function to get <code>attack_power</code> and a small refactor we can also move a generic <code>attack</code> function to the superclass using what Fowler labels the Form Template Method (Fowler, 2018, p.351):</p>
<pre><code class="language-python">class Enemy:
    def __init__(self, name, hp, mp):
        self.name = name
        self.hp = hp
        self.mp = mp

    def attack(self):
        return f"{self.name} attacked for {self.attack_power()} damage"

    def attack_power(self):
        raise NotImplementedError   # each creature supplies its own

class Werewolf(Enemy):
    def __init__(self, name, hp, mp, strength):
        super().__init__(name, hp, mp)
        self.strength = self.strength

    def attack_power(self):
        return self.strength

class Undead(Enemy):
    def __init__(self, name, hp, mp, intelligence):
        super().__init__(name, hp, mp)
        self.intelligence = intelligence

    def attack_power(self):
        return self.intelligence
    
</code></pre>
<h1>Data Clumps</h1>
<p>Data clumps describe entities that are frequently found together, such as a group of fields in multiple classes or parameters in several methods.</p>
<h2>Refactoring Solutions</h2>
<p>Data clumps can be dealt with by employing three refactors we have already seen before and go hand-in-hand. Once you've identified the common fields, group them into an object (Extract Class) to provide a layer of abstraction and separate concerns. This makes it easier to maintain since the data that changes together or are dependent on each other are now found together. If the data clump occurs in a method, you can pass the newly created class as a parameter (Introduce Parameter Object), reducing the bloated signature and allowing you to extract what you need in the function itself (Preserve Whole Object).</p>
<h1>Primitive Obsession</h1>
<p>Primitives are the most basic, built-in data types offered by a language, such as strings, arrays, integers, and booleans. The essence of this smell is an overuse of primitive types as opposed to developing your own simple classes to enforce special behaviors.</p>
<h2>Refactoring Solutions</h2>
<h3>Replace Primitive with Object</h3>
<p>Fowler (2018) comments that any time he wants to do more than simply printing an object, he creates a new class (p.174). This provides the developer with a clear place to add behavior specific to the data. Consider an integer <code>crispiness</code> level parameter in a pizza baking function.</p>
<pre><code class="language-python">def bake_pizza(order, crispiness):
    if crispiness &lt; 1 or crispiness &gt; 5:        # validation here
        raise ValueError("crispiness must be 1-5")
    minutes = 8 + crispiness * 2                # conversion logic here
    ...

def print_receipt(order, crispiness):
    labels = {1: "Soft", 3: "Golden", 5: "Extra Crispy"}   # formatting here
    print(f"Crispiness: {labels.get(crispiness)}")
</code></pre>
<p>While we can introduce guards and raise exceptions to enforce the behavior we want (integer between 1 and 5, bake time proportional to <code>crispiness</code>), the integer cannot validate itself, name itself, or tell you its bake time. To clean up this function, we can gather up all that information into a <code>Crispiness</code> object that implements all of that behavior:</p>
<pre><code class="language-python">class Crispiness:
    def __init__(self, level: dict):
        if not 1 &lt;= level &lt;= 5:
            raise ValueError("crispiness must be 1-5")
        self.level = level

    def bake_minutes(self):
        return 8 + self.level * 2

    def label(self):
        return {
            1: "Soft", 
            3: "Golden", 
            5: "Extra Crispy"
        }.get(self.level, "Medium") # Levels 2 and 4 default to "Medium"

# Revised function passing in whole object
def bake_pizza(order, crispiness):
    bake_for(crispiness.bake_minutes())

def print_receipt(order, crispiness):
    print(f"Crispiness: {crispiness.label()}")
</code></pre>
<h3>Replace Type Code with Subclasses</h3>
<p>A type code is a primitive field that determines what <em>kind</em> of thing an object is. Extending the game creatures from before, we might initialize the Enemy class with a <code>creature_type</code> type code:</p>
<pre><code class="language-python">class Enemy:
    def __init__(self, name, hp, mp, creature_type):
        self.name = name
        self.hp = hp
        self.mp = mp
        self.creature_type = creature_type # "werewolf" or "undead" — the type code

    def attack(self):
        if self.creature_type == "werewolf":
            return f"{self.name} slashes with claws"
        elif self.creature_type == "undead":
            return f"{self.name} breathes rot"

    def weakness(self):
        if self.creature_type == "werewolf":
            return "silver"
        elif self.creature_type == "undead":
            return "fire"
</code></pre>
<p>With this approach, we end up having to recreate the if-else branching logic for every method. This means an addition of a new enemy would require updating every single conditional (Fowler calls this smell "Repeated Switches"). The result is much simpler methods that leave the concrete implementation details to the subclasses to prevent bloated methods and duplicate code:</p>
<pre><code class="language-python">class Enemy:
    def __init__(self, name, hp, mp):
        self.name = name
        self.hp = hp
        self.mp = mp

class Werewolf(Enemy):
    def attack(self):
        return f"{self.name} slashes with claws"
    def weakness(self):
        return "silver"

class Undead(Enemy):
    def attack(self):
        return f"{self.name} breathes rot"
    def weakness(self):
        return "fire"
</code></pre>
<h1>Takeaways</h1>
<p>Something I noticed is that the book itself is written following Fowler's own refactoring principles. The names are clear and the refactoring strategies are designed to solve singular problems. He even breaks down smaller operations into their own strategies, such as "Encapsulate Variable" (p. 132) and "Rename Field" (p.244). The sheer amount of information contained in Fowler's book is overwhelming, but I do appreciate his consistency. While it's difficult at first when trying to digest the countless smells and refactors, the way he builds up each strategy help quickly create a mental model and solidify patterns.</p>
<p>References:</p>
<ol>
<li><p>Fowler, M. (2018). <em>Refactoring: Improving the design of existing code</em> (2nd ed.). Addison-Wesley Professional</p>
</li>
<li><p>Mäntylä, M.V., Lassenius, C. Subjective evaluation of software evolvability using code smells: An empirical study. <em>Empir Software Eng</em> <strong>11</strong>, 395–431 (2006). <a href="https://doi.org/10.1007/s10664-006-9002-8">https://doi.org/10.1007/s10664-006-9002-8</a></p>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[Developing as a Developer: 
Introduction]]></title><description><![CDATA[Overview: Motivation for writing this
Writing clean, maintainable code is crucial in developing software that stands the test of time. In today's landscape where there is a massive push for AI-generat]]></description><link>https://code-after-degree.hashnode.dev/developing-as-a-developer-introduction</link><guid isPermaLink="true">https://code-after-degree.hashnode.dev/developing-as-a-developer-introduction</guid><dc:creator><![CDATA[Benjamin Inglis]]></dc:creator><pubDate>Fri, 05 Jun 2026 20:08:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/68c19865376514b2a5927314/d2220dfa-9d0f-45c9-b94b-1878679fc7b6.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Overview: Motivation for writing this</h2>
<p>Writing clean, maintainable code is crucial in developing software that stands the test of time. In today's landscape where there is a massive push for AI-generated code, I suspect the art of producing readable code will become even more valuable as entire codebases are built and refactored using large language models. Furthermore, as this code and other AI-generated content pollutes the internet, it will becomes increasingly difficult for new engineers to identify "good" code. As such, I'm committed to understanding what is constitutes truly robust code. Moreover, the introduction of AI-assisted coding has minimized the need for the junior engineer, since senior engineers can now delegate more trivial tasks and code generation to an LLM. The danger, which likely won't be realized for several years, is that there is no longer a built-in path for new engineers to receive guidance and mentorship to give them the skills required to serve more senior roles. I suspect the consequences will become apparent over the next 10-20 years when senior and staff engineers retire and there are no suitable replacements. However, that won't stop the oversaturation of the current job market for entry-level software engineers. While companies may realize this and take efforts to alleviate the problem by pushing for more junior headcount, I would rather do everything in my power to write code like a senior engineer than wait around for companies to take action.</p>
<p>Hence, the introduction of my "Developing as a Developer" series. In each part, I plan to explore and apply a concept that will teach me (and anyone who reads it) to produce better code. While I'm going to do my best to keep each part narrowly scoped, the nature of software development is that several topics are inherently linked. For example, originally I wanted to <em>only</em> discuss code smells in this article, but quickly realized that the accompanying refactoring techniques are crucial in building a mental model of what constitutes clean code. Then I realized that I should probably investigate SOLID principles before getting into code smells, and now my original two part series has quickly become who-knows-how-many parts.</p>
<p>With that said, one constant in life and computer science is change. Before delving into the specific topics, I think it's important to understand the history of the ideas and how they have changed or evolved over time. Tracing the origins of the ideas serves two main purposes:</p>
<ol>
<li><p>It exercises our critical thinking muscles. We can speculate on reasons for change and improve our intuition of what will work/not work. We get to learn how subject matter experts adopt and adapt theories. This is important because blindly following principles will not make you a better programmer. In Martin's words: "principles have to be applied with judgement. If they are applied by rote it is just as bad as if they are not applied at all" (Martin, 2009). Furthermore, historizicing ideas gives us context into <em>how</em> theories are applied by different practitioners. Through investigating the origins of modern principles we develop a mutual understanding of their foundational underpinnings and the shared language to communicate and critique them. Without this, we would end up realizing Martin's concerns.</p>
</li>
<li><p>Proper attribution of content and ideas. A huge issue I've noticed (which has been exacerbated by the rise of LLM-generated content) is failing to properly cite sources. People may copy and paste or paraphrase a response from generative AI without ever knowing (or caring) where that information came from. That's fucked up. With time these types of posts flood the internet and it will grow increasingly difficult to trace the origin of knowledge.</p>
</li>
</ol>
<h2>History of Code Smells</h2>
<p>Refactoring describes "change[s] made to the internal structure of software to make it easier to understand and cheaper to modify without changing its observable behavior" (Fowler &amp; Beck, 2018, p.45). While they knew that refactoring was valuable for writing maintainable and scalable software, both authors recognized there was a gap in communicating about refactoring beyond the vague notion of programming aesthetics. Thus, when writing the first edition of their book in the 90's, Martin Fowler and Kent Beck sought to clarify how to identify <em>when</em> code needs to be refactored. The conception of "smells" (attributed to Kent Beck, perhaps inspired by his newborn daughter) refers to an indication (not definitive proof) that some refactoring may be needed (Fowler &amp; Beck, 2018, p. 71). Fowler and Beck outline 24 distinct code smells in the 2nd edition of their book, which represent common patterns and structures that likely benefit from refactoring. Not all instances of code that needs refactoring will fit cleanly into one of the named categories, but familiarity with the smells will point you in the right direction.</p>
<p>In their 2003 paper titled "A taxonomy and initial empirical study of bad smells in code," Finnish researchers at Helsinki University of Technology proposed grouping the original 22 code smells into 7 different classifications to make them more understandable and clarify relationships between the smells (Mäntylä et al., 2003):</p>
<p><strong>Bloaters:</strong> Something in the code that has grown out of control and can no longer be maintained.</p>
<p><strong>Object-Orientation Abusers:</strong> Cases where the solution fails to leverage object-oriented design effectively.</p>
<p><strong>Change Preventers:</strong> Code structures that when changed result in a cascade of other changes on dependent structures.</p>
<p><strong>Dispensables:</strong> Redundant or otherwise unnecessary code.</p>
<p><strong>Encapsulators</strong>: Issues pertaining to the chain of delegation in how objects hide or pass along data.</p>
<p><strong>Couplers:</strong> Code structures are too tightly coupled.</p>
<p><strong>Other Smells:</strong> Smells that don't fit into any of the other categories.</p>
<p>Later in 2006, Mäntylä refined the taxonomy down to five groups in, collapsing two of the categories related to coupling (Encapsulators was absorbed by Couplers).</p>
<table>
<thead>
<tr>
<th>Category</th>
<th>Code Smell</th>
</tr>
</thead>
<tbody><tr>
<td>Bloaters</td>
<td>Long Method, Large Class, Primitive Obsession, Long Parameter List, Data Clumps</td>
</tr>
<tr>
<td>Object-Orientation Abusers</td>
<td>Switch Statements, Temporary Field, Refused Bequest, Alternative Classes with Different Interfaces</td>
</tr>
<tr>
<td>Change Preventers</td>
<td>Divergent Change, Shotgun Surgery, Parallel Inheritance Hierarchies</td>
</tr>
<tr>
<td>Dispensables</td>
<td>Lazy Class, Data Class, Duplicate Code, Dead Code, Speculative Generality</td>
</tr>
<tr>
<td>Couplers</td>
<td>Feature Envy, Inappropriate Intimacy, Message Chains, Middle Man</td>
</tr>
<tr>
<td>Other Smells</td>
<td>Comments, Incomplete Library Class</td>
</tr>
</tbody></table>
<p>Since the taxonomy was published before the 2nd edition of <em>Refactoring</em>, there does not exist a perfect mapping to pull from the literature. Of note, a few code smells were renamed from the 1st to the 2nd edition:</p>
<ul>
<li><p>Long Method ➡️ Long Function</p>
</li>
<li><p>Switch Statements ➡️ Repeated Switches</p>
</li>
<li><p>Lazy Class ➡️ Lazy Element</p>
</li>
<li><p>Inappropriate Intimacy ➡️ Insider Trading (hmm, I wonder why).</p>
</li>
</ul>
<p>Four new smells related to newer concerns (such as code comprehension, shared state, immutability, and opting for first-class functions over loops) were also added in the 2nd edition: Mysterious Name, Global Data, Mutable Data, and Loops. For the sake of completeness and consistency, I will consider these part of a grouping I'll call "New Smells".</p>
<p>The following table provides a brief description and the category for each of the 24 smells that I will be investigating in more depth throughout this series.</p>
<table>
<thead>
<tr>
<th>Code Smell</th>
<th>Description</th>
<th>Category</th>
</tr>
</thead>
<tbody><tr>
<td>Long Function</td>
<td>A function that has grown to unmanageable size over time.</td>
<td>Bloaters</td>
</tr>
<tr>
<td>Long Parameter List</td>
<td>Excessively large list of parameters.</td>
<td>Bloaters</td>
</tr>
<tr>
<td>Data Clumps</td>
<td>A set of entities that always appear together.</td>
<td>Bloaters</td>
</tr>
<tr>
<td>Primitive Obsession</td>
<td>No small classes for small entities, so functionality is added to another class.</td>
<td>Bloaters</td>
</tr>
<tr>
<td>Large Class</td>
<td>A class that is trying to do too many things (often presents as having too many fields).</td>
<td>Bloaters</td>
</tr>
<tr>
<td>Repeated Switches</td>
<td>Using conditional statements when a subclass could be used instead.</td>
<td>Object-Orientation Abuser</td>
</tr>
<tr>
<td>Temporary Field</td>
<td>Variable is declared in the wrong scope (e.g., in class scope when it should be in the method scope).</td>
<td>Object-Orientation Abuser</td>
</tr>
<tr>
<td>Alternative Classes with Different Interfaces</td>
<td>Closely related classes are lacking a common interface.</td>
<td>Object-Orientation Abuser</td>
</tr>
<tr>
<td>Refused Bequest</td>
<td>A subclass only uses some methods and properties inherited from its parents.</td>
<td>Object-Orientation Abuser</td>
</tr>
<tr>
<td>Divergent Change</td>
<td>A class is changed in different ways for different reasons.</td>
<td>Change Preventers</td>
</tr>
<tr>
<td>Shotgun Surgery</td>
<td>A single change to the system requires modifying many classes.</td>
<td>Change Preventers</td>
</tr>
<tr>
<td>Lazy Element</td>
<td>An element that just doesn't do much of anything for the application.</td>
<td>Dispensables</td>
</tr>
<tr>
<td>Speculative Generality</td>
<td>Code that exists to handle <em>potential</em> changes to requirements or functionality.</td>
<td>Dispensables</td>
</tr>
<tr>
<td>Data Class</td>
<td>Exists to solely to hold data, containing only getting and setting methods.</td>
<td>Dispensables</td>
</tr>
<tr>
<td>Comments</td>
<td>Misuse of comments, such as explaining overcomplicated code rather than writing simpler code.</td>
<td>Dispensables</td>
</tr>
<tr>
<td>Duplicated Code</td>
<td>Code that is redundant or can be consolidated with abstraction.</td>
<td>Dispensables</td>
</tr>
<tr>
<td>Feature Envy</td>
<td>One method is tightly coupled to other classes (outside of its own).</td>
<td>Couplers</td>
</tr>
<tr>
<td>Message Chains</td>
<td>Entities depend on a chain of multiple other entities to get the data it needs. Any change to the intermediates requires the client to change as well.</td>
<td>Couplers</td>
</tr>
<tr>
<td>Middle Man</td>
<td>A class only exists to delegate work to another class</td>
<td>Couplers</td>
</tr>
<tr>
<td>Insider Trading</td>
<td>Two classes are coupled tightly together</td>
<td>Couplers</td>
</tr>
<tr>
<td>Global Data</td>
<td>Data that can be modified from anywhere in the code base.</td>
<td>New Smells</td>
</tr>
<tr>
<td>Mutable Data</td>
<td>Data that can be altered in-place after initialization.</td>
<td>New Smells</td>
</tr>
<tr>
<td>Loops</td>
<td>... Just loops (e.g., "for" and "while").</td>
<td>New Smells</td>
</tr>
<tr>
<td>Mysterious Name</td>
<td>Names that do not clearly indicate what things are or what functions do.</td>
<td>New Smells</td>
</tr>
</tbody></table>
<p><a href="https://code-after-degree.hashnode.dev/developing-as-a-developer-code-smells-bloaters">Link to Code Smell – Bloaters article</a></p>
<h2>History of SOLID Design Principles</h2>
<p>SOLID represents "five principles intended to improve the make source code more understandable, flexible, and maintainable" (<a href="https://en.wikipedia.org/wiki/SOLID">SOLID</a>, Wikipedia). These principles were first articulated across the late-1990s <em>Engineering Notebook</em> columns for the <em>C++ Report</em> and later consolidated in an abbreviated form in Robert C. Martin's 2000 paper <em>Design Principles and Design Patterns,</em> in which he discusses the concept of software rot: the process of a once-beautiful architecture degrading into an unmanageable mess. Interestingly, it was not Martin himself who formed the mnemonic device– that is attributed to Michael Feathers, who noted the opportunity to rearrange the letters via an email to Martin around 2004 (Martin, 2018, p.58).</p>
<p>Of note, two principles predate Martin:</p>
<h3>Open-Closed Principle</h3>
<p>The Open-Closed Principle originates from Bertrand Meyer's (1988) book <em>Object-Oriented Software Construction,</em> where he states that "modules should be both open and closed" (Meyer, 1997) (I can't seem to find a copy of the 1st edition, but he has published the 1997 2nd edition <a href="https://bertrandmeyer.com/oosc2/">for free on his website</a>, so I will reference that instead). A module is considered open if it's still available for extension (e.g., adding fields to the data structures or expanding its functionality). A module is closed if it is available for use by other modules and has been given a well-defined, stable description (Meyer, 1997). Meyer (1997) resolved the open-closed tension through inheritance: a concrete base class stays closed to modification while new functionality is added by subclassing it (pp.58-60).</p>
<p>Martin's version of the Open-Closed Principle maintains the slogan of "open for extension but closed for modification" but differs in the mechanism. Martin leverages abstraction– client code depends on a fixed abstract type, and the system is extended by deriving new implementations of that type, protecting existing code from future edits. This shift was likely due to issues related to implementation inheritance, whereby a subclass inherits the <em>actual</em> implementation (including code and state), resulting in tight coupling between child and the internal workings of the parent, thus violating encapsulation (Snyder, 1986; Gamma et al., 1994). Furthermore, Mikhajlov and Sekerinski (1998) highlight the fragility of the paradigm by demonstrating how revising a base class can result in breaking subclasses. Inheriting from an abstraction solves these issues by decoupling the implementation. The subtype only inherits the method signatures (name, arguments, return type) and behavioral promise (conditions for calling the method, guarantees once it returns, rules about state) and is responsible for supplying its own behavior.</p>
<h3>Liskov Substitution Principle</h3>
<p>The Liskov Substitution Principle was introduced by MIT's Barbara Liskov in her <a href="https://dl.acm.org/doi/10.1145/62138.62141">1987 OOPSLA Keynote</a>, titled <em>Data Abstraction and Hierarchy,</em> where she discusses the importance of data abstraction in developing programs that are highly maintainable and easy to modify when requirements change (Liskov, 1987, p.33). When discussing a type hierarchy composed of subtypes and supertypes, she proposes the following substitution property:</p>
<blockquote>
<p>If for each object o1, of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1, is substituted for o2, then S is a subtype of T.</p>
</blockquote>
<p>(Liskov, 1987, p.25).</p>
<p>In other words, an object of a superclass can be replaced by an object of a subclass without breaking the program.</p>
<p>The concept was formally introduced into academia by Liskov and Wing's 1994 paper <em>A Behavioral Notion of Subtyping</em>, in which they argued that "the objects of the subtype ought to behave the same as those of the supertype as far as anyone or any program using supertype objects can tell" (Liskov &amp; Wing, 1988).</p>
<h3>SOLID Principles: Current State</h3>
<p>Martin describes the principles more definitively in his later books <em>Agile Software Development: Principles, Patterns and Practices</em>, and <em>Agile Principles, Patterns, and Practices in C#.</em> Since then, SOLID principles have become a standard practice in the field for creating scalable and maintainable software, with several academic publications supporting their usage (Ali, 2002; Cabral et al., 2024; Yanakiev et al., 2025).</p>
<p>Below lists the SOLID principles from Martin (2002) that will be explored at length in a future article:</p>
<p><strong>Single Responsibility Principle (SRP)</strong> (p.95)</p>
<blockquote>
<p>A class should have only one reason to change"</p>
</blockquote>
<p><strong>Open-Closed Principle (OCP)</strong> (p.99)</p>
<blockquote>
<p>"Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification"</p>
</blockquote>
<p><strong>Liskov Substitution Principle (LSP)</strong> (p.111)</p>
<blockquote>
<p>"Subtypes must be substitutable for their base types"</p>
</blockquote>
<p><strong>Interface Segregation Principle (ISP)</strong> (p.138)</p>
<blockquote>
<p><strong>"Clients should not be forced to depend on methods that they do not use"</strong></p>
</blockquote>
<p><strong>Dependency Inversion Principle (DIP)</strong> (p.127)</p>
<blockquote>
<p>"a. High-level modules should not depend on low-level modules. Both should depend on abstractions.</p>
<p>b. Abstractions should not depend on details. Details should depend on abstractions."</p>
</blockquote>
<p>References:</p>
<ol>
<li><p>Fowler, M. (2018). <em>Refactoring: Improving the design of existing code</em> (2nd ed.). Addison-Wesley Professional</p>
</li>
<li><p>Gamma, E., Helm, R., Johnson, R., &amp; Vlissides, J. (1994). <em>Design Patterns: Elements of Reusable Object-Oriented Software</em>. Addison-Wesley.</p>
</li>
<li><p>Mäntylä, Mika &amp; Vanhanen, Jari. (2003). Bad Smells in Software – a Taxonomy and an Empirical Study.</p>
</li>
<li><p>Mäntylä, M.V., Lassenius, C. Subjective evaluation of software evolvability using code smells: An empirical study. <em>Empir Software Eng</em> <strong>11</strong>, 395–431 (2006). <a href="https://doi.org/10.1007/s10664-006-9002-8">https://doi.org/10.1007/s10664-006-9002-8</a></p>
</li>
<li><p>Martin, Robert C, (2000). <a href="https://web.archive.org/web/20150906155800/http://www.objectmentor.com/resources/articles/Principles_and_Patterns.pdf">Design Principles and Design Patterns</a></p>
</li>
<li><p>Raphael Cabral, Marcos Kalinowski, Maria Teresa Baldassarre, Hugo Villamizar, Tatiana Escovedo, and Hélio Lopes. 2024. Investigating the Impact of SOLID Design Principles on Machine Learning Code Understanding. <a href="https://doi.org/10.1145/3644815.3644957">https://doi.org/10.1145/3644815.3644957</a></p>
</li>
<li><p>Mohamed Ali, Azrajabeen. (2022). The Impact of SOLID Principles on Code Quality and Software Lifecycle. 7. 10.5281/zenodo.15062293</p>
</li>
<li><p>Yanakiev, I., Lazar, B. M., &amp; Capiluppi, A. (2025). Applying SOLID principles for the refactoring of legacy code: An experience report. <em>Journal of Systems and Software</em>, 220, 112254</p>
</li>
<li><p>Martin, Robert (2018). <a href="https://books.google.com/books?id=uGE1DwAAQBAJ&amp;q=2004+or+thereabouts+by+Michael+Feathers"><em>Clean Architecture: A Craftsman's Guide to Software Structure and Design</em></a>. Pearson. p. 58. <a href="https://en.wikipedia.org/wiki/ISBN_(identifier)">ISBN</a> <a href="https://en.wikipedia.org/wiki/Special:BookSources/978-0-13-449416-6">978-0-13-449416-6</a></p>
</li>
<li><p>Meyer, B. (1997). <em>Object-Oriented Software Construction</em> (2nd ed.) Prentice Hall.</p>
</li>
<li><p>Liskov, B. (1987). "Data Abstraction and Hierarchy." <em>OOPSLA '87</em> (pub. <em>SIGPLAN Notices</em> 23(5), 1988).</p>
</li>
<li><p>Liskov, B. H., &amp; Wing, J. M. (1994). A behavioral notion of subtyping. <em>ACM Transactions on Programming Languages and Systems (TOPLAS)</em>, 16(6), 1811-1841.</p>
</li>
<li><p>Cook, W. R., Hill, W. L., &amp; Canning, P. S. (1990). Inheritance is not subtyping. In <em>Proceedings of the 17th ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages</em> (pp. 125–135)</p>
</li>
<li><p>Mikhajlov, Leonid; Sekerinski, Emil (1998). <a href="https://web.archive.org/web/20170813041125/http://extras.springer.com/2000/978-3-540-67660-7/papers/1445/14450355.pdf"><em>A study of the fragile base class problem</em></a></p>
</li>
<li><p>Snyder, A. (1986). "Encapsulation and Inheritance in Object-Oriented Programming Languages." <em>OOPSLA '86</em>, pp. 38–45.</p>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[Developing as a Developer:
Strategy Pattern]]></title><description><![CDATA[What is the Strategy design pattern?
To start, let's look at the definition provided in Design Pattern Elements of Reusable Object-Oriented Software:

“Define a family of algorithms, encapsulate each ]]></description><link>https://code-after-degree.hashnode.dev/developing-as-a-developer-strategy-pattern</link><guid isPermaLink="true">https://code-after-degree.hashnode.dev/developing-as-a-developer-strategy-pattern</guid><dc:creator><![CDATA[Benjamin Inglis]]></dc:creator><pubDate>Wed, 03 Jun 2026 19:55:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/68c19865376514b2a5927314/be6d6ff6-ec13-4a75-9a9c-fa621270095a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>What is the Strategy design pattern?</h2>
<p>To start, let's look at the definition provided in Design Pattern Elements of Reusable Object-Oriented Software:</p>
<blockquote>
<p>“Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from the clients that use it”</p>
</blockquote>
<p>(Gamma, Helm, Johnson, &amp; Vlissides, p. 315).</p>
<p>To better understand the definition, let's break down the components of the Strategy Design Pattern:</p>
<ul>
<li><p><strong>Client:</strong> Determines what strategy to use and passes it to the context; interacts with the context exclusively.</p>
</li>
<li><p><strong>Context:</strong> Applies the strategy selected by the client; holds a reference to the strategy object.</p>
</li>
<li><p><strong>Strategy Interface:</strong> The common interface all concrete strategies must implement; declares a method the context invokes to execute a strategy</p>
</li>
<li><p><strong>Concrete Strategies:</strong> Encapsulates specific logic of each behavior/algorithm; can be swapped out based on requirements.</p>
</li>
</ul>
<p>Now let’s break down each portion of that definition:</p>
<blockquote>
<p><em>“family of algorithms”</em></p>
</blockquote>
<p>This refers to a related group of algorithms that can be selected or switched dynamically depending on the conditions (<a href="https://www.geeksforgeeks.org/system-design/behavioral-design-patterns/">Behavioral Design Patterns</a>). Consider each set of behaviors as a “family” of algorithms. For example, let’s say you wanted to switch between different database storage methods. You could choose from MongoDB, Postgres, or MySQL, all of which belong to the "storage" family. Similarly, if you wanted to switch between different API endpoints in a cryptocurrency tracker application. You could choose from CoinGecko, CoinMarketCap, or a "mock" endpoint that simulates an HTTP response for testing. These algorithms would all belong to the "data retrieval" family.</p>
<blockquote>
<p><em>“encapsulates each one”</em></p>
</blockquote>
<p>For a given family of algorithms, we can invoke different behaviors by interacting <em>only</em> with the interface for that family, leaving the implementation details to classes we define for each behavior. Continuing with the data retrieval example above, we might have an interface with a <code>get_latest_price(symbol)</code> method that delegates functionality to a specific data retrieval algorithm to get the latest price for a given cryptocurrency symbol. Given our interface <code>DataStrategy</code>, our Concrete Strategies are the specific variations: <code>CoinGeckoStrategy</code>, <code>CoinMarketCapStrategy</code>, and <code>MockStrategy</code>, each of which implement their own <code>get_latest_price()</code> method.</p>
<blockquote>
<p><em>“lets the algorithm vary independently from clients that use it”</em></p>
</blockquote>
<p>To "vary independently" is another way of saying the client and the algorithm are decoupled, meaning either side (client or strategy) can change without forcing a change on the other side. You could update the implementation of one retrieval algorithm without ever touching the calling code and vice-versa. For example, say you needed to update a JSON field in your CoinGecko strategy, or update the base URL. You can update the function that normalizes the data into your internal response shape, or edit HTTP call without ever opening the client code. On the other side, say your client code needs to be modified to balance load and fail over across two different APIs to account for rate limits, whereas previously it simply set one strategy. You could achieve this by wrapping existing the strategies in a new one, enabling you to implement a new feature without touching the context or any of the existing strategies.</p>
<p>Below I've provided a diagram to help visualize the strategy set up described above:</p>
<img src="https://cdn.hashnode.com/uploads/covers/68c19865376514b2a5927314/a357c2b1-dfd4-4ee9-a227-13d1dcca6d91.png" alt="Visualization of the strategy pattern described above." style="display:block;margin:0 auto" />

<h2>What problems does the Strategy Pattern Solve?</h2>
<p>From a readability perspective, the Strategy pattern eliminates conditional statements by encapsulating behavior to separate Strategy objects (Gamma, Helm, Johnson, &amp; Vlissides, pp. 317-318), which serves to prevent unruly, monolithic code blocks. For example, if we were to implement the data retrieval strategies naively, it would look something like this:</p>
<pre><code class="language-python">class PriceFetcher:
    def get_latest_price(self, symbol: str, fetch_strategy: str):

        if fetch_strategy == "CoinGecko":
            api_url = "" # CoinGecko's URL
            headers = {} # Required headers
            # ...
        elif fetch_strategy == "CoinMarketCap":
            api_url = "" # CoinMarketCap URL
            headers = {} # Required headers           
            # ...
        
        elif fetch_strategy == "Mock":
            return (
                {
                    "name": "Bitcoin",
                    "symbol": "btc",
                    "price": 10000,
                    "timestamp": datetime.now(timezone.utc),
                    "currency": "USD",
                },
            ) 
    
        else:
            print("Error: Unsupported fetch strategy")
</code></pre>
<p>It's clear how this could become a problematic class when more conditions are added or specific implementation details are modified. Furthermore, this implementation violates two principles of software engineering:</p>
<h3>Single Responsibility Principle</h3>
<p><a href="https://blog.cleancoder.com/uncle-bob/2014/05/08/SingleReponsibilityPrinciple.html">Single Responsibility Principle</a>. This principle states that a class should only have <em>one</em> reason to change (Martin, 2002, p. 95). In a later article, Martin specifies that a reasons for change are people, as they are the drivers of changes (Martin, 2014). In the example above, there are three different parties that might require changes for different reasons: The CoinGecko team, the CoinMarketCap team, and you (the developer). The first two are out of our control, such as changes to the base URL, required headers, or endpoints. However, changes driven by any of the parties requires an edit to the same method in the naive <code>PriceFetcher</code> . If the CoinGecko team renames a JSON field, you're editing the same method that also contains the CoinMarkerCap's auth logic, which has nothing to do with the change.</p>
<p>We can remedy this by assigning each condition its own strategy, ensuring the individual strategies have exactly one reason to change (its own vendor). The responsibility of the <code>PriceFetcher</code> class is to orchestrate the execution of the strategies, not manage the specific implementation details of each strategy. Thus, only changes related to the orchestration, such as adding caching, modifying error handling, or changing which source is preferred should elicit changes.</p>
<h3>(Polymorphic) Open Closed Principle</h3>
<p><a href="https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle">Open/Closed Principle</a>. This principle states that classes should be open for extension, but closed for modification (Martin, 2002, p. 99). In other words, when the requirements change, you should be able to extend the functionality by adding new code, rather than altering old code that already works. In the naive example above, adding a new new fetch strategy (say, to a third endpoint) would require opening the <code>PriceFetcher</code> class and adding a new <code>elif</code> block.</p>
<h3>Putting it all together</h3>
<p>The Strategy pattern implementation mirrors Martin's description of creating "abstractions that are fixed and yet represent an unbounded group of possible behaviors" (Martin, 2002, p.100). Refer to the following table to see how the Strategy pattern relates to Martin's Open/Closed Principle:</p>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Strategy Pattern</th>
<th>Martin's OCP</th>
</tr>
</thead>
<tbody><tr>
<td><code>FetchStrategy</code></td>
<td>Strategy (the interface)</td>
<td>fixed abstraction/abstract base class</td>
</tr>
<tr>
<td><code>CoinGeckoStrategy</code>, <code>CoinMarketCapStrategy</code>, <code>MockStrategy</code></td>
<td>Concrete Strategy</td>
<td>the unbounded derivatives</td>
</tr>
<tr>
<td><code>PriceFetcher</code></td>
<td>Context</td>
<td>the module closed for modification</td>
</tr>
</tbody></table>
<p>To achieve this, we start by creating the base class and derivatives:</p>
<pre><code class="language-python"># --- Base Class --- #
class FetchStrategy(ABC):
    @abstractmethod
    def fetch_all_prices(self) -&gt; list[dict]:
        raise NotImplementedError("Subclass must implement this method")


# --- Concrete Strategies --- #
class CoinGeckoStrategy(FetchStrategy):
    def fetch_all_prices(self) -&gt; list[dict]:
        api_url = "https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd"
        headers = {"x-cg-demo-api-key": os.getenv("COINGECKO_API_KEY")}
        response = requests.get(api_url, headers=headers)
        data = response.json()
        formatted_prices = []
        for coin in data:
            curr_coin = {
                "name": coin["name"],
                "symbol": coin["symbol"],
                "price": coin["current_price"],
                "timestamp": datetime.now(timezone.utc),
                "currency": "USD",
            }
            formatted_prices.append(curr_coin)

        return formatted_prices


class CoinMarketCapStrategy(FetchStrategy):
    def fetch_all_prices(self) -&gt; list[dict]:
        api_url = "https://pro-api.coinmarketcap.com/v1/cryptocurrency/listings/latest"
        headers = {
            "Accept": "application/json",
            "X-CMC_PRO_API_KEY": os.getenv("COINMARKETCAP_API_KEY"),
        }
        response = requests.get(api_url, headers=headers)
        json = response.json()
        data = json["data"]
        formatted_prices = []
        for coin in data:
            curr_coin = {
                "name": coin["name"],
                "symbol": coin["symbol"].lower(),
                "price": coin["quote"]["USD"]["price"],
                "timestamp": datetime.now(timezone.utc),
                "currency": "USD",
            }
            formatted_prices.append(curr_coin)
        return formatted_prices

class MockStrategy(FetchStrategy):
    def fetch_all_prices(self) -&gt; list[dict]:
        return [
            {
                "name": "Bitcoin",
                "symbol": "btc",
                "price": 10000,
                "timestamp": datetime.now(timezone.utc),
                "currency": "USD",
            },
            {
                "name": "CannoliCoin",
                "symbol": "ctc",
                "price": 999999,
                "timestamp": datetime.now(timezone.utc),
                "currency": "USD",
            },
        ]
</code></pre>
<p>Next, define the <code>PriceFetcher</code> context.</p>
<pre><code class="language-python">class PriceFetcher:
    def __init__(self):
        self._fetch_strategy: FetchStrategy | None = None

    def set_fetch_strategy(self, strategy: FetchStrategy):
        self._fetch_strategy = strategy

    def fetch_all_prices(self) -&gt; list[dict]:
        return self._fetch_strategy.fetch_all_prices()
</code></pre>
<p>Now for the client code:</p>
<pre><code class="language-python">price_fetcher = PriceFetcher()
# Set a strategy before calling fetch_all_prices()
price_fetcher.set_fetch_strategy(CoinGeckoStrategy()) 
latest_crypto_prices = price_fetcher.fetch_all_prices()
print(latest_crypto_prices)
</code></pre>
<p>Taken together, the behavior of the system is open for extension (we could add a new derivative to extend the functionality) but closed for modification (no changes needed to <code>PriceFetcher</code> or the <code>FetchStrategy</code> base class when a derivative is added).</p>
<h3>Drawbacks</h3>
<p>No software engineering topic is complete without a discussion of trade-offs, because at the end of the day there are no perfect solutions. The following are summarized from the Strategy Pattern section of <em>Design patterns: Elements of reusable object-oriented software</em> (Gamma, Helm, Johnson, &amp; Vlissides), which I recommend checking out for further reading since I only scratch the surface here:</p>
<ol>
<li><p>A client must be aware of the nuances of the strategies in order to select the appropriate one. For example, how would the calling code know whether to use CoinGecko or CoinMarketCap? While this is a non-issue in the demo since both strategies return the same standardized list of dictionaries, making the choice arbitrary. However, if we needed to account for operational logistics, such as rate limits, pricing tiers, coin coverage, or reliability, that knowledge needs to live in the client as selecting the appropriate strategy is the client's responsibility.</p>
</li>
<li><p>Since the Strategy base class is shared by all ConcreteStrategy classes regardless of complexity, there will likely be information passed through the interface that is never utilized, meaning additional overhead is created for no benefit. An example of this is in the <code>MockStrategy</code>, which would accept <code>symbol</code> as a parameter in the <code>get_latest_price</code> method but always returns the same response.</p>
</li>
<li><p>More objects, classes, and interfaces in the application. The naive single-class fetcher built with chained <code>if/elif</code> statements is only a single object. Meanwhile, three objects (one for each concrete strategy), a base class, and a context class were required to implement the Strategy pattern in the demo provided.</p>
</li>
</ol>
<h3>Final Thoughts</h3>
<p>The Strategy Pattern is a powerful tool for encapsulating interchangeable behaviors and letting them vary independently of the code that uses them. There are clear organizational benefits: giving each algorithm its own class adheres to the Single Responsibility Principle (each strategy only has one reason to change), which keeps the code maintainable and simpler to reason about as the codebase grows. It also embodies Martin's polymorphic reading of the Open/Closed Principle, where further changes are achieved by adding new code, not by modifying working code (Martin, 2002, p. 100), making system more resilient. However, these benefits come at a cost: more objects, communication overhead via a shared interface, and clients need to understand how strategies differ to choose the appropriate one. The Strategy pattern may not be the best option when variations are few and stable, or the behavioral logic in each condition is trivial. simple. In such cases a single-class approach would avoid creating unnecessary objects and abstractions. Indeed, the true skill is judging which dimensions of the system are likely to change, and only implementing the Strategy Pattern when there are meaningfully different variants for the dimension.</p>
<p><strong>References:</strong></p>
<ol>
<li><p>Gamma, E., Helm, R., Johnson, R., &amp; Vlissides, J. (1995). <em>Design patterns: Elements of reusable object-oriented software</em>. Addison-Wesley.</p>
</li>
<li><p>Martin, Robert C. (2014). <a href="https://blog.cleancoder.com/uncle-bob/2014/05/08/SingleReponsibilityPrinciple.html">"The Single Responsibility Principle"</a>. <em>The Clean Code Blog</em>.</p>
</li>
<li><p>Martin, R. C. (2002). <a href="https://books.google.com/books?id=0HYhAQAAIAAJ"><em>Agile Software Development, Principles, Patterns, and Practices</em></a>. Prentice Hall. <a href="https://en.wikipedia.org/wiki/ISBN_(identifier)">ISBN</a> <a href="https://en.wikipedia.org/wiki/Special:BookSources/978-0135974445">978-0135974445</a>.</p>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[Server-Sent Events]]></title><description><![CDATA[What is a Server-Sent Event?
A server-sent event (SSE for short) is a communication protocol that utilizes a persistent HTTP connection to enable a server to send data to a web page at any time by pus]]></description><link>https://code-after-degree.hashnode.dev/server-sent-events</link><guid isPermaLink="true">https://code-after-degree.hashnode.dev/server-sent-events</guid><dc:creator><![CDATA[Benjamin Inglis]]></dc:creator><pubDate>Mon, 18 May 2026 22:31:09 GMT</pubDate><content:encoded><![CDATA[<h2>What is a Server-Sent Event?</h2>
<p>A <a href="https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events">server-sent event</a> (SSE for short) is a communication protocol that utilizes a persistent HTTP connection to enable a server to send data to a web page at <em>any</em> time by pushing events to the client in <a href="https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format">text/event-stream format</a>. If there is a payload attached to the event, it is accessed via the “data” field (i.e., <code>event.data</code>). This differs from traditional HTTP polling in which the web page must send a request to the server in order to receive new data.</p>
<h3>Text/Event Stream Format</h3>
<p><code>text/event-stream</code> format is the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/MIME_types">MIME type</a> (a MIME type is a standardized format for a document, file, or assortment of bytes) used by SSEs. Each message in the data stream is a plain-text block comprised of one or more fields, separated by double newline characters (<code>\n\n</code>).</p>
<p>Key fields include:</p>
<ul>
<li><p><code>data</code>: carries the actual payload</p>
</li>
<li><p><code>event</code>: an optional name for the event type</p>
</li>
<li><p><code>id</code>: the last-event ID, used to automatically reconnect if connection is dropped</p>
</li>
<li><p><code>retry</code>: indicates length to wait before reconnecting (in milliseconds)</p>
</li>
<li><p><code>:</code> A colon with nothing preceding it is often used to keep the connection alive. Any text after the colon is ignored by the client.</p>
</li>
</ul>
<p><strong>Example event stream:</strong></p>
<pre><code class="language-shell">: connection established

event: status:
data: {"connected": true}

data: Hello world

data: Hello world
data: but with two lines

id: 4
event: update 
data: {"temperature": 67, "humidity": 0.4}
retry: 3000
</code></pre>
<p>When the SSE is established, the client reads the byte stream continuously, buffering each incoming line until reaching the double newline, then it treats everything buffered to that point as one event. In essence, SSE is just structured plaintext over an indefinite HTTP response.</p>
<h2>Server-Sent Events in the Real World</h2>
<h3>SSE vs. WebSockets</h3>
<p>SSEs are similar to <a href="https://developer.mozilla.org/en-US/docs/Web/API/WebSocket">WebSockets</a> as both use a continually open line of communication, but differ in that the connection is one-way, not bi-directional (only the server can send events to the client). This makes SSEs desirable in applications where only the server needs to send data and the client acts as a medium to display, store, or do something with it. Of particular interest in today's technology landscape is streaming AI API responses. Other popular use cases include social media feed or news feed updates, weather data, and stock tickers.</p>
<h3>The <code>EventSource</code> API</h3>
<p>The <a href="https://developer.mozilla.org/en-US/docs/Web/API/EventSource">EventSource</a> API offers a convenient interface for consuming and parsing server-sent events. The drawback is that <code>EventSource</code> only supports <code>GET</code> requests since it is designed for simple subscription-style streams, making it a poor choice for processing APIs that use <code>POST</code> requests. Below I will briefly introduce the <code>EventSource</code> API, then demonstrate two real-world examples of implementing SSE (one using <code>EventSource</code>, one without).</p>
<p>An <code>EventSource</code> instance takes in the url of the script that generates the events. If the event generator is hosted on another origin (commonly a decoupled API), the <code>withCredentials</code> option must be set to <code>true</code></p>
<pre><code class="language-javascript">const evtSource = new EventSource(
        "//api.example.com/sse-demo.js”, 
        {withCredentials: true,}"
    );
</code></pre>
<p>Once a connection is established, you can listen for events in two ways:</p>
<ul>
<li><p>Using the <code>onmessage</code> method to listen for all events.</p>
</li>
<li><p>Listen for named events by adding event listeners. Listener functions should take in an event, which can then be used to access any data attached.</p>
</li>
</ul>
<pre><code class="language-javascript">// Option 1: 
evtSource.onmessage(e){
    console.log(`unnamed event received:${e.data}`);
    // Do something with the data
}

// Option 2: listen for named events by adding Event Listeners 
evtSource.addEventListener("new-message", (e) {
    console.log(`\({e.event} event received: \){e.data}`);
}
</code></pre>
<h3>Simulating an AI-style response:</h3>
<p>I created a <a href="https://github.com/bingliscodes/server_sent_events">GitHub</a> repo which provides two sample implementations of server-sent events: one utilizing the EventSource API (<code>eventsource</code>), the other manually parsing the data stream with <code>fetch</code> (<code>manual</code>). What makes SSE format powerful is that the structure provides named event types, a delimiter between events, and an <code>id</code> field for resumeability.</p>
<p>When the server sends an event it is doing three things:</p>
<ol>
<li><p>Setting <code>Content-Type: text/event-stream</code></p>
</li>
<li><p>Keeping the connection open (handled by FastAPI's <code>StreamingResponse</code>)</p>
</li>
<li><p>Writing strings in a format that follow SSE naming and newline conventions (discussed above)</p>
</li>
</ol>
<p>To achieve this, we can use a simple <code>format_sse</code> function that takes in an event name and a dictionary containing all data as key-value pairs, then return a string with the newline characters integrated to adhere to SSE guidelines:</p>
<pre><code class="language-python">def format_sse(event: str, data: dict) -&gt; str:
    """Format a single SSE event with the given event type and JSON data."""
    payload = json.dumps(data)
    return f"event: {event}\ndata: {payload}\n\n"
</code></pre>
<h2>Tradeoffs</h2>
<p>When using EventSource, you are trading convenience and out-of-the-box functionality for flexibility. EventSource will automatically retry to connect to the server if something happens to the connection, whereas you'd have to detect it and rebuild the connection yourself with fetch. Additionally, EventSource has built-in resumeability, automatically sending a <code>Last-Event-ID</code> header to the server to pick up where it left off. Finally, EventSource offers standardized error handling, with clear readyState values (<code>CONNECTING</code>, <code>OPEN</code>, <code>CLOSED</code>) and an established lifecycle. However, with fetch you need to implement retry logic, backoff timing, and a way to resume the stream yourself.</p>
<p>Conversely, the manual approach offers some functionality that isn't possible with the EventSource API. EventSource only supports <code>GET</code> requests, but by using fetch and <code>ReadableStream</code> you can process a <code>POST</code> request, which would allow you to send a prompt in the body (important for AI APIs since sending it in the URL is not feasible or recommended for production). Additionally, you can set custom headers, such as auth tokens or custom content types (worth noting that while native browser EventSource does not support this, but can be included with polyfill). Finally, <code>ReadableStream</code> provides the ability to implement backpressure (a mechanism for dealing with a queue growing faster than the consumers can process events) at the application-level.</p>
<p>For example, if you have a stock ticker that pushes hundreds of updates per second via SSE to be processed by EventSource, the only backpressure mechanism it has is TCP-level flow control underneath, which eventually slows down the server's writes. The issue is that this is a blunt safety net, not something your application controls. Meanwhile you could keep the application data more fresh with a manual parser by reading everything that accumulated since your last read and discarding everything except the latest event, or aggregating them into a summary. With EventSource, each accumulated event becomes a separate callback queued on the event loop, and your handler runs them sequentially, regardless of how stale they are, meaning there is a gap between what is actually happening with the data and what the user sees.</p>
<p>Taken together, if you need a quick and reliable method for simply listening to SSEs, then use EventSource. If you require more control over headers or backpressure, or need to send data from the client in a <code>POST</code> request, opt for the fetch method.</p>
]]></content:encoded></item><item><title><![CDATA[Concurrency: A Pythonic Approach ]]></title><description><![CDATA[Parallelism vs. Concurrency
By default, Python code is executed synchronously, meaning that once the function is called, the calling code blocks execution of the main thread until the function finishe]]></description><link>https://code-after-degree.hashnode.dev/concurrency-a-pythonic-approach</link><guid isPermaLink="true">https://code-after-degree.hashnode.dev/concurrency-a-pythonic-approach</guid><dc:creator><![CDATA[Benjamin Inglis]]></dc:creator><pubDate>Fri, 24 Apr 2026 15:50:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/68c19865376514b2a5927314/e287c30d-6577-4187-a77a-051986a39293.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Parallelism vs. Concurrency</h1>
<p>By default, Python code is executed synchronously, meaning that once the function is called, the calling code blocks execution of the main thread until the function finishes. If the function raises an exception, the calling code can wrap the call site with a <code>try/except</code> block to handle the errors. While this is convenient and makes it easier to reason about the code we write, there may be times when we want to utilize more of our available resources, or handle more tasks at once. Enter, concurrency and parallelism:</p>
<p><strong>Concurrency:</strong> The ability of a system to handle multiple tasks by allowing <em>overlapping</em> (but not necessarily simultaneous) execution.</p>
<p><strong>Parallelism:</strong> The simultaneous execution of multiple tasks that utilize multiple processing units. (<a href="https://realpython.com/python-thread-lock/">Divakaran</a>). This requires a multicore CPU, multiple CPUs, a GPU, or multiple computers in a cluster (Ramalho, 2022, p.697)</p>
<p>While the two concepts are similar in that they involve the execution of multiple tasks, note that parallelism <em>requires</em> simultaneous execution. Concurrency is the umbrella term for structuring a program to complete more than one task at once, and parallelism is a specific form, or subset of concurrency. All parallel systems are concurrent, but not all concurrent systems are parallel (Ramalho, p. 695).</p>
<h3>I/O-bound tasks vs. CPU-bound tasks</h3>
<p>When discussing different types of concurrent programming, it's important to understand the distinction between <a href="https://realpython.com/ref/glossary/io-bound-task/">I/O-bound</a> (input/output) and <a href="https://realpython.com/ref/glossary/cpu-bound-task/">CPU-bound</a> tasks. A program is said to be CPU-bound if the bottleneck is the CPU's processing power, which occurs in problems involving significant CPU computation (often times operations requiring complex math processing). Examples include: audio or image processing, computer vision, and machine or deep learning. Meanwhile, a task is considered I/O-bound when the bottleneck is the communication between the program and the outside world. Common examples include: reading data from a user input or file, making a network request, interacting with external devices, or database operations.</p>
<h3>Processes vs. Threads</h3>
<p>Another important distinction is that of a process and a thread. For these I found the most helpful definitions came from <em>Fluent Python</em> (Ramalho, 2022):</p>
<p><strong>Process:</strong> An instance of a computer program in execution, containing it's own memory space and a portion of the CPU time. Processes are isolated entities that contain their own memory spaces. Processes communicate via pipes, sockets, or memory mapped files by converting Python objects into of raw bytes to pass from one process to another. A drawback with processes is the overhead involved in communication, and fact that not all Python objects can be serialized (converted to raw bytes). Processes can spawn subprocesses, or child processes, which are also isolated from each other and the parent. Processes facilitate <a href="https://www.geeksforgeeks.org/operating-systems/difference-between-preemptive-and-cooperative-multitasking/">preemptive multitasking</a>, in which the OS scheduler <em>preempts</em> (suspends execution of each process periodically so that other processes can run) (p.698)</p>
<p><strong>Thread</strong>: A thread is an execution unit (the general term for objects that execute code concurrently, each with independent state and call stack) within a process. A process initializes a single thread (the main thread), and can spawn more threads to operate concurrently by calling operating system APIs. All threads within a process share the same memory space, which contains the active Python objects, thereby facilitating easy data sharing among threads. The tradeoff here is data corruption when one or more thread attempt to update the same object concurrently. Threads also enable preemptive multitasking in conjunction with the OS scheduler (p.698).</p>
<h3>Types of Concurrent vs. Parallel Programming</h3>
<p><strong>Multithreading</strong> refers to a type of concurrent programming in which a process spawns and orchestrates multiple threads to complete a task. Multithreading implements <a href="https://www.geeksforgeeks.org/operating-systems/difference-between-preemptive-and-cooperative-multitasking/">preemptive multitasking</a>, meaning the OS scheduler is responsible for switching contexts (i.e., deciding which thread is running). Each thread can be executing its own set of instructions, but the the Global Interpreter Lock (GIL) ensures that <em>only one thread</em> can be running at any given time. Thus, it is <em>impossible</em> to utilize multiple processors with threads in Python. The GIL serves as a <a href="https://en.wikipedia.org/wiki/Lock_%28computer_science%29">mutex</a> (a type of lock that prevents more than one thread from accessing a state) to prevent multiple threads from executing Python <a href="https://docs.python.org/3/glossary.html#term-bytecode">bytecode</a> at the same time. This is necessary since CPython (the interpreter that executes the bytecode instructions) does not have thread-safe memory management (2).</p>
<p><strong>Asynchronous programming</strong> is another form of concurrent programming available through the <a href="https://docs.python.org/3/library/asyncio.html">asyncio</a> library via <code>async/await</code> syntax. A key difference between <code>asyncio</code> and multithreading is that <code>asyncio</code> implements <a href="https://en.wikipedia.org/wiki/Cooperative_multitasking">cooperative multitasking</a>, meaning there is only a single thread and control is passed between <a href="https://realpython.com/ref/glossary/coroutine/">coroutines</a> (functions defined with the <code>async def</code>; can suspend itself and resume later). When an <code>await</code> statement is reached, the current coroutine pauses execution, passing control back to the <a href="https://docs.python.org/3/library/asyncio-eventloop.html">event loop</a> until the current awaited operation returns. While an in-depth discussion of the <code>asyncio</code> event loop is beyond the scope of this article, you can think of the event loop as the manager of all the coroutines, coordinating what task to switch to and when to resume a paused coroutine (3).</p>
<p><a href="https://docs.python.org/3/library/multiprocessing.html">Multiprocessing</a> enables us to leverage multiple <a href="https://www.hp.com/us-en/shop/tech-takes/cpu-cores-how-many-do-i-need">CPU cores</a> via parallelization, with each core capable of handling its own tasks and processing information independently. This is achieved via the <code>multiprocessing</code> package in Python, and supports spawning multiple processes, circumventing the GIL since it uses subprocesses rather than threads. The key difference is that these subprocesses <strong>do not</strong> share the same memory space, meaning there is significant overhead cost associated with sharing data between processes.</p>
<h2>Race Conditions, &amp; Deadlock</h2>
<p>A common pitfall in multithreaded programming is known as a race condition, which occurs when two or more threads attempt to access and modify the same data at the same time and the final result is dependent on the order in which the threads run. For example, say you have two threads (T1 and T2) trying to access your email address in a system. T1 will update your address while T2 is reading the address as it’s been requested. If T1 and T2 are executed simultaneously, T2 could return your old (outdated) email, or the correct (updated) email, depending on which completes first. <a href="https://link.springer.com/rwe/10.1007/978-0-387-09766-4_282">Deadlock</a> is another concurrent computing phenomenon, which can arise when two or more processes or threads are blocked because each one is waiting for the other to release a resources, bringing progress to a halt.</p>
<h1>Synchronization Primitives</h1>
<p>To protect against race conditions when employing concurrent programming, we use <a href="https://en.wikipedia.org/wiki/Synchronization_(computer_science)#Implementation">synchronization primitives</a>, which are effectively mechanisms used to control access to shared resources. While these are similar in both multithreading and <code>asyncio</code>, the <a href="https://docs.python.org/3/library/asyncio-sync.html">synchronization primitives for <code>asyncio</code></a> are designed for cooperative multitasking, meaning they are not thread-safe and should only be used within the same event loop. Additionally, <code>asyncio</code> primitives do not accept the <em>timeout</em> argument, opting for <code>asyncio.wait_for()</code> to implement timeouts. The <code>multiprocessing</code> module also includes equivalents of all the synchronization primitives from <code>threading</code>, but these are not as necessary in a multiprocess program so they will not be discussed in this article. Furthermore, the <a href="https://docs.python.org/3/library/multiprocessing.html#programming-guidelines">official documentation</a> recommends avoiding shared state and using queues or pipes for communication between processes rather than lower level synchronization primitives.</p>
<h2>Multithreading</h2>
<h3><strong>Locks and RLock</strong></h3>
<p>A lock is used to allow only one thread to access a resource at a time, such that once the lock is acquired, no other threads can acquire the lock until the lock is released. locks are either locked or unlocked, and can be acquired by calling the <code>Lock.acquire()</code> method. The <code>release()</code> method, is used to unblock execution of other threads when called by the thread holding a locked lock (but will raise a <code>RuntimeError</code> if called on an unlocked lock). locks can be used as a context manager to automate acquisition and releasing of locks as shown below:</p>
<pre><code class="language-python">import threading 
import time
from concurrent.futures import ThreadPoolExecutor

lock = threading.Lock()
class UserAccount:
    def __init__(self):
        self.name = "John Doe"
        self.email = "John@gmail.com"
        self.account_lock = threading.Lock()
 
    def get_email(self):
        with self.account_lock:   
            return self.email
    
    def set_email(self, email):
        with self.account_lock:
            self.email = email

with ThreadPoolExecutor(max_workers=3) as executor:
    executor.submit(set_email, "Johndoe@gmail.com")
    executor.submit(get_email)
    executor.submit(set_email, "John@gmail.com")
</code></pre>
<p>One drawback with the regular lock is that if the same thread attempts to acquire the lock it already holds, a deadlock will occur. This could happen if within your locked function you invoke another function containing the same lock. Enter, the RLock, or reentrant lock. This synchronization primitive allows the same thread to acquire a lock multiple times before releasing it, preventing deadlock in situations where a thread needs to re-enter a locked resource. The tradeoff is increased overhead since the RLock has to keep track of how many times the same thread has acquired a lock, so it should only be used when necessary (4).</p>
<h3>Semaphores</h3>
<p>The <a href="https://en.wikipedia.org/wiki/Semaphore_(programming)">semaphore</a> was invented in the early 1960's by someone every CS student is familiar with: Edsger W. Dijkstra.</p>
<p>A semaphore is a type of atomic counter which guarantees that the OS will not interrupt the thread in the middle of incrementing or decrementing the counter. The internal counter is incremented/decremented with the <code>release()</code> and <code>acquire()</code> methods, respectively. Semaphores are frequently used to protect a resource with limited capacity, such as a connection pool (4). Semaphores are constructed by passing in the max number of concurrent threads acquiring it. Semaphores can be used as context managers, entering with a successful <code>acquire()</code> call and automatically calling <code>release()</code> when exiting the with block.</p>
<p>The example below (adapted from <a href="https://www.soumendrak.com/blog/semaphores-python-async-programming/">asyncio.Semaphore: Practical Guide with Real-World Use Cases</a>) highlights this use case by utilizing a semaphore to cap the number of queries that can execute concurrently using the <code>max_connections</code> variable.</p>
<pre><code class="language-python">import threading
from concurrent.futures import ThreadPoolExecutor
import psycopg2
from psycopg2 import pool


class DatabasePool:
    def __init__(self, dsn, max_connections=5):
        self.dsn = dsn
        self.semaphore = threading.Semaphore(max_connections)
        self.pool = None

    def init_pool(self):
        self.pool = psycopg2.pool.ThreadedConnectionPool(
            minconn=1,
            maxconn=5,
            dsn=self.dsn,
        )

    def query(self, sql, *args):
        with self.semaphore:
            conn = self.pool.getconn()
            try:
                with conn.cursor() as cur:
                    cur.execute(sql, args)
                    return cur.fetchall()
            finally:
                self.pool.putconn(conn)

    def close(self):
        self.pool.closeall()


def main():
    db = DatabasePool("postgresql://user:password@localhost/database")
    db.init_pool()

    with ThreadPoolExecutor(max_workers=5) as executor:
        futures = [
            executor.submit(db.query, "SELECT * FROM users WHERE id = %s", i)
            for i in range(1, 11)
        ]
        results = [f.result() for f in futures]

    db.close()


if __name__ == "__main__":
    main()
</code></pre>
<h3>Events</h3>
<p>Events are objects that allow threads to "communicate" via an internal flag that defaults to <code>False</code>, but can be set to <code>True</code> by calling the <code>set()</code> method, or reset to <code>False</code> by calling <code>clear()</code>. Individual threads can wait for the flag using the <code>wait()</code> method, which blocks execution until the flag is set. Flags can be used to coordinate actions across multiple threads, such as signalling state changes, thereby enabling efficient synchronization management. When a new Costco location opens the often go all out, selling limited-time champagne signed by celebrities and serving caviar samples. The example below uses events to simulate the opening of a new Costco location and the commencement of giving out caviar:</p>
<pre><code class="language-python">import threading
import time
from concurrent.futures import ThreadPoolExecutor

costco_open = threading.Event()
caviar_open = threading.Event()

def serve_customer(customer_data):
    print(f"{customer_data['name']} is waiting for the Costco to open.")

    costco_open.wait()
    print(f"{customer_data['name']} entered Costco")
    if customer_data["type"] == "VIP_EXPERIENCE":
        print(f"{customer_data['name']} is waiting for caviar to be served.")
        caviar_open.wait()
        print(f"{customer_data['name']} is getting their caviar.")

        # Simulate the time taken for eating caviar
        time.sleep(2)

        print(
            f"{customer_data['name']} finished eating and exited the store"
        )
    else:
        # Simulate the time taken for shopping
        time.sleep(2)
        print(f"{customer_data['name']} has exited the store")

customers = [
    {"name": "Customer 1", "type": "REGULAR_SHOP"},
    {"name": "Customer 2", "type": "VIP_EXPERIENCE"},
    {"name": "Customer 3", "type": "REGULAR_SHOP"},
    {"name": "Customer 4", "type": "VIP_EXPERIENCE"},
]

with ThreadPoolExecutor(max_workers=4) as executor:
    for customer_data in customers:
        executor.submit(serve_customer, customer_data)

    print("Costco manager is preparing to open the store.")
    time.sleep(2)
    print("Costco is now open!")
    costco_open.set()  # Signal that the new location is open

    time.sleep(3)
    print("Caviar is now being served!")
    caviar_open.set()

print("All customers have completed their experiences.")
</code></pre>
<h3>Conditional Waiting</h3>
<p>A condition object is built on top of a Lock or RLock and supports additional functionality allowing threads to wait for certain conditions to be met, and signal other threads of condition changes.</p>
<p><strong>Methods associated with Condition objects</strong>:</p>
<ul>
<li><p><code>acquire()</code>: Acquire the underlying lock; must be called before a thread can wait on or signal a condition</p>
</li>
<li><p><code>release()</code>: Releases the underlying lock</p>
</li>
<li><p><code>wait(timeout=None)</code>: Blocks the thread until it’s notified, or a specific timeout occurs. The lock is released before blocking and reacquired upon notification or when timeout expires. Useful when a thread needs to wait for a specific condition to be true before proceeding</p>
</li>
<li><p><code>notify(n=1)</code>: Wakes up one of the threads waiting for the condition (if any are waiting). Will select one randomly if multiple threads are waiting</p>
</li>
<li><p><code>notify_all():</code> Wakes up all threads waiting for the condition</p>
</li>
</ul>
<p>Condition objects are useful for coordinating across threads and managing the flow of execution in a multithreaded environment. The following example from from <a href="https://realpython.com/python-thread-lock/#conditions-for-conditional-waiting">Real Python</a> demonstrates how a condition can be used to synchronize access to the shared <code>customer_queue</code> object and signal the <code>teller_thread</code> when a new customer arrives.</p>
<pre><code class="language-python">import random
import threading
import time
from concurrent.futures import ThreadPoolExecutor

customer_available_condition = threading.Condition()

# Customers waiting to be served by the Teller
customer_queue = []

def now():
    return time.strftime("%H:%M:%S")

def serve_customers():
    while True:
        with customer_available_condition:
            # Wait for a customer to arrive
            while not customer_queue:
                print(f"{now()}: Teller is waiting for a customer.")
                customer_available_condition.wait()

            # Serve the customer
            customer = customer_queue.pop(0)
            print(f"{now()}: Teller is serving {customer}.")

        # Simulate the time taken to serve the customer
        time.sleep(random.randint(1, 5))
        print(f"{now()}: Teller has finished serving {customer}.")

def add_customer_to_queue(name):
    with customer_available_condition:
        print(f"{now()}: {name} has arrived at the bank.")
        customer_queue.append(name)

        customer_available_condition.notify()

customer_names = [
    "Customer 1",
    "Customer 2",
    "Customer 3",
    "Customer 4",
    "Customer 5",
]

with ThreadPoolExecutor(max_workers=6) as executor:
    teller_thread = executor.submit(serve_customers)
    for name in customer_names:
        # Simulate customers arriving at random intervals
        time.sleep(random.randint(1, 3))
        executor.submit(add_customer_to_queue, name)
</code></pre>
<h3>Barriers</h3>
<p>Barriers allow groups of threads to wait for each other before continuing execution. It blocks program execution until a specified number of threads reach the barrier</p>
<p>Barrier takes in one required and two optional arguments:</p>
<ul>
<li><p><code>parties</code>: the # of threads of the barrier object that the wait() method waits for before proceeding</p>
</li>
<li><p><code>action</code>: callable that will be executed by one of the threads when released</p>
</li>
<li><p><code>timeout</code>: timeout value for the wait() method</p>
</li>
</ul>
<p>Going back to Costco, let's say that we want to use a barrier to only initiate the Grand Opening (open the flood gates, if you will) once all employees are prepared for the stampede:</p>
<pre><code class="language-python">import random
import threading
import time
from concurrent.futures import ThreadPoolExecutor

employee_barrier = threading.Barrier(3)

def now():
    return time.strftime("%H:%M:%S")

def prepare_for_work(name):
    print(f"{now()}: {name} is preparing their station.")

    # Simulate the delay to prepare the station
    time.sleep(random.randint(1, 3))
    print(f"{now()}: {name} has finished preparing.")

    # Wait for all employees to finish preparing
    employee_barrier.wait()
    print(f"{now()}: {name} is now ready to serve customers.")

employees = ["Front door person 1", "Front door person 2", "Cashier 1", "Cashier 2", "Caviar sample distributor"]

with ThreadPoolExecutor(max_workers=5) as executor:
    for employee_title in employees:
        executor.submit(prepare_for_work, employee_title)

print(f"{now()}: All employees are ready to serve customers.")
</code></pre>
<p>Barriers are useful when multiple threads need to be in sync with each other before proceeding, or when you need to coordinate the start of a sequence across multiple threads (4). For example, you could use a barrier when threads are working concurrently to compute a set of results to ensure that all results are in before proceeding to the next stage of computation.</p>
<h2>Asyncio</h2>
<p>The <code>asyncio</code> synchronization primitives function very similarly to those of threading, with the goal of coordinating access to shared resources between tasks. This section will serve as a high-level overview, so refer to the <a href="https://docs.python.org/3/library/asyncio-sync.html">official docs</a> for more information!</p>
<h3><strong>Lock</strong></h3>
<p>Locks are used to guarantee <em>exclusive</em> access to a shared resource. The recommended utilization is an <code>async with</code> statement</p>
<pre><code class="language-python">lock = asyncio.Lock()

async with lock:
    # access shared state
</code></pre>
<p>The <code>acquire()</code> and <code>release()</code>methods are similar to those to <code>threading.Lock</code>:</p>
<ul>
<li><p><code>acquire()</code>waits until the lock is unlocked, then sets it to locked and returns <code>True</code>. Only one coroutine can proceed at a time when more than one are waiting for the lock to be unlocked. Acquiring a lock is <em>fair</em>, meaning the coroutines proceed in the order they arrived (FIFO)</p>
</li>
<li><p><code>release()</code> method resets the lock to <em>unlocked</em>, and raises a <code>RuntimeError</code> if called on an unlocked lock</p>
</li>
<li><p>The <code>locked()</code> method returns <code>True</code> if the lock is locked</p>
</li>
</ul>
<h3><strong>Event</strong></h3>
<p>Events are used to notify multiple <code>asyncio</code> tasks that an event has happened. events contain an internal flag that can be set to <code>True</code> with the <code>set()</code> method and reset to <code>False</code> with <code>clear()</code> method. The <code>wait()</code> method blocks execution until flag is set to <code>True</code>. When the <code>set()</code> method is invoked, all tasks waiting on the event will be immediately awakened.</p>
<h3><strong>Condition</strong></h3>
<p>Conditions combines functionality of events and locks. Conditions can be initialized by passing in an existing lock, otherwise a new lock will be created automatically. Conditions allow coordinating exclusive access to a shared resource between tasks.</p>
<p>Condition methods include:</p>
<ul>
<li><p>The <code>acquire()</code> method acquires the underlying lock, waiting until the lock is unlocked.</p>
</li>
<li><p><code>notify(n)</code> and <code>notify_all()</code> wake up <em>n</em> tasks or all tasks that are waiting on the condition object, respectively.  The lock must be acquired first, otherwise a <code>RuntimeError</code> will be raised.</p>
</li>
<li><p>The <code>locked()</code> method returns <code>True</code> if the underlying lock is currently acquired</p>
</li>
<li><p><code>release()</code>releases underlying lock, resulting in a <code>RuntimeError</code> if called on an unlocked lock</p>
</li>
<li><p><code>wait()</code> causes a task to wait, blocking execution until notified</p>
</li>
<li><p><code>wait_for(predicate)</code>blocks execution until the specified <em>predicate</em> (a callable that is interpreted as a boolean) evaluates to<code>True</code></p>
</li>
</ul>
<h3><strong>Semaphore</strong></h3>
<p>Manages an internal counter, decremented by each <code>acquire()</code> call and incremented by each <code>release()</code> call. Semaphores are instantiated by passing in a <em>value</em> (default is 1), specifying the number of tasks that can access the protected resource at a time. The counter can never go below zero, so if <code>acquire()</code> is called at 0, it blocks until another task releases it.</p>
<h3><strong>Bounded Semaphore</strong></h3>
<ul>
<li>A bounded semaphore is a version of a semaphore that raises a <code>ValueError</code> if the <code>release()</code> method would cause the internal counter to go above the initial value.</li>
</ul>
<h3><strong>Barrier</strong></h3>
<p>Similar to threads, barriers block execution at a point in the code until a specified number of <em>parties</em> reach it, at which point all tasks are unblocked simultaneously.</p>
<h2>Summary: When should we use what?</h2>
<p>Key point: neither multithreading nor asynchronous programming will ever increase the power in a system due to the cost of task switching and synchronization. As such, good candidates for these techniques are tasks that involve significant downtime waiting for <em>external</em> events (i.e., I/O-bound tasks).</p>
<p>Multithreading is desirable in Python due to preemptive task switching, meaning no additional code is required to context switch. Further, all threads share a state, so no additional overhead is required for threads to access a shared resources such as variables and data structures. However, this is a double-edged sword since multiple threads accessing and shared state can result in <a href="https://en.wikipedia.org/wiki/Race_condition">race conditions</a> affecting critical sections, and deadlock can arise as a result of protecting such sections. Locks are problematic because it becomes very difficult to reason about your code as more are added (1). Furthermore, locks don't actually "lock" anything– they're just a signal that a thread can check– but if a thread doesn't check to acquire a lock, they can <em>still access the resource</em>.</p>
<p>Between async and multithreading, <code>asyncio</code> may be desirable to multithreading since you no longer have to deal with locks or worry about arbitrary interruptions. Additionally, the cost of switching contexts is very low because it uses generators under the hood to store the state (1). The disadvantage of the asynchronous approach is that you have to explicitly add keywords such as <code>yield</code> or <code>await</code> to pass control back to the scheduler and manage the flow of execution. Furthermore, every single thing you do has to be non-blocking, and it requires all involved libraries to be async-compatible (1).</p>
<p>For CPU-bound tasks, we opt for multiprocessing, since such tasks block the event loop when executed asynchronously, and the Python GIL causes threads to perform <em>worse</em> than sequential code during intense computations. Indeed, Python is very popular in the modern data science community, supporting compute-intensive tools such as <a href="https://jupyter.org/">Project Jupyter</a>, <a href="https://www.tensorflow.org/">TensorFlow</a>, <a href="https://pytorch.org/">PyTorch</a>, and <a href="https://www.dask.org/">Dask</a>.</p>
<p><strong>Sources:</strong></p>
<ol>
<li><p><a href="https://www.youtube.com/watch?v=9zinZmE3Ogk"><strong>Raymond Hettinger, Keynote on Concurrency, PyBay 2017</strong></a></p>
</li>
<li><p><a href="https://python.land/python-concurrency/the-python-gil">The Python GIL (Global Interpreter Lock)</a></p>
</li>
<li><p><a href="https://realpython.com/async-io-python/?gad_source=1&amp;gad_campaignid=23282418443&amp;gbraid=0AAAAA_bFrtJLh04S5MREWA282BxKywlUr&amp;gclid=CjwKCAjwnZfPBhAGEiwAzg-VzI08vi1_Hf2VEfAe1vP-vwkJrVhjYlBr4WHg6H_v1bf0pxNut-xqTRoCs74QAvD_BwE">Python's asyncio: A Hands-On Walkthrough</a></p>
</li>
<li><p><a href="https://realpython.com/python-thread-lock/">Python Thread Safety: Using a Lock and Other Techniques</a></p>
</li>
<li><p>Ramalho, L. (2022). <em>Fluent Python : clear, concise, and effective programming</em>. O’reilly Media, Inc.</p>
<p>‌</p>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[Developing a Real-time Chat Application: Part 3 – File Attachments]]></title><description><![CDATA[GitHub
Live Demo
Video Demo
If you haven’t read them already, check these out first:
Part 1 – Messaging
Part 2 – User Permissions & Security
Overview
The nice thing about adding file attachments is th]]></description><link>https://code-after-degree.hashnode.dev/developing-a-real-time-chat-application-part-3-file-attachments</link><guid isPermaLink="true">https://code-after-degree.hashnode.dev/developing-a-real-time-chat-application-part-3-file-attachments</guid><category><![CDATA[Web Development]]></category><category><![CDATA[Node.js]]></category><dc:creator><![CDATA[Benjamin Inglis]]></dc:creator><pubDate>Wed, 14 Jan 2026 21:35:53 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1768426522344/d4de827d-baf3-4ba7-8ae4-cc6515588ffb.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a href="https://github.com/bingliscodes/chattycat"><strong>GitHub</strong></a></p>
<p><a href="https://github.com/bingliscodes/chattycat"><strong>Live</strong></a> <a href="https://chattycat.netlify.app/"><strong>D</strong></a><a href="https://github.com/bingliscodes/chattycat"><strong>emo</strong></a></p>
<p><a href="https://youtu.be/1RhYNEVxVxk"><strong>Video Demo</strong></a></p>
<p>If you haven’t read them already, check these out first:</p>
<p><a href="https://code-after-degree.hashnode.dev/developing-a-real-time-chat-application-with-web-sockets">Part 1 – Messaging</a></p>
<p><a href="https://code-after-degree.hashnode.dev/developing-a-real-time-chat-application-part-2-user-permissions-and-security?showSharer=true">Part 2 – User Permissions &amp; Security</a></p>
<h1>Overview</h1>
<p>The nice thing about adding file attachments is that the user experience is straightforward. I knew that I wanted to have some sort of button that the user clicks which opens up a file explorer where they can then select the attachment(s) they wish to upload. What didn’t occur to me is the complexity involved in both the frontend to seamlessly display the attachments, and the backend to store the attachments with the messages.</p>
<h3>File Storage Best Practices</h3>
<p>When I first started this project, I didn’t understand why we can’t simply store the file in the database itself. In short, objects smaller than 256KB are best stored in a database whereas larger objects (&gt;1MB) are best stored in the file system (<a href="https://www.microsoft.com/en-us/research/publication/to-blob-or-not-to-blob-large-object-storage-in-a-database-or-a-filesystem/">Gray, 2006</a>). In other words, databases are optimized for structured queries whereas filesystems are optimized for streaming large object. <a href="https://wisedataman.com/should-i-store-blobs-in-the-database">Other reasons</a> why you shouldn’t store files in your database include such as excessive size, high costs, slow backups, and poor performance. Instead, we will upload the file into a cloud object storage system, such as an <a href="https://aws.amazon.com/s3/">S3 bucket</a>, from which we can create a key that is stored in the database. We can then use that key to access the object.</p>
<p>I think it helps to break down the process into distinct steps, and then break down the implementation at each step:</p>
<ol>
<li><p>User uploads file to a message</p>
</li>
<li><p>User sends message with file attachment</p>
</li>
<li><p>Message displays in chat with attachment (optimistic update)</p>
</li>
<li><p>Retrieve pre-signed URL from backend</p>
</li>
<li><p>Upload directly to S3</p>
</li>
<li><p>Send message via WebSocket</p>
</li>
<li><p>Server saves to database</p>
</li>
<li><p>Other clients receive the message</p>
</li>
</ol>
<h1>Updating the Data Model</h1>
<p>In order for this to be possible, some modifications need to be made to the data model. The approach I took was to create a <code>MessageAttachment</code> model that has a many-to-one relationship with the message table (each message attachment relates to a single message).</p>
<pre><code class="language-javascript">// MessageAttachment model
export const MessageAttachment = sequelize.define('messageAttachment', {
  id: {
    type: DataTypes.UUID,
    defaultValue: DataTypes.UUIDV4,
    primaryKey: true,
  },
  fileUrl: {
    type: DataTypes.STRING,
    allowNull: false,
  },
  fileName: {
    type: DataTypes.STRING,
    allowNull: false,
  },
  mimeType: {
    type: DataTypes.STRING,
    allowNull: false,
  },
  messageId: {
    type: DataTypes.UUID,
    allowNull: false,
    references: {
      model: Message,
      key: 'id',
    },
  },
});

// Establish relationships for Sequelize
  Message.hasMany(MessageAttachment, {
    foreignKey: 'messageId',
    as: 'attachments',
  });

  MessageAttachment.belongsTo(Message, {
    foreignKey: 'messageId',
    as: 'message',
  });
</code></pre>
<p>Next, I have to modify all controllers that query messages to include the attachments. For example, the following controller gets all messages for a channel:</p>
<pre><code class="language-javascript">export const getChannelMessages = catchAsync(async (req, res, next) =&gt; {
  const channelId = req.params.id;

  const messages = await Message.findAll({
    where: { [Op.and]: [{ channelId }, { parentMessageId: null }] },
    include: [
      {
        model: Channel,
        as: 'Channel',
        attributes: ['channelName', 'id'],
      },
      {
        model: User,
        as: 'Sender',
        attributes: ['firstName', 'lastName', 'avatarUrl'],
      },
      { model: MessageAttachment, as: 'attachments' },
    ],
  });

  res.status(200).json({
    status: 'success',
    results: messages.length,
    data: messages,
  });
});
</code></pre>
<p>Note: the where statement includes the condition <code>parentMessageId: null</code>, which is how I filter out thread replies.</p>
<h1>Implementing File Attachments</h1>
<h2>Step 1: The File Upload</h2>
<p>Since we need to access the files outside of the attachment button component, and we need our attachments variable to update when changes are made, we can define a state <code>attachments</code> and <code>setAttachments</code> in the parent component and then pass them in to the upload button component. To create the file upload button I used Chakra’s built-in <code>Input</code> element with the <code>type</code> property set to <code>file</code>. Next, I defined a function <code>handleFileChange</code> which creates an array from the selected files and then updates the attachments state. Finally, I added a <code>handleRemoveFile</code> function to allow the user to remove files that they uploaded by mistake. The entire component looks like this:</p>
<pre><code class="language-javascript">import { AiOutlinePaperClip } from 'react-icons/ai';
import { Button, Flex, Text, Input, Box } from '@chakra-ui/react';

export default function ChatFileUploadButton({ attachments, setAttachments }) {
  const handleFileChange = (e) =&gt; {
    const selectedFiles = Array.from(e.target.files);
    setAttachments((prev) =&gt; [...prev, ...selectedFiles]);
    e.target.value = null; // allow re‑uploading same file name
  };

  const handleRemoveFile = (index) =&gt; {
    setAttachments((prev) =&gt; prev.filter((_, i) =&gt; i !== index));
  };

  return (
    &lt;Box w="100%"&gt;
      &lt;Flex align="center" gap={2}&gt;
        &lt;Input
          type="file"
          multiple
          display="none"
          id="chat-file-input"
          onChange={handleFileChange}
        /&gt;

        &lt;Button
          as="label"
          htmlFor="chat-file-input"
          rounded="full"
          w="1rem"
          marginLeft={2}
          size="sm"
          bg="bg.primaryBtn"
          color="text.primaryBtn"
          _hover={{ bg: 'bg.navHover' }}
        &gt;
          &lt;AiOutlinePaperClip /&gt;
        &lt;/Button&gt;
      &lt;/Flex&gt;

      {attachments.length &gt; 0 &amp;&amp; (
        &lt;Flex direction="column" gap={2} mt={2}&gt;
          {attachments.map((file, idx) =&gt; (
            &lt;Flex key={idx} align="center" gap={2}&gt;
              &lt;Text fontSize="sm" flex="1"&gt;
                {file.name}
              &lt;/Text&gt;
              &lt;Button
                fontSize="sm"
                p={0}
                size="2xs"
                onClick={() =&gt; handleRemoveFile(idx)}
              &gt;
                x
              &lt;/Button&gt;
            &lt;/Flex&gt;
          ))}
        &lt;/Flex&gt;
      )}
    &lt;/Box&gt;
  );
}
</code></pre>
<p>Then I can render my upload button from the parent component (the <code>ChatInput</code> where I initialized the state), which will allow me to access the attachments as they are updated by the user interacting with the interface.</p>
<h2>Step 2: Message Submission</h2>
<p>An uneventful but imperative stage in which the user clicks the send button or presses <code>enter</code>, initiating the message submission process.</p>
<h2>Step 3: Optimistic Update</h2>
<p>The optimistic updates are very important to the user experience and performance of the chat application. Without optimistic updating, we would essentially have query the messages from the database <strong>every time</strong> a message is sent and then re-render the messages for the user. With optimistic updating, we can give the illusion of instantaneous communication without having to query the database each time. Additionally, it allows us to populate the chat immediately as opposed to forcing the user to wait until the file finishes processing, which would be a poor overall experience.</p>
<pre><code class="language-javascript">const createOptimisticMessage = ({ messageBody, attachments }) =&gt; {
    const now = new Date();
    const datestamp = now.toLocaleDateString('en-US', {
      weekday: 'long',
      year: 'numeric',
      month: 'long',
      day: 'numeric',
    });
    const timestamp = now.toLocaleTimeString([], {
      hour: 'numeric',
      minute: '2-digit',
      hour12: true,
    });

    return {
      tempId: uuidv4(),
      messageBody,
      attachments: attachments.map((file) =&gt; ({
        name: file.name,
        type: file.type,
        preview: URL.createObjectURL(file),
      })),
      sender: {
        firstName: userData.firstName,
        lastName: userData.lastName,
      },
      timestamp,
      datestamp,
      status: 'sending',
    };
  };
</code></pre>
<p>To summarize, the function above takes in a string <code>messageBody</code> and an array <code>attachments</code>, then returns an object with all information necessary to display a <code>ChatMessage</code>. Of note, we are adding two fields: <code>tempId</code>, which will be used to match the optimistic message with the “real” message from the database, and <code>status</code>, which indicates whether a message has successfully been processed by the server.</p>
<p>Now that we have the ability to create an optimistic message, we can invoke the helper function at the beginning of the <code>onSubmit</code> handler:</p>
<pre><code class="language-javascript">const onSubmit = handleSubmit(async (data) =&gt; {
    const optimisticMsg = createOptimisticMessage({
      messageBody: data.message,
      attachments,
    });
    
    onMessageSent(optimisticMsg);
    // ...
}
</code></pre>
<p>Note: the <code>onMessageSent</code> function is a helper function that is passed down from the <code>ChatInterface</code> and is used to update the <code>messages</code> state. It’s purpose is to add the newly created optimistic message and then sort them based on the date and time.</p>
<pre><code class="language-javascript">  const handleMessageSent = (msg) =&gt; {
    setMessages((prev) =&gt; insertAndSortMessages([...prev, msg]));
  };

  const insertAndSortMessages = (messagesArray) =&gt; {
      return [...messagesArray].sort(
        (a, b) =&gt;
          new Date(a.createdAt || a.sentAt) - new Date(b.createdAt || b.sentAt)
      );
  };
</code></pre>
<p>Now that the chat has been updated with the most recent message, we can continue with the “…” part of the <code>handleSubmit</code> function.</p>
<h2>Step 4: Retrieve pre-signed URL from backend</h2>
<p>We do not want to send the entire file via WebSockets for several reasons, including but not limited to:</p>
<ul>
<li><p>It would block other messages while the file is being transmitted</p>
</li>
<li><p>Poor recovery, meaning dropped connection requires the process to restart, whereas HTTP-based uploads can be chunked/resumed</p>
</li>
<li><p>Long uploads can trigger timeouts</p>
</li>
<li><p>Adds significant overhead when encoding binary as base64</p>
</li>
</ul>
<p>Moreover, if you were paying attention earlier you would recall that we shouldn’t be storing files in the database in the first place, so we don’t really have a reason to transmit them in the first place!</p>
<h3>Step 4a: Processing Attachments</h3>
<p>The better approach is to delegate the file upload process to a helper function that takes in both <code>files</code> and <code>messageId</code>, loads the files into our object storage system, and then returns the data that we want to store in the database. There is one small problem with this approach, however: the backend has the credentials to our S3 bucket, and we want to keep them out of the frontend to avoid exposing them.</p>
<p>The solution here is to generate pre-signed, authenticated URLs generated by AWS. First, we need to create a controller function that will create a unique key for each file, then return an <code>uploadUrl</code> (the URL used for the <code>POST</code> request) and a <code>fileUrl</code> (the location where the uploaded file will be stored).</p>
<pre><code class="language-javascript">export const generateUploadUrls = catchAsync(async (req, res, next) =&gt; {
  const { files } = req.body; // Array of {name, mimeType}

  if (!files || !Array.isArray(files))
    return next(new AppError('Files array is required', 400));

  const presignedUrls = await Promise.all(
    files.map(async (file) =&gt; {
      const ext = path.extname(file.name) || '';
      const key = `messageFiles/\({uuidv4()}\){ext}`;

      const command = new PutObjectCommand({
        Bucket: process.env.AWS_S3_BUCKET_NAME,
        Key: key,
        ContentType: file.mimeType,
      });

      // Generate presigned URL (valid for 5 minutes)
      const uploadUrl = await getSignedUrl(s3, command, { expiresIn: 300 });

      // The final public URL after upload
      const fileUrl = `https://\({process.env.AWS_S3_BUCKET_NAME}.s3.\){process.env.AWS_REGION}.amazonaws.com/${key}`;

      return {
        uploadUrl, // Use this to upload
        fileUrl, // This is the final URL to save in DB
        fileName: file.name,
        mimeType: file.mimeType,
      };
    }),
  );

  res.status(200).json({ status: 'success', data: presignedUrls });
});
</code></pre>
<h3>Step 4b: Retrieve pre-signed URLs</h3>
<p>Now we can define a helper function to retrieve those URLs by making a <code>POST</code> request from the frontend, passing in the necessary data in the request body:</p>
<pre><code class="language-javascript">const getPresignedUrls = async (files) =&gt; {
  const fileMetadata = files.map((file) =&gt; ({
    name: file.name,
    mimeType: file.type,
  }));

  const res = await axios.post(
    `${import.meta.env.VITE_DEV_API_BASE_URL}uploads/generateUploadUrls`,
    {
      files: fileMetadata,
    },
    { withCredentials: true }
  );
  return res.data.data;
};
</code></pre>
<h2>Step 5: Upload directly to S3</h2>
<p>We will also want a helper function to handle the upload itself. While I originally had both functions combined into one, I decided to separate them out into distinct helper functions to abide by the single responsibility principle. It also makes it easier to debug since each HTTP request is made in its own function.</p>
<pre><code class="language-javascript">const uploadToS3 = async (file, uploadUrl) =&gt; {
  const blob =
    file instanceof File ? file : await fetch(file).then((r) =&gt; r.blob());

  await axios.put(uploadUrl, blob, {
    headers: {
      'Content-Type': file.type,
    },
  });
};
</code></pre>
<p>Now we can put it all together into our main <code>uploadAttachments</code> function (the one that will be invoked when the user submits the message):</p>
<pre><code class="language-javascript">export const uploadAttachments = async (files, messageId) =&gt; {
  try {
    // Get presigned URLs from backend
    const presignedData = await getPresignedUrls(files);

    if (!presignedData || !Array.isArray(presignedData)) {
      throw new Error('Invalid presigned URLs response');
    }
    // Upload each file to S3 using presigned URLs
    await Promise.all(
      files.map((file, index) =&gt;
        uploadToS3(file, presignedData[index].uploadUrl)
      )
    );

    // Return the file records to save in DB
    return presignedData.map((data) =&gt; ({
      fileName: data.fileName,
      messageId,
      fileUrl: data.fileUrl,
      mimeType: data.mimeType,
    }));
  } catch (error) {
    console.error('Error uploading attachments:', error);
    if (error.response) {
      console.error('Server response:', error.response.data);
    }
    throw error;
  }
};
</code></pre>
<h2>Step 6: Send message via WebSocket</h2>
<p>Because we processed the files into an array of objects containing metadata about the attachments (instead of the files themselves), we can modify the <code>sendMessage</code> function to include that array in the <code>messageContent</code> object that is sent over the socket.</p>
<pre><code class="language-javascript">  const sendMessage = async ({ messageBody, attachments = [], tempId }) =&gt; {
    if (!userSocket?.connected) return; // Ensure socket is connected

    let uploadedAttachments = [];
    if (attachments.length) {
      try {
        uploadedAttachments = await uploadAttachments(attachments, null);
      } catch (err) {
        console.error('Failed to upload attachments: ', err);
        return;
      }
    }

    const now = new Date();
    const datestamp = now.toLocaleDateString('en-US', {
      weekday: 'long',
      year: 'numeric',
      month: 'long',
      day: 'numeric',
    });
    const timestamp = now.toLocaleTimeString([], {
      hour: 'numeric',
      minute: '2-digit',
      hour12: true,
    });

    const messageContent = {
      messageBody,
      attachments: uploadedAttachments,
      sender: { firstName: userData.firstName, lastName: userData.lastName },
      timestamp,
      datestamp,
    };

    const messageData = {
      messageContent: messageBody,
      senderId: userData.id,
      tempId,
      type: chatMode === 'ch' ? 'channel' : 'direct',
    };

    // Conditional logic to ensure message includes necessary information
    if (chatMode === 'ch') messageData.channelId = channel?.id;
    if (chatMode === 'dm') {
      messageData.receiverId = directMessage?.id;
      messageData.roomId = channel?.id;
    }

    if (chatMode === 'thread') {
      messageData.parentMessageId = thread?.parentMessage?.messageId;
      userSocket.emit('send-thread-message', messageContent, messageData);
    } else {
      userSocket.emit('send-message', messageContent, messageData);
    }

    return messageContent;
  };
</code></pre>
<h2>Step 7: Server saves to database</h2>
<p>After the <code>sendMessage</code> function completes, our socket connection in the backend will receive the data since it is listening for the <code>send-message</code> event. Upon receiving the data from the frontend, we need to store the attachment metadata and ensure that it is properly associated with the message it came attached to. I accomplished this using a helper function <code>saveAttachmentRecords</code> which takes in an array of attachment metadata <code>attachments</code> and the <code>messageId</code>:</p>
<pre><code class="language-javascript">export const saveAttachmentRecords = async (attachments, messageId) =&gt; {
  // Save to your Attachment model
  const results = [];

  for (const att of attachments) {
    const record = await MessageAttachment.create({
      fileName: att.fileName,
      fileUrl: att.fileUrl,
      mimeType: att.mimeType,
      messageId,
    });

    results.push(record);
  }

  return results;
};
</code></pre>
<h3>Step 7a: Save attachments to database</h3>
<p>The first addition we need to make to the <code>send-message</code> handler function is to call the helper function above, which is what will actually create the relationship between message and attachment in the database:</p>
<pre><code class="language-javascript"> if (messageContent.attachments?.length) {
    const attachments = await saveAttachmentRecords(
       messageContent.attachments,
       messageId,
     );
   }
</code></pre>
<h3>Step 7b: Build confirmed message with the real <code>messageId</code></h3>
<p>Remember the <code>tempId</code> and <code>status</code> fields we added earlier? This is where they resurface. Now that the files have been processed and the messages are stored in the database, we create the <code>confirmedMessage</code>, which replaces the optimistic message from Step 3.</p>
<pre><code class="language-javascript"> // Build confirmed message with real ID
 const confirmedMessage = {
    ...messageContent,
    id: messageId,
    tempId: messageData.tempId,
    status: 'sent',
};

 // Send confirmation to sender
 socket.emit('message-confirmed', confirmedMessage);
</code></pre>
<p>Now we can add an event listener to the frontend to replace the optimistic message with our confirmed message:</p>
<pre><code class="language-javascript">const handleMessageConfirmed = (msg) =&gt; {
    // Replace optimistic message with confirmed one
    setMessages((prev) =&gt;
      prev.map((m) =&gt; (m.tempId === msg.tempId ? msg : m))
    );
};
</code></pre>
<h2>Step 8: Other clients receive the message</h2>
<p>Finally, we broadcast the <code>confirmedMessage</code> to the appropriate room (using the <code>channelId</code> if it’s a channel message and <code>roomId</code> for direct messages) by emitting the <code>receive-message</code> event.</p>
<pre><code class="language-javascript">// Broadcast to others in the room
if (messageData.type === 'channel' &amp;&amp; messageData.channelId) {
    socket.broadcast
      .to(messageData.channelId)
      .emit('receive-message', confirmedMessage);
  } else if (messageData.type === 'direct' &amp;&amp; messageData.roomId) {
      socket.broadcast
      .to(messageData.roomId)
      .emit('receive-message', confirmedMessage);
  }
</code></pre>
<p>This event is then received by our socket connection in the frontend and the involved clients’ interfaces are updated with the message (and potentially attachment):</p>
<pre><code class="language-javascript">const handleReceiveMessage = (msg) =&gt; {
  setMessages((prev) =&gt; insertAndSortMessages([...prev, msg]));
};
</code></pre>
<h1>Final Thoughts</h1>
<p>This process is confusing and took me a <strong>LONG</strong> time to fully wrap my head around. It involved making several changes and additions to the existing architecture. I also implemented it entirely differently at first and then had to completely revise my approach when I realized it wasn’t working. My first attempt involved sending the entire file over the socket connection (encoded in Base64). I eventually got it to work sending small files, but I quickly ran into issues trying to scale because the file uploads kept blocking the connection (because as discussed earlier, Web Sockets are not meant for sending files). The second time I blundered was when I added my AWS credentials into the frontend environment variables, not realizing that would expose them to anyone who decided to look in the developer tools.</p>
<p>This project taught me about choosing the right tool for the job, and the importance of understanding the benefits and drawbacks of different systems, services, data structures, and algorithms. In other words, just because you <em>can</em> do something doesn’t mean you <em>should</em> do that thing. Instead, seek to understand if the system or tool you’re using is designed to complete the task at hand. If you’re not sure, it might be worth doing some research to deepen your understanding and explore alternatives before proceeding. As much as it pains me to admit it, I do see the benefit of Leetcode style assessments that train your problem solving ability by teaching you to recognize patterns and then find the best solution using the tools (data structures and algorithms) at your disposal. There are often many ways to solve such a problem, but the “optimal” solution is not always obvious without comparing different approaches.</p>
]]></content:encoded></item><item><title><![CDATA[Developing a Real-time Chat Application:
Part 2 – User Permissions & Security]]></title><description><![CDATA[GitHub
Live Demo
Video Demo
In the first part of this series, I outlined the basic project set up and reviewed how I implemented real-time messaging in my application, so give that a read if you haven]]></description><link>https://code-after-degree.hashnode.dev/developing-a-real-time-chat-application-part-2-user-permissions-and-security</link><guid isPermaLink="true">https://code-after-degree.hashnode.dev/developing-a-real-time-chat-application-part-2-user-permissions-and-security</guid><category><![CDATA[Web Development]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Node.js]]></category><dc:creator><![CDATA[Benjamin Inglis]]></dc:creator><pubDate>Tue, 13 Jan 2026 17:16:11 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1768324524298/a4fd00b7-824d-4bb0-95dd-b8b16ae0e206.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a href="https://github.com/bingliscodes/chattycat"><strong>GitHub</strong></a></p>
<p><a href="https://github.com/bingliscodes/chattycat"><strong>Live</strong></a> <a href="https://chattycat.netlify.app/"><strong>D</strong></a><a href="https://github.com/bingliscodes/chattycat"><strong>emo</strong></a></p>
<p><a href="https://youtu.be/1RhYNEVxVxk"><strong>Video Demo</strong></a></p>
<p>In the <a href="https://code-after-degree.hashnode.dev/developing-a-real-time-chat-application-with-web-sockets">first part of this series</a>, I outlined the basic project set up and reviewed how I implemented real-time messaging in my application, so give that a read if you haven’t already!</p>
<h1>User Permissions</h1>
<h2>Roles and Role-Specific Actions</h2>
<p>In order to enforce user permissions, we must first create a hierarchy of roles which dictate what actions a user can or cannot do within the application. In ChattyCat, I decided on the following roles: <code>owner</code>, <code>admin</code>, <code>superuser</code>, <code>member</code>.</p>
<p>Refer to the table below which contains the basic actions available and to whom they are available:</p>
<table>
<thead>
<tr>
<th><strong>Action</strong></th>
<th><strong>Authorized Roles</strong></th>
</tr>
</thead>
<tbody><tr>
<td>Create organization</td>
<td>Anyone with an account</td>
</tr>
<tr>
<td>Delete organization</td>
<td><code>owner</code></td>
</tr>
<tr>
<td>Assign roles to users within an organization</td>
<td><code>owner</code></td>
</tr>
<tr>
<td>Channel creation/deletion</td>
<td><code>owner</code>, <code>admin</code></td>
</tr>
<tr>
<td>Add/remove users to an organization</td>
<td><code>owner</code>, <code>admin</code>, <code>superuser</code></td>
</tr>
<tr>
<td>Add/remove users to a channel <em>within</em> an organization (user must already be in the organization)</td>
<td><code>owner</code>, <code>admin</code>, <code>superuser</code></td>
</tr>
<tr>
<td>Send message to a channel</td>
<td>Anyone in the channel</td>
</tr>
<tr>
<td>Direct messaging (between users in an organization)</td>
<td>Anyone in organization</td>
</tr>
</tbody></table>
<p>At this time, any individual can create an account and then create their own organization, which would automatically make them the owner. Of note, the owner of the organization is the only member who has the ability to assign roles to members in their organization– all users are initially assigned the <code>member</code> role by default.</p>
<h2>Enforcing User Permissions</h2>
<p>Now the question is: how do we actually <em>enforce</em> the roles and role-specific actions from above? At a high level we need to:</p>
<ol>
<li><p>Keep track of who has what role (within the organization).</p>
</li>
<li><p>Verify that the user making the request has the required permissions to execute an action.</p>
</li>
</ol>
<p>Well, couldn’t we simply send the role with the rest of the user data when the request is made from the frontend? It seems logical, and that’s what my more naive self thought when first posed with this challenge. However, there is a significant security concern: what if someone were to make a request directly to the backend and simply modify the role parameter? Indeed, a better solution involves implementing middleware in our server to “protect” certain endpoints. Before going into detail on that middleware, we first have to understand how roles are managed in our data model.</p>
<h3>Roles in the Data Model</h3>
<p>Initially when building ChattyCat, it was designed such that each user could only be part of a single organization, and “role” was a property of the user. As the application developed, I realized this would require a new user to be created for the same person who wants to be part of multiple organizations, so I revised the relationship. Given the many-to-many cardinality between users and organizations, this presented a new challenge: a user could have different roles in each organization they are a member of!</p>
<p>This required the creation of a through table (also commonly referred to as junction or bridge table), which effectively creates two one-to-many relationships and stores extra details about the specific relationship. In this example, we create the <code>UserOrganization</code> table to keep track of the user’s <code>role</code> and <code>joinedAt</code> date. The result is a new table that has a one-to-many relationship from the <code>User</code> table on the <code>userId</code> key, and a one-to-many relationship from the <code>Organization</code> table on the <code>organizationId</code> key.</p>
<p>To create the table in Sequelize, you define a new model which contains foreign keys for the tables you wish to join, and then you can specify the additional properties.</p>
<pre><code class="language-javascript">import { DataTypes } from 'sequelize';

import sequelize from '../utils/database.js';
import User from './userModel.js';
import Organization from './channelModel.js';

const UserOrganization = sequelize.define(
  'UserOrganization',
  {
    userId: {
      type: DataTypes.UUID,
      references: {
        model: User,
        key: 'id',
      },
    },
    organizationId: {
      type: DataTypes.UUID,
      references: {
        model: Organization,
        key: 'id',
      },
    },
    role: {
      type: DataTypes.ENUM('owner', 'admin', 'member', 'superuser'),
      allowNull: false,
      defaultValue: 'member',
    },
    joinedAt: {
      type: DataTypes.DATE,
      defaultValue: DataTypes.NOW,
    },
  },
  {
    timestamps: false,
  },
);

export default UserOrganization;
</code></pre>
<p>Next, you establish the many-to-many relationship and specify the through table:</p>
<pre><code class="language-javascript">  Organization.belongsToMany(User, { through: UserOrganization, as: 'Users' });
  User.belongsToMany(Organization, {
    through: UserOrganization,
    as: 'Organizations',
  });
</code></pre>
<p>The code above is establishes the following relationships depicted as an entity relationship diagram:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767988100979/532c9793-6545-4889-8b4d-a6868b514f5a.png" alt="" style="display:block;margin:0 auto" />

<p>Now to set the roles we can use instance methods provided by Sequelize, passing in values for columns in the through table (read about associations in the <a href="https://sequelize.org/docs/v6/core-concepts/assocs/">documentation</a> if interested):</p>
<pre><code class="language-javascript">export const assignRole = catchAsync(async (req, res, next) =&gt; {
  // This function will only be available to the owner
  const { userId, role } = req.body;
  const orgId = req.headers['x-organization-id'];

  const org = await Organization.findByPk(orgId);
  const user = await User.findByPk(userId);

  // addUser will create association if it doesn't exist, or update the through table if it does.
  await org.addUser(user, { through: { role } });

  res.status(200).json({
    status: 'success',
    message: `User \({user.firstName} \){user.lastName} was assigned role: \({role} in organization \){org.organizationName}`,
  });
});
</code></pre>
<h3>Writing the Middleware</h3>
<p>The next step is to ensure that only the owner of the organization can successfully execute the <code>assignRole</code> function. To accomplish this, I created the <code>requireOrgRole</code> middleware, which takes in a list of allowed roles, looks up the user’s role in the organization, then verifies whether or not the user’s role falls within the accepted roles. Additionally, it adds the <code>organizationId</code> and the user’s role to the request for easy access by subsequent middleware.</p>
<pre><code class="language-javascript">export const requireOrgRole = (allowedRoles = []) =&gt; {
  return async (req, res, next) =&gt; {
    const orgId = req.headers['x-organization-id'];

    if (!orgId) {
      return next(new AppError('requireOrgRole: Missing organization ID', 400));
    }

    // Lookup the user's role in the org
    const membership = await UserOrganization.findOne({
      where: {
        userId: req.user.id,
        organizationId: orgId,
      },
    });
    
    // Throw an error if the user is not part of the organization
    if (!membership) {
      return next(
        new AppError('User is not a member of this organization', 403),
      );
    }

    if (!allowedRoles.includes(membership.role)) {
      return next(new AppError('Insufficient permissions', 403));
    }

    // Attach org and role info to request if needed later
    req.organizationId = orgId;
    req.orgRole = membership.role;

    next();
  };
};
</code></pre>
<p>Since requests pass through middleware in the order they appear, we can restrict the <code>assignRole</code> functionality to only users who have role <code>owner</code> in our router:</p>
<pre><code class="language-javascript">router.use(requireOrgRole(['owner']));
router.post('/assignRole', assignRole);
</code></pre>
<p>This pattern can be repeated to enforce all the role-specific actions defined previously. For example, in our channel router adding the following ensures that only those with role <code>admin</code>, <code>owner</code>, or <code>superuser</code> can create or delete channels:</p>
<pre><code class="language-javascript">router.use(requireOrgRole(['admin', 'owner', 'superuser']));
router.post('/', createChannel);
router.route('/:id').delete(deleteChannel);
</code></pre>
<h1>Security</h1>
<h2>Protecting User Data</h2>
<h3>Password Hashing &amp; Salting</h3>
<p>Without users, an application is nothing, so it is imperative to protect the user’s sensitive information at all costs. First and foremost, user passwords should <em><strong>never</strong></em> be stored in the database itself. Instead, upon registration a hashed password will be created and stored in the database. Hashing refers to a one-way function that takes in some string and produces a fixed output. Salts are commonly added to hashing functions to add random data, thus increasing security and reducing risk of identifying duplicate or common passwords (<a href="https://www.geeksforgeeks.org/python/how-to-hash-passwords-in-python/">Source</a>). After registering, future login attempts will take in the input password and generate a new hash, then compare it to the existing one in the database to authenticate the user. This can be completed using packages such as <code>bcrypt</code> combined with the <code>beforeSave</code> hook provided by Sequelize.</p>
<pre><code class="language-javascript">User.beforeSave(async (user, options) =&gt; {
  if (user.changed('password')) {
    const hashedPassword = await bcrypt.hash(user.password, 12);
    user.password = hashedPassword;

    if (!user.isNewRecord) user.passwordChangedAt = Date.now() - 1000;
  }
  user.passwordConfirm = undefined;
});
</code></pre>
<h3>JSON Web Tokens for Authentication</h3>
<p>Additionally, we can keep track of a timestamp <code>passwordChangedAt</code> to ensure that the JSON Web Token issued to the user is current. To make sense of this, let’s review how authentication takes place in ChattyCat.</p>
<p><a href="https://datatracker.ietf.org/doc/html/rfc7519">JSON Web Tokens</a> (JWTs) provide a a secure method for sending information between a client and a server, consisting of JSON data secured with a cryptographic signature. There are three components of a JWT:</p>
<p><a href="https://media.geeksforgeeks.org/wp-content/uploads/20250403112157763854/structure_of_a_json_web_token_jwt_.webp"><img src="https://media.geeksforgeeks.org/wp-content/uploads/20250403112157763854/structure_of_a_json_web_token_jwt_.webp" alt="structure_of_a_json_web_token_jwt_" /></a></p>
<p><em>Source: JSON Web Token (JWT), Geeksforgeeks</em>.</p>
<p>The <strong>header</strong> contains information about the token, such as the algorithm used for signing. The <strong>payload</strong> contains the data that is being sent. The <strong>signature</strong> verifies the integrity of the token, and is generated using the header, payload, and a secret key. We can work with JWTs by installing the <code>jsonwebtoken</code> library (<code>npm i jsonwebtoken</code>), which exposes methods for signing and verifying tokens that are sent and received. For example, the following helper function is used to sign a JWT:</p>
<pre><code class="language-javascript">const signToken = (id) =&gt;
  jwt.sign({ id }, process.env.JWT_SECRET, {
    expiresIn: process.env.JWT_EXPIRES_IN,
  });
</code></pre>
<p>We can then create middleware to send the signed token to the client as a cookie, which is how the user will remain logged in:</p>
<pre><code class="language-javascript">const createSendToken = (user, statusCode, req, res) =&gt; {
  const token = signToken(user.id);
  const cookieOptions = {
    expires: new Date(
      Date.now() + process.env.JWT_COOKIE_EXPIRES_IN * 24 * 60 * 60 * 1000,
    ),
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: process.env.NODE_ENV === 'production' ? 'None' : 'Lax',
  };

  res.cookie('jwt', token, cookieOptions);

  user.password = undefined;

  res.status(statusCode).json({
    status: 'success',
    token,
    data: {
      user,
    },
  });
};
</code></pre>
<p>Finally, we can create our login function which brings everything together, first checking that the entered password matches the hashed password, then sending a signed JWT:</p>
<pre><code class="language-javascript">export const login = catchAsync(async (req, res, next) =&gt; {
  const { email, password } = req.body;
  // 1) Check if email and password are populated
  if (!email || !password)
    return next(new AppError('Please provide email and password!', 400));

  // 2) Check if user exists &amp;&amp; password is valid
  const user = await User.findOne({ where: { email } });
  if (!user || !(await user.correctPassword(password, user.password))) {
    return next(new AppError('Incorrect email or password', 401));
  }
  // 3) Send token to client
  createSendToken(user, 200, req, res);
});
</code></pre>
<p>Now since we sent the JWT as a cookie to the frontend, future requests coming from the frontend will have the JWT attached. We can use this to create an additional middleware that will be used for <em>all</em> actions where we want to ensure a valid user is making the request. This is also where we will confirm that the user hasn’t changed their password since the JWT was issued, which prevents unauthorized users from using stale logins.</p>
<pre><code class="language-javascript">export const protect = catchAsync(async (req, res, next) =&gt; {
  // 1) Get token and check if it's there
  const token = req.cookies.jwt;

  if (!token)
    return next(
      new AppError('You are not logged in! Please log in to get access.', 401),
    );

  // 2) Verify token
  const decoded = await promisify(jwt.verify)(token, process.env.JWT_SECRET);

  // 3) Check if user still exists
  const currentUser = await User.findByPk(decoded.id);
  if (!currentUser)
    return next(
      new AppError('The user belonging to this token no longer exists', 401),
    );

  // 4) Check if user changed password after token was issued
  if (currentUser.changedPasswordAfter(decoded.iat)) {
    return next(
      new AppError('User recently changed password! Please log in again.', 401),
    );
  }

  // Finally grant access to protected route and add user to the request
  req.user = currentUser;
  next();
});
</code></pre>
<h2>Security Middleware</h2>
<p>In addition to the methods above to protect passwords and provide secure authentication, there are other common attacks that we need to defend against. I’ll preface this section by saying it is <strong>NOT</strong> comprehensive by any means and I am not an expert in cybersecurity. I’m simply sharing what I’ve learned about what to watch out for and how I handled them in this application.</p>
<h3>Brute Force Attacks</h3>
<p>These attacks involve continuous attempts to login with common passwords. While this is partially mitigated by the delay provided when using <code>bcrypt</code>, other strategies such as rate limiting (setting a limit for the number of requests that can come from one user or IP in a given time period) or implementing a maximum number of login attempts can provide additional protection.</p>
<p>I used <code>express-rate-limit</code> package to implement this in my application:</p>
<pre><code class="language-javascript">// Limit requests from same IP
const limiter = rateLimit({
  max: 100,
  windowMs: 60 * 60 * 1000,
  message: 'Too many requests from this IP, please try again in an hour!',
});
app.use('/api', limiter);
</code></pre>
<h3>Cross-Site Scripting (XSS) Attacks</h3>
<p>An XSS attack involves an attacker tricking the target site into executing malicious code. For example, an attacker might send a malicious code within the request’s URL parameter that gets embedded into the page that is then return to the browser. There are two primary methods to protect against XSS attacks:</p>
<ol>
<li><p><strong>Output Encoding:</strong> escaping potentially harmful characters. Modern frontend frameworks such as React, Vue, and Angular provide output encoding by default when rendering variables.</p>
</li>
<li><p><strong>Sanitization:</strong> removing unsafe features from a string of HTML, such as a <code>&lt;script&gt;</code> tag or inline event handler. It’s worth noting that sanitization is something that’s only really needed when rendering raw HTML, which does not occur when using normal JSX components and standard HTML elements; raw HTML is only rendered when using the <code>dangerouslySetInnerHTML</code> element in React. Thus, we don’t need to do anything here, but you ever render raw HTML in your application, Mozilla recommends using a trusted third party library such as <a href="https://www.npmjs.com/package/dompurify">DOMPurify</a> for sanitization.</p>
</li>
</ol>
<p>Furthermore, packages such as <a href="https://www.npmjs.com/package/helmet">Helmet</a> can be added to the Express middleware stack to set special headers which further secure your application. Finally, use a <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP">Content Security Policy</a> as a failsafe to control which resources the browser is allowed to execute.</p>
<h3>Denial of Service (DoS) Attacks</h3>
<p>DoS attacks involve overwhelming the server with a large number of requests or requests that require significant resources to complete. Rate limiting will provide protection against such attacks, but we can further defend against such attacks by restricting the size of the payload attached to the request body. The middleware below enforces limits on the size of the JSON payload and URL-encoded form data respectively.</p>
<pre><code class="language-javascript">app.use(express.json({ limit: '10kb' }));
app.use(express.urlencoded({ extended: true, limit: '10kb' }));
</code></pre>
<h2>Reseting Passwords via Email</h2>
<p>Since the passwords are never stored anywhere in plaintext and hashing is one-way (meaning you cannot “unhash” a password), a forgotten password can never be recovered. Given how many times I have had to reset passwords through email, it never occurred to me how complicated this process would be to implement, so I’ll do my best to walk through it. For this to work, the user model must include properties to keep track of a reset token and an associated expiration date (I used fields <code>passwordResetToken</code> and <code>passwordResetExpires</code>). When a user is unable to login, the following should occur:</p>
<ol>
<li><p>User clicks “forgot password” button and is directed to a form where they can enter the email associated with their account.</p>
</li>
<li><p>Upon submission, a request to the backend is made and invokes a <code>forgotPassword</code> function.</p>
</li>
<li><p>The <code>forgotPassword</code> function checks if there is a user matching the provided email and then generates a password reset token and expiration time.</p>
</li>
<li><p>User receives an email with a link for them to set a new password.</p>
</li>
<li><p>Once new password is set, the user is logged in and the information is updated in the database.</p>
</li>
</ol>
<h3><strong>Creating the Form</strong> (<a href="https://github.com/bingliscodes/chattycat/blob/main/frontend/src/pages/ForgotPassword.jsx">ForgotPassword.jsx</a>)</h3>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768247584266/a52c19d3-ec9b-4897-96bf-fa6fa6896fb3.png" alt="" style="display:block;margin:0 auto" />

<p>The form is pretty basic, containing a single input element and a submit button, which invokes a helper function <code>sendResetEmail</code> that makes an API call to the backend with the email provided (not going into depth about the API call as that’s something I discussed at length in <a href="https://code-after-degree.hashnode.dev/deploying-a-full-stack-application-with-the-mern-stack#heading-making-api-calls-with-axios">this article</a>).</p>
<h3><strong>The</strong> <code>forgotPassword</code> <strong>Function</strong></h3>
<p>This function should do three things in order: find the user, generate the reset token, and then send the email with the password reset link. Finding the user is straightforward, but to create the reset token I added an instance method that utilizes the <code>crypto</code> module (built-in to Node.js) to generate and random token. This function also hashes the token, sets the token as the user’s <code>passwordResetToken</code>, and finally sets the <code>passwordResetExpires</code> :</p>
<pre><code class="language-javascript">User.prototype.createPasswordResetToken = function () {
  const resetToken = crypto.randomBytes(32).toString('hex');

  this.passwordResetToken = crypto
    .createHash('sha256')
    .update(resetToken)
    .digest('hex');

  this.passwordResetExpires = Date.now() + 10 * 60 * 1000;

  return resetToken;
};
</code></pre>
<p>Once we generate that token, we then have to call <code>user.save</code> to update the database (note that I disabled validators to prevent them from throwing an error):</p>
<pre><code class="language-javascript">  // 2) Generate Reset Token
  const resetToken = user.createPasswordResetToken();
  await user.save({ validate: false });
</code></pre>
<p>Next, we construct the reset URL:</p>
<pre><code class="language-javascript">const resetURL = `\({req.protocol}://\){process.env.FRONTEND_URL}/resetPassword/${resetToken}`;
</code></pre>
<ul>
<li><p><code>req.protocol</code> refers to HTTP/HTTPS, whatever protocol the sender is using</p>
</li>
<li><p><code>process.env.FRONTEND_URL</code> is self-explanatory, but this allows us to dynamically send the email based on the environment</p>
</li>
<li><p><code>resetPassword</code> indicates that this is the route on the frontend we are targeting (i.e., in our React Router we render a resetPassword page when that route is hit).</p>
</li>
<li><p><code>resetToken</code> is the token we just generated and will be appended to the URL so that it’s available to React Router as a parameter (by using the <code>useParams</code> hook).</p>
</li>
</ul>
<p>Finally, we send the email to the user, which I handled by creating an <code>Email</code> class containing a <code>sendPasswordReset</code> method (will elaborate on this in the next section):</p>
<pre><code class="language-javascript">await new Email(user, resetURL).sendPasswordReset();
</code></pre>
<p>Putting it all together we get the following:</p>
<pre><code class="language-javascript">export const forgotPassword = catchAsync(async (req, res, next) =&gt; {
  // 1) Find user by POSTed email
  const user = await User.findOne({ where: { email: req.body.email } });

  if (!user)
    return next(
      new AppError('There is no user associated with that email address.', 404),
    );

  // 2) Generate Reset Token
  const resetToken = user.createPasswordResetToken();
  await user.save({ validate: false });

  // 3) Send to user's email
  try {
    const resetURL = `\({req.protocol}://\){process.env.FRONTEND_URL}/resetPassword/${resetToken}`;
    await new Email(user, resetURL).sendPasswordReset();

    res.status(200).json({
      status: 'success',
      message: 'Token sent to email!',
    });
  } catch (err) {
    console.error(err);
    user.passwordResetToken = undefined;
    user.passwordResetExpires = undefined;
    await user.save({ validate: false });

    return next(
      new AppError('There was an error sending the email. Try again later!'),
      500,
    );
  }
});
</code></pre>
<h3>Sending Emails</h3>
<p>I used Nodemailer (Node.js library for sending emails) and <a href="https://www.mailgun.com/products/send/?utm_source=google&amp;utm_medium=cpc&amp;utm_campaign=NA%20%7C%20Brand&amp;utm_id=750089235&amp;utm_content=44926653532&amp;utm_term=mailgun&amp;gad_source=1&amp;gad_campaignid=750089235&amp;gbraid=0AAAAAofVnce1XaIRdszGXsqq6IO7oMRkP&amp;gclid=Cj0KCQiA1JLLBhCDARIsAAVfy7gryAdhsvgo9PpZBP-BQtPdv0B4CSwNbFAsFQ5nrM_96_RN5wuqi2oaAsq4EALw_wcB">Mailgun</a> (email service) to send emails. To do so, you must create a transporter with Nodemailer, which is the object that handles connecting to your email service. Once created, it can then be used to send an email by calling the <code>sendMail</code> function. Since email services are typically not free (or you don’t want to use up the free tier testing), I utilized <a href="https://ethereal.email/">Ethereal</a> in development to emulate an email service. For more information, the <a href="https://nodemailer.com/usage">Nodemailer documentation</a> is very helpful!</p>
<p>The <code>Email</code> class constructor takes in a user and a url, then initializes the variables that we will need to pass in when creating an email.</p>
<pre><code class="language-javascript">class Email {
  constructor(user, url) {
    this.to = user.email;
    this.firstName = user.firstName || '';
    this.url = url;
    this.from = 'ChattyCat &lt;no-reply@chattycat.dev&gt;';
  }
// ...
}
</code></pre>
<p>The generic <code>newTransport</code> method is used to create a transport using either Mailgun or Ethereal, depending on the environment.</p>
<pre><code class="language-javascript">  newTransport() {
    if (process.env.NODE_ENV === 'production') {
      const mailgunOptions = {
        auth: {
          api_key: process.env.MAILGUN_API_KEY,
          domain: process.env.MAILGUN_DOMAIN,
        },
      };
      return nodemailer.createTransport(mg(mailgunOptions));
    }

    // Development
    return nodemailer.createTransport({
      host: 'smtp.ethereal.email',
      port: 587,
      auth: {
        user: testAccount.user,
        pass: testAccount.pass,
      },
    });
</code></pre>
<p>The generic <code>send</code> method takes in a subject and a message, constructs a <code>mailOptions</code> object, and then invokes the Nodemailer <code>sendMail</code> function with those options.</p>
<pre><code class="language-javascript">  async send(subject, message) {
    const transporter = await this.newTransport();

    const mailOptions = {
      from: this.from,
      to: this.to,
      subject,
      text: message,
    };

    const info = await transporter.sendMail(mailOptions);
  }
</code></pre>
<p>Finally, the <code>sendPasswordReset</code> method is just a send method where the subject and message are pre-populated. This pattern allows us to keep all the logic for sending emails contained to the Email module.</p>
<pre><code class="language-javascript">  async sendPasswordReset() {
    await this.send(
      'Your password reset token (valid for 10 minutes)',
      `Forgot your password? Navigate to ${this.url}`,
    );
  }
</code></pre>
<p>The end result:</p>
<pre><code class="language-javascript">import nodemailer from 'nodemailer';
import mg from 'nodemailer-mailgun-transport';

const testAccount = await nodemailer.createTestAccount();

class Email {
  constructor(user, url) {
    this.to = user.email;
    this.firstName = user.firstName || '';
    this.url = url;
    this.from = 'ChattyCat &lt;no-reply@chattycat.dev&gt;';
  }

  newTransport() {
    if (process.env.NODE_ENV === 'production') {
      const mailgunOptions = {
        auth: {
          api_key: process.env.MAILGUN_API_KEY,
          domain: process.env.MAILGUN_DOMAIN,
        },
      };
      return nodemailer.createTransport(mg(mailgunOptions));
    }

    // Development
    return nodemailer.createTransport({
      host: 'smtp.ethereal.email',
      port: 587,
      auth: {
        user: testAccount.user,
        pass: testAccount.pass,
      },
    });
  }

  async send(subject, message) {
    const transporter = await this.newTransport();

    const mailOptions = {
      from: this.from,
      to: this.to,
      subject,
      text: message,
    };

    const info = await transporter.sendMail(mailOptions);
  }

  async sendPasswordReset() {
    await this.send(
      'Your password reset token (valid for 10 minutes)',
      `Forgot your password? Navigate to ${this.url}`,
    );
  }
}

export default Email;
</code></pre>
<h2>Preventing Unwanted Messages</h2>
<p>The steps outlined above ensure that user data is secure, all requests are coming from valid users, users can reset their password if they forget it or have a security concern, and old JWTs cannot be used to spoof a user login after their password is changed. However, this is not sufficient to prevent valid users from sending messages or files to channels they should not have access to.</p>
<p>Protecting users from unauthorized messages takes place in the backend when the <code>send-message</code> event is received by the socket. To implement this, I created a helper function <code>validateUserPermissions</code> to ensure that the user sending the message is a member of the channel they are sending the message to:</p>
<pre><code class="language-javascript">const validateUserPermissions = (userId, channelId) =&gt; {
  if (!channelId) return true;
  const channels = userChannelMap.data.get(userId);
  return channels.includes(channelId);
};
</code></pre>
<p>Note that we need the check at the start so that messages without a <code>channelId</code> (which are direct messages) will not get blocked erroneously. At this time, all users are capable of sending messages to each other within an organization, and the only way to be added to an organization is by an administrator, so we only need to verify the channel permissions at this time.</p>
<p>You may also be wondering about the <code>userChannelMap</code> in this function. Originally, this method involved making a database query to get a list of all the channels the user is a part of. However, given this function is invoked <em>every time</em> a message is sent, it would become very resource intensive. To address this, I implemented the <code>UserChannelMap</code> data structure to keep track of all users and their associated channels. The map is initially populated with all users and their respective channels when the server spins up, and is updated whenever a request to add or remove somebody from a channel is processed. While maintaining this cache does increase the memory requirements, I believe that the trade-off is worth it compared to a database query whenever a message is sent.</p>
<h1>Final Thoughts</h1>
<p>Most of the concepts covered in this article fall under the umbrella of “things I didn’t know I had to worry about before this project.” 🤷‍♂️</p>
<p>Stay tuned for Part 3 of this series, where I will be discussing file attachments!</p>
]]></content:encoded></item><item><title><![CDATA[Developing a Real-time Chat Application: Part 1 – Messaging]]></title><description><![CDATA[GitHub
Live Demo
Video Demo
Project Overview
Given the complexity of this project, I will be publishing 3 separate posts that build off one another. The first in the series (this post) will focus on m]]></description><link>https://code-after-degree.hashnode.dev/developing-a-real-time-chat-application-with-web-sockets</link><guid isPermaLink="true">https://code-after-degree.hashnode.dev/developing-a-real-time-chat-application-with-web-sockets</guid><category><![CDATA[Programming Blogs]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[websockets]]></category><category><![CDATA[webdev]]></category><dc:creator><![CDATA[Benjamin Inglis]]></dc:creator><pubDate>Thu, 08 Jan 2026 20:02:20 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1767982875022/a0ce4779-3684-48a4-8319-98c964033893.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a href="https://github.com/bingliscodes/chattycat">GitHub</a></p>
<p><a href="https://chattycat.netlify.app/">Live Demo</a></p>
<p><a href="https://youtu.be/1RhYNEVxVxk">Video Demo</a></p>
<h1>Project Overview</h1>
<p>Given the complexity of this project, I will be publishing 3 separate posts that build off one another. The first in the series (this post) will focus on messaging. The next post will focus on user permissions and security, and the final installment on attachment handling.</p>
<p>My most recent project is easily the most complex challenge I’ve tackled to date, combining everything I’ve learned about RESTful APIs, frontend development, user experience, and security. ChattyCat is a web application where users can send messages to channels and other users, reply in message threads, upload profile pictures, and even securely reset their passwords via email. Users can send messages to any other user within their organization, but can only send messages to channels that they have been added to by a superuser or administrator.</p>
<h2>Backend</h2>
<p>The backend for this application is written with Node.js and Express, using a <a href="https://neon.com/">NeonDB</a> PostgreSQL instance to store user and message data. Profile picture uploads are supported via Multer for processing the file and the AWS S3 client for loading the image into an S3 bucket. The data models and relationships are constructed using <a href="https://sequelize.org/docs/v6/core-concepts/model-instances/">Sequelize V6</a>.</p>
<h2>Frontend</h2>
<p>The frontend is created using <a href="https://react.dev/">React</a>, <a href="https://vite.dev/guide/">Vite</a>. I used <a href="https://reactrouter.com/">React Router</a> (version 7) to implement routing, and <a href="https://www.chakra-ui.com/">Chakra V3</a> for some components and additional styling.</p>
<h1>Implementing Web Sockets with Socket.io</h1>
<p>The first challenge this project presented was utilizing web sockets to facilitate real-time communication.</p>
<h2>What is a WebSocket?</h2>
<p>A WebSocket is used to establish real-time, bidirectional communication between a client and a server (<a href="https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API">Mozilla Docs</a>). This differs from the HTTP request-response model since the TCP connection stays open until it is <em>explicitly closed.</em> Additionally, both client and server can send messages along the connection without making new requests, reducing overhead costs and making it ideal for web chat applications (<a href="https://www.geeksforgeeks.org/web-tech/what-is-web-socket-and-how-it-is-different-from-the-http/">source</a>).</p>
<h2>Setting up Socket.io</h2>
<p>For this project I used <a href="https://socket.io/docs/v4/tutorial/introduction">Socket.IO</a> (version 4) to establish the channel between server and client. Once the connection is established, the server and client interact by emitting and listening for events, and then responding accordingly.</p>
<p>If you want to follow the Socket.io tutorial, you can do that <a href="https://socket.io/docs/v4/tutorial/step-1">here</a>, although my implementation is slightly different so I want to outline the high-level steps and design patterns I followed rather than focus on the specific details.</p>
<h3>Socket.IO Lifecycle</h3>
<p>The Socket.IO lifecycle can be summarized in 3 main steps (<a href="https://socket.io/docs/v4/how-it-works/">Socket.IO docs</a>):</p>
<ol>
<li><p>IO server starts and begins listening for new socket connections</p>
</li>
<li><p>Client creates a socket, which initiates a WebSocket handshake to connect to the IO server</p>
</li>
<li><p>IO server receives the connection</p>
</li>
</ol>
<h3>Step 1: Server-side</h3>
<p>To create the connection from the server side we need to to first initialize a Socket.IO Server (imported from the <code>socket.io</code> package), which is created by passing in an HTTP server. In the example below, <code>app</code> is an Express instance, so calling <code>app.listen</code> returns an HTTP server. Thus, <code>io</code> is an instance of the Socket.IO server where we add a listener for the “connection” event for incoming sockets, which will fire each time a new connection is established. Each connection represents an <em>individual client’s session</em>, meaning all <code>socket.on</code> listeners are bound to that specific client socket.</p>
<pre><code class="language-javascript">import { Server } from 'socket.io';

import app from './app.js';

const PORT = process.env.PORT || 3000;

const server = app.listen(PORT, () =&gt; {
  console.log(`App is running on port ${PORT}...`);
  console.log(`mode is ${process.env.NODE_ENV}`);
});

// Step 1: IO Server starts and begins listening for new socket connections
const io = new Server(server, {
  cors: {
    origin: [
      'http://localhost:5173',
      'http://127.0.0.1:5173',
      // Make sure to include your development and production domains here
],
    credentials: true,
  },
});

io.on('connection', (socket) =&gt; {
  console.log('a user connected');
});
</code></pre>
<h3>Step 2: Client-side</h3>
<p>To establish the connection from the client-side we will use the Socket.IO client library (<code>npm i socket.io-client</code>). I decided to move this code into a utility function that “promisifies” the socket creation to allow for asynchronous execution. The purpose of the <code>createConnection()</code> function is to <em>initialize</em> the connection, which is why I chose to contain the logic of socket event handlers (e.g., <code>socket.on(‘receive-message’)</code>) within the component(s) that use them.</p>
<pre><code class="language-javascript">import { io } from 'socket.io-client';

export const createConnection = () =&gt; {
  return new Promise((resolve, reject) =&gt; {
    // Step 2: Client creates socket
    const socket = io(import.meta.env.VITE_SERVER_URL, {
      withCredentials: true,
    });

    socket.on('connect', () =&gt; {
      console.log('[CLIENT] Connected with socket ID:', socket.id);
      resolve(socket); 
    });

    socket.on('connect_error', (err) =&gt; {
      console.error('Socket connect_error:', err);
      reject(err);
    });

  });
};
</code></pre>
<p>Note: we set <code>withCredntials: true</code> since it is a cross-origin request and we want the browser to send credentials (cookies, authorization headers, TLS certificates) with the request.</p>
<h3>Step 3: Bidirectional Communication</h3>
<p>Following the lifecycle steps above, let’s say we started our IO server and then call <code>createConnection()</code> from our frontend application. At this point the connection event listener will fire, and log a the message <code>“a user connected”</code> to the console. This is not particularly useful just yet, but I think it’s important to conceptualize what is happening and where before we can start to develop with the WebSockets. What we just established is the ability to programmatically interface with either part (server-side or client-side) of the <em>same persistent connection.</em> While this may be obvious to some, it was not to me, and I’m sure at least one other person out there has struggled to wrap their head around this concept.</p>
<h2>Implementing the Messaging Functionality</h2>
<h3>Emitting events</h3>
<p>The client communicates with the server by emitting (<code>socket.emit</code>) and listening (<code>socket.on</code>) for events which accepts an object as data. The name of the event is the first argument, and will be used by the server to identify the emitted event. For example, you can send a message from an input form:</p>
<pre><code class="language-javascript">  const socket = await createConnection(); // Just pretend this is valid for now

  const form = document.getElementById('form');
  const input = document.getElementById('input');
  const messages = document.getElementById('messages'); // Where we will display messages

  form.addEventListener('submit', (e) =&gt; {
      e.preventDefault();
      if (input.value) {
        socket.emit('send-message', input.value);
        input.value = '';
      }
  });
</code></pre>
<p>The above snippet emits a <code>send-message</code> event along with the value of the input, which now needs to be received by the server.</p>
<p>To respond, we can add a send message listener by modifying our backend code where we initialized the IO server and listened for the <code>connection</code> event. Inside the listener, the first argument is the name of the event (should match the one we emitted above), and the second argument is a callback function that takes in the object we passed in as our data:</p>
<pre><code class="language-javascript">io.on('connection', (socket) =&gt; {
  console.log('a user connected');

  socket.on('send-message', (msg) =&gt; {
    console.log(`[SERVER] message received from client \({socket.id}: \){msg}`);
    socket.emit('receive-message', msg); // Send back to client so we can display in frontend
  })    
});
</code></pre>
<p>At this point, the user has sent a message (presumably “hello world!”), upon which a <code>send-message</code> event was emitted by the client socket. The IO server then executes the functionality defined in the send-message <em>listener</em>, logging both the client’s socket id and message to the console, and then emits a <code>receive-message</code> event.</p>
<p>To finish this sequence of events and display the message in the chat, we need to define a <code>receive-message</code> listener, which will render the message data in the UI. Recall earlier I mentioned how I used <code>createConnection()</code> ONLY for establishing the initial connection. This is because I required the use of React hooks such as <code>useEffect</code> to manage sockets and clean up listeners upon unmount or socket change. To achieve this functionality, we retrieve a <code>userSocket</code> from our UserContext (refer to the GitHub if you want to see how that is initialized), and then we add the <code>receive-message</code> listener function which updates a <code>messages</code> state array containing all messages.</p>
<p>After the IO server fires the <code>receive-message</code> event, our client-side socket is set up to update the messages array, which will subsequently re-render the component due to the state update. So all that’s left is to display the messages, which can be completed by iterating through our <code>messages</code> state array and rendering a new message element for each item in the array!</p>
<pre><code class="language-javascript">import { useState, useEffect } from 'react';

export default function ChatMessages() {
  const { messages, setMessages } = useState([]);
  const { socketReady, userSocket } = useContext(UserContext);

  useEffect(() =&gt; {
    if (!userSocket) return;

    const handleReceiveMessage = (msg) =&gt; {
      setMessages((prev) =&gt; [...prev, msg]);
    };

    userSocket.on('receive-message', handleReceiveMessage);

    return () =&gt; {
      userSocket.off('receive-message', handleReceiveMessage);
    };
  }, [userSocket, setMessages]);
  return (
    &lt;&gt;
      {messages.forEach((msg, idx) =&gt; (
        &lt;p key={idx}&gt;{msg}&lt;/p&gt;
      ))}
    &lt;/&gt;
  );
}
</code></pre>
<h1>Learning through Development</h1>
<p>The more I develop “real” applications, the more I appreciate the difficulty and art of writing clean code that is both reusable and maintainable. A project can get out of hand very quickly, making it difficult locate the component, function, or sometimes even the file you are searching for. In order to avoid this common pitfall, you need both logical project structuring and strong pattern recognition.</p>
<h2>Creating Reusable React Components</h2>
<p>My focus for this project was to improve the organization and iteratively refactor to minimize duplicate code. Tech debt racks up quickly when you start duplicating code, often causing simple tasks to become burdensome when you have to make changes to multiple files/components instead of just one.</p>
<p>The first component that I refactored was my chat layout, which initially stored and displayed all messages for a chat and the input box. I knew this component would be reused <em>at least</em> once because I would require different chat interfaces for direct messaging vs. sending messages to a channel, and I wanted to avoid maintaining two separate React components.</p>
<p>First, I identified which components could be separated functionally, which resulted in a split between the <code>ChatInput</code> (sending messages) and <code>MessageLayout</code> (displaying messages). However, if I were to separate these two components, I would then have to lift the <code>messages</code> state <em>outside</em> of these components since it needs to be accessed by both. Ultimately, I lifted up the <code>messages</code> state into the parent components where I return the <code>ChatInterface</code>. After this initial separation of <code>ChatInput</code> and <code>MessageLayout</code>, I further deconstructed <code>MessageLayout</code> into two components: <code>DividerText</code> and <code>ChatMessage</code>. The reason for this decision was based on more on reusability than any logical separation, since each component could be useful independently for future design. For example, <code>DividerText</code> is now used for both separating messages and differentiating the start of a message thread from its replies. Furthermore, it can be customized by passing a <code>style</code> prop, which would not have been possible before without duplicating many lines of code.</p>
<p>The image below provides a visual representation of how I reduced my original <code>ChatInterface</code> component into groups of smaller components which are more reusable and maintainable. This offers the added benefit of improving the project’s file structure since I can now move each component to its own aptly-named file, which is a significant improvement to lumping everything into the more abstract parent <code>ChatInterface</code> component.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767819698463/aae76a3d-13bd-4916-8cd1-8b48c163152c.png" alt="" style="display:block;margin:0 auto" />

<p>Throughout this project I improved my ability to identify where a component can be refactored to make the codebase more maintainable. Previously, my components would continue to grow as I added functionality, increasing the number of dependencies, making it very difficult to debug, and resulting in lots of duplicated code. This time I was guided by the <a href="https://www.geeksforgeeks.org/system-design/single-responsibility-in-solid-design-principle/">Single Responsibility Principle</a>, which posits that a component should only have <em>one</em> responsibility. Ultimately this required a shift in my coding mentality, causing me to frequently evaluate whether the changes or additions I’m making would be better suited in their own component or helper function. While it can feel bad in the moment to remove code from a function or component that “works,” it often saves time in the long run and leads to a significantly more manageable codebase. I also noticed that it makes it easier to <em>return</em> to the project after not looking at it for a time and quickly understand the purpose of each component.</p>
<h2>Commitment Issues…</h2>
<p>Something else I improved on this project is strategically utilizing Git to “tell the story” of the project’s development. In the past I would often forget to commit for long periods of time, which made it difficult to create meaningful commit messages since the commit would contain so many changes. I tried to make each commit address a specific change or addition, which resulted in many more commits but also a more robust version history. While I’m still working on the art of deliberate commits, a good formula to follow is a short summary of the change (≤50 characters), an empty line, then a detailed summary focusing on the <em>intent</em> and <em>approach</em> (wrapped at 72 characters). For more guidance, I’d recommend checking out this <a href="https://who-t.blogspot.com/2009/12/on-commit-messages.html">blog post by Peter Hutterer</a> or this <a href="https://www.freecodecamp.org/news/how-to-write-better-git-commit-messages/">FreeCodeCamp blog</a>.</p>
<h1>Final Thoughts</h1>
<p>I think I am finally starting to understand the “engineering” part of software engineering. The ability to write code is simply the prerequisite for being a software engineer. Most of the real work is the planning, design, and scaling a project using all the tools (i.e., languages and frameworks) at your disposal. For example, when implementing threaded messages I had two approaches in mind:</p>
<ol>
<li><p>Create a new table <code>threads</code> with a 1:many relationship with messages? (each thread can have many messages; each message can have at most 1 thread)?</p>
</li>
<li><p>Add a <code>parentMessageId</code> column to the <code>message</code> table, which indicates that a message belongs to a thread.</p>
</li>
</ol>
<p>In the first approach, it allows me to leave the <code>message</code> table as is, and I could easily distinguish between a “parent” message and a “thread” message; however, I would have to create a whole new table. The latter approach involves modifying the existing <code>message</code> table and requires adding some additional logic to distinguish between different message types, BUT I wouldn’t have to add any new tables or create new relationships.</p>
<p>Ultimately I decided to go with approach number two because it kept the data model leaner and contained all messages to a single table, but perhaps I could have achieved the same results with the first approach. The choices made throughout development will impact the performance and sustainability of the project, and being able to identify the best (or at least better) approach is integral to successful software engineering.</p>
]]></content:encoded></item><item><title><![CDATA[Deploying a Full Stack Application with the MERN Stack]]></title><description><![CDATA[GitHub Repo
Deployed Project
Overview
The MERN stack (MongoDB, Express.js, React, and Node.js) consists of all the tools and frameworks you need to deploy a fully functional, modern web application by only writing code in a single language (JavaScrip...]]></description><link>https://code-after-degree.hashnode.dev/deploying-a-full-stack-application-with-the-mern-stack</link><guid isPermaLink="true">https://code-after-degree.hashnode.dev/deploying-a-full-stack-application-with-the-mern-stack</guid><category><![CDATA[MERN Stack]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Express]]></category><category><![CDATA[Express.js]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[React]]></category><category><![CDATA[http]]></category><category><![CDATA[software development]]></category><category><![CDATA[Full Stack Development]]></category><dc:creator><![CDATA[Benjamin Inglis]]></dc:creator><pubDate>Fri, 31 Oct 2025 16:02:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1761928830540/7e63e0de-5db1-41e6-ab1f-9ea2a4ea97ec.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a target="_blank" href="https://github.com/bingliscodes/movie_reviews_github">GitHub Repo</a></p>
<p><a target="_blank" href="http://mediacat.netlify.app">Deployed Project</a></p>
<h1 id="heading-overview">Overview</h1>
<p>The MERN stack (MongoDB, Express.js, React, and Node.js) consists of all the tools and frameworks you need to deploy a fully functional, modern web application by only writing code in a single language (JavaScript). By using Express and Node you can construct RESTful API endpoints to interact with your MongoDB database. Meanwhile, React is used to build a responsive front-end user experience. Throughout this blog post I will discuss my experience process of building my first complete application and deploying it to the web.</p>
<h1 id="heading-project-planning">Project Planning</h1>
<p>I started the project by first defining the requirements and technical details. I started by mapping out the user experience, answering questions such as:</p>
<ul>
<li><p>What should the user see when first visiting the page?</p>
</li>
<li><p>What actions should the user be able to take?</p>
</li>
<li><p>What function does this application serve for the user?</p>
</li>
</ul>
<p>With the answers to the questions above guiding me, I moved onto the high-level technical requirements:</p>
<ul>
<li><p>React frontend using Chakra UI component library for styling.</p>
</li>
<li><p>Frontend should implement routing for different pages</p>
</li>
<li><p>RESTful API back end, completely separate from frontend.</p>
</li>
<li><p>Secure authorization and authentication using JavaScript web tokens.</p>
</li>
<li><p>Mobile responsive.</p>
</li>
</ul>
<p>The next step was to draw a wireframe, outlining the components needed for each page of the website. You can see an example of one of the pages below. If you’d like to see the whole wireframe on Excalidraw, click <a target="_blank" href="https://excalidraw.com/#json=nl8xWRNwQnBH7f4Y05OWY,4Od4J20gu7bA96ojH-wNKQ">here</a>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761596922703/23fd8b11-ff6b-4892-bc48-65a48b692004.png" alt /></p>
<h1 id="heading-building-the-frontend">Building the Frontend</h1>
<h2 id="heading-project-set-up">Project Set Up</h2>
<p>Once the initial project planning was complete, it’s time to set up your project structure. Best practice is to initialize a source folder as a git repository, then create folders for the frontend and backend to help prevent dependency/environment clashes and make it easier to deploy them independently. Getting started on a full stack project can be incredibly daunting, but I’ve found that sometimes you just have to start <em>somewhere</em>. For me, it made the most sense to start with the frontend because I already had the basic outline. Additionally, it’s more motivating when I can visualize the progress for what I’m working on.</p>
<p>Within your frontend folder, use Vite to initialize the React project by running the command <code>npm create vite@latest ./</code> and answering the prompts (don’t forget to <a target="_blank" href="https://vite.dev/guide/">install Vite</a> if you don’t already have it). Make sure to select <code>React</code> as the framework, then choose your preferred variant (I used JavaScript). Upon completion, you will have a basic React app that can be run using the command <code>npm run dev</code>.</p>
<h3 id="heading-setting-environment-variables-with-vite">Setting Environment Variables with Vite</h3>
<p>The first thing I do is create a <code>.env</code> file to store the variables necessary to execute the code in my development environment. When using Vite, environment variables must follow the naming convention of starting with “VITE_” and containing all capital letters. For example: <code>VITE_API_KEY=abc123</code> is a valid environment variable, but <code>API_KEY=abc123</code> is not. To access these variables within your code, utilize the syntax <code>import.meta.env.VITE_API_KEY</code>.</p>
<h2 id="heading-getting-started-with-react">Getting Started with React</h2>
<p>Now that we have a way to <em>see</em> the changes made in real time, it’s time to start building the home page.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761672134830/c30034b9-44b0-4095-b0d1-9b933a1101b9.png" alt class="image--center mx-auto" /></p>
<p>Using my wireframe as a reference, I was quickly able to identify the key components I would need to build:</p>
<ol>
<li><p>Navigation bar</p>
</li>
<li><p>Navigation bar buttons</p>
</li>
<li><p>Search bar</p>
</li>
<li><p>Movie/Show Carousel</p>
</li>
<li><p>Movie/Show Carousel Card</p>
</li>
</ol>
<h3 id="heading-implementing-chakra-v3">Implementing Chakra (v3)</h3>
<p><a target="_blank" href="https://chakra-ui.com/">Chakra UI</a> provides a library of sleek, customizable components, which significantly cuts down on the time spent building the individual pieces of the user interface. It also allows provides a built-in <code>ColorModeButton</code> component for toggling between light and dark mode, which is an essential feature in any modern application.</p>
<p>I used Chakra v3 for this project, which can be installed by running <code>npm i @chakra-ui/react @emotion/react</code>. Next add the code snippets with <code>npx @chakra-ui/cli snippet add</code>, which generates the provider component (among others) which is essential for the next step. I’d also recommend installing React Slick (<code>npm i react-slick</code>), which is used for later implementing the Slider portion of the carousel.</p>
<p>Now all you have to do to start developing with Chakra is wrap your App component in the Provider that was generated by Chakra</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { Provider } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/components/ui/provider"</span>;
<span class="hljs-keyword">import</span> App <span class="hljs-keyword">from</span> <span class="hljs-string">"./App.jsx"</span>;

createRoot(<span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">"root"</span>)).render(
   <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Provider</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">App</span> /&gt;</span>
   <span class="hljs-tag">&lt;/<span class="hljs-name">Provider</span>&gt;</span></span>
);
</code></pre>
<p>Just ONE more thing before we can <em>actually</em> get started… Given that I wanted to be able to implement routing in my web app, we first have to set that up with React Router (I used v7 for this project).</p>
<h3 id="heading-setting-up-react-router">Setting up React Router</h3>
<p>Install React Router by running <code>npm install react-router</code>. Once installed, you need to wrap your App component inside the <code>BrowserRouter</code></p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { BrowserRouter } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-router-dom"</span>;      
<span class="hljs-keyword">import</span> App <span class="hljs-keyword">from</span> <span class="hljs-string">"./App.jsx"</span>;

createRoot(<span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">"root"</span>)).render(        
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">BrowserRouter</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Provider</span>&gt;</span>
         <span class="hljs-tag">&lt;<span class="hljs-name">App</span> /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">Provider</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">BrowserRouter</span>&gt;</span></span>
);
</code></pre>
<p>Inside your App component return a <code>&lt;Routes&gt;</code> element which wraps your individual <code>&lt;Route&gt;</code> elements (the only difference here is the “s”, confusing, I know). Each <code>&lt;Route&gt;</code> element specifies a path and the element it should render.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { Routes, Route } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-router"</span>;
<span class="hljs-keyword">import</span> RootLayout <span class="hljs-keyword">from</span> <span class="hljs-string">"./pages/Root"</span>;
<span class="hljs-keyword">import</span> HomePage <span class="hljs-keyword">from</span> <span class="hljs-string">"./pages/HomePage"</span>;

<span class="hljs-keyword">import</span> <span class="hljs-string">"./App.css"</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Routes</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Route</span> <span class="hljs-attr">path</span>=<span class="hljs-string">"/"</span> <span class="hljs-attr">element</span>=<span class="hljs-string">{</span>&lt;<span class="hljs-attr">RootLayout</span> /&gt;</span>}&gt;
        <span class="hljs-tag">&lt;<span class="hljs-name">Route</span> <span class="hljs-attr">index</span> <span class="hljs-attr">element</span>=<span class="hljs-string">{</span>&lt;<span class="hljs-attr">HomePage</span> /&gt;</span>} /&gt;
      <span class="hljs-tag">&lt;/<span class="hljs-name">Route</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">Routes</span>&gt;</span></span>
  );
}
</code></pre>
<p>You can also specify query parameters on the path, for example: <code>&lt;Route path="/movie/:mediaId" element={&lt;MovieDetails /&gt;} /&gt;</code>. Then to access the parameter, React Router provides the <code>usePrams</code> hook, which exposes an object of all specified parameters:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { useParams } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-router-dom"</span>;
<span class="hljs-keyword">let</span> { mediaId } = useParams();
</code></pre>
<p>Note that I created an element <code>&lt;RootLayout&gt;</code> which wraps my other routes. The use of the <code>&lt;Outlet&gt;</code> is what allows my navigation bar to persist on all pages without having to import it and render it in each individual component. Here you can also see how to use Chakra elements by importing them from the library we installed earlier.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { Outlet } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-router-dom"</span>;
<span class="hljs-keyword">import</span> { Box } <span class="hljs-keyword">from</span> <span class="hljs-string">"@chakra-ui/react"</span>;
<span class="hljs-keyword">import</span> MainNavigation <span class="hljs-keyword">from</span> <span class="hljs-string">"../components/MainNavigation"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">RootLayout</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Box</span> <span class="hljs-attr">as</span>=<span class="hljs-string">"main"</span> <span class="hljs-attr">minHeight</span>=<span class="hljs-string">"100vh"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">MainNavigation</span> /&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">main</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">Outlet</span> /&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">main</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">Box</span>&gt;</span></span>
  );
}
</code></pre>
<p>With all that out of the way, we can start building the components that the user will interact with. I won’t go through each component individually as this isn’t intended to serve as a “how to frontend”, but you’re welcome to check out the source code from the <a target="_blank" href="https://github.com/bingliscodes/movie_reviews_github">GitHub</a> repo if interested.</p>
<h3 id="heading-getting-data-from-the-movie-database-tmdb">Getting Data from The Movie Database (TMDB)</h3>
<p>With the components built using some sample movie data, the next step is to integrate live data from TMDB using their API. Once you <a target="_blank" href="https://developer.themoviedb.org/docs/getting-started">setting up a developer account</a> and generate an API key, make sure to add it to your <code>.env</code> file with an appropriate name. Next, we will make some test requests using <a target="_blank" href="https://www.postman.com/">Postman</a> by making a <code>GET</code> request and providing our API token in the “Authorization” section of the request (Navigate to Authorization and select “Bearer Token” from the Auth Type drop down menu).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761675625645/f009df90-7f97-4f32-adb4-3133b83710bf.png" alt class="image--center mx-auto" /></p>
<p>To make your life even easier, <a target="_blank" href="https://learning.postman.com/docs/sending-requests/variables/managing-environments/">set up a new environment</a> within Postman and create variables for Base Url and your API token, which can be accessed using the double bracket notation shown above.</p>
<p>Sending the request above will yield a JSON object response containing the popular TV shows, which we can then reference when writing building the frontend components.</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"page"</span>: <span class="hljs-number">1</span>,
    <span class="hljs-attr">"results"</span>: [
        {
            <span class="hljs-attr">"adult"</span>: <span class="hljs-literal">false</span>,
            <span class="hljs-attr">"backdrop_path"</span>: <span class="hljs-string">"/2746UvsbkZINd873Yd3o3TxOwCP.jpg"</span>,
            <span class="hljs-attr">"genre_ids"</span>: [
                <span class="hljs-number">9648</span>,
                <span class="hljs-number">18</span>
            ],
            <span class="hljs-attr">"id"</span>: <span class="hljs-number">200875</span>,
            <span class="hljs-attr">"origin_country"</span>: [
                <span class="hljs-string">"US"</span>
            ],
            <span class="hljs-attr">"original_language"</span>: <span class="hljs-string">"en"</span>,
            <span class="hljs-attr">"original_name"</span>: <span class="hljs-string">"IT: Welcome to Derry"</span>,
            <span class="hljs-attr">"overview"</span>: <span class="hljs-string">"In 1962, a couple with their son move to Derry, Maine just as a young boy disappears. With their arrival, very bad things begin to happen in the town."</span>,
            <span class="hljs-attr">"popularity"</span>: <span class="hljs-number">370.308</span>,
            <span class="hljs-attr">"poster_path"</span>: <span class="hljs-string">"/nyy3BITeIjviv6PFIXtqvc8i6xi.jpg"</span>,
            <span class="hljs-attr">"first_air_date"</span>: <span class="hljs-string">"2025-10-26"</span>,
            <span class="hljs-attr">"name"</span>: <span class="hljs-string">"IT: Welcome to Derry"</span>,
            <span class="hljs-attr">"vote_average"</span>: <span class="hljs-number">8.008</span>,
            <span class="hljs-attr">"vote_count"</span>: <span class="hljs-number">123</span>
        },
       ...]
</code></pre>
<p>For example, when building our carousel cards we now know the names of the properties to use to access the title, ratings, number of votes, and even poster image.</p>
<h3 id="heading-making-api-calls-with-axios">Making API Calls with Axios</h3>
<p>In a new folder (I like to create a <code>utils</code> folder, then a <code>js</code> folder nested within that), we will write the code to handle our asynchronous API calls using Axios and get the data into our main application. Start by installing Axios (<code>npm i axios</code>), then try and replicate the HTTP request we just made with Postman (a <code>GET</code> request to the <code>https://api.themoviedb.org/3/tv/popular</code> endpoint). To do this, we will use the async/await pattern and then a try/catch block to handle errors. If all goes well, this function will return an object containing the <code>results</code> array we saw in the JSON response above. If not, it will throw an error that will be propagated to where the function is invoked.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> axios <span class="hljs-keyword">from</span> <span class="hljs-string">"axios"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> fetchData = <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> popularShowRes = <span class="hljs-keyword">await</span> axios({
      <span class="hljs-attr">method</span>: <span class="hljs-string">"GET"</span>,
      <span class="hljs-attr">url</span>: <span class="hljs-string">`<span class="hljs-subst">${<span class="hljs-keyword">import</span>.meta.env.VITE_TMDB_API_BASE_URL}</span>tv/popular`</span>,
      <span class="hljs-attr">headers</span>: {
        <span class="hljs-attr">Authorization</span>: <span class="hljs-string">`Bearer <span class="hljs-subst">${<span class="hljs-keyword">import</span>.meta.env.VITE_TMDB_API_TOKEN}</span>`</span>,
        <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>,
      },
    });

    <span class="hljs-keyword">if</span> (popularShowRes.status !== <span class="hljs-number">200</span>)
      <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">"Failed to fetch popular shows"</span>);

    <span class="hljs-keyword">return</span> {
      <span class="hljs-attr">popularShowData</span>: popularShowRes.data.results,
    };
  } <span class="hljs-keyword">catch</span> (err) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Error fetching data: "</span>, err);
    <span class="hljs-comment">// Handle errors here or throw them to be handled where the function is called</span>
    <span class="hljs-keyword">throw</span> err;
  }
};
</code></pre>
<h3 id="heading-working-with-the-api-data-in-react">Working with the API Data in React</h3>
<p>Now that we have a way to <em>receive</em> the data, we need a way to elegantly work with it in React. To avoid an infinite loop of loading data into a state variable, then the component re-rendering due to a state change, we use React’s <code>useEffect</code> hook, which allows us to specify an array of variables that will trigger a the effect on change. The implementation below also employs a common React pattern of having state variables for loading and error, which are manipulated inside <code>useEffect</code> and allow us to provide an enhanced user experience by indicating the status of the request. In this case, we do not need to add any variables to the array, which indicates that the <code>useEffect</code> code should only be invoked when the component is rendered.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { useState, useEffect, useContext } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;

<span class="hljs-keyword">import</span> { fetchData, fetchMediaDetails } <span class="hljs-keyword">from</span> <span class="hljs-string">"../utils/js/apiCalls"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">HomePage</span>(<span class="hljs-params"></span>)</span>{
    <span class="hljs-keyword">const</span> [data, setData] = useState(<span class="hljs-literal">null</span>);  
    <span class="hljs-keyword">const</span> [loading, setLoading] = useState(<span class="hljs-literal">true</span>);
    <span class="hljs-keyword">const</span> [error, setError] = useState(<span class="hljs-literal">null</span>);  

    useEffect(<span class="hljs-function">() =&gt;</span> {
        fetchData()
          .then(<span class="hljs-function">(<span class="hljs-params">data</span>) =&gt;</span> {
            setData(data);
            setLoading(<span class="hljs-literal">false</span>);
          })
          .catch(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
            setError(err);
            setLoading(<span class="hljs-literal">false</span>);
          });
      }, []);
    }  

  <span class="hljs-comment">// Example of displaying conditional messages to user while loading or in case of error</span>
  <span class="hljs-keyword">if</span> (loading) <span class="hljs-keyword">return</span> <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Loading...<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span></span>;
  <span class="hljs-keyword">if</span> (error) <span class="hljs-keyword">return</span> <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Error loading data!<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span></span>;
</code></pre>
<p>With the <code>useEffect</code> in place, we can now access the data from our API call as if it were a normal state variable, since we will not reach the return unless the data loads without issue. For example, we could create a carousel using the popular show data by destructuring the object returned from our utility function, and then passing it to our custom carousel component!</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> { popularShowData } = data || {};
<span class="hljs-keyword">return</span> ( 
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">ChakraCarousel</span>
        <span class="hljs-attr">carouselData</span>=<span class="hljs-string">{popularShowCarouselData}</span>
        <span class="hljs-attr">title</span>=<span class="hljs-string">"Popular Shows"</span>
        <span class="hljs-attr">type</span>=<span class="hljs-string">"tv"</span>
  /&gt;</span></span>);
</code></pre>
<h1 id="heading-building-the-backend">Building the Backend</h1>
<h2 id="heading-planning-for-the-backend">Planning for the Backend</h2>
<p>Similarly to the frontend, we start by outlining the building blocks of the backend:</p>
<ul>
<li><p>MongoDB entities (what documents we will need and how we will organize them)</p>
</li>
<li><p>HTTP endpoints for our API (i.e., <code>GET</code>, <code>POST</code>, <code>PATCH</code>, and <code>DELETE</code>)</p>
</li>
</ul>
<p>For the MongoDB entities, I knew I wanted to have a User document that contains both personal information and the data for each user’s wishlist, watched, and favorites. Thus, each user should have the following properties:</p>
<ul>
<li><p>First Name</p>
</li>
<li><p>Last Name</p>
</li>
<li><p>Email</p>
</li>
<li><p>Password (hashed)</p>
</li>
<li><p>Movie Wish List</p>
</li>
<li><p>Movie Favorites List</p>
</li>
<li><p>Movie Watched List</p>
</li>
<li><p>TV Wish List</p>
</li>
<li><p>TV Favorites List</p>
</li>
<li><p>TV Watched List</p>
</li>
</ul>
<p>For the HTTP End Points, I organized these by verb, including what data the request should include for <code>POST</code> and <code>PATCH</code> requests:</p>
<ul>
<li><p><code>GET</code></p>
<ul>
<li><p>/users (get all users)</p>
</li>
<li><p>/users/userId (get one user by id)</p>
</li>
<li><p>/users/:userId/watched (get specified list for a specific user)</p>
</li>
</ul>
</li>
<li><p><code>POST</code></p>
<ul>
<li><p>/users/signup</p>
<ul>
<li>includes: name, email, password, and password confirm</li>
</ul>
</li>
<li><p>/users/login</p>
<ul>
<li>includes: includes email and password</li>
</ul>
</li>
<li><p>/users/me/watched (add an item to a specific list for current user)</p>
<ul>
<li>includes: the id of the item to delete</li>
</ul>
</li>
</ul>
</li>
<li><p><code>PATCH</code></p>
<ul>
<li><p>/users/:userId (update specific user data)</p>
<ul>
<li>includes: new key-value pairs for data to update, such as first name, last name, or email (but not password)</li>
</ul>
</li>
<li><p>/users/updateMe (update data for the currently logged in user)</p>
<ul>
<li>includes: new key-value pairs for data to update, such as first name, last name, or email (but not password)</li>
</ul>
</li>
</ul>
</li>
<li><p><code>DELETE</code></p>
<ul>
<li><p>/users/:userId (delete a specific user)</p>
</li>
<li><p>/users/deleteMe (delete currently logged in user)</p>
</li>
<li><p>/users/me/watched (delete an item from a specific list for current user)</p>
<ul>
<li>includes: the id of the item to delete</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>I decided to only implement endpoints for updating the list for the current user since the functionality would require a user to be logged in. Thus, we could add the user to the request using additional middleware <em>before</em> it reaches the endpoint where the controller is invoked, since middleware is called in the order it is declared in an express application.</p>
<h2 id="heading-setting-up-the-backend">Setting up the Backend</h2>
<p>Start by initializing a new project in your backend directory (remember how we created two folders at the beginning, one for frontend and one for backend?) with <code>npm init</code>. The only important configuration when setting this up is to enter "module” when the set-up prompts you to select a type (the default is “commonJS”), which will allow you to use ES6 import syntax. If you miss that step, you can always edit it directly in your <code>package.json</code> file! Once you finish initializing your project, it’s time to start installing dependencies:</p>
<ul>
<li><p>MongoDB</p>
</li>
<li><p>Mongoose</p>
</li>
<li><p>Express</p>
</li>
</ul>
<p>You can install all 3 at once with the command <code>npm i mongodb mongoose express</code></p>
<h3 id="heading-setting-up-our-app-with-express">Setting up our App with Express</h3>
<p>To initialize our application with Express we will set up a test route that will handle <code>GET</code> requests and send a simple response of “API is running”</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> express <span class="hljs-keyword">from</span> <span class="hljs-string">'express'</span>;

<span class="hljs-keyword">const</span> app = express();

<span class="hljs-comment">// Test Route</span>
app.get(<span class="hljs-string">'/'</span>, <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
  res.send(<span class="hljs-string">'API is running...'</span>);
});

<span class="hljs-comment">// Default error handling</span>
app.get(<span class="hljs-string">'/{*any}'</span>, <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
  res.status(<span class="hljs-number">404</span>).json({
    <span class="hljs-attr">status</span>: <span class="hljs-string">'fail'</span>,
    <span class="hljs-attr">message</span>: <span class="hljs-string">`Can't find <span class="hljs-subst">${req.originalUrl}</span> on this server!`</span>,
  });
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> app;
</code></pre>
<h3 id="heading-connecting-to-mongodb">Connecting to MongoDB</h3>
<p>To connect to MongoDB, we will use the <code>mongoose.connect</code> function, which takes in a connection string and a configuration object.</p>
<p>First, you’ll need to <a target="_blank" href="https://www.mongodb.com/cloud/atlas/register">register</a> for an Atlas account. Once you answer the registration prompts, you’ll have the option to deploy a free cluster and then create a user. After creating your user, add the password to your backend environment variables since this is how we will connect to MongoDB.</p>
<p>To get your connection string, navigate to your Atlas dashboard and click “Connect,” go to “Drivers”, then select “view full code sample.”</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761758026440/c0e5e7bc-9cae-4a4d-8999-6ecab169501f.gif" alt class="image--center mx-auto" /></p>
<p>Add the connection string to your environment variables, then in <code>server.js</code> (the main entry point to our backend) replace the password with your MongoDB password by using <code>dotenv</code>.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> dotenv <span class="hljs-keyword">from</span> <span class="hljs-string">'dotenv'</span>;
dotenv.config({ <span class="hljs-attr">path</span>: <span class="hljs-string">'./config.env'</span> });

<span class="hljs-keyword">const</span> DB = process.env.DATABASE.replace(
  <span class="hljs-string">'&lt;db_password&gt;'</span>,
  process.env.MONGODB_PASSWORD,
);
</code></pre>
<p>Next, we’ll use <code>mongoose</code> to connect to the database:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> mongoose <span class="hljs-keyword">from</span> <span class="hljs-string">'mongoose'</span>;

mongoose.connect(DB, {}).then(<span class="hljs-function">() =&gt;</span> {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'DB connection successful!'</span>);
});
</code></pre>
<p>Finally, we’ll start our server by having our App listen for requests on port 3000:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> port = process.env.PORT || <span class="hljs-number">3000</span>;

<span class="hljs-keyword">const</span> server = app.listen(port, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`App running on port <span class="hljs-subst">${port}</span>...`</span>);
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'mode is'</span>, process.env.NODE_ENV);
});
</code></pre>
<h3 id="heading-modelling-data-with-mongoose">Modelling Data with Mongoose</h3>
<p>Starting with the MongoDB entities, since we want to implement individualized profiles where users can keep track of their own lists, we will need a User document. To keep things simple, in addition to the basic user information, we will initialize the lists that will later hold the id’s of TV shows and movies as empty arrays within our user schema. Using Mongoose, we define the properties each user will have by creating a schema, and then export it as a model.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> mongoose <span class="hljs-keyword">from</span> <span class="hljs-string">'mongoose'</span>;
<span class="hljs-keyword">const</span> userSchema = <span class="hljs-keyword">new</span> mongoose.Schema(
  {
    <span class="hljs-attr">firstName</span>: {
      <span class="hljs-attr">type</span>: <span class="hljs-built_in">String</span>,
      <span class="hljs-attr">required</span>: [<span class="hljs-literal">true</span>, <span class="hljs-string">'User must have a first name'</span>],
      <span class="hljs-attr">trim</span>: <span class="hljs-literal">true</span>,
    },
    <span class="hljs-attr">lastName</span>: {
      <span class="hljs-attr">type</span>: <span class="hljs-built_in">String</span>,
      <span class="hljs-attr">required</span>: [<span class="hljs-literal">true</span>, <span class="hljs-string">'User must have a last name'</span>],
      <span class="hljs-attr">trim</span>: <span class="hljs-literal">true</span>,
    ...
    tvWishlist: <span class="hljs-built_in">Array</span>,
    <span class="hljs-attr">tvWatchlist</span>: <span class="hljs-built_in">Array</span>,
    <span class="hljs-attr">movieWishlist</span>: <span class="hljs-built_in">Array</span>,
    <span class="hljs-attr">movieWatchlist</span>: <span class="hljs-built_in">Array</span>,
    <span class="hljs-attr">tvFavoritelist</span>: <span class="hljs-built_in">Array</span>,
    <span class="hljs-attr">movieFavoriteList</span>: <span class="hljs-built_in">Array</span>,
  },
);

<span class="hljs-keyword">const</span> User = mongoose.model(<span class="hljs-string">'User'</span>, userSchema);

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> User;
</code></pre>
<p>Next we need to build the user controller, which is where we will create the functions that <em>will be invoked</em> when our server handles requests from different endpoints. This will be more clear once we mount the routers, so for now just pretend that it makes sense. For example, if we want an endpoint that returns a list of all users, we write the following <code>getAllUsers</code> function:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> User <span class="hljs-keyword">from</span> <span class="hljs-string">'../models/userModel.js'</span>;
<span class="hljs-keyword">import</span> catchAsync <span class="hljs-keyword">from</span> <span class="hljs-string">'../utils/catchAsync.js'</span>;

<span class="hljs-comment">/* catchAsync is a helper function that avoids having to
   put all async functions in try/catch blocks */</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> getAllUsers = <span class="hljs-function">() =&gt;</span>
  catchAsync(<span class="hljs-keyword">async</span> (req, res, next) =&gt; {
    <span class="hljs-keyword">const</span> doc = <span class="hljs-keyword">await</span> User.find();

    res.status(<span class="hljs-number">200</span>).json({
      <span class="hljs-attr">status</span>: <span class="hljs-string">'success'</span>,
      <span class="hljs-attr">results</span>: doc.length,
      <span class="hljs-attr">data</span>: {
        <span class="hljs-attr">data</span>: doc,
      },
    });
  });
</code></pre>
<p>Using the user model we created with Mongoose, <a target="_blank" href="https://mongoosejs.com/docs/queries.html">we can use the <code>find()</code> method</a>, which will return a list with all of the documents from that model.</p>
<p>Next, we need some way for the server to “know” when to invoke the functions that we are defining in our controller files. To do this, we will mount routers, then define the functions we want to execute when our server receives a request. At the top of the app (below where we initialize the app with express), import the userRouter (which we are about to build). At the bottom of the app (right below the test route), add <code>app.use(‘/api/v1/users’, userRouter)</code> which will direct our server to the router we are about to define when requests are made to the <code>/api/v1/users</code> endpoint.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> app = express();
<span class="hljs-keyword">import</span> userRouter <span class="hljs-keyword">from</span> <span class="hljs-string">'./routes/userRoutes.js'</span>;

<span class="hljs-comment">// Test Route</span>
app.get(<span class="hljs-string">'/'</span>, <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
  res.send(<span class="hljs-string">'API is running...'</span>);
});

app.use(<span class="hljs-string">'/api/v1/users'</span>, userRouter);
</code></pre>
<p>To define the routes, create a new <code>routes</code> folder, then within that a new file <code>userRoutes.js</code>. Initialize the router by calling <code>express.Router()</code>, then use the <code>route</code> function to define the path. Then we can define the desired behaviors for different HTTP verbs using chaining.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> express <span class="hljs-keyword">from</span> <span class="hljs-string">'express'</span>;
<span class="hljs-keyword">import</span> {
  getAllUsers,
} <span class="hljs-keyword">from</span> <span class="hljs-string">'../controllers/userController.js'</span>;

<span class="hljs-keyword">const</span> router = express.Router();

router.route(<span class="hljs-string">'/'</span>).get(getAllUsers);

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> router;
</code></pre>
<p>For example, this will invoke the <code>getAllUsers</code> method we defined previously when the server receives a <code>GET</code> request at the <code>api/v1/users/</code> endpoint.</p>
<p>Now all you have to do is start the server locally, and it will respond to requests as they come in!</p>
<p>Using the process outlined above, I developed a REST API to handle user signup, login/logout, updating personal information, and even updating the movie/TV show lists. Since all of this data is stored in MongoDB and I use Mongoose to create and update the documents, any changes made to the data will persist in the cloud, meaning requests made from <em>anywhere</em> (including the frontend of the application) to the API are capable of sending and receiving current data about the users.</p>
<h2 id="heading-a-note-on-cookie-settings">A Note on Cookie Settings:</h2>
<p>When working with HTTPS, make sure to set the <code>secure</code> and <code>sameSite</code> <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie">cookie options</a>! I set these conditionally based on the <code>NODE_ENV</code> since the local development server is not secure.</p>
<pre><code class="lang-javascript">secure: process.env.NODE_ENV === <span class="hljs-string">'production'</span>,
<span class="hljs-attr">sameSite</span>: process.env.NODE_ENV === <span class="hljs-string">'production'</span> ? <span class="hljs-string">'None'</span> : <span class="hljs-string">'Lax'</span>,
</code></pre>
<h1 id="heading-deploying-the-frontend">Deploying the Frontend</h1>
<p>To deploy the frontend I used <a target="_blank" href="https://www.netlify.com/">Netlify</a>. After signing up, navigate to “Add new project” &gt; “Import an existing project” &gt; “GitHub”, then select the repository that contains your application. During the set up, specify <code>frontend</code> as the base directory and <code>npm run build</code> as the build command.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761768840901/775ef3a6-c12c-4cd5-85f6-f5e9b25e4c65.gif" alt class="image--center mx-auto" /></p>
<p>Note: you may also have to set the “Publish directory” to <code>frontend/dist</code> since this is where Vite stores the production ready code.</p>
<p>Next, paste in your environment variables from the <code>.env</code> file (the button is right above the “Deploy” button), and then Netlify will generate a url where you can view your live application!</p>
<h1 id="heading-deploying-the-backend">Deploying the Backend</h1>
<p>I used <a target="_blank" href="https://render.com/">Render</a> to deploy the backend, which I discussed in more depth in a <a target="_blank" href="https://code-after-degree.hashnode.dev/deploying-a-nodejs-app-with-cicd-in-render">previous blog</a>. The process is similar to the above (using GitHub to link the repository), but the root directory will be <code>backend</code>, the build command will be <code>npm install</code>, and the start command <code>NODE_ENV=production node server.js</code>. You will also have to specify your environment variables, which can be pasted from your backend <code>.env</code> file. Depending on how your project is set up (i.e., if you have conditional code that depends on the environment), you would set the <code>NODE_ENV</code> to “production” in Render. At this point, Render will build and deploy your code and you will have a <strong>fully deployed web application</strong>!</p>
<h1 id="heading-connecting-the-frontend-and-backend">Connecting the Frontend and Backend</h1>
<p>Since the two parts of the application operate entirely independently, how do they communicate with one another? At a high level, our backend works by <a target="_blank" href="https://stackoverflow.com/questions/70384927/what-does-the-listen-method-in-express-look-like">creating an HTTP server object that is configured to receive TCP connections on a specified port and IP address</a>. Since we didn’t bind the app to a specific IP address, <a target="_blank" href="https://stackoverflow.com/questions/33953447/express-app-server-listen-all-interfaces-instead-of-localhost-only">it will run on all interfaces available</a>, including <code>http://localhost</code> or <code>http://127.0.0.1</code>, <a target="_blank" href="https://www.geeksforgeeks.org/computer-networks/what-is-local-host/">which act as virtual servers for testing our web application</a>.</p>
<p>When building network requests, best practice is to use an environment variable <em>inside</em> a template string rather than hard-coding the hostname. For example: <code>`${import.meta.env.VITE_DEV_API_BASE_URL}api/v1/users/me` </code> (where <code>VITE_DEV_API_BASE_URL= http://localhost:3000/</code> in our local development environment). Then, all we have to do is <em>update that variable</em> to integrate the two parts of the application. Once the backend is deployed, Render will generate a url ending in “onrender.com,” that can be used to update the base url environment variable in your frontend (on Netlify). Once complete, all the network requests coming from frontend will now be directed to the <em>deployed</em> backend!</p>
<h1 id="heading-concluding-remarks">Concluding Remarks</h1>
<p>This is easily my largest software development project to date and it really pushed me to go out of my comfort zone and learn about things that I never would have even considered (the cookie settings, for example). This endeavor represents a milestone in bridging the gap between writing code in a local development environment (i.e., my laptop) and producing an application that can be used by anyone with access to the internet.</p>
<p>While I learned a lot, there are still parts of the code that I don’t fully understand– I know enough to make it work, but not enough to explain what’s happening behind the scenes, or explore the <em>why</em>. With that said, appreciating the gaps in our knowledge and and acknowledging how much we <em>don’t</em> know is a crucial step in the learning process.</p>
]]></content:encoded></item><item><title><![CDATA[Deploying a Node.js app with CI/CD in Render]]></title><description><![CDATA[Overview
I DEFINITELY underestimated how much work is involved in taking a project that runs on my local machine and making it accessible from the web.
In this article I will walk through the steps and tools involved in taking an app from your local ...]]></description><link>https://code-after-degree.hashnode.dev/deploying-a-nodejs-app-with-cicd-in-render</link><guid isPermaLink="true">https://code-after-degree.hashnode.dev/deploying-a-nodejs-app-with-cicd-in-render</guid><category><![CDATA[Programming Blogs]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[render]]></category><category><![CDATA[Express.js]]></category><category><![CDATA[deployment]]></category><category><![CDATA[GitHub]]></category><category><![CDATA[Git]]></category><category><![CDATA[ci-cd]]></category><category><![CDATA[CI/CD]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Developer]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Devops]]></category><dc:creator><![CDATA[Benjamin Inglis]]></dc:creator><pubDate>Fri, 19 Sep 2025 17:22:11 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1761929204066/0343595e-bdfb-485e-a0c1-3216f6bd017f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-overview">Overview</h1>
<p>I DEFINITELY underestimated how much work is involved in taking a project that runs on my local machine and making it accessible from the web.</p>
<p>In this article I will walk through the steps and tools involved in taking an app from your local machine and deploying it to Render. I will not, however, go into specifics of the technologies utilized or building the application.</p>
<p><a target="_blank" href="https://github.com/bingliscodes/natours">Link to GitHub Repo</a></p>
<h1 id="heading-deploying-to-render">Deploying to Render</h1>
<h2 id="heading-prerequisites"><strong>Prerequisites:</strong></h2>
<p>In order to deploy your app to render, you must first have a project in GitHub.</p>
<p>Deploying my app to Render was a relatively painless process.</p>
<ol>
<li><p>Click the “Add new” button in the top right corner.</p>
</li>
<li><p>Click “Web Service”.</p>
</li>
<li><p>Sign into GitHub, then select the Repository containing the project.</p>
</li>
<li><p>Fill out the fields</p>
<ol>
<li>Note: make sure the “Start Command” field is correct. It is set to <code>$ node app.js</code> by default, but I had to change it to <code>$ node server.js</code> since I use that as my main entry point.</li>
</ol>
</li>
<li><p>Select “Instance Type” (I used the “Free” one since it is for learning purposes).</p>
</li>
<li><p>Copy and paste in your environment variables from your <code>.env</code> file.</p>
</li>
<li><p>Click “Deploy Web Service” and let Render work it’s magic 🙂</p>
</li>
</ol>
<p>Additionally, make sure that the Auto-Deploy setting is set to “After CI Checks Pass” from the default “On Commit” if you plan to integrate CI/CD (which I walk through how to do using GitHub Actions later on).</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.loom.com/share/65908700ccb44577b2170cf9dce724cc?sid=3035e839-9716-40c5-a42e-1b71ed11eb35">https://www.loom.com/share/65908700ccb44577b2170cf9dce724cc?sid=3035e839-9716-40c5-a42e-1b71ed11eb35</a></div>
<p> </p>
<h1 id="heading-writing-unit-tests-with-jest-and-supertest">Writing Unit Tests with Jest and SuperTest</h1>
<p><a target="_blank" href="https://www.loom.com/share/65908700ccb44577b2170cf9dce724cc">Fig</a>uring out how to get Jest and SuperTest working was overwhelming due to the amount of information that exists about these, so I’ll try to explain the steps that I did to get it working as simply as possible, but first, what are Jest and SuperTest?</p>
<p><a target="_blank" href="https://jestjs.io/"><strong>Jest</strong></a>: A <em>framework</em> used for testing JavaScript code. Basically you define conditions that indicate whether or not a function or component is working as intended, and Jest will execute your tests so you can see what is or is not behaving as intended.</p>
<p><strong>SuperTest</strong>: A Node.js library used for testing APIs by simulating requests and asserting responses.</p>
<h2 id="heading-preparing-your-environment">Preparing Your Environment</h2>
<ol>
<li><p>Install Jest and Supertest using npm</p>
<p> <code>npm i jest --save-dev</code> and <code>npm i supertest --save-dev</code></p>
</li>
<li><p>In your <code>package.json</code> file, add the following to your scripts: <code>“test”: “jest”</code>. Now you can execute all your testing via Jest simply by calling <code>npm test</code></p>
</li>
<li><p>Inside your project root, create a folder <code>tests</code>. Since my application involves a REST API, the examples will be organized by controller. What’s important to note here is that each of these files will be treated as one Test Suite by Jest (Test Suites are the organizational unit that contain the unit tests).</p>
</li>
<li><p>Create the file to contain the tests with the convention <code>fileName.test.js</code>. For example, <code>tourController.test.js</code> in my application.</p>
</li>
<li><p>At the top of the file, require <code>request</code> from SuperTest, and <code>app</code> from the <strong>entry point</strong> of your application (recall that I said <code>server.js</code> was the entry point for my application)</p>
<pre><code class="lang-javascript"> <span class="hljs-keyword">const</span> request = <span class="hljs-built_in">require</span>(<span class="hljs-string">'supertest'</span>);
 <span class="hljs-keyword">const</span> app = <span class="hljs-built_in">require</span>(<span class="hljs-string">'../server'</span>);
</code></pre>
</li>
</ol>
<h2 id="heading-writing-unit-tests">Writing Unit Tests</h2>
<p>Recall that to thoroughly test our application, we will create unit tests using Jest, and then utilize functionality from the SuperTest library to handle the HTTP requests.</p>
<h3 id="heading-what-is-unit-testing">What is Unit Testing?</h3>
<p><a target="_blank" href="https://www.geeksforgeeks.org/software-testing/unit-testing-software-testing/">Unit testing involves testing the smallest possible functional unit of code</a>. In the case of this demo app are the <em>route handlers</em>, which are essentially middleware that are executed when an HTTP request is made to the route specific by the handler.</p>
<h3 id="heading-your-first-unit-test-with-jest">Your First Unit Test with Jest</h3>
<p>While there is an abundance of information and <a target="_blank" href="https://jestjs.io/docs/getting-started">documentation for Jest</a>, I will try to distill down the most crucial information needed to get to get Jest <em>working</em> with your application, and then as you develop your testing suites refer to the documentation to troubleshoot specific issues.</p>
<p>Within your testing directory, create a file <code>sum.js</code> with the following code:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// sum.js</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">sum</span>(<span class="hljs-params">a, b</span>) </span>{
  <span class="hljs-keyword">return</span> a + b;
}
<span class="hljs-built_in">module</span>.exports = sum;
</code></pre>
<p>Next, create a <code>sum.test.js</code> file in the same directory:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// sum.test.js</span>
<span class="hljs-keyword">const</span> sum = <span class="hljs-built_in">require</span>(<span class="hljs-string">'./sum'</span>);

<span class="hljs-comment">// Demo test</span>
test(<span class="hljs-string">'adds 1 + 2 to equal 3'</span>, <span class="hljs-function">() =&gt;</span> {
  expect(sum(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>)).toBe(<span class="hljs-number">3</span>);
});
</code></pre>
<p>Finally, from the root of the project, call <code>npm test</code> (the script we defined in “Preparing Your Environment”).</p>
<p>his will do is invoke Jest, which will locate your test files (the ones with the <code>.test</code> extension), and then it will execute all the tests specified within the Test Suite. In order to demonstrate what the different components of the function above do, I changed the <code>expect(sum(1, 2)).toBe(3)</code> to <code>expect(sum(1, 2)).toBe(4)</code> and put a screenshot of the results below.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757970928753/a5580aa8-f998-43f9-a664-5aa55aed1f85.png" alt class="image--center mx-auto" /></p>
<p>When the test fails, you can see the test description (what we input as the first argument into the <code>test</code> function) is displayed in red below the failed test, along with the actual <code>received</code> and <code>expected</code> values. This is the default behavior of Jest, but if you’d like to view the content in the <code>describe</code>, <code>it</code>, and <code>test</code> blocks <em>regardless of pass/fail status</em> then execute the command <code>npm test -- --verbose</code></p>
<p>Side note on <code>it</code> vs. <code>test</code>: both of these functions essentially do the same thing in Jest, it’s primarily a <a target="_blank" href="https://stackoverflow.com/questions/45778192/what-is-the-difference-between-it-and-test-in-jest">matter of readability</a>. Moving forward I will use <code>it</code>, in which I describe the expected functionality of the test (i.e., what it <em>should</em> do).</p>
<p><strong>Understanding the Basic Components of a Jest test</strong></p>
<ul>
<li><p><strong>Test description:</strong> This is the first argument entered into a the <code>test</code> or <code>it</code> function and should be used to describe the expected result of the test.</p>
</li>
<li><p><strong>Expect:</strong> Statements that are evaluated and determine whether a test passes or fails. Typically in the format of <code>expect(value).[operator](comparator)</code>.</p>
<ul>
<li><p>In the example above, the value we are expecting is the <em>result</em> of calling <code>sum(1, 2)</code>, then we call the <code>toBe</code> operator and pass in the comparator value of 4, which checks if the result of <code>sum(1,2)</code> evaluates to 4.</p>
</li>
<li><p>This is a very simple example, so please refer to the <a target="_blank" href="https://jestjs.io/docs/expect#expectvalue">Jest documentation</a> for more information</p>
</li>
</ul>
</li>
</ul>
<h3 id="heading-testing-your-api-endpoint-with-jest-supertest">Testing your API Endpoint with Jest + SuperTest</h3>
<p>Now that we’ve reviewed the basic anatomy of a Jest test, let’s bring in the SuperTest <code>request</code> functionality.</p>
<p>Essentially what this function will do is asynchronously make an HTTP request to our application. Using async/await, we store the results in a variable <code>res</code>, then use chaining to specify the the request type and other details, such as what data we will send (in the event of a POST request).</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// userController.test.js</span>
<span class="hljs-keyword">const</span> request = <span class="hljs-built_in">require</span>(<span class="hljs-string">'supertest'</span>);
<span class="hljs-keyword">const</span> app = <span class="hljs-built_in">require</span>(<span class="hljs-string">'../server'</span>); <span class="hljs-comment">// Main entry point to your app</span>

describe(<span class="hljs-string">'POST /api/v1/users/login'</span>, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> userData = {
    <span class="hljs-attr">email</span>: <span class="hljs-string">'name@example.io'</span>,
    <span class="hljs-attr">password</span>: <span class="hljs-string">'test1234'</span>,
  };

    it(<span class="hljs-string">'should authenticate the user properly'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> request(app).post(<span class="hljs-string">'/api/v1/users/login'</span>).send(userData);

    expect(res.status).toBe(<span class="hljs-number">200</span>);
    expect(res.body.toHaveProperty(<span class="hljs-string">'authToken'</span>));
  });

 <span class="hljs-comment">// Additional tests here</span>
});
</code></pre>
<p>Three things I want to point out about the code snippet above:</p>
<ol>
<li><p><code>describe</code> is used here to <a target="_blank" href="https://jestjs.io/docs/api#describename-fn">create a logical grouping of related tests</a>. In the example above, I created a block that would run several tests on the specified endpoint.</p>
</li>
<li><p>We pass in <code>app</code> to the <code>request</code> function call. In order for this to work, your entry point module file must export the app (<code>module.exports = app</code> at the end of <code>server.js</code> in my case, where <code>app</code> is required at the top of <code>server.js</code>)</p>
</li>
<li><p>In order to get all my testing working, I had to wrap the code which initialized my server inside a condition that checks if we are in a testing environment:</p>
</li>
</ol>
<pre><code class="lang-javascript"><span class="hljs-keyword">if</span> (process.env.NODE_ENV !== <span class="hljs-string">'test'</span>) {
  <span class="hljs-keyword">const</span> server = app.listen(port, <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`App running on port <span class="hljs-subst">${port}</span>...`</span>);
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'mode is'</span>, process.env.NODE_ENV);
  });
</code></pre>
<h2 id="heading-integrating-github-actions">Integrating GitHub Actions</h2>
<p>Now that we’ve deployed the app to Render and have some unit tests in place, it’s time to utilize GitHub Actions for our Continuous Integration and Continuous Deployment (CI/CD).</p>
<h3 id="heading-what-are-github-actions">What are GitHub Actions?</h3>
<p><a target="_blank" href="https://docs.github.com/en/actions/get-started/understand-github-actions">GitHub Actions</a> is a service that enables us to automate the build, test, and deployment pipeline for our application. The basic building block of GitHub Actions is a <a target="_blank" href="https://docs.github.com/en/actions/get-started/understand-github-actions#workflows">workflow</a>, which will run one or more <a target="_blank" href="https://docs.github.com/en/actions/get-started/understand-github-actions#jobs">jobs</a> when triggered by certain <a target="_blank" href="https://docs.github.com/en/actions/get-started/understand-github-actions#events">events</a> in the repository (such as push or pull requests).</p>
<p>I’d recommend following the steps outlined in the <a target="_blank" href="https://docs.github.com/en/actions/get-started/quickstart#creating-your-first-workflow">official documentation to get started with GitHub Actions</a>. Once you have you have completed the steps outlined in the Quickstart guide, you can simply modify the YAML file to build and test your project by running <code>npm test</code>.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">run-unit-tests:</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>
    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v5</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/setup-node@v4</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">node-version:</span> <span class="hljs-string">'24'</span> <span class="hljs-comment"># Specify the node version of your project</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">run:</span> <span class="hljs-string">npm</span> <span class="hljs-string">ci</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">run:</span> <span class="hljs-string">npm</span> <span class="hljs-string">test</span>
</code></pre>
<h3 id="heading-setting-environment-variables-in-github-actions">Setting Environment Variables in GitHub Actions</h3>
<p>This is the step that caused me the most trouble so I will outline how I resolved it. When building an app, there are almost always going to be details that cannot be exposed (and thus published) to a public repository such as GitHub. These include credentials for services, API secrets, or any other configuration details that shouldn’t be common knowledge. These details are typically saved in a <code>.env</code> file locally, and then injected into the code using a library such as Dotenv.</p>
<p>In order to set the environment variables for your workflow we will <a target="_blank" href="https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets">use secrets in GitHub Actions</a>:</p>
<ol>
<li><p>Navigate to your GitHub repository, then go to Settings &gt; Secrets and variables &gt; Actions &gt; Repository secrets.</p>
</li>
<li><p>For each variable in your <code>.env</code> file, add it as a secret by clicking “New repository secret,” then filling in the Name (as it appears in your <code>.env</code> file) and Secret fields.</p>
</li>
<li><p>In your GitHub Actions workflow, define your environment variables using the following format for each variable defined in your <code>.env</code> file.</p>
</li>
</ol>
<pre><code class="lang-yaml"><span class="hljs-attr">env:</span>
  <span class="hljs-attr">DATABASE:</span> <span class="hljs-string">${{secrets.DATABASE}}</span>
  <span class="hljs-attr">DATABASE_PASSWORD:</span> <span class="hljs-string">${{secrets.DATABASE_PASSWORD}}</span>
</code></pre>
<p>By using the <code>${{secrets.VARIABLE_NAME}}</code> syntax we are accessing the <code>secrets</code> <a target="_blank" href="https://docs.github.com/en/actions/reference/workflows-and-actions/contexts#context-availability">context made available to us via GitHub Actions</a>.</p>
<p>Once you have set all necessary environment variables, your app run as intended, using the unit tests we defined earlier as the condition for whether or not the code passes the CI test. Additionally, because of the change we made to the Auto-Deploy setting at the beginning, Render will automatically use this workflow to only re-deploy the application <em>when the continuous integration testing passes</em>. Congratulations! Your app is now live with CI/CD 😄</p>
<h1 id="heading-final-thoughts">Final Thoughts</h1>
<p>I know that each and every one of these steps deserve it’s own whole article, but for the sake of brevity, learning, and sanity, I tried to include only the information that was absolutely essential. From my experience, one of the most daunting (but also exciting) parts about coding is opening up documentation and realizing just how much you <strong>don’t</strong> know.</p>
<p>As corny as it sounds, I consider myself a lifelong student. There will <em>always</em> be more to learn and better ways to do things. In fact, there are likely one (or more) better ways to accomplish exactly what I just did in this article. However, striving to make everything as “perfect” as possible from the start can impede progress. Let best practices guide you, and ask yourself often: “how can I improve this?”</p>
]]></content:encoded></item></channel></rss>