<?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>DEV Community: Sanatel Consulting</title>
    <description>The latest articles on DEV Community by Sanatel Consulting (@sanatel_net).</description>
    <link>https://dev.to/sanatel_net</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3427738%2F6ac0fff8-5e9f-459d-871d-7b0ebadd6f6e.jpg</url>
      <title>DEV Community: Sanatel Consulting</title>
      <link>https://dev.to/sanatel_net</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sanatel_net"/>
    <language>en</language>
    <item>
      <title>How I Built a High-Performance Backend for a Viral Content App with Fastify</title>
      <dc:creator>Sanatel Consulting</dc:creator>
      <pubDate>Fri, 27 Mar 2026 21:40:43 +0000</pubDate>
      <link>https://dev.to/sanatel_net/how-i-built-a-high-performance-backend-for-a-viral-content-app-with-fastify-52li</link>
      <guid>https://dev.to/sanatel_net/how-i-built-a-high-performance-backend-for-a-viral-content-app-with-fastify-52li</guid>
      <description>&lt;p&gt;We developed an app where users can swipe through images, and based on their likes, we generate new ones. Below is a story about the interesting challenges we faced during the development of this application.&lt;/p&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%2Fcsgl9v4uiixm7399wp9d.jpg" 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%2Fcsgl9v4uiixm7399wp9d.jpg" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choosing the ID&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The hardest part of development is naming variables and functions. Even harder is coming up with an ID format for entities.&lt;/p&gt;

&lt;p&gt;In this project, there will be many images and many users, so IDs in GUID or UUID format are too taxing for table indexing and expensive in terms of backend load. We needed an ID that is "incremental" — meaning a new ID is added to the end of the table index — while remaining unique at the same time.&lt;/p&gt;

&lt;p&gt;We settled on an ID like 19bc19e00ab-eh5kkeh99q7, where the first half is the time converted to HEX, and the second half is a random set of characters. The hyphen between these two halves doesn't serve a functional purpose, but it makes the ID more visual and easier to read.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateID&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&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;randomPart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generateRandomSequence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="mi"&gt;11&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;timestamp&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;randomPart&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ID is conditionally incremental, as it changes every millisecond, and it's sufficiently unique.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should we use a "created" field in tables?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The chosen ID format has a small perk: you don't need a "Creation Date" field in the table because the date can be decoded directly from the ID. Yes, it’s slightly inconvenient to build filters for selecting by creation date, but the absence of an extra field outweighs the selection inconveniences.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bloom Filters&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The app is expected to work under high load: thousands of simultaneous sessions and tens of thousands of images to build user feeds. At the same time, the backend must be very "cheap" in terms of CPU and memory consumption. The most resource-intensive task is keeping track of which images a user has already seen and selecting the next ones for them. Obviously, a "traditional" solution via SQL queries to the database is too heavy, slow, and inapplicable here. So, we use a Bloom filter.&lt;/p&gt;

&lt;p&gt;If you're not in the loop, a Bloom filter is a very fast filter that says with 99.9999% probability that a specific ID is NOT in an array of tens of thousands of IDs. In terms of CPU and memory load, the savings are colossal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;App Authorization&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We don’t want to force users to authorize because users hate authorization. Besides, apps without authorization pass store moderation more easily.&lt;/p&gt;

&lt;p&gt;But we still need to identify users. So, upon installation, we generate an ID and save it in the app.&lt;/p&gt;

&lt;p&gt;What if some enthusiasts start generating these IDs and sending them to the backend? We add a limitRate and validate new IDs for correctness. I’ll mention limitRate below, but here is the logic for checking new users:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;At backend startup, we load all existing user IDs into a Bloom filter.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;For a user addition request, we first check the new ID's correctness at the Fastify level by declaring a JSON schema for the request:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;fastify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/backend&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="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;required&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;device_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;minLength&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;maxLength&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;^[a-z0-9]{11}-[a-z0-9]{11}$&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;If the new ID passes the Fastify check, we check it against the user Bloom filter. If the ID is not in the array, it’s a new user.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We verify the new user's ID for correctness.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first half of the ID is the encoded date; this date must fall within the period between our project's launch date and today.&lt;/p&gt;

&lt;p&gt;How to check the second half, which is a random set of characters? What if enthusiasts generate IDs like 19bc19e00ab-aaaaaaaaaaaa? We check the second half for entropy; it must be greater than 2.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getEntropyString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&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;frequencies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
   &lt;span class="k"&gt;for &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;char&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="nx"&gt;frequencies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;char&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frequencies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;char&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&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;frequencies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;()].&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;count&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;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
     &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;acc&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;);&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;If the new ID is valid, we add it to the user Bloom filter and send an INSERT to the user table without waiting for the INSERT to finish. Yes, there's a problem: something could go wrong with the insert, and ideally, we should complete the insert and handle errors first. But that's taxing for the backend. Мы accept the risk of losing a user after a restart. By "losing a user," we mean that after a restart, this user will be treated as new, and their swipe history won't be saved.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Static Content via Nginx&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The backend collects like stats, generates new images, and puts them in folders with a Year — Month — Day structure. In the app, images are loaded via URL. But how to serve static content with minimal server load? Fastify can do it well, but it’s even better to run Nginx on the server under a separate domain to serve content directly from the folder where the backend app stores the images.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Image Series for the User&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We generate a new series of image URLs for the user in batches of 10. We add these IDs to a "Seen Images" Bloom filter for each user. When generating a new series, we check if the image was in previous batches using that filter.&lt;/p&gt;

&lt;p&gt;Thus, the backend and mobile app communicate in series of 10 images.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;At app startup, two series of 10 are loaded.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Every 10 likes, the app sends reactions to the backend and receives a new series of 10 images.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;While the user is liking a series of 10 images, the app sends reactions for the previous 10 and fetches the next 10 images.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rate Limiting&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Fastify has a rateLimit plugin. It can be configured to use Redis, but if you have only one backend server for now, you can do without Redis and store everything in Node.js memory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Validating Image Likes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The mobile app collects reactions and sends them to the backend in batches of 10. One reaction is an Image ID and a like/dislike. Again, how to solve the problem with enthusiasts who might flood the backend with fake reactions? We add a limitRate and validate reactions with the following logic:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;User ID and Image ID must pass Fastify schema validation. Reactions (like/dislike) can also be validated as an enum in the schema.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;User ID must pass the "Existing Users" Bloom filter check.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Image ID must pass the "Seen Images" Bloom filter check for that user.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Just in case, we check likes for "alternation." If a user just swipes everything in one direction or alternates left-right-left-right, the batch is untrustworthy and ignored:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;trustLikeInBatch10&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&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;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nx"&gt;feedLimit&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;likes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;like&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nx"&gt;likes&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;likes&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;feedLimit&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;switches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&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;acc&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;1&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="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
   &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nx"&gt;switches&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;If all checks pass, we send an INSERT to the reactions table without awaiting completion. Furthermore, we don't use CONSTRAINT FOREIGN KEY for User ID or Image ID in the reactions table to reduce DB load during inserts. Although the lack of FOREIGN KEYs feels very unusual.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Masking Fastify Validation Errors&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To make life a bit harder for "curious" enthusiasts, we mask Fastify validation errors. By default, Fastify tells you exactly what went wrong (length, pattern, etc.). To avoid giving hints, we override the error handler:&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="nx"&gt;fastify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setErrorHandler&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="nx"&gt;FastifyError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FastifyRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FastifyReply&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;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="s1"&gt;setErrorHandler&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="nf"&gt;countErrorStatistics&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello World!&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mobile app must be ready to receive this masked error instead of a standard one from the backend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Creating a Type from a String Array&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A small problem with an elegant solution. Let's say you have an array of strings (e.g., image states: new, broken, ready, etc.). You also need a TypeScript type that only allows these values. You can do both at once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Usually it's like this:&lt;/span&gt;
 &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;imageStateArray1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;new&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;broken&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gpt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ready&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;download&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;done&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bad&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
 &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;tpImageState1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;new&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;broken&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gpt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ready&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;download&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;done&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bad&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// or better yet:&lt;/span&gt;
 &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;imageStateArray2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;new&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;broken&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gpt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ready&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;download&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;done&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bad&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;tpImageState2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;imageStateArray2&lt;/span&gt; &lt;span class="p"&gt;)[&lt;/span&gt; &lt;span class="kr"&gt;number&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;Project Summary&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Essentially, the app can live almost without a database. After startup and loading IDs into memory, the only DB tasks are INSERTs without await. Inside the controller, it’s just math and Bloom filters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Current Status and Roadmap&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Phase 1: The Foundation (Current)&lt;br&gt;
The current build focuses on infrastructure stability. We are testing the Bloom filters’ efficiency and the non-blocking insert logic under real-world traffic. Note: AI-feed adaptation based on likes is currently disabled.&lt;/p&gt;

&lt;p&gt;Phase 2: Intelligence&lt;br&gt;
The next stage involves enabling the real-time feedback loop. We will be connecting our AI generation models to the live data stream, allowing the feed to “evolve” based on global and individual user preferences.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://youtube.com/shorts/zW__OFDpHOM?feature=share" rel="noopener noreferrer"&gt;https://youtube.com/shorts/zW__OFDpHOM?feature=share&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Check out aura8 here: &lt;a href="https://play.google.com/store/apps/details?id=kz.crm.aura8" rel="noopener noreferrer"&gt;https://play.google.com/store/apps/details?id=kz.crm.aura8&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What’s next?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We plan to add other categories of images besides cute kittens and puppies, such as Nature, Fantasy, and so on. Please let me know in the comments which category of AI images would make sense to add to the app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Nature.&lt;/li&gt;
&lt;li&gt;Fantasy.&lt;/li&gt;
&lt;li&gt;Stylized Portraits (Girls/Characters).&lt;/li&gt;
&lt;li&gt;Other (what?)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By “Stylized Portraits (Girls/Characters)” I mean something like:&lt;/p&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%2Fgr68wv6c0ullspeavroe.jpg" 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%2Fgr68wv6c0ullspeavroe.jpg" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>fastify</category>
      <category>nanobanana</category>
      <category>node</category>
      <category>flatterflow</category>
    </item>
    <item>
      <title>PrimeNG + NestJS + STT API = Speech Analytics</title>
      <dc:creator>Sanatel Consulting</dc:creator>
      <pubDate>Wed, 03 Sep 2025 17:26:11 +0000</pubDate>
      <link>https://dev.to/sanatel_net/primeng-nestjs-stt-api-speech-analytics-4d6k</link>
      <guid>https://dev.to/sanatel_net/primeng-nestjs-stt-api-speech-analytics-4d6k</guid>
      <description>&lt;p&gt;We developed speech analytics for a contact center. Speech recognition is handled via the Yandex Speechkit service, while the analysis of the resulting text is performed within our own solution. During development, we encountered some interesting aspects that I will try to describe.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Audio player in the transcript card.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://speechkit-demo.sanatel.net/#/speech-transcript-form/SP0003" rel="noopener noreferrer"&gt;transcript card&lt;/a&gt; looks like all other transcript cards in analytics: information about the audio recording and a player slider at the top, and the dialogue replicas at the bottom.&lt;/p&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%2Fmgnphwcb49kgi6n7lnzq.webp" 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%2Fmgnphwcb49kgi6n7lnzq.webp" alt=" " width="800" height="463"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When clicking on a replica, the player should jump to the corresponding fragment of the audio. This turned out to be simple: we read the audio element from the document, and this element already has the necessary methods. We added a &lt;code&gt;start_time&lt;/code&gt; field to replicas, measured in seconds from the beginning of the audio recording. When clicking on a replica, we call the method of the audio element and pass the required seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;this.audio = document.querySelector('audio');

playAudioOnClickOnItem(time: number) {
this.audio.currentTime = time; // В секундах
this.audio.play();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Highlighting words in replicas and tags.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In replicas, we need to underline or highlight with color the words found in dictionaries. Also, in replicas, there is a column where dictionary tags are placed. The question arose: where should we prepare this HTML markup of replicas? On the one hand, it seems like a frontend task. On the other hand, since the backend analytics already parses the replicas and counts the words, why not also prepare the tags there? Moreover, if we generate the HTML markup on the frontend, the backend would still need to send data about where and how many words were found for each replica. In the end, we settled on preparing it on the backend. For each replica, the backend sends a ready-made HTML string with the necessary styles, and the frontend simply displays them.&lt;/p&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%2Fzz6uary6uo609dbfasih.webp" 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%2Fzz6uary6uo609dbfasih.webp" alt=" " width="800" height="219"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Editing words in a fragment.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://speechkit-demo.sanatel.net/#/fragment" rel="noopener noreferrer"&gt;Fragments &lt;/a&gt; are needed to compose a conversation script to control adherence to scripts during dialogue. A fragment consists of a set of words. The frontend receives from the backend a fragment entity, and inside it is an array of words included in this fragment. On the frontend, we created a form in which the user can change the set of words, correct words, etc. When the “Save” button is pressed, the fragment with the new word array is sent back to the backend.&lt;/p&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%2Fok3dg3yt1npeokgwkblj.webp" 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%2Fok3dg3yt1npeokgwkblj.webp" alt=" " width="800" height="503"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Selecting columns in the records registry.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Usually, the &lt;a href="https://speechkit-demo.sanatel.net/#/speech-transcript" rel="noopener noreferrer"&gt;registry of records&lt;/a&gt; does not contain many columns, so the set of columns is static. But in the case of audio transcripts, the entity can have a lot of fields, ranging from “Incoming” or “Outgoing” call to department, city, call score, number of words from one side, and so on. Therefore, in the call registry in analytics, there should be a choice of columns that the user considers necessary. The only static (mandatory) column is the clickable column with the call ID, which links to the call card.&lt;/p&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%2F52mg9ejpucxscapzaslg.webp" 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%2F52mg9ejpucxscapzaslg.webp" alt=" " width="800" height="503"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The selected set of columns in the registry is saved in cookies in the browser, and when the user re-enters the section, the selected columns are restored.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. JWT and user account.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There are many articles and videos on the Internet about authorization and roles in NestJs, but the topic is often not fully covered. Typically, the author explains how to create a JWT token, declares strategies and guards, and shows that authorization works — and stops there. But that’s only about 5–10% of the topic of authorization and user roles. First, the backend needs to understand which user is making the request, and then work with that.&lt;/p&gt;

&lt;p&gt;To achieve this, we declare a simple decorator. In this example, the user account is equal to their email. The decorator selects the fields available in the payload of the JWT token. If our JWT token contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;async login(user: any) {

const payload = {
email: user.email,
username: user.username,
role: user.role
};

return {
access_token: this.jwtService.sign(payload),
};
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in the decorator, we can extract any field from the payload, for example, the email:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export const User = createParamDecorator(
(data: any, ctx: ExecutionContext): string =&amp;gt; {
const request = ctx.switchToHttp().getRequest();

if(
request.user !== undefined
&amp;amp;&amp;amp;
request.user.email !== undefined
)
{
return request.user.email;
}
else {
return 'unknown_user'
}
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in the controller, we receive the user data and can do something with it, for example, check access rights to read records:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { User } from '../user/user.decorator';

@Get()
@ApiOperation({ summary: 'Get coversation list' })
@ApiOkResponse({ description: 'Coversation list', type: SpeechEntity, isArray: true })
findAll(@User() userMail: string): Promise&amp;lt;SpeechEntity[]&amp;gt; {
return this.speechService.findAll( userMail );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;6. Roles and access rights — the most controversial topic.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Several questions arise:&lt;/p&gt;

&lt;p&gt;Where in the NestJs project architecture is it correct to check access rights? In the controller or already in the service?&lt;/p&gt;

&lt;p&gt;It seems logical to check access rights in the controller and pass conditions to the service for search. For example, a user has rights to read only the records where they are responsible. &lt;/p&gt;

&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Get()
@ApiOperation({ summary: 'Get contact list' })
@ApiOkResponse({ description: 'Contact list', type: ContactEntity, isArray: true })
findAll( @User() userMail: string ): Promise&amp;lt;ContactEntity[]&amp;gt; {
const userAccessWhereOptions = this.userService.getUserAccessWhereOptions( userMail, controllerName );
return this.contactService.findAll( userAccessWhereOptions);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But inside the &lt;code&gt;findAll&lt;/code&gt; service, there may be its own conditions for search, for example, &lt;code&gt;category = new&lt;/code&gt;, which will need to be attached to the conditions of responsibility. And there might also be cases where “own” records are those where the user is either responsible or the author. Then the &lt;code&gt;findAll&lt;/code&gt; service would receive two OR conditions, and we would have to add the &lt;code&gt;category = new&lt;/code&gt; condition to both of them.&lt;/p&gt;

&lt;p&gt;Additionally, there’s the issue of deleting records, which in reality is not deletion but setting &lt;code&gt;deleted = yes&lt;/code&gt;. A regular user sees “own” records under the condition &lt;code&gt;deleted = no&lt;/code&gt;, while an admin sees all records.&lt;/p&gt;

&lt;p&gt;So far, we’ve settled on this: the access rights service returns the condition for &lt;code&gt;find&lt;/code&gt;, already considering access rights, and also with the condition for deleted records.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;where = [
{
'owner': userEmail, // Responsible for record
'deleted': 'no'
}, // OR
{
'father': userEmail, // Creator of record
'deleted': 'no'
}
];
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And inside &lt;code&gt;findAll&lt;/code&gt;, there’s a function that iterates through all conditions and adds its own conditions to them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;async findAll( userAccessWhereOptions: {}[] ): Promise&amp;lt;ContactEntity[]&amp;gt; {
const options: FindManyOptions = {
where: UtilWhereAssign(
userAccessWhereOptions,
{
'category': 'new'
}
),
order: { updated: 'DESC' },
};
const result = await this.contactRepository.find( options );
return result;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From &lt;code&gt;UtilWhereAssign&lt;/code&gt;, for a “regular user,” a new search condition will be returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;where = [
{
'owner': userEmail,
'deleted': 'no',
'category': 'new'
}, // ИЛИ
{
'father': userEmail,
'deleted': 'no',
'category': 'new'
}
];
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;7. On the frontend, we consider user rights.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;But we also need to inform the user on the frontend that they don’t have rights for the requested operation. The backend has a method that returns the &lt;a href="https://speechkit-demo.sanatel.net/#/role" rel="noopener noreferrer"&gt;user’s role&lt;/a&gt;. Accordingly, if a user doesn’t have rights to add records, the frontend should disable the “Add record” or “Delete record” button, etc.&lt;/p&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%2F8jjvnapbs428nrjr60dp.webp" 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%2F8jjvnapbs428nrjr60dp.webp" alt=" " width="800" height="503"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. Change history.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Since we need to keep a log of who made what changes to entities, we create a &lt;code&gt;historyService&lt;/code&gt;, and in all &lt;code&gt;create&lt;/code&gt;, &lt;code&gt;update&lt;/code&gt;, and &lt;code&gt;remove&lt;/code&gt; methods in various services, we pass to the &lt;code&gt;historyService&lt;/code&gt; data about the ID of the modified record and the user.&lt;/p&gt;

&lt;p&gt;To avoid implementing change history creation for each entity, we created a universal method &lt;code&gt;calcChangeAndCreate&lt;/code&gt; in &lt;code&gt;historyService&lt;/code&gt;, to which we pass the old entity instance, the new entity instance, and the username. Example of update for a contact:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;async update(recordId: string, updateDto: ContactUpdateDTO, userMail: string): Promise&amp;lt;ContactEntity&amp;gt; {
const options = {
where: [{ 'id': recordId }]
}
const oldResult = await this.contactRepository.findOne( options );
await this.contactRepository.update( { "id": recordId }, updateDto);
const newResult = await this.contactRepository.findOne( options );

await this.historyService.calcChangeAndCreate( 'contact', newResult, oldResult, userMail, recordId );

return newResult;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the &lt;code&gt;calcChangeAndCreate&lt;/code&gt; method, we compare the fields of the old and new objects, and if there are changes, we create a record in the change history:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;calcChange( newEntity: object, oldEntity: object ): HistoryRecordChange {
let result = {} as HistoryRecordChange;
result.fieldChangeList = [];
result.haveChange = false;
result.user = 'no_user';
result.table = 'no_table';
result.record = 'no_record';

for( let key of Object.keys( newEntity) ) {
if( key != 'created' &amp;amp;&amp;amp; key != 'updated' ) {

let newValue: string;
if(
typeof newEntity[key] == 'object'
&amp;amp;&amp;amp;
newEntity[key] != null
) {
newValue = newEntity[key].toISOString() + ' (UTC)';
}
else {
newValue = newEntity[key];
}

let oldValue: string;
if(
typeof oldEntity[key] == 'object'
&amp;amp;&amp;amp;
oldEntity[key] != null
) {
oldValue = oldEntity[key].toISOString() + ' (UTC)';
}
else {
oldValue = oldEntity[key];
}

if( newValue != oldValue ) { // Field changed
result.haveChange = true;
let fieldChange = {} as HistoryFieldChange;
fieldChange.field = key;
fieldChange.oldValue = oldValue;
fieldChange.newValue = newValue;
result.fieldChangeList.push( fieldChange );
}
}

return result;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same applies to creating and deleting entities. There are also child entities, for example, a phone number in the contact’s phone list. Changes to child entities are recorded in the history of the parent entity.&lt;/p&gt;

&lt;p&gt;Accordingly, we add the section &lt;a href="https://speechkit-demo.sanatel.net/#/history" rel="noopener noreferrer"&gt;“Change History”&lt;/a&gt;.&lt;/p&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%2F0ohnaocbi8zin4dnpysc.webp" 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%2F0ohnaocbi8zin4dnpysc.webp" alt=" " width="800" height="503"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>PrimeNG + NestJS = CRM — part 1</title>
      <dc:creator>Sanatel Consulting</dc:creator>
      <pubDate>Tue, 12 Aug 2025 17:01:51 +0000</pubDate>
      <link>https://dev.to/sanatel_net/primeng-nestjs-crm-part-1-49on</link>
      <guid>https://dev.to/sanatel_net/primeng-nestjs-crm-part-1-49on</guid>
      <description>&lt;p&gt;We decided to develop a CRM system in-house. During the development process, there were some interesting moments that I will try to describe in several articles. In the articles, I will try to avoid banalities like: downloaded, unpacked, launched, and look, swagger out of the box. There are already a lot of such articles, as well as videos on YouTube. I will try to share just interesting details that I came across during the development process. I will get ahead of myself - the system was configured and launched.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why development, and not buying a ready-made system&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For two reasons. Secondly, because purchased systems over time become so overgrown with additional settings that there is little left from the "box". And firstly, because frameworks have rapidly developed into some kind of platforms in which you have to code little.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choosing a framework&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I wanted to find a framework that already had all the Boiler code needed for a business application: menus, sections, graphs, users, etc. While searching for such a framework, we noticed the .Net frameworks &lt;a href="https://aspnetboilerplate.com" rel="noopener noreferrer"&gt;https://aspnetboilerplate.com&lt;/a&gt; and &lt;a href="https://abp.io" rel="noopener noreferrer"&gt;https://abp.io&lt;/a&gt;, which already have a lot of things "out of the box". As far as I understand, both frameworks are being developed either by related teams, or even by one team. And the teams are from Turkey. The ASP.NET Boilerplate framework has legacy code from older versions of the .Net Framework. The newer ABP framework does not have legacy, it is on .Net Core. Both frameworks have a decent number of stars on github.&lt;/p&gt;

&lt;p&gt;Then I came across an interesting library for the front - &lt;a href="https://www.primefaces.org/primeng" rel="noopener noreferrer"&gt;PrimeNG&lt;/a&gt;, it has three branches, for Angular, for React, for Vue. Each branch has a store with design themes, there are paid themes, there are free themes. Everything looks very beautiful, "out of the box" has everything you need for the front, menu, tabs, buttons, pop-up notifications. PrimeNG is again backed by a team from Turkey, PrimeTek.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://demo.sanatel.net/#/contact" rel="noopener noreferrer"&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%2Fd1klezqzhmv9v2mml9xo.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As a result, we decided to develop on a bundle of PrimeNG (Angular) + NestJs. Because the front really wants to be on Angular, then there is a desire to save on the expertise of developers, and therefore let both the front and the backend be TypeScript.&lt;/p&gt;

&lt;p&gt;The disadvantages of Node.js are known:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TypeScript is still an add-on, I consider the lack of data types in JavaScript a disadvantage.&lt;/li&gt;
&lt;li&gt;The node_modules folder will contain several tens (hundreds) of thousands of files written by no one knows who.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;1. Logging TypeORM queries&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To work with the database, the NestJs framework uses TypeORM. The TypeORM library surprised, it handles changes made to the table structure well, replacing column types, even with data in tables. And in order to view the logs of SQL queries generated by TypeORM, you need to add logging parameter:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;TypeOrmModule.forRoot({&lt;br&gt;
type: 'mysql', // ...&lt;br&gt;
entities: [ ContactEntity, ],&lt;br&gt;
logging: true, // For logging SQL queries to the console&lt;br&gt;
}),&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Generating UUID primary key in TypeORM&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Boiler columns in tables that should be in each table by default are the primary key, creation date, update date. If you declare in TypeORM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export class ContactEntity {
@ApiProperty({description: 'Primary key'})
@PrimaryGeneratedColumn()
id: string;

@ApiProperty({description: 'Creation date'})
@CreateDateColumn()
created: Date;

@ApiProperty({description: 'Update date'})
@UpdateDateColumn()
updated: Date;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in MySQL database it will turn into in:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CREATE TABLE contacts (&lt;br&gt;
id int(11) NOT NULL AUTO_INCREMENT,&lt;br&gt;
created datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),&lt;br&gt;
updated datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),&lt;br&gt;
PRIMARY KEY (id)&lt;br&gt;
);&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Everything is fine. But for a business application, you want a UUID primary key, not an integer.&lt;/p&gt;

&lt;p&gt;And if you declare in TypeORM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export class ContactEntity {
@ApiProperty({description: 'Primary key'})
@PrimaryGeneratedColumn('uuid')
id: string;

@ApiProperty({description: 'Creation date'})
@CreateDateColumn()
created: Date;

@ApiProperty({description: 'Update date'})
@UpdateDateColumn()
updated: Date;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in the SQL database it will turn into:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;br&gt;
CREATE TABLE contacts (&lt;br&gt;
id varchar(36) NOT NULL,&lt;br&gt;
created datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),&lt;br&gt;
updated datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),&lt;br&gt;
PRIMARY KEY (id)&lt;br&gt;
);&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;That is, the primary key is just a string, without auto-generation of the UUID value! At first it seemed strange. But it turned out that in TypeORM it is done this way deliberately, the UUID is generated in the TypeORM code and the insertion of records occurs with the UUID key field already filled. Because in the case of an auto-generated UUID column, for some types of insertion, TypeORM would then have to read the inserted records and update them again. This would ultimately work slower than generating UUID on the TypeORM side.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Notifications in the main menu&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In the main menu, near the section names, you can display an indicator of the number of records in the section. For example, on the menu item "Orders", you need to display an indicator of orders in the "New" status so that the employee immediately pays attention to the fact that new orders have dropped into the system from the site, and these new orders need to be processed faster. For this, PrimeNG has a badge parameter.&lt;/p&gt;

&lt;p&gt;In the AppMenuComponent module, in the menu and sections model, for the "Orders" item, specify the badge and an integer value:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;this.model = [&lt;br&gt;
  {&lt;br&gt;
    label: 'Разделы',&lt;br&gt;
      items:[ &lt;br&gt;
        {label: 'Contacts', icon: 'pi pi-fw pi-user', routerLink: ['/contact']},&lt;br&gt;
        {label: 'Orders', icon: 'pi pi-fw pi-table', routerLink: ['/order'], badge: 0},&lt;br&gt;
      ]&lt;br&gt;
  },&lt;br&gt;
]&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The value will have to be updated by the absolute address of the menu item in the model:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;this.model[ 0 ].items[ 1 ].badge = countNewOrder;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://demo.sanatel.net/#/lead" rel="noopener noreferrer"&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%2Fru95nhjrszz59ikatk6b.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;System Roadmap&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;a) It is necessary to transfer the generation of reports in Excel from the front to the backend. Generate Excel files on the backend, and send the finished files to the front to the user. Why this seems preferable, I will explain in the next article.&lt;/p&gt;

&lt;p&gt;b) You need to attach a task queue. Obviously, some tasks can be performed indefinitely, accordingly, such tasks need to be put in a queue, and then the results can be collected.&lt;/p&gt;

&lt;p&gt;c) You need a workflow, for example, document processing, at least in an elementary form, for example, in the form of a reference book with stages of document approval.&lt;/p&gt;

&lt;p&gt;d) You need to attach chats and a chat bot.&lt;/p&gt;

&lt;p&gt;We will show some pieces of what we get on the demo stand - &lt;a href="https://demo.sanatel.net" rel="noopener noreferrer"&gt;PrimeNG demo&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>nestjs</category>
      <category>frontend</category>
      <category>backend</category>
    </item>
    <item>
      <title>PrimeNG + NestJS = CRM — part 1</title>
      <dc:creator>Sanatel Consulting</dc:creator>
      <pubDate>Tue, 12 Aug 2025 16:35:25 +0000</pubDate>
      <link>https://dev.to/sanatel_net/primeng-nestjs-crm-part-1-4ji9</link>
      <guid>https://dev.to/sanatel_net/primeng-nestjs-crm-part-1-4ji9</guid>
      <description>&lt;p&gt;We decided to develop a CRM system in-house. During the development process, there were some interesting moments that I will try to describe in several articles. In the articles, I will try to avoid banalities like: downloaded, unpacked, launched, and look, swagger out of the box. There are already a lot of such articles, as well as videos on YouTube. I will try to share just interesting details that I came across during the development process. I will get ahead of myself - the system was configured and launched.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Why development, and not buying a ready-made system&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For two reasons. Secondly, because purchased systems over time become so overgrown with additional settings that there is little left from the "box". And firstly, because frameworks have rapidly developed into some kind of platforms in which you have to code little.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Choosing a framework&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I wanted to find a framework that already had all the Boiler code needed for a business application: menus, sections, graphs, users, etc. While searching for such a framework, we noticed the .Net frameworks &lt;a href="https://aspnetboilerplate.com" rel="noopener noreferrer"&gt;https://aspnetboilerplate.com&lt;/a&gt; and &lt;a href="https://abp.io" rel="noopener noreferrer"&gt;https://abp.io&lt;/a&gt;, which already have a lot of things "out of the box". As far as I understand, both frameworks are being developed either by related teams, or even by one team. And the teams are from Turkey. The ASP.NET Boilerplate framework has legacy code from older versions of the .Net Framework. The newer ABP framework does not have legacy, it is on .Net Core. Both frameworks have a decent number of stars on github.&lt;/p&gt;

&lt;p&gt;Then I came across an interesting library for the front - &lt;a href="https://www.primefaces.org/primeng" rel="noopener noreferrer"&gt;PrimeNG&lt;/a&gt;, it has three branches, for Angular, for React, for Vue. Each branch has a store with design themes, there are paid themes, there are free themes. Everything looks very beautiful, "out of the box" has everything you need for the front, menu, tabs, buttons, pop-up notifications. PrimeNG is again backed by a team from Turkey, PrimeTek.&lt;/p&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%2Fd1klezqzhmv9v2mml9xo.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%2Fd1klezqzhmv9v2mml9xo.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As a result, we decided to develop on a bundle of PrimeNG (Angular) + NestJs. Because the front really wants to be on Angular, then there is a desire to save on the expertise of developers, and therefore let both the front and the backend be TypeScript.&lt;/p&gt;

&lt;p&gt;The disadvantages of Node.js are known:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TypeScript is still an add-on, I consider the lack of data types in JavaScript a disadvantage.&lt;/li&gt;
&lt;li&gt;The node_modules folder will contain several tens (hundreds) of thousands of files written by no one knows who.&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;Logging TypeORM queries&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To work with the database, the NestJs framework uses TypeORM. The TypeORM library surprised, it handles changes made to the table structure well, replacing column types, even with data in tables. And in order to view the logs of SQL queries generated by TypeORM, you need to add logging parameter:&lt;/p&gt;

&lt;p&gt;TypeOrmModule.forRoot({&lt;br&gt;
type: 'mysql', // ...&lt;br&gt;
entities: [ ContactEntity, ],&lt;br&gt;
logging: true, // For logging SQL queries to the console&lt;br&gt;
}),&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generating UUID primary key in TypeORM&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Boiler columns in tables that should be in each table by default are the primary key, creation date, update date. If you declare in TypeORM:&lt;/p&gt;

&lt;p&gt;export class ContactEntity {&lt;br&gt;
@ApiProperty({description: 'Primary key'})&lt;br&gt;
@PrimaryGeneratedColumn()&lt;br&gt;
id: string;&lt;/p&gt;

&lt;p&gt;@ApiProperty({description: 'Creation date'})&lt;br&gt;
@CreateDateColumn()&lt;br&gt;
created: Date;&lt;/p&gt;

&lt;p&gt;@ApiProperty({description: 'Update date'})&lt;br&gt;
@UpdateDateColumn()&lt;br&gt;
updated: Date;&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Then in MySQL database it will turn into in:&lt;/p&gt;

&lt;p&gt;CREATE TABLE contacts (&lt;br&gt;
id int(11) NOT NULL AUTO_INCREMENT,&lt;br&gt;
created datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),&lt;br&gt;
updated datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),&lt;br&gt;
PRIMARY KEY (id)&lt;br&gt;
);&lt;/p&gt;

&lt;p&gt;Everything is fine. But for a business application, you want a UUID primary key, not an integer.&lt;/p&gt;

&lt;p&gt;And if you declare in TypeORM:&lt;/p&gt;

&lt;p&gt;export class ContactEntity {&lt;br&gt;
@ApiProperty({description: 'Primary key'})&lt;br&gt;
@PrimaryGeneratedColumn('uuid')&lt;br&gt;
id: string;&lt;/p&gt;

&lt;p&gt;@ApiProperty({description: 'Creation date'})&lt;br&gt;
@CreateDateColumn()&lt;br&gt;
created: Date;&lt;/p&gt;

&lt;p&gt;@ApiProperty({description: 'Update date'})&lt;br&gt;
@UpdateDateColumn()&lt;br&gt;
updated: Date;&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Then in the SQL database it will turn into:&lt;/p&gt;

&lt;p&gt;CREATE TABLE contacts (&lt;br&gt;
id varchar(36) NOT NULL,&lt;br&gt;
created datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),&lt;br&gt;
updated datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),&lt;br&gt;
PRIMARY KEY (id)&lt;br&gt;
);&lt;/p&gt;

&lt;p&gt;That is, the primary key is just a string, without auto-generation of the UUID value! At first it seemed strange. But it turned out that in TypeORM it is done this way deliberately, the UUID is generated in the TypeORM code and the insertion of records occurs with the UUID field of the key already filled in. Because in the case of an auto-generated UUID column, for some types of insertion TypeORM would then have to read the insert&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
