<?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: Wecasa</title>
    <description>The latest articles on Forem by Wecasa (@wecasa).</description>
    <link>https://forem.com/wecasa</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%2Forganization%2Fprofile_image%2F8891%2Fe6047421-d791-4872-924c-943dd8e27f5a.png</url>
      <title>Forem: Wecasa</title>
      <link>https://forem.com/wecasa</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/wecasa"/>
    <language>en</language>
    <item>
      <title>Letting the ducks fly: How we’re switching from Redux to React Query</title>
      <dc:creator>Mathieu Tinoco</dc:creator>
      <pubDate>Thu, 06 Nov 2025 18:54:26 +0000</pubDate>
      <link>https://forem.com/wecasa/letting-the-ducks-fly-how-were-switching-from-redux-to-react-query-559c</link>
      <guid>https://forem.com/wecasa/letting-the-ducks-fly-how-were-switching-from-redux-to-react-query-559c</guid>
      <description>&lt;p&gt;&lt;em&gt;An incremental journey from Redux modules to intuitive async data management&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Years ago we adopted Redux to manage the client state, handle the local state, and control data fetching. More recently, we began transitioning to React Query. Redux is primarily designed as a synchronous state manager, while React Query specializes in managing the asynchronous state.&lt;/p&gt;

&lt;p&gt;Both are agnostic to the underlying fetching library, but Redux requires significant boilerplate to handle async operations, whereas React Query offers built-in patterns that make data fetching, caching, and updating feel more natural. We decided these advantages made React Query a better fit for our codebase.&lt;/p&gt;

&lt;p&gt;Depending on the size of the application, migrating to React Query can require a significant amount of time. In our case, rather than setting a strict deadline, we are treating the migration as an ongoing effort over a year or so as we gradually replace the asynchronous Redux logic with React Query in the flow of continuous integration.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. A compelling library
&lt;/h2&gt;

&lt;p&gt;In Redux, managing server data involves manually updating state through actions and reducers, which can be cumbersome and repetitive, especially when dealing with asynchronous API calls. Because Redux is a synchronous state library, handling asynchronous behavior requires middleware like &lt;strong&gt;redux-thunk&lt;/strong&gt;, adding even more boilerplate to the process.&lt;/p&gt;

&lt;p&gt;State synchronization and caching are hard problems in their own right, especially when they involve keeping frontend and backend data in sync. React Query simplifies this process by automatically caching server data and offering built-in tools for refreshing and invalidating queries.&lt;/p&gt;

&lt;p&gt;Instead of handling complex state updates, React Query’s &lt;code&gt;staleTime&lt;/code&gt; and &lt;code&gt;gcTime&lt;/code&gt; settings handle caching and data refreshing automatically; queries are invalidated when data might be outdated, and refreshed only when necessary. This lets React Query act as a specialized cache for server-state, which frees us from having to manually synchronize state with the API and allows us to keep our API as the single source of truth.&lt;/p&gt;

&lt;p&gt;Therefore, we avoid manual data updates and instead use React Query’s default caching setup to stay synchronized with the server. For most queries, we use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;staleTime&lt;/strong&gt;: Set to 0ms (default), so that data is always considered stale and automatically refetched on each rerender, along with other situations. Some are controlled by configuration, others handled by default. (We sometimes make an exception here - see further down)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;gcTime (formerly cacheTime in V4)&lt;/strong&gt;: Set to 5 minutes (default). &lt;code&gt;gcTime&lt;/code&gt; stands for garbage collection time. It is a countdown that starts once the query becomes inactive (e.g., when the component unmounts or the app goes to the background), after which the data is removed from memory. &lt;code&gt;cacheTime&lt;/code&gt; was a misleading name for a countdown, which is why, in React Query v5, it was renamed &lt;code&gt;gcTime&lt;/code&gt; to better reflect its role.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This approach lets us keep our code light. Rather than tracking and updating cache manually as in Redux, we simply invalidate queries when needed, and React Query handles refetching and cache expiration for us.&lt;/p&gt;

&lt;p&gt;For example, after successfully uploading a document with &lt;code&gt;putDocuments&lt;/code&gt;, we invalidate the &lt;code&gt;['documents']&lt;/code&gt; query and React Query refreshes the document list automatically.&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useMutation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useQueryClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tanstack/react-query&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getDocuments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;putDocuments&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useDocuments&lt;/span&gt; &lt;span class="o"&gt;=&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;documents&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;getDocuments&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;useUploadDocument&lt;/span&gt; &lt;span class="o"&gt;=&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;queryClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useQueryClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;useMutation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;mutationFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;putDocuments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;onSuccess&lt;/span&gt;&lt;span class="p"&gt;:&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="nx"&gt;queryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invalidateQueries&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;documents&lt;/span&gt;&lt;span class="dl"&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="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;Documents&lt;/span&gt; &lt;span class="o"&gt;=&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;documents&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useDocuments&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;mutate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;uploadDocument&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useUploadDocument&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ul&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;li&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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="nx"&gt;doc&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/li&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="p"&gt;))}&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/ul&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="nx"&gt;onClick&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;uploadDocument&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;New Document&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nx"&gt;Upload&lt;/span&gt; &lt;span class="nx"&gt;Document&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;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;
  
  
  2. Rethinking loading / error logic
&lt;/h2&gt;

&lt;p&gt;React Query makes working with asynchronous data feel seamless. Because data fetching, caching, and synchronization are built into its core, handling loading or error states becomes part of the natural flow.&lt;/p&gt;

&lt;p&gt;We choose between &lt;strong&gt;3 main solutions&lt;/strong&gt; to deal with a loading state:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Let the component render (with no data) and re-render naturally when data is available&lt;/strong&gt;. If the data being fetched is low priority and has little impact on the main content, we tend to just let the component render. The missing data will appear on the page once the content has been successfully received.
&lt;/li&gt;
&lt;/ul&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;useGetDocumentsQuery&lt;/span&gt; &lt;span class="o"&gt;=&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="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isLaodingDocuments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;isError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isDocumentsError&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useQuery&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Document&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DOCUMENTS_QUERY_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/pro/documents&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="nx"&gt;response&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="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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isLaodingDocuments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isDocumentsError&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;DocumentPage&lt;/span&gt; &lt;span class="o"&gt;=&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;documents&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useGetDocumentsQuery&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Documents&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/title&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;documents&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;You&lt;/span&gt; &lt;span class="nx"&gt;have&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;documents&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="nx"&gt;documents&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;}
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;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;ul&gt;
&lt;li&gt;
&lt;strong&gt;Return null as long as the data isn’t ready&lt;/strong&gt; (your screen will remain blank in the meantime). In most cases, when loading time is less than 300ms, we choose to return null while data is fetching and let the page render once the component has everything it needs.
&lt;/li&gt;
&lt;/ul&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;HomePage&lt;/span&gt; &lt;span class="o"&gt;=&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isLoadingDocuments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isDocumentsError&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useGetDocumentsQuery&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;isLoadingDocuments&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;null&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;isDocumentsError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;Error&lt;/span&gt; &lt;span class="nx"&gt;handling&lt;/span&gt; &lt;span class="nx"&gt;here&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;;
&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;HomePage&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;;
&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Display a loader&lt;/strong&gt; (spinner, skeleton, progress bar, etc.). If the page relies heavily on the data and fetching takes time, we like to let the user know that something is cooking up! A loader is quick to implement and a shimmer effect often makes the experience feel smoother.
&lt;/li&gt;
&lt;/ul&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;HomePage&lt;/span&gt; &lt;span class="o"&gt;=&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;documents&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useGetDocumentsQuery&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;LoaderComponent&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ChildComponent&lt;/span&gt; &lt;span class="nx"&gt;documents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;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;&lt;strong&gt;Side note&lt;/strong&gt;: in the example above, we check &lt;code&gt;!documents&lt;/code&gt; instead of &lt;code&gt;isLoadingDocuments&lt;/code&gt; so that &lt;code&gt;documents&lt;/code&gt; can't be undefined in &lt;code&gt;ChildComponent&lt;/code&gt;. This ensures that by the time &lt;code&gt;ChildComponent&lt;/code&gt; is rendered, &lt;code&gt;documents&lt;/code&gt; is always defined. In TypeScript, this narrows the type so we don’t have to handle &lt;code&gt;documents&lt;/code&gt; being undefined inside &lt;code&gt;ChildComponent&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. How we got started
&lt;/h2&gt;

&lt;p&gt;In our codebase, we usually have one duck per API resource. In Redux, a duck is simply a module that organizes everything related to one feature (its action types, action creators, and reducer) into a single file. Most of our ducks are independent and can be migrated one by one, allowing PRs to be kept small and comprehensible.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;After identifying which duck to migrate, we review the API calls handled by its action functions. For each API resource, we create a dedicated custom hook to handle that request. These custom hooks encapsulate the React Query logic and manage the state related to that request. We consider this a good practice since it avoids duplicating request-related code and provides a clear, reusable interface for consuming components.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Next, we focus on the highest-level component in the stack that consumes the data, since this is usually where most of the work is required. Instead of deleting the existing Redux code right away, we often comment it out. This makes it easier to refer back to during the migration, as parts of the logic can still be reused.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;From there, the process is pretty straightforward: we plan for what should happen while the data is loading, once it's available, and if an error occurs. React Query provides a &lt;a href="https://tanstack.com/query/v4/docs/framework/react/reference/useQuery" rel="noopener noreferrer"&gt;rich set of status flags&lt;/a&gt; that let us adapt our component’s logic.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  4. Challenges &amp;amp; corner cases
&lt;/h2&gt;

&lt;p&gt;We have encountered a few bumps along the road to implementing React Query...&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Testing&lt;/strong&gt;&lt;br&gt;
Testing our components was the first roadblock that we hit. We believe in integration testing and wanted to keep it up with the components that now use React Query.&lt;/p&gt;

&lt;p&gt;We ended up using msw (mock service worker) which is a middleware that intercepts real API calls and returns a mocked response. Our configuration lets us mock all API endpoints at once in a dedicated file, so that we don’t need to mock for each individual test case. For specific test cases, we can still override individual responses directly within the test.&lt;/p&gt;

&lt;p&gt;Compared to Redux, where we mocked the API layer extensively, this approach feels closer to real-world behavior and it gave us greater confidence in how our components interact with the API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;gcTime&lt;/strong&gt;&lt;br&gt;
We also encountered challenges in specific cases with &lt;code&gt;staleTime&lt;/code&gt; and &lt;code&gt;gcTime&lt;/code&gt; due to the default &lt;code&gt;staleTime&lt;/code&gt; being set to 0, which sometimes triggered redundant API calls. This can occur when multiple components using the same query mount nearly simultaneously but with a slight delay of just a few milliseconds. React Query treats the data as stale for each mount, causing multiple requests. Setting a short &lt;code&gt;staleTime&lt;/code&gt; of 1 second was enough to prevent this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Some dispatch functions can overlap between ducks.&lt;/strong&gt;&lt;br&gt;
In a few cases, ducks in our codebase call dispatch functions from another ducks. Normally, ducks handle a single API resource, but we occasionally violated this rule, creating cross-duck side effects. During migration, we move such logic into a temporary regular function so the other duck can keep working; it can be removed once the second duck is migrated.&lt;/p&gt;

&lt;p&gt;In these cases, if the temporary function mutates data, make sure to invalidate the React Query cache for any affected API resources.&lt;/p&gt;




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

&lt;p&gt;Switching from Redux to React Query has greatly simplified the way we manage server state. Redux works well for the synchronous state, but asynchronous workflows require middleware like redux-thunk and extra boilerplate. React Query was built for that purpose, with caching, query invalidation, and automatic refetching, making it easier to keep the API as the single source of truth while improving maintainability.&lt;/p&gt;

&lt;p&gt;I hope this article inspires you to explore React Query or refine your own migration strategies. Let us know your thoughts, challenges, or tips in the comments!&lt;/p&gt;

</description>
      <category>react</category>
      <category>reactnative</category>
      <category>redux</category>
      <category>reactquery</category>
    </item>
    <item>
      <title>How to schedule an update of a React component in the future</title>
      <dc:creator>Antoine Quinquenel</dc:creator>
      <pubDate>Thu, 30 Jan 2025 09:35:50 +0000</pubDate>
      <link>https://forem.com/wecasa/how-to-schedule-an-update-of-a-react-component-in-the-future-222m</link>
      <guid>https://forem.com/wecasa/how-to-schedule-an-update-of-a-react-component-in-the-future-222m</guid>
      <description>&lt;h3&gt;
  
  
  &lt;code&gt;useRerenderTimeout&lt;/code&gt; &amp;amp; &lt;code&gt;useRerenderInterval&lt;/code&gt; : 2 custom hooks to re-render a React component at a specific time, or at a regular interval of time.
&lt;/h3&gt;

&lt;p&gt;⚡️ In a hurry ? Go directly to the TL;DR section.&lt;br&gt;
&lt;br&gt;&lt;/p&gt;




&lt;p&gt;Most of the time, we develop user interfaces (UI) that react to user interactions.&lt;br&gt;
But what if you need the UI to react to the current time ?&lt;/p&gt;

&lt;p&gt;At Wecasa, we recently needed some content of our React Native app to update automatically at a specific time.&lt;br&gt;
To do so, we created two custom hooks that allow a component to re-render itself at a specific time or at a regular interval of time, with a simple and generic implementation for a great developer experience. ✨&lt;br&gt;
&lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Trigger a re-render at a specific time
&lt;/h2&gt;

&lt;p&gt;Let's say we want to create a &lt;code&gt;Meeting&lt;/code&gt; component that renders differently depending on whether the meeting has started or not.&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
&lt;br&gt;
Currently, nothing forces the component to change once the meeting starts. This means, if it is displayed at 13:59 and the meeting starts at 14:00, after one minute it will continue showing the meeting as upcoming even though it has started.

&lt;p&gt;To force a re-render of the component, we can simply trigger a state update with a local timer that executes at the meeting's start time.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The timer is set via a custom &lt;code&gt;useTimeout&lt;/code&gt; hook, inspired by &lt;a href="https://www.joshwcomeau.com/snippets/react-hooks/use-timeout/" rel="noopener noreferrer"&gt;this article from Josh Comeau&lt;/a&gt;. It's also available in popular hooks libraries, such as &lt;a href="https://www.npmjs.com/package/usehooks-ts" rel="noopener noreferrer"&gt;usehooks-ts&lt;/a&gt; or &lt;a href="https://www.npmjs.com/package/@uidotdev/usehooks" rel="noopener noreferrer"&gt;@uidotdev/usehooks&lt;/a&gt;.&lt;br&gt;
We use &lt;code&gt;useReducer&lt;/code&gt; instead of &lt;code&gt;useState&lt;/code&gt; to not bother passing params to the state setter, but naturally &lt;code&gt;useState&lt;/code&gt; works as well.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
&lt;br&gt;
Now, our &lt;code&gt;Meeting&lt;/code&gt; component already refreshes itself at the meeting's start time automatically! 🎉&lt;br&gt;
But there is still room for improvement, especially in terms of performance ⚡️ and readability 🤓&lt;br&gt;
&lt;br&gt;

&lt;h3&gt;
  
  
  Optimizing the execution
&lt;/h3&gt;

&lt;p&gt;Let’s say the meeting starts at 14:00:00, and the &lt;code&gt;Meeting&lt;/code&gt; component first renders one minute earlier, at 13:59:00.&lt;br&gt;
Now, if the &lt;code&gt;Meeting&lt;/code&gt; component re-renders at 13:59:30 for any reason (for instance, if one of its parent components re-renders), &lt;code&gt;delay&lt;/code&gt; will be recalculated, changing from 60 seconds to 30 seconds.&lt;/p&gt;

&lt;p&gt;As a result, &lt;code&gt;useTimeout&lt;/code&gt; will destroy the current timer and create a new one that will still execute at the same time, 14:00:00, since &lt;code&gt;startsAt&lt;/code&gt; remains unchanged.&lt;br&gt;
To prevent this unnecessary "clean-up", we can memoize &lt;code&gt;delay&lt;/code&gt; so that it only changes when &lt;code&gt;startsAt&lt;/code&gt; is updated.&lt;br&gt;
Also, we don’t want to set a timer if the meeting has already started, so in that case, we set &lt;code&gt;delay&lt;/code&gt; to &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
 &lt;br&gt;
Now, our &lt;code&gt;Meeting&lt;/code&gt; component will set a single timer throughout its entire lifecycle&lt;br&gt;
(as long as &lt;code&gt;startsAt&lt;/code&gt; remains unchanged), and only if the meeting has not yet started.&lt;br&gt;
&lt;br&gt;

&lt;h3&gt;
  
  
  Encapsulate into a custom hook
&lt;/h3&gt;

&lt;p&gt;Our &lt;code&gt;Meeting&lt;/code&gt; component is becoming difficult to read, and we'd like to extract this logic into a reusable piece of code: we can create a custom &lt;code&gt;useRerenderTimeout&lt;/code&gt; hook that takes the date at which the component should re-render as an argument.&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
 &lt;br&gt;
Since the &lt;code&gt;date&lt;/code&gt; parameter is a dependency of this &lt;code&gt;useMemo&lt;/code&gt;, we want to ensure its reference remains stable across renders. For this reason, we define it as a &lt;code&gt;string&lt;/code&gt; rather than a &lt;code&gt;Date&lt;/code&gt; object.

&lt;p&gt;Then, the &lt;code&gt;Meeting&lt;/code&gt; component would look like this:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
 &lt;br&gt;
By moving the logic into a custom hook, the &lt;code&gt;Meeting&lt;/code&gt; component only needs a single line of code to re-render itself at the desired time. ✨&lt;br&gt;
This keeps the code light and clear, and offers a generic solution that's easy to use.&lt;br&gt;
&lt;br&gt;

&lt;h2&gt;
  
  
  Trigger multiple re-renders at specific times
&lt;/h2&gt;

&lt;p&gt;Let's say we now want to display a progress bar indicating the remaining time, starting 30 minutes before the meeting begins.&lt;br&gt;
The &lt;code&gt;Meeting&lt;/code&gt; component will now have a third possible status," imminent", in addition to "upcoming" and "started".&lt;br&gt;
This means the component needs to re-render twice: once 30 minutes before the meeting starts, and once when the meeting starts.&lt;br&gt;
To improve code cohesion, we can associate each status with the time at which it ends, which is also the time we want the &lt;code&gt;Meeting&lt;/code&gt; component to re-render.&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
 &lt;br&gt;
When &lt;code&gt;Meeting&lt;/code&gt; mounts, it will set a timeout to trigger a re-render 30 minutes before the meeting starts.&lt;br&gt;
Once this timeout executes, &lt;code&gt;getMeetingStatus&lt;/code&gt; will be called again, returning a new value for &lt;code&gt;status.endsAt&lt;/code&gt;, which will set a new timeout to trigger a re-render at the meeting start time.&lt;br&gt;
This "call loop", based on the component's lifecycle, allows us to handle both re-renders using a single instance of the &lt;code&gt;useRerenderTimeout&lt;/code&gt; hook. 🎉&lt;br&gt;
 &lt;br&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
 

&lt;h2&gt;
  
  
  Trigger a re-render at a regular interval of time
&lt;/h2&gt;

&lt;p&gt;The "imminent" status lasts 30 minutes, but we want to avoid running a 30-minute-long animation for the progress bar.&lt;br&gt;
Instead, we refresh the progress bar at 1% increments, which means updating it every 18 seconds.&lt;/p&gt;

&lt;p&gt;While we could use the same "call loop" approach with &lt;code&gt;useRerenderTimeout&lt;/code&gt;, it's simpler in this case to use an interval instead of a timeout, as the time between re-renders is constant.&lt;/p&gt;

&lt;p&gt;We can create a similar hook to &lt;code&gt;useRerenderTimeout&lt;/code&gt; that runs an interval instead of a timeout.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The interval is set via a custom &lt;code&gt;useInterval&lt;/code&gt; hook, inspired by&lt;br&gt;
&lt;a href="https://overreacted.io/making-setinterval-declarative-with-react-hooks/" rel="noopener noreferrer"&gt;this article from Dan Abramov&lt;/a&gt;.&lt;br&gt;&lt;br&gt;
Just like we did for &lt;code&gt;useRerenderTimeout&lt;/code&gt;, we use &lt;code&gt;useReducer&lt;/code&gt; instead of &lt;code&gt;useState&lt;/code&gt; to not bother passing params to the state setter, but naturally &lt;code&gt;useState&lt;/code&gt; works as well.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
 &lt;br&gt;
The interval continues as long as &lt;code&gt;ms&lt;/code&gt; is a positive value, and it stops when &lt;code&gt;ms&lt;/code&gt; is set to &lt;code&gt;null&lt;/code&gt;.&lt;br&gt;
 &lt;br&gt;
Depending on the faith you have in yourself, you may want to add a safeguard to prevent unexpected excessively frequent intervals (recommended).&lt;br&gt;
For example, we chose an arbitrary minimum interval of 1 second.&lt;br&gt;
 &lt;br&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
 &lt;br&gt;
The &lt;code&gt;ProgressBar&lt;/code&gt; component can then use this hook to re-render itself every 18 seconds, independently of the &lt;code&gt;Meeting&lt;/code&gt; component:&lt;br&gt;
 

&lt;blockquote&gt;
&lt;p&gt;Using this hook directly in the &lt;code&gt;Progress&lt;/code&gt; component instead of its parent ensures that the minimum amount of JSX is updated.&lt;br&gt;
In general, these hooks should be used as low as possible in the component tree.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
 

&lt;p&gt;Every 18 seconds, &lt;code&gt;useRerenderInterval&lt;/code&gt; triggers a re-render,&lt;br&gt;
recalculating &lt;code&gt;remainingPercentage&lt;/code&gt; and automatically updating the relevant UI. ✅&lt;br&gt;
&lt;br&gt;&lt;/p&gt;

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

&lt;p&gt;Managing timers and intervals in React components can be tricky, and can quickly make the code hard to read and to maintain.&lt;br&gt;
Creating these two hooks &lt;code&gt;useRerenderTimeout&lt;/code&gt; and &lt;code&gt;useRerenderInterval&lt;/code&gt; allowed us to easily implement complicated business logics based on timers and intervals in just a few lines of code.&lt;/p&gt;

&lt;p&gt;They take advantage of the component's lifecycle, by triggering re-renders via local state updates, to keep the rest of the component's logic decoupled from its need to refresh at specific times, in a simple and generic way for a great developer experience. &lt;br&gt;
&lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;useRerenderTimeout&lt;/code&gt; hook
&lt;/h3&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
 

&lt;h4&gt;
  
  
  Use case:
&lt;/h4&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
 

&lt;h3&gt;
  
  
  &lt;code&gt;useRerenderInterval&lt;/code&gt; hook
&lt;/h3&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
 

&lt;h4&gt;
  
  
  Use case:
&lt;/h4&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
 

</description>
      <category>react</category>
      <category>reacthooks</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Click, Ship, Done: Our Journey to Effortless Deployments in JS</title>
      <dc:creator>JuP</dc:creator>
      <pubDate>Thu, 26 Sep 2024 11:26:41 +0000</pubDate>
      <link>https://forem.com/wecasa/click-ship-done-our-journey-to-effortless-deployments-in-js-19l8</link>
      <guid>https://forem.com/wecasa/click-ship-done-our-journey-to-effortless-deployments-in-js-19l8</guid>
      <description>&lt;h2&gt;
  
  
  Starting point
&lt;/h2&gt;

&lt;p&gt;At Wecasa, on the frontend side, we have 2 mobile apps that we are maintaining regularly, delivering new versions on both Android and iOS stores. Multiple teams with different scopes are working on these apps, and it's the responsibility of each team to deploy their work.&lt;/p&gt;

&lt;p&gt;When one or more features are ready to be shipped in production, one dev has to prepare the new build. Let's dig into the different steps the developer has to do:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Merge commits that are going to prod on master&lt;/li&gt;
&lt;li&gt; From the monorepo root, &lt;strong&gt;run a bash script&lt;/strong&gt; (one bash script per app)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;!! All our front code is located in one monorepo. Severals projects are living inside, including our 2 mobiles apps. Until now we had two separate bash scripts to deploy either one or the other app&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; The script defines the new app version number&lt;/li&gt;
&lt;li&gt; Then it updates build.gradle and info.plist with the new version number&lt;/li&gt;
&lt;li&gt; It creates a &lt;strong&gt;new tag on GitHub that will trigger a workflow on our CI&lt;/strong&gt;. This workflow will create the actual builds and send them to the stores&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And that's pretty much it.&lt;/p&gt;

&lt;p&gt;Job done ✅&lt;/p&gt;

&lt;h2&gt;
  
  
  Improve the experience
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1esrteir3bt77kkcc7sg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1esrteir3bt77kkcc7sg.png" alt="Meme: "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Recently, we decided to &lt;strong&gt;improve our deployment process&lt;/strong&gt;. But why? The old process was just working fine, right?&lt;/p&gt;

&lt;p&gt;Well, yes, it got the job done, but a few things were missing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Until now, &lt;strong&gt;we didn't have any changelog for any release&lt;/strong&gt;. So sometimes, when you wanted to know which commit had been introduced in which specific version, it was not so easy to find out.&lt;/li&gt;
&lt;li&gt;  The version number generated by the old bash script was a &lt;strong&gt;patch release by default&lt;/strong&gt;. We had to force a version number if we wanted to do a minor or major one. We wanted it to be easier to follow a proper semantic versioning notation.&lt;/li&gt;
&lt;li&gt;  It was a bash script. Bash is great, but &lt;strong&gt;few people know how to code in Bash&lt;/strong&gt;, so few people were comfortable understanding the script and, furthermore, making adjustments to it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A CLI to Rule Them All
&lt;/h2&gt;

&lt;p&gt;To improve the experience of releasing, we decided to make a &lt;strong&gt;CLI&lt;/strong&gt; in JS. The CLI will do all the tasks that the Bash script did and a few more.&lt;/p&gt;

&lt;p&gt;We chose to build a CLI because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  It's &lt;strong&gt;interactive&lt;/strong&gt; and fun to play with as a developer&lt;/li&gt;
&lt;li&gt;  It allows some &lt;strong&gt;flexibility&lt;/strong&gt; and different options through the prompts&lt;/li&gt;
&lt;li&gt;  It's &lt;strong&gt;made in JS&lt;/strong&gt;, so our tech team will be more comfortable adding updates to it&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Inquirer JS
&lt;/h3&gt;

&lt;p&gt;To build this CLI, we used the &lt;a href="https://github.com/SBoudrias/Inquirer.js" rel="noopener noreferrer"&gt;Inquirer&lt;/a&gt; library that allows us to quickly ask questions and store the responses:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Forxtd2rhqiyiu3vvnhz9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Forxtd2rhqiyiu3vvnhz9.png" alt="Terminal showing first line of cli"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;select&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@inquirer/select&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;askRepo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Choose the app you want to deploy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;REPOS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;repo&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;repo&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;
  
  
  Bash to JS
&lt;/h3&gt;

&lt;p&gt;Moving from a bash script to a JS CLI implies that we had to &lt;strong&gt;run bash commands in our JS environment&lt;/strong&gt;. In order to execute bash commands in JS environment, we used the &lt;code&gt;execSync&lt;/code&gt; method from Node a lot.&lt;/p&gt;

&lt;p&gt;With this utility function ⬇️ , it was easy to migrate all bash commands to the JS scripts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;execSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;child_process&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;executeBashCommand&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&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="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;execSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;encoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf-8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;REPO_ROOT&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="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Error executing command:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&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;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw1db71za2dddv05kmf81.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw1db71za2dddv05kmf81.png" alt="Meme: wait it's bash? always has been"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Automatize Github release with changelogs
&lt;/h2&gt;

&lt;p&gt;This refactor was also a good opportunity to create a proper &lt;strong&gt;release in GitHub&lt;/strong&gt; with a clean &lt;strong&gt;changelog&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;To do that, we used the GitHub API to retrieve the information we needed from our repo. More specifically by using &lt;a href="https://octokit.github.io/rest.js/v21/" rel="noopener noreferrer"&gt;Octokit&lt;/a&gt; .&lt;/p&gt;

&lt;p&gt;&lt;u&gt;&lt;strong&gt;Create a tag:&lt;/strong&gt;&lt;/u&gt;&lt;/p&gt;

&lt;p&gt;A release in GitHub is coupled to a &lt;code&gt;tag&lt;/code&gt;. You can't have a release if it is not associated with a tag. So the first step was to create that tag:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; We want the tag to reflect the type of release we want to make, so in our CLI, we now ask &lt;strong&gt;what kind of release&lt;/strong&gt; we want: patch, minor or major.&lt;/li&gt;
&lt;li&gt; Once the type of release known, we generate the &lt;strong&gt;version number&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; Once we have the version number, we build our tag name and create it on GitHub.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmofjm1uzsftws4zd9mif.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmofjm1uzsftws4zd9mif.png" alt="Terminal image"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjne6acabu8kk7iytk5ww.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjne6acabu8kk7iytk5ww.png" alt="Terminal image"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Generate a string based on the app we want to deploy, the next version number and the stores&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;newTag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getNewTag&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stores&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;nextVersion&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// We check if this new generated tag already exists in Github before continuing&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;isTagAlreadyExists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newTag&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`⚠️ This tag already exists on Github, which means release &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-v&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;nextVersion&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; probably already exists too`&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Maybe this tag was published by mistake and no build has been sent to any store. You have two options:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1. You are sure that this release on Github is a mistake so you can delete it with the associated tag&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`2. You can choose another release number, different from &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;nextVersion&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;whatToDo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;askIfCustomVersion&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;whatToDo&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;exit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;👋 Okay bye !&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&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;whatToDo&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;nextVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;nlThenInput&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Type the desired version number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;newTag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getNewTag&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stores&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;nextVersion&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;As you can see, we will also verify if this tag already exists in our repo and do specific actions in this case.&lt;/p&gt;

&lt;p&gt;&lt;u&gt;&lt;strong&gt;Generate release content and publish:&lt;/strong&gt;&lt;/u&gt;&lt;/p&gt;

&lt;p&gt;Now that we have a tag name, all we need to do is create a release with its appropriate changelog.&lt;/p&gt;

&lt;p&gt;To build this changelog, we:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Get the latest release date&lt;/li&gt;
&lt;li&gt; Fetch all the commits merged to master since this date&lt;/li&gt;
&lt;li&gt; Create the body of the changelog, grabbing each commit message and its author&lt;/li&gt;
&lt;li&gt; Once we have the content, the final step is to push the release to GitHub:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// #1 Get the latest release date&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getLatestReleases&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Release&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;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;releases&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;octokit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listReleases&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;REPO_OWNER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;REPO&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;per_page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&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="nx"&gt;releases&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="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;published_at&lt;/span&gt;&lt;span class="p"&gt;)&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;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;published_at&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// #2 Fetch all the commits merged to master since this date&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getMergedCommitsFromDate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Commit&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&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;commits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;octokit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listCommits&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;REPO_OWNER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;REPO&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sha&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;master&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;since&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;per_page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&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="nx"&gt;commits&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="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// #3 Create the body of the changelog, grabbing each commit message and its author&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getReleaseContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;commits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Commit&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;stores&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AppStore&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;mdFormattedCommits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;commits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;commitMarkdowned&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;`## What is included?\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;mdFormattedCommits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;\n\n## Stores:\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;stores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; and &lt;/span&gt;&lt;span class="dl"&gt;"&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// #4 Push the release to Github&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createRelease&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;tagName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;releaseName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;CreateReleaseProps&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;release&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;octokit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createRelease&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;REPO_OWNER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;REPO&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;tag_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tagName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;releaseName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;make_latest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;"&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="nx"&gt;release&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;And it’s done 🎉 Below, you can have a quick preview of what the experience looks like in terminal for a dev who wants to release:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx5v6gbhpi1mwj4l5i8v4.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx5v6gbhpi1mwj4l5i8v4.gif" alt="Giff showing the CLI in action"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;To summarize, we changed from an entire bash script that was quite intimidating for devs with no experience in bash language, to a CLI made in JavaScript, so very close to our stack. And as a result, easier to read, understand, and maintain.&lt;/p&gt;

&lt;p&gt;We grabbed this opportunity to also create proper releases in our GitHub with changelogs, which will bring tech and product teams more transparency through a clean feature history.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;New&lt;/th&gt;
&lt;th&gt;Old&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Code&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;td&gt;Bash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;One script for both app&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Changelogs in Git&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Choice of version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CLI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maintainability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Easier in JS&lt;/td&gt;
&lt;td&gt;Harder because it’s bash&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>cli</category>
      <category>typescript</category>
      <category>git</category>
      <category>wecasa</category>
    </item>
    <item>
      <title>Secure your Stripe Webhooks and protect yourself from Captain Hook</title>
      <dc:creator>Antoine Braconnier</dc:creator>
      <pubDate>Mon, 29 Jul 2024 10:14:01 +0000</pubDate>
      <link>https://forem.com/wecasa/secure-your-stripe-webhooks-and-protect-yourself-from-captain-hook-4mmo</link>
      <guid>https://forem.com/wecasa/secure-your-stripe-webhooks-and-protect-yourself-from-captain-hook-4mmo</guid>
      <description>&lt;p&gt;If you handle your app payments with Stripe, there's a strong chance that at some point you end up needing using their &lt;strong&gt;webhooks&lt;/strong&gt;. But payments are such a sensitive matter and you certainly don't want to expose your endpoints to every pirate sailing around, now, would you?&lt;/p&gt;

&lt;p&gt;That is why it's really important to setup secured routes without being plundered and loot by some evil corsairs! So grab a grog, and let's find out how to write &lt;strong&gt;a basic Stripe webhook endpoint&lt;/strong&gt;. Following some simple recommendations, it will &lt;strong&gt;safely receive and process events&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Webhook security
&lt;/h2&gt;

&lt;p&gt;But what is the actual risk of having a webhook endpoint exposed, cast adrift? There's two main issues :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Forged requests&lt;/strong&gt; : someone can mimic the same data structure and send forged requests with fake data to your endpoint.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu2n5dvqd3gg0nfs4ec0n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu2n5dvqd3gg0nfs4ec0n.png" alt="An image explaining how forged requests work" width="800" height="207"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Replay attack&lt;/strong&gt; : provided that your endpoint is HTTPS and not HTTP, no one but you can read the events sent by Stripe to you. They can, however, capture the encrypted data sent and re-send them again and again, which definitely could cause some harm.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe79rrtqodi3ejbr053ck.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe79rrtqodi3ejbr053ck.png" alt="an image explaining metaphorically how replay attacks work" width="800" height="337"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Luckily enough, Stripe sends us something really useful in the Headers called &lt;code&gt;STRIPE-SIGNATURE&lt;/code&gt; that looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"t=1721661059,v1=ef9344dc37fe005b41d7b9d29871cfe1287b2deefbb8d3lk10b9a74b859345734"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this header, we have two values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;t&lt;/code&gt; , a &lt;strong&gt;timestamp&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;v1&lt;/code&gt; , an &lt;strong&gt;encrypted value&lt;/strong&gt; generated each call that will be used in our app to verify the authenticity of the event sent. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With those two values, and the help of the Stripe SDK, we can build a &lt;code&gt;Stripe::Event&lt;/code&gt; object in our app with the payload. This will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Assert the data structure&lt;/strong&gt; sent by Stripe;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decrypt the encrypted value&lt;/strong&gt; (&lt;code&gt;v1&lt;/code&gt;), so we know the event is legit;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assert that the timestamp is from less than 5 minutes ago&lt;/strong&gt;, to mitigate the replay attack risk. We'll see that this 5 minutes value can be modified.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, let's implement a Stripe webhook!&lt;/p&gt;




&lt;h2&gt;
  
  
  Setting up a Stripe webhook endpoint
&lt;/h2&gt;

&lt;p&gt;First, add an endpoint on your Stripe dashboard ( let's call it &lt;code&gt;https://www.myapp.com/payments/events&lt;/code&gt; ) and add an event to listen to. Let's say we want to start shipping goods to the user once we have a successful payment intent. The event we want to listen to is thus &lt;code&gt;payment_intent.succeeded&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0z7c5hmrzh0gonuy4qij.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0z7c5hmrzh0gonuy4qij.png" alt="Stripe dashboard " width="800" height="836"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now let's go to our app and let's code a route with a matching controller. The stripe webhook endpoint is supposed to be a POST.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/routes.rb&lt;/span&gt;
&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;draw&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="c1"&gt;#...&lt;/span&gt;
  &lt;span class="n"&gt;namespace&lt;/span&gt; &lt;span class="ss"&gt;:payments&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="ss"&gt;:events&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/controllers/payments/events_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Payments&lt;/span&gt;
  &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;EventsController&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ok, great, that's a good start. We have now a controller that will listen to all of your webhooks.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building a Stripe::Event object
&lt;/h2&gt;

&lt;p&gt;Now, we actually need to transform the payload into a &lt;code&gt;Stripe::Event&lt;/code&gt; object to make sure that it is safe.&lt;/p&gt;

&lt;p&gt;For the signature decryption, we'll need one last thing : the signing secret of the endpoint.  You can find in the stripe dashboard, in the new webhook endpoint page that you just created. You can add it in your &lt;code&gt;.env&lt;/code&gt; files, under the name &lt;code&gt;PAYMENT_WEBHOOK_SECRET&lt;/code&gt; for example.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwvbqa1r1uuxpqn9uxvtc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwvbqa1r1uuxpqn9uxvtc.png" alt="Showing where the secret key is in the endpoint page" width="610" height="104"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then we have everything necessary to create a service to build this event. This service will accept as a parameter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the &lt;strong&gt;payload&lt;/strong&gt;, which will be a string;&lt;/li&gt;
&lt;li&gt;and the &lt;strong&gt;signature header&lt;/strong&gt; (with the &lt;code&gt;v1&lt;/code&gt; and &lt;code&gt;t&lt;/code&gt; values, remember?) for the security check.
We'll also need to use the &lt;strong&gt;stripe webhook secret&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The building of the event can raise two kind of errors in case of bad data, and it's important we handle these errors properly. So let's add a basic error handler with this in mind.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/services/payments/build_event_service.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Payments&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BuildEventService&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationService&lt;/span&gt;
    &lt;span class="no"&gt;WEBHOOK_SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'PAYMENT_WEBHOOK_SECRET'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;signature_header&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
      &lt;span class="vi"&gt;@payment_provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;construct_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;signature_header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;secret_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;# :tolerance argument will change the 5 minutes limit to one minute&lt;/span&gt;
        &lt;span class="ss"&gt;tolerance: &lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ParserError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Stripe&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;SignatureVerificationError&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
      &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;WebhookSecurityCheckError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, we have a service that will effectively build and return a &lt;code&gt;Stripe::Event&lt;/code&gt; object.&lt;/p&gt;

&lt;p&gt;Let's add it in our controller! And let's take the opportunity to think about what the controller needs to respond here. Stripe will expect a &lt;code&gt;200&lt;/code&gt; response from its webhook call, so let's return that, and let's maybe return a &lt;code&gt;401&lt;/code&gt; if our security check didn't pass.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/controllers/payments/events_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Payments&lt;/span&gt;
  &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;EventsController&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
      &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;BuildEventService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;payload: &lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;signature_header: &lt;/span&gt;&lt;span class="n"&gt;signature_header&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="c1"&gt;# do stuff with the event created&lt;/span&gt;
      &lt;span class="c1"&gt;# ...&lt;/span&gt;
      &lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="ss"&gt;:ok&lt;/span&gt;
    &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;WebhookSecurityCheckError&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
       &lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="kp"&gt;private&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;payload&lt;/span&gt;
      &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;signature_header&lt;/span&gt;
      &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'HTTP_STRIPE_SIGNATURE'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ok! We have now an endpoint that is all setup for a basic security check.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dispatching Stripe events
&lt;/h2&gt;

&lt;p&gt;However, this is still not entirely ideal. Stripe expects to receive an answer quickly. In fact, this controller should do the most minimal thing possible :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;check&lt;/strong&gt; that the event is indeed sent by Stripe,&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;delegate ASAP the responsability&lt;/strong&gt; and the payload to a corresponding job.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So let's quickly add a &lt;strong&gt;dispatcher&lt;/strong&gt; that will do just that. For this, we'll need to have a hash with the corresponding event types mapping to jobs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/services/payments/dispatch_event_service.rb&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Payments&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DispatchEventService&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationService&lt;/span&gt;
    &lt;span class="no"&gt;EVENT_TYPE_TO_JOB&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="s1"&gt;'payment_intent.succeeded'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;SendProductToClientJob&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="c1"&gt;#...&lt;/span&gt;
    &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
      &lt;span class="no"&gt;EVENT_TYPE_TO_JOB&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;perform_later&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;event: &lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, we can use this dispatcher in our controller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/controllers/payments/events_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Payments&lt;/span&gt;
  &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;EventsController&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
      &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;BuildEventService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;payload: &lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;signature_header: &lt;/span&gt;&lt;span class="n"&gt;signature_header&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="no"&gt;DispatchEventService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;event: &lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="ss"&gt;:ok&lt;/span&gt;
    &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;WebhookSecurityCheckError&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
       &lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# [...]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Testing
&lt;/h2&gt;

&lt;p&gt;For testing, you'll probably need to &lt;strong&gt;stub&lt;/strong&gt; your method call &lt;code&gt;construct_event&lt;/code&gt; to make it return what you need. And to test manually and locally the webhook, a good and easy way to do it without having to use a tool like ngrok is to use the &lt;strong&gt;Stripe CLI&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is the command to install Stripe CLI via Homebrew :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;stripe/stripe-cli/stripe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then login with your credentials :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;stripe login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And now, you can easily forward any events necessary to your local server :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;stripe listen &lt;span class="nt"&gt;--events&lt;/span&gt; payment_intent.succeeded &lt;span class="se"&gt;\&lt;/span&gt;
 &lt;span class="nt"&gt;--forward-to&lt;/span&gt; localhost:3000/payments/events
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And to trigger the webhook when necessary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;stripe trigger payment_intent.succeeded
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;Of course, you can go further in securing your webhook. For example, you can use the &lt;strong&gt;idempotency key&lt;/strong&gt; that each event provide to make sure that the event is unique.&lt;/p&gt;

&lt;p&gt;With this basic setup, you can already receive pretty safely Stripe webhooks, and you can add events in the &lt;code&gt;EVENT_TYPE_TO_JOB&lt;/code&gt; whenever you need to. Your code will safely build a &lt;code&gt;Stripe::Event&lt;/code&gt; object and  check the correct event type to delegate the attributes of the payload to a matching job.&lt;/p&gt;

&lt;p&gt;Thank you for reading. I invite you to subscribe so you don't miss the next articles that will be released.&lt;/p&gt;

&lt;p&gt;Happy sailing (and coding)! 🦜 🏴‍☠️&lt;/p&gt;

</description>
      <category>rails</category>
      <category>stripe</category>
      <category>ruby</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Binary Decision Tree in Ruby ? Say hello to Composite Pattern 🌳</title>
      <dc:creator>Just The V</dc:creator>
      <pubDate>Mon, 01 Jul 2024 07:23:16 +0000</pubDate>
      <link>https://forem.com/wecasa/binary-decision-tree-in-ruby-say-hello-to-composite-pattern-h8n</link>
      <guid>https://forem.com/wecasa/binary-decision-tree-in-ruby-say-hello-to-composite-pattern-h8n</guid>
      <description>&lt;p&gt;Decision trees are very common algorithms in the world of development. On paper, they are quite simple. You chain if-else statements nested within each other. The problem arises when the tree starts to grow. If you're not careful, it becomes complex to read and, therefore, difficult to maintain.&lt;/p&gt;

&lt;p&gt;In this article, we'll see how we implemented decision trees at Wecasa to make them as readable and maintainable as possible.&lt;/p&gt;

&lt;p&gt;Before we go any further, let's take some time to define what a Composite is.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Links Composite and Binary Decision Tree?
&lt;/h2&gt;

&lt;p&gt;Composite is a structural design pattern. It is a way of arranging objects in a tree structure, which allows us to have a logical hierarchy.&lt;/p&gt;

&lt;p&gt;When we talk about a decision tree, we can represent it as boxes that can contain boxes, which can contain boxes, which can contain… a final result.&lt;/p&gt;

&lt;p&gt;It's precisely this recursive aspect that makes the decision tree so powerful.&lt;br&gt;
Let's take an example. We want to create a decision tree to calculate the amount of the promotion a customer is eligible for on our e-commerce site.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In input, our tree receives a User object with all its attributes.&lt;/li&gt;
&lt;li&gt;In output, our tree returns a number.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F13j40ncub4tuw1wfldb7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F13j40ncub4tuw1wfldb7.png" alt=" " width="800" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each node in our tree has a condition. You can notice by the color code that we have three types of Composites here: Creation Date, Zip Code, and whether the user has already made a purchase. Now that our example is set, let's see how this translates into code!&lt;/p&gt;


&lt;h2&gt;
  
  
  Firstly, we need tools !
&lt;/h2&gt;

&lt;p&gt;In our implementation of the decision tree, we will need three classes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Node Class&lt;/strong&gt;: The base class for all other classes. Its purpose is to encapsulate the common logic for all different Nodes in my tree.
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Node&lt;/span&gt;
  &lt;span class="nb"&gt;attr_accessor&lt;/span&gt; &lt;span class="ss"&gt;:left_node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:right_node&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;NotImplementedError&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt;

  &lt;span class="c1"&gt;# this method is used to ease composites definition&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call_with_condition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt;
      &lt;span class="n"&gt;left_node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="n"&gt;right_node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Leaf Class&lt;/strong&gt;: Its purpose is to end the tree. You can find them as bottom nodes without any child nodes. The only job they have to accomplish is to return the value they encapsulate.
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Leaf&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Node&lt;/span&gt;
  &lt;span class="nb"&gt;attr_accessor&lt;/span&gt; &lt;span class="ss"&gt;:value&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Composite Classes&lt;/strong&gt;: Their role is to contain the condition to guide between the success branch and the failure branch, to proceed to the next step in the algorithm. They also contain a method to set arguments. We will see later why this method is useful.
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreationDateComposite&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Node&lt;/span&gt;
  &lt;span class="nb"&gt;attr_accessor&lt;/span&gt; &lt;span class="ss"&gt;:days&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;left_node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;right_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;left_node&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;left_node&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;right_node&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;right_node&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;call_with_condition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;days&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ago&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;created_at&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;set_days&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;days&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;days&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ZipCodeComposite&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Node&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HaveBoughtComposite&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Node&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;I only show &lt;code&gt;CreationDateComposite&lt;/code&gt; so you understand the logic, no need to show the rest.&lt;/p&gt;


&lt;h2&gt;
  
  
  How We Finally Manage to Create a Binary Decision Tree
&lt;/h2&gt;

&lt;p&gt;Thanks to all the classes we have built, we will now create our first complete tree. Here is the proposed implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PromotionTree&lt;/span&gt;
  &lt;span class="c1"&gt;# We use Singleton because we don't need to rebuild the tree&lt;/span&gt;
  &lt;span class="c1"&gt;# everytime we call the class&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Singleton&lt;/span&gt;

  &lt;span class="no"&gt;TREE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;CreationDateComposite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="no"&gt;ZipCodeComposite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="no"&gt;BoughtComposite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="no"&gt;Leaf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="no"&gt;Leaf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="no"&gt;BoughtComposite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="no"&gt;Leaf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="no"&gt;Leaf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&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="nf"&gt;set_zip_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'75'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="no"&gt;BoughtComposite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="no"&gt;Leaf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="no"&gt;ZipCodeComposite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="no"&gt;Leaf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="no"&gt;ZipCodeComposite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="no"&gt;Leaf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;78&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="no"&gt;Leaf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;set_zip_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'78'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;set_zip_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'75'&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="nf"&gt;set_days&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;TREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And now we can simply use the tree :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;last&lt;/span&gt;
&lt;span class="no"&gt;PromotionTree&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# =&amp;gt; 60 🎉&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That being explained, this implementation offers several benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No if-else mixed in my business logic&lt;/strong&gt;: By encapsulating conditions in separate objects, we have a clear separation of responsibilities. Each condition is handled by a specific composite, making the code more readable, elegant and maintainable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Centralization of conditions in separate objects&lt;/strong&gt;: This allows easy reuse and modification of conditions without touching the main application logic. Conditions can be changed or extended by creating new composites or modifying existing ones thanks to OOP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ease of testing&lt;/strong&gt;: Each component of the decision tree can be tested in isolation, simplifying the unit testing process. Tests can focus on specific conditions without having to simulate the entire decision tree.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extensibility&lt;/strong&gt;: Adding new conditions or modifying existing conditions is simple and straightforward. Just create new composite nodes and insert them into the existing decision tree.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's easy to build a new tree&lt;/strong&gt;: Let's say tomorrow I want to build a new kind of tree. I always have all my Nodes ready to be used.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But apart from this, I can see some downsides :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The setup cost is High&lt;/strong&gt;: I think it's not that pertinent to use this if you setup this kind of architecture if you don't plan to create other trees.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It uses recursion&lt;/strong&gt;: The limit of your tree will be the limit of your Stack Size. So if your tree is bigger than your allowed stack size, you won't be able to use it. Fortunately, this value is quite big : 10867 for my setup. But it can depend on your setup !&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need to get into the habit to read it quickly&lt;/strong&gt;: At first it's kinda disturbing. The fact that we need to read on the top the Composite, and the bottom the value it used is quite weird. But don't be afraid of this syntax, I will publish another article to show you how I implemented a DSL for Tree building based on this architecture.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Using Composite to implement decision trees allows us to structure our conditions in a clear and maintainable way. Rather than getting lost in a maze of nested if-else statements, we have a logical hierarchy of decisions, each node playing a well-defined role.&lt;br&gt;
Thank you for reading. I invite you to subscribe so you don't miss the next articles that will be released.&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>tutorial</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
