<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Viktor Sivulskiy</title>
    <description>The latest articles on Forem by Viktor Sivulskiy (@softmaker).</description>
    <link>https://forem.com/softmaker</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3000187%2F1b4e3745-c631-4830-bd33-65bdfdff6ab6.jpeg</url>
      <title>Forem: Viktor Sivulskiy</title>
      <link>https://forem.com/softmaker</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/softmaker"/>
    <language>en</language>
    <item>
      <title>Designing a Smart Commentator Bot: Applying Software Patterns and Principles</title>
      <dc:creator>Viktor Sivulskiy</dc:creator>
      <pubDate>Sat, 05 Apr 2025 06:15:37 +0000</pubDate>
      <link>https://forem.com/softmaker/designing-a-smart-commentator-bot-applying-software-patterns-and-principles-4h0</link>
      <guid>https://forem.com/softmaker/designing-a-smart-commentator-bot-applying-software-patterns-and-principles-4h0</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Imagine a typing race game where participants compete in real-time. What if a commentator bot could enhance the experience — announcing racers, tracking progress, and throwing in quirky jokes? This isn't just a fun feature — it's a chance to apply real-world software design patterns and principles to create a maintainable, extensible solution.&lt;/p&gt;

&lt;p&gt;In this article, I’ll walk you through designing a &lt;strong&gt;smart commentator bot&lt;/strong&gt;, highlighting how established software practices can guide even seemingly playful features toward clean and scalable implementations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
The Problem: Commentating a Typing Race
&lt;/li&gt;
&lt;li&gt;
High-Level Architecture
&lt;/li&gt;
&lt;li&gt;
Choosing the Right Design Patterns
&lt;/li&gt;
&lt;li&gt;
Principles in Action: SOLID and DRY
&lt;/li&gt;
&lt;li&gt;
Handling Real-Time Updates via WebSockets
&lt;/li&gt;
&lt;li&gt;
Adding Personality: Jokes, Stories, and Behavior
&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  The Problem: Commentating a Typing Race
&lt;/h2&gt;

&lt;p&gt;Your mission is to create a &lt;strong&gt;commentator bot&lt;/strong&gt; that accompanies a real-time typing race. The bot should behave like a sports announcer — introducing participants, giving regular updates on positions, and reporting final results.&lt;/p&gt;

&lt;p&gt;But it's not just about spitting out messages. The bot needs to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Welcome users and introduce itself&lt;/li&gt;
&lt;li&gt;Announce participants with details like name and vehicle&lt;/li&gt;
&lt;li&gt;Track position and report status every 30 seconds&lt;/li&gt;
&lt;li&gt;Notify when someone is approaching or crossing the finish line&lt;/li&gt;
&lt;li&gt;Share race results at the end&lt;/li&gt;
&lt;li&gt;Tell jokes or stories between announcements&lt;/li&gt;
&lt;li&gt;Avoid offensive language&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And, importantly — it should do this &lt;strong&gt;live&lt;/strong&gt;, using technologies like &lt;strong&gt;HTTP&lt;/strong&gt;, &lt;strong&gt;REST&lt;/strong&gt;, and &lt;strong&gt;WebSockets&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The challenge? Making this fun and dynamic feature &lt;strong&gt;well-structured, reusable, and testable&lt;/strong&gt;. That’s where patterns and principles come in.&lt;/p&gt;




&lt;h2&gt;
  
  
  High-Level Architecture
&lt;/h2&gt;

&lt;p&gt;Before jumping into code, let's define the architecture. Here's a high-level breakdown of how the commentator bot fits into the system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Client UI] &amp;lt;--&amp;gt; [WebSocket Server] &amp;lt;--&amp;gt; [Race Engine]
                               |
                      [Commentator Bot]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Client UI&lt;/strong&gt; – Shows the race and displays commentator messages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebSocket Server&lt;/strong&gt; – Broadcasts race updates to connected clients and to the bot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Race Engine&lt;/strong&gt; – Simulates race logic (progress, positions, timing).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commentator Bot&lt;/strong&gt; – Subscribes to race events and produces dynamic commentary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The commentator bot acts as a &lt;strong&gt;consumer of race events&lt;/strong&gt; and a &lt;strong&gt;producer of commentary messages&lt;/strong&gt;, which are sent back through the same WebSocket pipeline to be rendered in the UI.&lt;/p&gt;

&lt;p&gt;This separation keeps race logic clean, while allowing the commentator logic to evolve independently.&lt;/p&gt;




&lt;h2&gt;
  
  
  Choosing the Right Design Patterns
&lt;/h2&gt;

&lt;p&gt;To keep our bot maintainable and adaptable, we can apply a few classic design patterns:&lt;/p&gt;

&lt;h3&gt;
  
  
  🧠 Strategy Pattern
&lt;/h3&gt;

&lt;p&gt;The bot can use different &lt;strong&gt;strategies&lt;/strong&gt; for formatting commentary — e.g., serious sports-style, humorous, or meme-based. Switching strategies becomes easy with the Strategy pattern.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;CommentaryStrategy&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;generateComment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RaceEvent&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SportsStyleCommentary&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;CommentaryStrategy&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;generateComment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`🚦 Racer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;racerName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is now in position &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;!`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FunnyCommentary&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;CommentaryStrategy&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;generateComment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`😄 &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;racerName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; just zoomed past like it's Monday morning!`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  📢 Observer Pattern
&lt;/h3&gt;

&lt;p&gt;The bot needs to &lt;strong&gt;listen&lt;/strong&gt; to race events. The Observer pattern allows it to subscribe to relevant updates without tightly coupling it to the race engine.&lt;/p&gt;

&lt;h3&gt;
  
  
  🧩 Decorator Pattern
&lt;/h3&gt;

&lt;p&gt;Want to add jokes or fun facts dynamically? The Decorator pattern can help wrap standard comments with personality.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;withRandomJoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseComment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jokes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Did you know? Typing fast burns calories!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;This racer is powered by coffee ☕&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseComment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; — &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;jokes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;jokes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)]}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Principles in Action: SOLID and DRY
&lt;/h2&gt;

&lt;p&gt;Building the bot around key software principles makes it easier to test, scale, and extend.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;S: Single Responsibility&lt;/strong&gt; — Each class (commentator, strategy, announcer) has one job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;O: Open/Closed Principle&lt;/strong&gt; — Add new commentary styles without modifying existing logic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;L: Liskov Substitution&lt;/strong&gt; — Interchange strategies or announcers freely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I: Interface Segregation&lt;/strong&gt; — Keep interfaces small and focused (e.g., &lt;code&gt;RaceListener&lt;/code&gt;, &lt;code&gt;Announcer&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;D: Dependency Inversion&lt;/strong&gt; — Use abstractions so we can swap out services (e.g., for tests or mocks).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And of course:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Don't Repeat Yourself (DRY)&lt;/strong&gt; — Reuse message templates, formatting logic, and event handling wherever possible.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Handling Real-Time Updates via WebSockets
&lt;/h2&gt;

&lt;p&gt;A commentator bot is only as good as its timing. To be effective, it must react in real-time to race events — start, progress updates, finish — and WebSockets are the perfect tool for this.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting Up WebSocket Communication
&lt;/h3&gt;

&lt;p&gt;The WebSocket server acts as a &lt;strong&gt;hub&lt;/strong&gt; that delivers race events both to players (for UI updates) and to the bot (for commentary).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Sample WebSocket event from the server&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RACE_UPDATE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;racerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;racer-1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;74&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;distanceLeft&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;timeRemaining&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;34&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Subscribing to Race Events
&lt;/h3&gt;

&lt;p&gt;The bot can connect as a client and listen to these events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;socket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WebSocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ws://localhost:3000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;comment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;commentator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateComment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;sendToUI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Emitting Comments Back to the UI
&lt;/h3&gt;

&lt;p&gt;Once a comment is generated, it’s pushed back to clients via a &lt;code&gt;COMMENTARY_UPDATE&lt;/code&gt; message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendToUI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;COMMENTARY_UPDATE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;comment&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This design creates a full-duplex loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Players' actions update the race state.&lt;/li&gt;
&lt;li&gt;Race events trigger commentator reactions.&lt;/li&gt;
&lt;li&gt;Comments are sent back to the UI for real-time display.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It’s reactive, engaging, and scalable — even if more bots are added later (e.g., cheering fans, announcers in different languages, etc.).&lt;/p&gt;




&lt;h2&gt;
  
  
  Adding Personality: Jokes, Stories, and Behavior
&lt;/h2&gt;

&lt;p&gt;A good commentator doesn’t just deliver dry stats — they entertain. To make the bot feel alive and fun, we can inject humor, trivia, and a bit of unpredictability.&lt;/p&gt;

&lt;h3&gt;
  
  
  Commentary Templates
&lt;/h3&gt;

&lt;p&gt;Create reusable templates for different race events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;templates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;racer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`🔥 Racer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;racer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is at the starting line!`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;racer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`🏁 &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;racer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;racer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;% through the race.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;finish&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;racer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`🎉 &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;racer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; has finished the race in &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;racer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; seconds!`&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Random Jokes and Facts
&lt;/h3&gt;

&lt;p&gt;Use a small pool of random inserts to spice things up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;interjections&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Did you know? The world record for typing speed is 216 wpm.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;This racer types faster than I can think!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Legends say this keyboard is still smoking...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;insertFlavor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;flavor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;interjections&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;interjections&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)];&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;flavor&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Dynamic Timing and Frequency
&lt;/h3&gt;

&lt;p&gt;To avoid sounding robotic, vary the timing between comments, and don’t repeat the same phrases too often:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lastCommentTimestamps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;shouldComment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;racerId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lastCommentTimestamps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;racerId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;last&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  &lt;span class="c1"&gt;// at least 10 seconds between updates&lt;/span&gt;
    &lt;span class="nx"&gt;lastCommentTimestamps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;racerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By combining &lt;strong&gt;structured logic&lt;/strong&gt; with &lt;strong&gt;randomized content&lt;/strong&gt;, your bot becomes more engaging without losing clarity. Think of it like combining TypeScript types with improv comedy 🤹.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Building a commentator bot for a typing race might seem like a playful experiment — and it is — but it’s also a serious opportunity to practice &lt;strong&gt;clean software architecture&lt;/strong&gt;, apply &lt;strong&gt;classic design patterns&lt;/strong&gt;, and reinforce &lt;strong&gt;core engineering principles&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;From the use of the Strategy and Observer patterns to the application of SOLID and DRY principles, every part of this bot is an example of thoughtful design. Add in WebSocket-based real-time communication and a dash of personality, and you have a fun, extensible, and maintainable project.&lt;/p&gt;

&lt;p&gt;Whether you’re building games, internal tools, or complex SaaS platforms — these patterns and principles will serve you well. And maybe, just maybe, your next system could use a funny commentator too 😉&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Thanks for reading! If you found this helpful or entertaining, share it with your team or follow me for more content on clean code, architecture, and real-time applications.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>designpatterns</category>
      <category>websockets</category>
      <category>javascript</category>
      <category>node</category>
    </item>
    <item>
      <title>How I Set Up CI/CD with GitHub Actions in a Real Node.js Project</title>
      <dc:creator>Viktor Sivulskiy</dc:creator>
      <pubDate>Tue, 01 Apr 2025 02:33:47 +0000</pubDate>
      <link>https://forem.com/softmaker/how-i-set-up-cicd-with-github-actions-in-a-real-nodejs-project-112p</link>
      <guid>https://forem.com/softmaker/how-i-set-up-cicd-with-github-actions-in-a-real-nodejs-project-112p</guid>
      <description>&lt;p&gt;In this post, I’ll show how we used GitHub Actions to automate CI/CD for a real open-source fullstack application — &lt;a href="https://github.com/BinaryStudioAcademy/bsa-2022-streamlet" rel="noopener noreferrer"&gt;BinaryStudioAcademy/bsa-2022-streamlet&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧱 About the Project
&lt;/h2&gt;

&lt;p&gt;The project is a collaborative platform for video content, built as part of an educational initiative. It includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js backend (NestJS)&lt;/li&gt;
&lt;li&gt;PostgreSQL, Prisma&lt;/li&gt;
&lt;li&gt;Frontend on React&lt;/li&gt;
&lt;li&gt;Docker-based development&lt;/li&gt;
&lt;li&gt;Tests and linting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This made it a perfect playground to implement CI workflows.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚙️ Why CI/CD with GitHub Actions
&lt;/h2&gt;

&lt;p&gt;We wanted to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run tests on every pull request&lt;/li&gt;
&lt;li&gt;Validate that code builds&lt;/li&gt;
&lt;li&gt;Lint for consistency&lt;/li&gt;
&lt;li&gt;Eventually — deploy to environments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And we didn’t want to add extra tools or services — just GitHub.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 Creating the GitHub Actions Workflow
&lt;/h2&gt;

&lt;p&gt;All CI/CD workflows live in &lt;code&gt;.github/workflows/&lt;/code&gt;. Here’s a minimal setup for Node.js:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CI&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Use Node.js&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v3&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;18&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm install&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run lint&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;✅ Tip: You can create multiple workflows for frontend/backend separately if needed.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🧪 Test Output in GitHub UI
&lt;/h2&gt;

&lt;p&gt;After pushing a PR, GitHub shows CI status directly in the PR tab. Green means ✅ everything is passing.&lt;/p&gt;

&lt;p&gt;You can also explore logs in the &lt;strong&gt;Actions&lt;/strong&gt; tab. This helps reviewers catch problems early — before merging anything into &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  📦 Expanding to Deployments
&lt;/h2&gt;

&lt;p&gt;In advanced scenarios, we also:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Built Docker images&lt;/li&gt;
&lt;li&gt;Deployed to staging (with secrets)&lt;/li&gt;
&lt;li&gt;Used &lt;code&gt;concurrency&lt;/code&gt; to cancel stale builds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But even with just the basics, we improved code quality and confidence.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚠️ Gotchas &amp;amp; Lessons Learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Always cache &lt;code&gt;node_modules&lt;/code&gt; to speed up runs (with &lt;code&gt;actions/cache&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Keep workflows &lt;strong&gt;fast&lt;/strong&gt; — slow pipelines discourage devs&lt;/li&gt;
&lt;li&gt;If you split into microservices — build and test them in parallel&lt;/li&gt;
&lt;li&gt;Protect &lt;code&gt;main&lt;/code&gt; branch using CI rules in GitHub settings&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  ✅ Conclusion
&lt;/h2&gt;

&lt;p&gt;GitHub Actions is a powerful and free way to bring CI/CD into any project — even student or hobby apps. Our team benefited a lot from having automated checks, and it made collaboration smooth and scalable.&lt;/p&gt;

&lt;p&gt;If you're just starting, don’t overcomplicate. Start with one workflow file. Add jobs as your project grows.&lt;/p&gt;

&lt;p&gt;Got questions or want to share your setup? Drop a comment!&lt;/p&gt;




</description>
      <category>github</category>
      <category>node</category>
      <category>devops</category>
      <category>fullstack</category>
    </item>
  </channel>
</rss>
