<?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: Petr Tcoi</title>
    <description>The latest articles on Forem by Petr Tcoi (@petrtcoi).</description>
    <link>https://forem.com/petrtcoi</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%2F449519%2F941726ce-4865-4f36-821e-03300a32526c.jpg</url>
      <title>Forem: Petr Tcoi</title>
      <link>https://forem.com/petrtcoi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/petrtcoi"/>
    <language>en</language>
    <item>
      <title>How I Tried to Handle Customer Support in Telegram and Ended Up Building a Tool for It</title>
      <dc:creator>Petr Tcoi</dc:creator>
      <pubDate>Thu, 26 Mar 2026 22:39:56 +0000</pubDate>
      <link>https://forem.com/petrtcoi/how-i-tried-to-handle-customer-support-in-telegram-and-ended-up-building-a-tool-for-it-2knl</link>
      <guid>https://forem.com/petrtcoi/how-i-tried-to-handle-customer-support-in-telegram-and-ended-up-building-a-tool-for-it-2knl</guid>
      <description>&lt;p&gt;This didn’t start as a startup idea. I wasn’t trying to build a SaaS product. I just needed a simple way to handle customer support for my own projects without exposing my personal Telegram account or dealing with complex tools.&lt;/p&gt;

&lt;p&gt;At first, Telegram felt perfect. Everyone already uses it, there’s no onboarding, no friction, no extra accounts. A client just sends a message and you reply. It’s fast, natural, and works out of the box.&lt;/p&gt;

&lt;p&gt;And for a while, it really does work.&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%2Fubh30rnfig7wa4c72x3w.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%2Fubh30rnfig7wa4c72x3w.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The problems don’t show up immediately. In the beginning, you have a few conversations per day, everything is manageable, and you can keep context in your head. Then you launch another project. Then another. Messages start coming in at different times, from different people, about different things.&lt;/p&gt;

&lt;p&gt;You reply from your personal account, then maybe create a separate work account, then start forwarding messages to yourself or teammates. Over time, Telegram turns into a mix of personal chats, client conversations, and random forwarded messages. It becomes harder to understand what’s going on at any given moment.&lt;/p&gt;

&lt;p&gt;The tricky part is that it doesn’t break in an obvious way. It slowly degrades. Responses get delayed, some messages are missed, sometimes two people reply to the same client, sometimes no one replies because everyone assumes someone else already did. Nothing critical, but enough to hurt the overall experience.&lt;/p&gt;

&lt;p&gt;I tried a few workarounds. Multiple accounts, groups, pinned messages, manual labeling, forwarding flows. All of them help a bit, but none of them solve the core issue. Telegram is just a list of chats. There is no structure, no clear overview, no shared context.&lt;/p&gt;

&lt;p&gt;Switching to a CRM or helpdesk didn’t feel right either. Not because those tools are bad, but because they’re often overkill for small teams or solo projects. You get dashboards, ticket statuses, workflows, onboarding, and another system you need to constantly keep open. Meanwhile, your actual communication still happens in Telegram, just routed through something else.&lt;/p&gt;

&lt;p&gt;At some point it became clear that I didn’t need another tool. I needed a way to make Telegram itself more structured.&lt;/p&gt;

&lt;p&gt;The idea ended up being very simple. Instead of clients messaging me directly, they message a bot. Every incoming message is forwarded into a private Telegram group where each client gets their own thread. One client, one thread. All history stays in one place, no forwarding, no confusion.&lt;/p&gt;

&lt;p&gt;You reply directly inside that thread, and the bot sends the response back to the client.&lt;/p&gt;

&lt;p&gt;Nothing fundamentally changes from the user’s perspective. It still feels like a normal Telegram chat. But internally, everything becomes structured. You can immediately see all active conversations, understand which ones need attention, and multiple people can work in parallel without stepping on each other.&lt;/p&gt;

&lt;p&gt;Most importantly, your personal account is no longer part of the support flow at all.&lt;/p&gt;

&lt;p&gt;I originally built this just to solve my own problem. But after using it for a while, I realized this pattern applies to a lot of people. Freelancers, small teams, indie projects, small businesses. Many of them rely on Telegram for communication, run into the same limitations, and don’t want to switch to heavy tools.&lt;/p&gt;

&lt;p&gt;That’s how it turned into a small product.&lt;/p&gt;

&lt;p&gt;If you're curious how this approach works in practice, I put together a simple explanation here:&lt;br&gt;&lt;br&gt;
👉 &lt;a href="https://gramdeskbot.com" rel="noopener noreferrer"&gt;https://gramdeskbot.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It’s not trying to replace a CRM or compete with full helpdesk platforms. It’s just adding a missing layer to Telegram so it can function as a lightweight support system.&lt;/p&gt;

&lt;p&gt;And in practice, for small setups, that turned out to be a much better fit than anything more complex.&lt;/p&gt;

&lt;p&gt;If you’re already using Telegram as your main communication channel, you’ll likely run into the same issues at some point. The question is whether you keep patching it manually or introduce a bit of structure on top of what you already use.&lt;/p&gt;

</description>
      <category>telegram</category>
      <category>saas</category>
      <category>elixir</category>
    </item>
    <item>
      <title>Monavo Architecture — Building a Telegram-First Solana Swap Service</title>
      <dc:creator>Petr Tcoi</dc:creator>
      <pubDate>Thu, 12 Mar 2026 10:20:19 +0000</pubDate>
      <link>https://forem.com/petrtcoi/monavo-architecture-building-a-telegram-first-solana-swap-service-2e9l</link>
      <guid>https://forem.com/petrtcoi/monavo-architecture-building-a-telegram-first-solana-swap-service-2e9l</guid>
      <description>&lt;p&gt;I recently launched the beta version of &lt;a href="https://monavoapp.com/" rel="noopener noreferrer"&gt;Monavo&lt;/a&gt;, a Telegram-first service for token swaps on the Solana network. The project is currently running in public testing, and the main goal of this stage is to observe how the architecture behaves in a real production environment. In this article, I want to explain the Monavo architecture, why the system is split between an edge API and a private backend service, and what engineering decisions help protect the system from duplicate requests, network issues, and unreliable external APIs.&lt;/p&gt;

&lt;p&gt;When working with cryptocurrency transactions, infrastructure becomes just as important as the user interface. A duplicated request or network retry can potentially trigger the same operation twice. Because of that, the main focus during the development of Monavo was reliability. The system needed to remain predictable even when dealing with unstable networks, webhook retries, and third-party API limits.&lt;/p&gt;

&lt;p&gt;Monavo was designed as a Telegram-first product. Telegram is responsible for authentication, notifications, and action triggers. The web application (&lt;code&gt;/app&lt;/code&gt;) is used to confirm operations and display transaction details before they are executed.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧩 Overall System Architecture
&lt;/h2&gt;

&lt;p&gt;Monavo is implemented as a monorepo with clearly separated modules. This structure keeps business logic isolated from infrastructure and makes the project easier to evolve over time.&lt;/p&gt;

&lt;p&gt;The system has several core components. A public API runs on Cloudflare Workers and receives all incoming requests. A separate backend service performs heavier operations and interacts with the Solana ecosystem. The web application acts as the user interface where transactions are reviewed and confirmed.&lt;/p&gt;

&lt;p&gt;Shared data contracts and types live in a dedicated package, while the database schema and migrations are maintained in a separate module. This structure allows infrastructure or UI layers to evolve independently without affecting the domain logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  ⚡ Edge Architecture
&lt;/h2&gt;

&lt;p&gt;One of the most important architectural decisions in Monavo was to adopt an edge-first design.&lt;/p&gt;

&lt;p&gt;The public API runs entirely on Cloudflare Workers. Workers process Telegram webhooks, handle requests from the web app, manage user sessions, and apply rate limiting.&lt;/p&gt;

&lt;p&gt;Workers effectively act as a gateway between users and the internal backend service. This approach ensures that the public interface remains fast while critical logic stays isolated from direct internet access.&lt;/p&gt;

&lt;p&gt;Since Workers run on Cloudflare’s edge network across multiple regions, API responses remain fast regardless of where users are located.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔐 Private Swap Engine
&lt;/h2&gt;

&lt;p&gt;The heavy business logic is handled by a separate internal service, often referred to as the swap engine. This service prepares swap transactions and interacts with Solana infrastructure.&lt;/p&gt;

&lt;p&gt;The backend server is not publicly accessible. It runs behind a Cloudflare Tunnel and accepts requests only from Workers.&lt;/p&gt;

&lt;p&gt;This separation means the internal service is never exposed directly to the internet. Even if the public API is targeted by attacks, the core swap engine remains unreachable from outside the system.&lt;/p&gt;

&lt;p&gt;Workers communicate with the backend using a service authorization key that is validated by the server. This creates a clear security boundary between the edge layer and the internal infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧠 API Contracts and DTOs
&lt;/h2&gt;

&lt;p&gt;All data exchange within the system is built around DTO contracts. Every incoming and outgoing payload is validated at the API boundary.&lt;/p&gt;

&lt;p&gt;These contracts are defined using Zod and serve as a shared source of types across all parts of the system. This ensures that every service interprets the same data structures consistently.&lt;/p&gt;

&lt;p&gt;This approach significantly reduces the risk of runtime errors and makes the API easier to maintain as the project evolves.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧯 Result-Based Error Handling Instead of Exceptions
&lt;/h2&gt;

&lt;p&gt;Monavo follows an approach similar to functional languages such as Elixir or Rust. Instead of throwing exceptions, functions return structured result objects.&lt;/p&gt;

&lt;p&gt;Each response includes an &lt;code&gt;isFault&lt;/code&gt; flag that indicates whether an error occurred. If an error happens, the response also includes an error code and a message.&lt;/p&gt;

&lt;p&gt;This approach prevents unexpected exceptions from interrupting execution flows. Clients always receive a predictable response structure and can safely handle errors.&lt;/p&gt;

&lt;p&gt;Example responses look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"isFault"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"isFault"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RATE_LIMIT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Too many requests"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🔁 Protection Against Duplicate Requests
&lt;/h2&gt;

&lt;p&gt;One of the most common issues in distributed systems is duplicate requests. These may appear due to network retries, repeated HTTP requests, or duplicated webhook deliveries.&lt;/p&gt;

&lt;p&gt;To address this, Monavo implements idempotency for command operations. Each request receives a unique key, and the system stores both the request hash and its result.&lt;/p&gt;

&lt;p&gt;If the same request is received again, the previously stored response is returned. If the payload differs, the server returns a conflict error.&lt;/p&gt;

&lt;p&gt;This mechanism ensures that operations cannot be accidentally executed multiple times.&lt;/p&gt;

&lt;h2&gt;
  
  
  🛡 Session Security
&lt;/h2&gt;

&lt;p&gt;User authentication begins in Telegram. After authentication, the web application receives a one-time token used to establish a session.&lt;/p&gt;

&lt;p&gt;This token has a limited lifetime and is stored in the database only as a hash. Once consumed, it becomes invalid.&lt;/p&gt;

&lt;p&gt;The session itself is stored in a secure cookie with the following security attributes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HttpOnly&lt;/li&gt;
&lt;li&gt;SameSite&lt;/li&gt;
&lt;li&gt;Secure (in production)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Session tokens are also periodically rotated, which reduces the risk of token compromise.&lt;/p&gt;

&lt;h2&gt;
  
  
  ⚙️ Reliability of External Integrations
&lt;/h2&gt;

&lt;p&gt;Monavo interacts with several external services, including Solana infrastructure providers.&lt;/p&gt;

&lt;p&gt;To prevent API overload and accidental blocking, these integrations are wrapped in request rate limiters. Additional caching and periodic updates are used to reduce external load and improve system responsiveness.&lt;/p&gt;

&lt;p&gt;This combination ensures that the system remains stable even when external services behave unpredictably.&lt;/p&gt;

&lt;h2&gt;
  
  
  📦 Deployment
&lt;/h2&gt;

&lt;p&gt;Workers are deployed using GitHub Actions. Whenever code changes are pushed, the deployment pipeline automatically updates the Cloudflare Workers environment.&lt;/p&gt;

&lt;p&gt;Deployment uses configuration options that prevent accidental overwriting of runtime secrets and environment variables.&lt;/p&gt;

&lt;p&gt;The internal backend service runs on a VPS and is managed with PM2. This setup allows fast updates and provides a simple mechanism for monitoring and restarting the service if necessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  ⚠️ Problems and Lessons Learned
&lt;/h2&gt;

&lt;p&gt;Despite the planned architecture, several issues appeared during early testing of Monavo.&lt;/p&gt;

&lt;p&gt;The first problem involved Telegram webhooks. Initially the system did not use idempotency. In practice, Telegram sometimes delivers the same event multiple times, especially during network delays or webhook retries.&lt;/p&gt;

&lt;p&gt;This occasionally caused the same operation to run twice. The solution was to introduce idempotency keys and payload hashing so duplicate requests return the original result.&lt;/p&gt;

&lt;p&gt;Another issue appeared after moving the entire API to Cloudflare Workers. Workers are excellent for fast API operations, but they are not ideal for long-running or computationally heavy tasks.&lt;/p&gt;

&lt;p&gt;Some swap flows required longer execution times than edge functions comfortably support. As a result, the architecture was adjusted so Workers act as a gateway while heavy operations run on a dedicated backend service.&lt;/p&gt;

&lt;p&gt;A third challenge appeared when interacting with Solana RPC nodes. Everything seemed stable during testing, but higher request volumes led to rate limit errors and inconsistent responses.&lt;/p&gt;

&lt;p&gt;This required adding request limiters, caching layers, and controlled request queues for RPC calls.&lt;/p&gt;

&lt;p&gt;Another important lesson came from error handling. Early versions of the system relied heavily on exceptions. Over time this created complex try/catch chains and unpredictable API responses.&lt;/p&gt;

&lt;p&gt;Eventually the system was refactored to the result-based approach described earlier. This change made error handling much more predictable and simplified client-side logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧭 Why Monavo Uses This Architecture
&lt;/h2&gt;

&lt;p&gt;The core idea behind the Monavo architecture is to separate the system into two layers.&lt;/p&gt;

&lt;p&gt;The edge layer handles user interaction and must remain extremely fast. The core layer executes critical business logic and stays isolated from direct public access.&lt;/p&gt;

&lt;p&gt;This approach reduces the attack surface, simplifies scaling, and makes the system more resilient to network instability.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧪 What Comes Next
&lt;/h2&gt;

&lt;p&gt;Monavo is currently running in beta. The primary goal of this stage is to observe how the architecture performs under real-world conditions and identify potential bottlenecks as the user base grows.&lt;/p&gt;

&lt;p&gt;If the system proves stable under production traffic, the next step will be expanding the platform with additional tools for interacting with the Solana ecosystem.&lt;/p&gt;

&lt;p&gt;The beta version is already live and gradually opening to more users, so the coming months will provide a good test of how well the current architecture performs in practice.&lt;/p&gt;

</description>
      <category>solana</category>
      <category>web3</category>
      <category>node</category>
      <category>react</category>
    </item>
    <item>
      <title>How I Stopped Checking amoCRM Every Morning and Built a Telegram Bot Instead</title>
      <dc:creator>Petr Tcoi</dc:creator>
      <pubDate>Fri, 06 Feb 2026 12:50:10 +0000</pubDate>
      <link>https://forem.com/petrtcoi/how-i-stopped-checking-amocrm-every-morning-and-built-a-telegram-bot-instead-2cj9</link>
      <guid>https://forem.com/petrtcoi/how-i-stopped-checking-amocrm-every-morning-and-built-a-telegram-bot-instead-2cj9</guid>
      <description>&lt;p&gt;I use amoCRM every day as a small business owner. We do not have a large sales department or a complex hierarchy, but we still rely on pipelines, deals, and tasks. Very quickly I noticed that a significant part of my time was spent not on decision-making, but on repetitive manual checks inside the CRM.&lt;/p&gt;

&lt;p&gt;Every morning looked almost the same. Open pipelines. Check if there are any unassigned leads. See whether active deals have tasks. Look for overdue tasks where nothing has happened for a while. None of this is difficult, but it is routine. And when you skip it or postpone it, the problem usually surfaces later, when the lead is already cold or the opportunity is lost.&lt;/p&gt;

&lt;p&gt;At some point, it became obvious that automating these checks was easier than forcing myself to repeat them manually every day.&lt;/p&gt;

&lt;p&gt;That is how &lt;strong&gt;LeadsAlarm&lt;/strong&gt; started.&lt;/p&gt;

&lt;p&gt;The project is already live: &lt;a href="https://leadsalarm.ru/" rel="noopener noreferrer"&gt;https://leadsalarm.ru/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Using it is intentionally simple. You just message the Telegram bot at &lt;a href="https://t.me/LeadsAlarmBot" rel="noopener noreferrer"&gt;https://t.me/LeadsAlarmBot&lt;/a&gt; go through the official amoCRM OAuth flow, select pipelines, and that is it. The service runs in the background and sends notifications when something in your deals requires attention.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Alerts Instead of Reports
&lt;/h2&gt;

&lt;p&gt;CRM reports and analytics are useful, but they solve a different problem. They are great for weekly or monthly reviews.&lt;/p&gt;

&lt;p&gt;In daily operations, what matters more is not an overview but a timely signal. I did not want to constantly open the CRM UI, filter deals, and manually check timestamps. I wanted the system to tell me when something went wrong.&lt;/p&gt;

&lt;p&gt;That is why alerts turned out to be the most convenient format.&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%2F1zaom9vuh2gs0p3o0aky.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%2F1zaom9vuh2gs0p3o0aky.png" alt=" " width="800" height="476"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Telegram
&lt;/h2&gt;

&lt;p&gt;The answer is simple. Telegram is always open.&lt;/p&gt;

&lt;p&gt;A separate dashboard is just another place you need to remember to visit. A message in a messenger appears exactly where attention already is and does not turn monitoring into a separate task.&lt;/p&gt;

&lt;p&gt;The bot does not spam. It sends short, contextual messages only when it makes sense. If everything is fine, it stays silent.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Signals Look Like
&lt;/h2&gt;

&lt;p&gt;When an actual issue appears, the bot sends a clear and focused signal. No long tables, no excessive details.&lt;/p&gt;

&lt;p&gt;For example, if a deal has an overdue task, the message contains the pipeline name and deal ID. This is enough to decide whether you need to react immediately or can look at it later.&lt;/p&gt;

&lt;p&gt;For unassigned leads, the logic is slightly different. The bot does not notify on every new lead. It waits for a configured time window and sends an aggregated alert only if the problem persists. This significantly reduces noise.&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%2F5e4uncjyy8sc86l359tl.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%2F5e4uncjyy8sc86l359tl.png" alt=" " width="548" height="696"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Data Safety by Design
&lt;/h2&gt;

&lt;p&gt;From the beginning, I decided that the service should not work with deal contents.&lt;/p&gt;

&lt;p&gt;LeadsAlarm uses only metadata: deal and task statuses, pipeline stages, timestamps, and responsible users. Contacts, notes, comments, and message texts are neither fetched nor stored.&lt;/p&gt;

&lt;p&gt;This was a conscious design choice. It minimizes data leakage risks simply by excluding sensitive data from storage. It also keeps the architecture simpler and more predictable. On top of that, full deal content would generate too much data for a stable and economically reasonable analysis, especially within a low-cost subscription model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why No Working Hours or Calendars
&lt;/h2&gt;

&lt;p&gt;Initially, I considered building checks around schedules: business hours, weekends, holidays, vacations.&lt;/p&gt;

&lt;p&gt;Very quickly it became clear that this approach adds complexity while still producing false positives. Every team works differently, and there is no universal calendar that fits everyone.&lt;/p&gt;

&lt;p&gt;Instead, I switched to a simpler activity-based model. The system does not care about days of the week or hours. It only looks at facts: whether there were any events related to a deal within a given time window.&lt;/p&gt;

&lt;p&gt;If there was activity, it is considered working time.&lt;/p&gt;

&lt;p&gt;This approach has limitations. If a manager has zero activity during the day, a signal might not trigger. In practice, such cases are rare, and if there is no activity at all, the problem is usually obvious even without automated alerts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech Stack and Architecture Choices
&lt;/h2&gt;

&lt;p&gt;I also used this project as an opportunity to work with PHP more intentionally. Before that, my experience with PHP was mostly limited to WordPress projects. Here, I wanted to take a popular and modern framework and go through the full cycle from idea to production.&lt;/p&gt;

&lt;p&gt;I chose &lt;strong&gt;Laravel 12&lt;/strong&gt;, which at the time was the latest and most up-to-date release. It turned out to be a good fit for this type of service. Clear structure, built-in queues, solid database tooling. Everything needed for a small SaaS is already there.&lt;/p&gt;

&lt;p&gt;Development felt a bit slower compared to my usual Node.js stack, but that was expected and not a real issue. Predictability and ease of maintenance were more important. Over time, I expect this choice to pay off.&lt;/p&gt;

&lt;p&gt;On the frontend side, I deliberately avoided complexity. The service has only a few public pages whose purpose is to explain what the tool does and how it works. I implemented them as simple static HTML pages without introducing a separate frontend stack. This allowed me to focus entirely on core logic and notifications.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Production Issue I Did Not Expect
&lt;/h2&gt;

&lt;p&gt;One interesting problem appeared after the system was already running.&lt;/p&gt;

&lt;p&gt;At some point, I noticed that background workers occasionally stopped processing jobs, while the jobs themselves were still marked as successfully completed. From the outside, everything looked fine, but in reality some checks were silently skipped.&lt;/p&gt;

&lt;p&gt;It took some time to understand what was happening, review logs, and adjust how workers were supervised and restarted. This was not a Laravel-specific issue. Similar problems can happen in Node.js systems as well. It was a good reminder that background processing in production needs proper monitoring and defensive design, regardless of the stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI and Future Direction
&lt;/h2&gt;

&lt;p&gt;At the moment, LeadsAlarm checks only basic situations that I previously monitored manually. The service has a free tier and a paid tier, which I continue to extend gradually.&lt;/p&gt;

&lt;p&gt;In parallel, I am already experimenting with AI-based analysis to reduce noise and highlight deals that deserve attention first. This is not about surveillance or employee control. The goal is to build a prioritization assistant, not a judge.&lt;/p&gt;

&lt;p&gt;I do not expect AI to be 100 percent accurate. Its role is to help focus attention, not to make decisions.&lt;/p&gt;

&lt;p&gt;I also plan to port the service to &lt;strong&gt;Kommo CRM&lt;/strong&gt;, which has a very similar API to amoCRM. This should make the transition mostly architectural rather than conceptual.&lt;/p&gt;

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

&lt;p&gt;For me, this project started as a way to remove routine from daily CRM usage and focus on things that actually matter. If you use amoCRM and find yourself repeatedly checking the same things manually, this alert-based approach might be useful for you as well.&lt;/p&gt;

&lt;p&gt;Project:  &lt;a href="https://leadsalarm.ru/" rel="noopener noreferrer"&gt;https://leadsalarm.ru/&lt;/a&gt;&lt;br&gt;
Telegram bot:  &lt;a href="https://t.me/LeadsAlarmBot" rel="noopener noreferrer"&gt;https://t.me/LeadsAlarmBot&lt;/a&gt;&lt;/p&gt;

</description>
      <category>php</category>
      <category>laravel</category>
      <category>saas</category>
      <category>telegram</category>
    </item>
    <item>
      <title>When You Should “Wet” Your Code: Why Blindly Following DRY Can Hurt Your Project</title>
      <dc:creator>Petr Tcoi</dc:creator>
      <pubDate>Wed, 12 Nov 2025 09:06:15 +0000</pubDate>
      <link>https://forem.com/petrtcoi/when-you-should-wet-your-code-why-blindly-following-dry-can-hurt-your-project-5hl2</link>
      <guid>https://forem.com/petrtcoi/when-you-should-wet-your-code-why-blindly-following-dry-can-hurt-your-project-5hl2</guid>
      <description>&lt;p&gt;The &lt;strong&gt;DRY principle (Don’t Repeat Yourself)&lt;/strong&gt; is probably one of the first rules we learn as developers. Its idea seems flawless: don’t repeat code, don’t duplicate knowledge, and move everything that repeats into a single place.  &lt;/p&gt;

&lt;p&gt;In practice, this means that if a piece of logic appears in multiple places, it should be centralized - as a function, class, module, or component.  &lt;/p&gt;

&lt;p&gt;This makes the project cleaner, easier to maintain, and safer to modify. Fix a bug in one place - and it disappears everywhere.  &lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;business logic&lt;/strong&gt;, especially on the &lt;strong&gt;backend&lt;/strong&gt;, DRY works beautifully. Data validation, discount calculations, and authorization rules are perfect examples of knowledge that must be centralized. When such logic is scattered across different services, inconsistencies and bugs are inevitable. In these cases, DRY can literally save months of work, make the system predictable, and simplify refactoring.  &lt;/p&gt;

&lt;p&gt;But my personal experience - and I mostly work with startups and fast-changing frontend projects — has shown that this principle, when applied dogmatically, can do real damage. When requirements change every week and design lives its own life, strict DRY often becomes a development bottleneck rather than a path to clean code. The more “mature” a project becomes, the “drier” it can be. But in the early stages, too much dryness can be fatal.&lt;/p&gt;

&lt;h2&gt;
  
  
  DRY in Theory: The Original Idea and Its Strengths
&lt;/h2&gt;

&lt;p&gt;DRY wasn’t created as a fight against repeating code just for aesthetics - it was a fight against repeating &lt;strong&gt;knowledge&lt;/strong&gt;. The authors of the principle, &lt;strong&gt;Andrew Hunt and David Thomas&lt;/strong&gt;, explained in &lt;em&gt;The Pragmatic Programmer&lt;/em&gt; that if the same business rule appears in multiple places, it’s a &lt;strong&gt;sign of an architectural flaw&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The core idea is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every piece of knowledge should exist &lt;strong&gt;in one and only one place&lt;/strong&gt;.
&lt;/li&gt;
&lt;li&gt;Changing that knowledge should require changing &lt;strong&gt;only one fragment of code&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This ensures consistency, reduces the risk of bugs, and simplifies testing. DRY makes code stable and transparent when it comes to recurring data structures, formulas, business rules, calculations, or constants.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where DRY Really Works
&lt;/h2&gt;

&lt;p&gt;Everything related to &lt;strong&gt;backend logic&lt;/strong&gt; is where DRY shines. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;shared database access functions
&lt;/li&gt;
&lt;li&gt;centralized error handling
&lt;/li&gt;
&lt;li&gt;unified pricing and discount calculation rules
&lt;/li&gt;
&lt;li&gt;recurring data models and validation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In these areas, duplicating logic leads to divergence and unpredictable behavior. Here, DRY doesn’t just save lines of code — it preserves the integrity of the system’s meaning.&lt;/p&gt;

&lt;h2&gt;
  
  
  When DRY Turns Into a Trap
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Risk of Over-Abstraction and Poor Readability
&lt;/h3&gt;

&lt;p&gt;The biggest mistake is applying DRY automatically without context.&lt;br&gt;&lt;br&gt;
The desire to “combine everything” often creates monstrous abstractions.  &lt;/p&gt;

&lt;p&gt;Developers merge similar chunks of code and end up with a giant function full of conditions, flags, and optional parameters that tries to do everything — but no one remembers exactly what.  &lt;/p&gt;

&lt;p&gt;Such “universal” modules become &lt;strong&gt;black boxes&lt;/strong&gt;: hard to modify, painful to debug, and confusing to newcomers.  &lt;/p&gt;

&lt;p&gt;Once, I worked on a project where I just needed to change the &lt;strong&gt;color of a button&lt;/strong&gt; on one page. But the button component was so “dried up” — with factories, dozens of props, and global styles — that it took me &lt;strong&gt;four hours&lt;/strong&gt; to make a tiny change. Ironically, there were only about ten buttons in the entire interface. That was a textbook example of how blind DRY can turn simplicity into chaos.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tight Coupling and Fragile Design
&lt;/h3&gt;

&lt;p&gt;When everything depends on one shared module, every small change becomes a minefield. You fix something for one page — and something breaks elsewhere. This creates &lt;strong&gt;tight coupling&lt;/strong&gt;, where independent parts of the system become hostages of shared logic.  &lt;/p&gt;

&lt;p&gt;Ironically, the famous DRY argument “fix it once, fix it everywhere” works in reverse: introduce a bug once — and it spreads everywhere. The result is a fragile, rigid system where touching one line can cause a chain reaction.&lt;/p&gt;

&lt;h3&gt;
  
  
  Premature Abstraction – the Enemy of Flexibility
&lt;/h3&gt;

&lt;p&gt;DRY was never meant to eliminate every visual repetition — only repetition of &lt;strong&gt;knowledge&lt;/strong&gt;. But developers often merge pieces that are only &lt;strong&gt;visually similar&lt;/strong&gt;, not conceptually identical. At first, it feels smart: “Why write this twice?”. Then you realize these pieces evolve differently, and the abstraction becomes a burden.  &lt;/p&gt;

&lt;p&gt;In startups, I see this all the time. Requirements change weekly, designs daily. Premature abstraction slows everything down — you keep adding exceptions, flags, and branching logic just to preserve a “shared” structure.  &lt;/p&gt;

&lt;p&gt;It’s often far easier to &lt;strong&gt;duplicate a small piece of code&lt;/strong&gt;, let it evolve independently, and only later refactor it once the functionality stabilizes. This follows the &lt;strong&gt;“Rule of Three”&lt;/strong&gt;: if you write the same thing three times, that’s the moment to abstract.&lt;/p&gt;

&lt;h3&gt;
  
  
  Architectural Traps and the “Distributed Monolith”
&lt;/h3&gt;

&lt;p&gt;In microservice architectures, DRY can be downright dangerous.&lt;br&gt;&lt;br&gt;
Developers create shared libraries “to avoid duplication,” but in reality, they end up with a &lt;strong&gt;distributed monolith&lt;/strong&gt; — a system where all services depend on one shared core.  &lt;/p&gt;

&lt;p&gt;Now you can’t deploy anything independently; every minor update risks breaking multiple components. Over time, those shared “commons” libraries turn into &lt;strong&gt;dumping grounds&lt;/strong&gt; for everything no one dares to delete. Instead of elegance, DRY breeds technical debt.  &lt;/p&gt;

&lt;p&gt;Frontend teams are especially vulnerable: shared “UI utility” libraries quickly become bloated, outdated, and terrifying to touch. Everything looks dry, but nobody understands how it works anymore.&lt;/p&gt;

&lt;h3&gt;
  
  
  When DRY Conflicts With Common Sense
&lt;/h3&gt;

&lt;p&gt;In the real world, systems are rarely symmetrical or stable.&lt;br&gt;&lt;br&gt;
In frontend projects especially, “universal components” often become nightmares: a single mega-component for buttons, taking ten props and supporting countless variants — until someone clones it anyway to make “a rounded one with a gradient hover.”  &lt;/p&gt;

&lt;p&gt;When the designer suddenly wants three shapes of buttons, forty color shades, and unique hover states, the DRY dream collapses. Sometimes it’s far better to have &lt;strong&gt;three separate, simple components&lt;/strong&gt; than one monstrous one.  &lt;/p&gt;

&lt;p&gt;In young, evolving projects, universal abstractions don’t speed things up — they &lt;strong&gt;create inertia&lt;/strong&gt;. When the priority is speed, iteration, and experimentation, duplication is often &lt;strong&gt;cheaper and safer&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  When It’s Better to “Wet” Your Code
&lt;/h2&gt;

&lt;p&gt;Sometimes, duplication is an intentional, &lt;strong&gt;pragmatic decision&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;
If similar pieces of code serve different purposes, evolve at different paces, or belong to different contexts — keep them separate. This mindset even has a name: &lt;strong&gt;WET — Write Everything Twice.&lt;/strong&gt;  &lt;/p&gt;

&lt;p&gt;It’s not an argument for chaos — it’s an acknowledgment that &lt;strong&gt;duplication is often cheaper than the wrong abstraction&lt;/strong&gt;. This is especially true in &lt;strong&gt;frontend development&lt;/strong&gt; and &lt;strong&gt;young products&lt;/strong&gt;, where requirements change faster than architecture can stabilize.  &lt;/p&gt;

&lt;p&gt;In business logic and backend development, DRY still shines — for calculations, validation, and data access layers. But in user interfaces and early-stage products, &lt;strong&gt;pragmatism must win over dogma.&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;DRY is an excellent principle — when used consciously. It keeps systems consistent and maintainable. But when treated like a religion, it becomes a source of rigidity and pain.  &lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;frontend development&lt;/strong&gt; and &lt;strong&gt;fast-moving projects&lt;/strong&gt;, dogmatic DRY causes more harm than good.  The faster your product evolves, the more important &lt;strong&gt;flexibility, independence, and clarity&lt;/strong&gt; become.  &lt;/p&gt;

&lt;p&gt;Let the code repeat a little — as long as it stays readable, local, and easy to change. After all, programming isn’t about eliminating repetition — it’s about &lt;strong&gt;balancing clarity, speed, and common sense&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>discuss</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Example of caching Google Translate translation results for a multilingual site on NextJS</title>
      <dc:creator>Petr Tcoi</dc:creator>
      <pubDate>Wed, 21 May 2025 21:08:28 +0000</pubDate>
      <link>https://forem.com/petrtcoi/example-of-caching-google-translate-translation-results-for-a-multilingual-site-on-nextjs-4ana</link>
      <guid>https://forem.com/petrtcoi/example-of-caching-google-translate-translation-results-for-a-multilingual-site-on-nextjs-4ana</guid>
      <description>&lt;p&gt;I am sharing my experience organizing the translation of content into different languages. I work on a multilingual website using the Next.js i18n + MongoDB (Mongoose) stack. The site contains a significant amount of text that is occasionally updated.&lt;/p&gt;

&lt;p&gt;Google Translate is used to translate the texts in the project. Initially, a simple function was created to translate the view.&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;// @/lib/translate.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;server-only&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&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;z&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;zod&lt;/span&gt;&lt;span class="dl"&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;Translate&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@google-cloud/translate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;v2&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;ApiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GOOGLE_TRANSLATION_API_KEY&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;googleTranslate&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;Translate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ApiKey&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;translateFromEn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&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="nx"&gt;locale&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&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;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&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;translations&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;googleTranslate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;locale&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;translations&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;return&lt;/span&gt; &lt;span class="nx"&gt;result&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="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="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;Then it was used everywhere when preparing pages. To avoid translating the same texts repeatedly, I used the "unstable_cache" function (despite its name, I've never had any problems with it).&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;// .../page.tsx&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;locale&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="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Page&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Params&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="p"&gt;{&lt;/span&gt;  &lt;span class="nx"&gt;locale&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;params&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;translatedTitle&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;unstable_cache&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;translateFromRu&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="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`post:title:&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="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;revalidate&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;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`post:&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="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;)();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;translatedShortDescription&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;unstable_cache&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;translateFromRu&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="nx"&gt;metaCustom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shortdescription&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`post:shortDescription:&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="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;revalidate&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;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`post:&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="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
   &lt;span class="p"&gt;)();&lt;/span&gt;


    &lt;span class="k"&gt;return&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;p&gt;Accordingly, whenever the blog post was updated, the revalidateTag() method was called and the translations were updated.&lt;/p&gt;

&lt;p&gt;While this was a working solution, the cache was reset every time the site was updated, which was a flaw. Consequently, this risked a large bill from Google.&lt;/p&gt;

&lt;p&gt;Therefore, I added a feature that stores the translation results in the Mongo database. The idea is as follows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a unique ID for each translation entity.&lt;/li&gt;
&lt;li&gt;Before accessing the Google API, we check the database to see if there is a record with this ID and if it matches the text to be translated.&lt;/li&gt;
&lt;li&gt;If so, we return the record from the database.&lt;/li&gt;
&lt;li&gt;If not, we request a translation via the API, return the result, and save it in the database.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I use hashing for simple text matching. With the new logic, the translateFromEn function looks like this:&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;server-only&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;crypto&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;crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&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;z&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;zod&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&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;TranslateResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;zTranslateResult&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;../mongoModels&lt;/span&gt;&lt;span class="dl"&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;Translate&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@google-cloud/translate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;v2&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;ApiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GOOGLE_TRANSLATION_API_KEY&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;googleTranslate&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;Translate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ApiKey&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// -------------------------------------------------------------------&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;GetTranslationKeyProps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;locale&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="c1"&gt;// en, ko, ch...&lt;/span&gt;
  &lt;span class="nl"&gt;entity&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="c1"&gt;// user, post, comment&lt;/span&gt;
  &lt;span class="nl"&gt;entityType&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="c1"&gt;// shortDescription, metaTitle&lt;/span&gt;
  &lt;span class="nl"&gt;entityId&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="c1"&gt;// slug, _id ...&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getTranslationKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GetTranslationKeyProps&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;entityType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;entityId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;entityType&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;entityId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// --------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;// 1. If locale === 'en' - just return text&lt;/span&gt;
&lt;span class="c1"&gt;// 2. Generate Key and Check if translation exists in DB&lt;/span&gt;
&lt;span class="c1"&gt;// 3. Generate hash from text&lt;/span&gt;
&lt;span class="c1"&gt;// 4. If old Db Translation exist and hash is the same - return translation&lt;/span&gt;
&lt;span class="c1"&gt;// 5. If hash is different or hash not the save - create new translation&lt;/span&gt;
&lt;span class="c1"&gt;// 6. Return translation&lt;/span&gt;
&lt;span class="c1"&gt;// --------------------------------------------------------------------&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;translateFromEn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;text&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="nx"&gt;idData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GetTranslationKeyProps&lt;/span&gt;&lt;span class="p"&gt;,&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="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&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="c1"&gt;// 1. If locale === 'en' - just return text&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;idData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&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="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;return&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. Generate Key and Check if translation exists in DB&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getTranslationKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;idData&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;oldTranslateDoc&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;TranslateResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;lean&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// 3. Generate hash from text&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;textHash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SHA-256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 4. If old Db Translation exist and hash is the same - return translation&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;oldTranslateDoc&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;oldTranslateDoc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;textHash&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;oldTranslateDoc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;translation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// 5. If hash is different or hash not the save - create new translation&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;translations&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;googleTranslate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;idData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;locale&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;translateResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;translations&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newTranslationResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;zTranslateResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;textHash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;translation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;translateResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;TranslateResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOneAndUpdate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;newTranslationResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;upsert&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// 6. Return translation&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;translateResult&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="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="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="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 allowed us to significantly reduce the number of calls to the Google Translate API and stay within budget.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>i18n</category>
    </item>
    <item>
      <title>My stack for online store on NextJS</title>
      <dc:creator>Petr Tcoi</dc:creator>
      <pubDate>Mon, 16 Sep 2024 07:25:53 +0000</pubDate>
      <link>https://forem.com/petrtcoi/my-stack-for-online-store-on-nextjs-5cjo</link>
      <guid>https://forem.com/petrtcoi/my-stack-for-online-store-on-nextjs-5cjo</guid>
      <description>&lt;p&gt;I recently finished another online store for heating equipment. I would like to share the technology stack I used for it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Website: &lt;a href="https://radiatorlux.ru" rel="noopener noreferrer"&gt;https://radiatorlux.ru&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/petrtcoi/radiatorlux-pub" rel="noopener noreferrer"&gt;Github&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At the time of its launch, the store had about 19,000 products grouped by models and collections. In the future, it is planned to expand the assortment to 40-50 thousand products.&lt;/p&gt;

&lt;h2&gt;
  
  
  Framework and database
&lt;/h2&gt;

&lt;p&gt;The basis of the project is NextJS framework and MongoDB database (I used Mongoose to work with it). The latter was chosen mainly because of its flexibility (there are many specific characteristics for different models of radiators) and the availability of an excellent application MongoDB Compass, which allows you to fill and update the database via CSV import (the database of goods is initially stored in Google Sheets).&lt;/p&gt;

&lt;p&gt;Communication between documents and collections is done via the &lt;code&gt;slug&lt;/code&gt; text field instead of &lt;code&gt;_id&lt;/code&gt;. This approach makes updating and transferring data much easier. For example, if products and models are not successfully populated into the database, they can simply be deleted and re-populated by importing a CSV file without worrying about preserving the links between entities.&lt;/p&gt;

&lt;p&gt;The same approach works well with SQL databases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Database query caching
&lt;/h2&gt;

&lt;p&gt;I had some concerns about how &lt;code&gt;unstable_cache&lt;/code&gt; would work with &lt;code&gt;mongoose&lt;/code&gt;, but so far the caching is working as expected. Since the data on the site is rarely updated, the caching time is set to 12 hours.&lt;/p&gt;

&lt;p&gt;A typical request might look like this&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&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;unstable_cache&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lineSlug&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;lean&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="s2"&gt;cache:line&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lineSlug&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; 
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;revalidate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="p"&gt;)()&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;TLine&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Wordpress as CMS
&lt;/h2&gt;

&lt;p&gt;WordPress is used here as a CMS. This greatly simplifies the work of managing page content. To identify the needed content, the values of the &lt;code&gt;slug&lt;/code&gt; field are used for the corresponding entities.&lt;br&gt;
For example, the description for the collection "Cube" is located at &lt;code&gt;page-content-line-cube&lt;/code&gt;. The exact configuration of WP for use as a CMS I have seen in &lt;a href="https://www.youtube.com/watch?v=AJu7CaSROXk" rel="noopener noreferrer"&gt;this video&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Unfortunately, I couldn't use WP's built-in meta tags, so I used the &lt;a href="https://wordpress.org/plugins/advanced-custom-fields/" rel="noopener noreferrer"&gt;Advanced Custom Fields&lt;/a&gt; plugin, which allows you to create custom fields for pages and records.&lt;/p&gt;
&lt;h2&gt;
  
  
  Image Component and CDN
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://cdnnow.pro" rel="noopener noreferrer"&gt;CDNNow&lt;/a&gt; is used to deliver product images.&lt;/p&gt;

&lt;p&gt;Customization is easy, just specify in the &lt;code&gt;image&lt;/code&gt; component the required image sizes based on the window size and the component itself will pass the appropriate width to the &lt;code&gt;loader&lt;/code&gt; function:&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Image&lt;/span&gt;
  &lt;span class="nx"&gt;loader&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{({&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;CdnUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?width=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="nx"&gt;alt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;sizes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;(max-width: 768px) 50vw, (max-width: 1280px) 33vw, 25vw&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CdnUrl' - the address the server provides when registering your site.&lt;/p&gt;

&lt;h2&gt;
  
  
  UI
&lt;/h2&gt;

&lt;p&gt;The visuals are generated by &lt;code&gt;tailwindcss&lt;/code&gt; and &lt;code&gt;shadcn&lt;/code&gt;. This is my default for most projects. Although with the last library and occasionally there are difficulties. For example, in this project, the &lt;code&gt;Drawer&lt;/code&gt; component was responsible for displaying variants of paint and radiator connections and did not work correctly on mobile phones. So it had to be replaced by a similar component called &lt;code&gt;Sheet&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>mongodb</category>
      <category>tailwindcss</category>
    </item>
    <item>
      <title>My experiment with HTMX and Astro</title>
      <dc:creator>Petr Tcoi</dc:creator>
      <pubDate>Thu, 20 Jun 2024 16:44:25 +0000</pubDate>
      <link>https://forem.com/petrtcoi/my-experiment-with-htmx-and-astro-3io9</link>
      <guid>https://forem.com/petrtcoi/my-experiment-with-htmx-and-astro-3io9</guid>
      <description>&lt;p&gt;I share my first experience with the &lt;a href="https://htmx.org"&gt;htmx&lt;/a&gt; library. I created a simple site (&lt;a href="https://tubog-showcase.ru"&gt;https://tubog-showcase.ru&lt;/a&gt;) consisting of a home page with an initial set of apartment cards and 5 buttons that display apartments filtered by number of rooms.&lt;/p&gt;

&lt;p&gt;Stack: Astro + HTMX + Tailwind&lt;/p&gt;

&lt;p&gt;Implementation is very simple:&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;// src/pages/index.astro&lt;/span&gt;

&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;
  &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;filter-form&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="nx"&gt;hx&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;trigger&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="nx"&gt;hx&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/cases&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="nx"&gt;hx&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#serch-results&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="nx"&gt;hx&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;swap&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;innerHTML&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="nx"&gt;hx&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;indicator&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#loading&lt;/span&gt;&lt;span class="dl"&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;
    &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;radio&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rooms&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
        &lt;span class="kd"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hidden peer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hover:border-slate-500 peer-checked:opacity-100 peer-checked:shadow-xl peer-checked:border-slate-400&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="nx"&gt;комната&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&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;/label&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;/form&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;div&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loading&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;Loading&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;/div&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;div&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;serch-results&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Соответственно, при выборе нового варианта фильтра, по адресу &lt;code&gt;/api/cases&lt;/code&gt; выполняется POST-запрос с данными о выбранном варианте.&lt;/p&gt;

&lt;p&gt;Индикатором рабты запроса является &lt;code&gt;&amp;lt;div id='loading'&amp;gt;&lt;/code&gt;. Результат (готовый HTML) выводится внутри &lt;code&gt;&amp;lt;div id='serch-results'&amp;gt;&amp;lt;/div&amp;gt;&lt;/code&gt;.&lt;br&gt;
На стороне &lt;code&gt;api&lt;/code&gt; код выглядит примерно так:&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;// src/pages/api/cases.ts&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;POST&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;APIRoute&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;request&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;formData&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;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;formData&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;rooms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;formData&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rooms&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

   &lt;span class="c1"&gt;// Получаем список комнат на основе rooms&lt;/span&gt;

   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
     &lt;span class="s2"&gt;`
      &amp;lt;div&amp;gt;
         &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;filteredFlats&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;flat&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;` {some flat html here} `&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;
      &amp;lt;/div&amp;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;status&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="na"&gt;headers&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;Content-Type&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;text/html+htmx&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;p&gt;And that's it.&lt;/p&gt;

&lt;h2&gt;
  
  
  First Impressions
&lt;/h2&gt;

&lt;p&gt;This approach to create web applications is rather not for me. Implementing with the same React seems simpler and with more potential for improvement. What I didn't like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The blurring of application logic. The backend doesn't just return data, but also deals with frontend tasks;&lt;/li&gt;
&lt;li&gt;The need to harmonize the layout on the client side and the server side (in our case it's the default display of maps and search results output).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Yes, I made it a bit more difficult for myself by not using page partials, but their use within the same Astro in conjunction with HTMX seemed a bit too far-fetched to me.&lt;/p&gt;

&lt;p&gt;Still, I have a number of tasks where HTMX is a perfect fit: I have several old static sites and sites implemented on CMS like WordPress. For these, the will to add interactivity (especially using page partials) is a great prospect. For sites written in Next or Astro, I haven't seen a use for it yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  P.S. An unexpected problem
&lt;/h2&gt;

&lt;p&gt;In this project I've connected View Transitions and it seems to break the HTMX library. As soon as you make a transition between pages, the site ends up static. Fortunately, I came across this &lt;a href="https://flaviocopes.com/htmx-and-astro-view-transitions/"&gt;article&lt;/a&gt; which describes the solution: you need to add the following script to the page with HTXM code.&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;// src/pages/index.astro&lt;/span&gt;

&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;astro:page-load&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="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;contentElement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;filter-form&lt;/span&gt;&lt;span class="dl"&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;contentElement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
      &lt;span class="nx"&gt;htmx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&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="p"&gt;})&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/script&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>astro</category>
      <category>htmx</category>
    </item>
    <item>
      <title>A small example of how to use the satisfies operator in TypeScript</title>
      <dc:creator>Petr Tcoi</dc:creator>
      <pubDate>Fri, 15 Mar 2024 15:23:43 +0000</pubDate>
      <link>https://forem.com/petrtcoi/a-small-example-of-how-to-use-the-satisfies-operator-in-typescript-elj</link>
      <guid>https://forem.com/petrtcoi/a-small-example-of-how-to-use-the-satisfies-operator-in-typescript-elj</guid>
      <description>&lt;p&gt;The &lt;code&gt;satisfies&lt;/code&gt; operator tells TS what type of variable we expect, without overriding its own type. In some cases, this feature can be very useful. Let's look at a simple example where this operator can be useful.&lt;/p&gt;

&lt;p&gt;We have a list of products &lt;code&gt;listA&lt;/code&gt; and we need to prepare an object whose keys would be arbitrary strings and whose values would be any of the items in the list.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;listA&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;apples&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;oranges&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;peaches&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;ListA&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;listA&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="c1"&gt;// "apples" | "oranges" | "peaches"&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;listMap1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ListA&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;apples&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;oranges&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;peaches&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;d&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;apples&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;If we try to specify a value that is not in the list, for example &lt;code&gt;carrot&lt;/code&gt;, we get a warning, which was necessary.&lt;/p&gt;

&lt;p&gt;Now we need to infer the type of the keys contained in the &lt;code&gt;listMap1&lt;/code&gt; object. And here we have a problem, because at the very beginning we defined its type as &lt;code&gt;Record&amp;lt;string, ListA&amp;gt;&lt;/code&gt;. This means that the key is an arbitrary string:&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;type&lt;/span&gt; &lt;span class="nx"&gt;Key1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;listMap1&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;keyOne&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Key1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Y&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// const keyOne: string&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We don't get an error, although we should. This is where the &lt;code&gt;satisfies&lt;/code&gt; operator comes in handy, telling TS that we don't define the type of the variable, but just expect the keys to be strings and the values to be something from the fruit list.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;listMap2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;apples&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;oranges&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;peaches&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;d&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;apples&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;satisfies&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ListA&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this case, the above code will work as it should:&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;type&lt;/span&gt; &lt;span class="nx"&gt;Key2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;listMap2&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;keyTwo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Key2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Y&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// const keyTwo: "a" | "b" | "c" | "d"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we get the error &lt;code&gt;Type '"Y"' is not assignable to type '"a" | "b" | "c" | "d"&lt;/code&gt;, which is what we needed.&lt;/p&gt;

</description>
      <category>typescript</category>
    </item>
    <item>
      <title>NextJS API + React Query + Zod = type safety</title>
      <dc:creator>Petr Tcoi</dc:creator>
      <pubDate>Wed, 06 Sep 2023 15:49:05 +0000</pubDate>
      <link>https://forem.com/petrtcoi/nextjs-api-react-query-zod-type-safety-4c5e</link>
      <guid>https://forem.com/petrtcoi/nextjs-api-react-query-zod-type-safety-4c5e</guid>
      <description>&lt;p&gt;One of the advantages of Next.js is the ability to combine frontend and backend within a single project and use shared types and interfaces. Unfortunately, the server-side part of the framework operates independently from the frontend, functioning as a largely standalone application. Therefore, ensuring type safety requires additional effort. In this article, I will share my experience in addressing this issue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Folders
&lt;/h2&gt;

&lt;p&gt;For each entity, a separate folder is created in which all methods related to it are defined.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;│
├── database
│   ├── items
│   │   ├── api
│   │   │   ├── get-item-by-id
│   │   │   │   ├── getItemByIdClient.ts
│   │   │   │   ├── getItemByIdServer.ts
│   │   │   │   ├── config.ts
│   │   │   │   └── index.ts
│   │   │   └── index.ts
│   │   ├── lib
│   │   │   ├── getItemById.ts
│   │   │   └── index.ts
│   │   ├── hooks
│   │   │   ├── use-get-item-by-id
│   │   │   │   ├── queryKey.ts
│   │   │   │   ├── useGetItemById.ts
│   │   │   │   └── index.ts
│   │   │   └── index.ts
│   │   ├── items.types.ts
│   │   └── index.ts
│   └── index.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's go through these levels, starting from the base.&lt;/p&gt;

&lt;h2&gt;
  
  
  items.types.ts
&lt;/h2&gt;

&lt;p&gt;Main File. This is where types related to the entity are defined: how they are stored in the database, how data is retrieved from the database, and the required fields for creation or editing.&lt;/p&gt;

&lt;p&gt;To define the structure, Zod is used. In my last project, I used Strapi, so it looked something like this:&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;// How data is stored in the database&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;DbItem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;infer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;zDbItem&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;zDbItem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&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="nx"&gt;int&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;positive&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;z&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="nx"&gt;nonempty&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;City name cannot be empty&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&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="nx"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&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="nx"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// How data is returned from the database when queried&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;DbItemQuery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;infer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;zDbItemQuery&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;zDbItemQuery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;zDbItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pick&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;zDbItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;omit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;omitDates&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="c1"&gt;// If needed, you can also define an object here for populating the response&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;itemQueryPopulate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;brand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fields&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="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&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;manufacturer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fields&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="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rating&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  /lib - Database Interaction
&lt;/h2&gt;

&lt;p&gt;This folder contains all the methods that will be used in the code. It is the only place where direct database interaction occurs.&lt;/p&gt;

&lt;p&gt;In the method files, zod schemas and expected responses are defined. For example:&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;// .../lib/getItemById.ts&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;GetProps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;infer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;zGetProps&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;zGetProps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coerce&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="nx"&gt;int&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;positive&lt;/span&gt;&lt;span class="p"&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;type&lt;/span&gt; &lt;span class="nx"&gt;GetResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;infer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;zGetResult&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;zGetResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;zDbItemQuery&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;getProductById&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;id&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;GetProps&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;GetResult&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;try&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;query&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;strapiSdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;findOne&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DbItemQuery&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;dbTables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&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="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;populate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;itemQueryPopulate&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;query&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;fault&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ErrCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CantFindError&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="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="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object&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;zDbItemQuery&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&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;success&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="c1"&gt;//catch&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;ZodError&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;fault&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ErrCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DbReturnWrongFormat&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;fault&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ErrCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SomeError&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;Now we have isolated methods in a separate folder for working with the database. If something changes in its logic or if you need to optimize a specific query, you can do it right here while preserving the argument and response types.&lt;/p&gt;

&lt;h2&gt;
  
  
  /api - Bridging Frontend and Backend
&lt;/h2&gt;

&lt;p&gt;Functions in this folder are responsible for executing the application's business logic. Each feature is isolated in a separate folder and contains both a function that will be called on the frontend side (&lt;code&gt;getItemByIdClient&lt;/code&gt;) and a function that will perform work on the server (&lt;code&gt;getItemByIdServer&lt;/code&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;api
 ├── get-item-by-id
       ├── getItemByIdClient.ts
       ├── getItemByIdServer.ts
       ├── config.ts
       └── index.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The main file here is &lt;code&gt;config.ts&lt;/code&gt;, which contains information about the types and interfaces used by the functions, as well as the path to the API route that will be called from the frontend.&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;// .../api/get-item-by-id/config.ts&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;z&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;zod&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&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;paths&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;configs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&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;zGetResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;zGetProps&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;../../lib/getItemById&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// path&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;PATH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// type Props&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;TPropsServer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;infer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;zPropsServer&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;zPropsServer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;zGetProps&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;type&lt;/span&gt; &lt;span class="nx"&gt;TPropsClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;infer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;zPropsClient&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;zPropsClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;zPropsServer&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;type&lt;/span&gt; &lt;span class="nx"&gt;TResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;infer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;zGetResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Essentially, this is a set of re-exports needed to simplify code organization.&lt;/p&gt;

&lt;p&gt;In this example, the arguments for both Client and Server are the same, but they can differ if, for instance, the Server function requires additional user ID obtained from cookies in the &lt;code&gt;route.ts&lt;/code&gt; file and passed to the &lt;code&gt;getItemByIdServer&lt;/code&gt; function.&lt;/p&gt;

&lt;p&gt;Yes, the necessary code is located in another folder. Currently, this is the way it needs to be organized. When &lt;code&gt;Server Actions&lt;/code&gt; are no longer experimental, you can eliminate this step. Additionally, I haven't tried the ts-rest library, which is aimed at solving a similar problem.&lt;/p&gt;

&lt;p&gt;The file looks something like this:&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;// app/api/items/[id]/route.ts&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;pathname&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="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&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;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pathname&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&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;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;part&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;part&lt;/span&gt; &lt;span class="o"&gt;!==&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;at&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&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;getItemByIdServer&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&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 &lt;code&gt;getItemByIdServer&lt;/code&gt; function not only returns a response but also an HTTP status code:&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;// getItemByIdServer.ts&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;getItemByIdServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TPropsServer&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;TResult&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;checkProps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;zQueryProps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;safeParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&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;checkProps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&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="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;WrongData&lt;/span&gt;&lt;span class="dl"&gt;"&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="nx"&gt;checkProps&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;HttpStatusCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BadRequest&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="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="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;checkProps&lt;/span&gt;

    &lt;span class="k"&gt;try&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;result&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;getCompanyById&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isSuccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;HttpStatusCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Ok&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="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;HttpStatusCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BadRequest&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;e&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="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ErrCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;UnknownError&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;Неизвестная ошибка&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;HttpStatusCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;InternalServerError&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;Here, instead of returning a regular error code, we throw an error because the function will be used in the &lt;code&gt;useQuery&lt;/code&gt; hook from &lt;code&gt;ReactQuery&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Both the server-side and client-side validate the received data using &lt;code&gt;Zod&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  /hooks - Adding ReactQuery
&lt;/h2&gt;

&lt;p&gt;Hooks are placed in separate folders.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;│   │   ├── hooks
│   │   │   ├── use-get-item-by-id
│   │   │   │   ├── queryKey.ts
│   │   │   │   ├── useGetItemById.ts
│   │   │   │   └── index.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the &lt;code&gt;queryKey&lt;/code&gt; file, you simply define and export the key that will be used for this hook. It can be used in other parts of the application, for example, if you need to perform an invalidate operation for the query.&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;// .../queryKey.ts&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;QUERY_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;item-get-by-id&lt;/span&gt;&lt;span class="dl"&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 hook itself is simple - it only calls a previously created function for the frontend part:&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="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@tanstack/react-query&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&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;GetItemByIdPropsClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getItemByIdClient&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;shared/database&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&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;QUERY_KEY&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;./queryKey&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;type&lt;/span&gt; &lt;span class="nx"&gt;UseGetItemProps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;GetItemByIdPropsClient&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;useGetItemById&lt;/span&gt; &lt;span class="o"&gt;=&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="nx"&gt;UseGetItemProps&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;queryKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;QUERY_KEY&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="k"&gt;return&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;queryKey&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="nx"&gt;getItemByIdClient&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="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;suspense&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...and all of this is exported in the &lt;code&gt;index.ts&lt;/code&gt; file:&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;// index.ts&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;QUERY_KEY&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;QUERY_KEY_GET_ITEM&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;./queryKey&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useGetItemById&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;./useGetItemById&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, in your component, you can simply call the hook to retrieve the necessary data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="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;item&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useGetItemById&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;In the example provided, it may seem like an excessive separation of a simple query into so many parts. However, this approach allows you to organize your code into isolated and reusable components, each focused on handling a specific level of abstraction.&lt;/p&gt;

&lt;p&gt;As a result, understanding the code and conducting refactoring becomes easier, as it is immediately clear which types of tasks are being solved where. This modular structure also promotes code maintainability and scalability, making it easier to work on and extend the application over time.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>zod</category>
      <category>react</category>
      <category>typescript</category>
    </item>
    <item>
      <title>My experience with FSD (Feature-Sliced Design) architecture.</title>
      <dc:creator>Petr Tcoi</dc:creator>
      <pubDate>Mon, 03 Jul 2023 07:45:08 +0000</pubDate>
      <link>https://forem.com/petrtcoi/my-experience-with-fsd-feature-sliced-design-architecture-5a6n</link>
      <guid>https://forem.com/petrtcoi/my-experience-with-fsd-feature-sliced-design-architecture-5a6n</guid>
      <description>&lt;p&gt;In this article, I want to share my experience of developing applications using the FSD (Feature-Sliced Design) approach. I won't go into detail about it here, as plenty of good resources are already available, primarily the &lt;a href="https://feature-sliced.design" rel="noopener noreferrer"&gt;official website&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I found this approach to be very convenient. It not only applies well to new projects that can be structured appropriately from the start but also to projects that someone else had previously worked on and left in a tangled state. By following simple rules step by step, it is possible to organize and make any codebase convenient for further development. This article will focus on these simple rules, which I couldn't find and had to develop through my own experience. I hope my experience will be useful to others.&lt;/p&gt;

&lt;h2&gt;
  
  
  The basic idea
&lt;/h2&gt;

&lt;p&gt;The entire code is divided into layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;app&lt;/strong&gt; - the top-level of the application;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pages&lt;/strong&gt; - individual pages or screens of the application;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;widgets&lt;/strong&gt; - interface blocks that make up a page. For example, a top menu, a shopping cart, etc. Ideally, a page should be as "thin" as possible and primarily consist of widgets, each of which operates independently of others;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;features&lt;/strong&gt; - a business feature that provides value to the user. For example, adding and removing items from a shopping cart, calculating the total amount and discounts;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;entities&lt;/strong&gt; - simplified, these are the project's data entities: products, users, blog posts, etc.;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;shared&lt;/strong&gt; - resources used by all other layers within the application. This can include utilities, interfaces, and configurations for third-party services (database connections, Twilio CLI, etc.).&lt;/li&gt;
&lt;/ul&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%2F4dgq3sjatrqkzelpt5er.jpg" 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%2F4dgq3sjatrqkzelpt5er.jpg" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each element is self-contained and contains everything necessary for its operation: UI components, types, and interfaces, utilities, etc. Only what needs to be accessible from the outside, via a public API, is exported.&lt;/p&gt;

&lt;p&gt;Hierarchy is important here. Elements can only import and use elements from lower-level layers. For example, an element in the features layer can use elements from the entities and shared layers, but it cannot utilize elements from its layer or higher layers. This rule ensures that the project maintains a clear and structured organization.&lt;/p&gt;

&lt;p&gt;For instance, when making changes to the "Cart" widget, we don't have to worry about impacting the "Product List" widget or altering the logic of the feature responsible for calculating discount amounts. The changes will only affect the widget itself and the pages where it is located. Therefore, the lower the level we descend, the more global and impactful the changes become.&lt;/p&gt;

&lt;p&gt;Now, let me describe the approach that helps me create projects with a convenient structure for development.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1. Shared Layer: Third-Party Services
&lt;/h2&gt;

&lt;p&gt;Configuring database connections, authentication, SMS sending services, and so on. For these purposes, I have created a separate folder called "shared/services."&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

shared
├── services
│     ├── pinata
│     ├── prisma
│     │     ├── config
│     │     │     ├── prisma.ts
│     │     ├── index.ts
│     ├── twilio


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

&lt;/div&gt;

&lt;p&gt;You can also include the main types related to the operation of a REST API and similar functionalities.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

shared
├── types
│     ├── api
│     ├── result


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

&lt;/div&gt;

&lt;p&gt;Now that all the third-party services with which our application will interact are neatly organized in one place, we can move on to the second step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2. Defining Entities
&lt;/h2&gt;

&lt;p&gt;This step also does not require any complex project assessments. Here, you define the business entities and the interfaces for interacting with them. The goal of this stage is to abstract the work with entities as much as possible for all the elements in the higher layers.&lt;/p&gt;

&lt;p&gt;For example, let's consider the business entity "NFT" (Non-Fungible Token) in the context of using Prisma as an example.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

entities
├── collection
├── drop
├── nft
│     ├── db
│     │     ├── dbGetNftBasic.ts
│     │     ├── dbGetNftBasicList.ts
│     │     ├── dbGetNftMint.ts
│     │     ├── ...
│     ├── selectors
│     ├── types
│     ├── ui
│     │     ├── BuyButton
│     ├── utils
│     ├── index.ts


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

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;db&lt;/strong&gt; - This is where all the database queries and interactions used in the application are defined. This allows for easier testing and enables query optimization if needed.
selectors - These are selectors used for querying the database through Prisma.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;types&lt;/strong&gt; - These are types generated from the selectors. In principle, the selectors and types can be combined into a single folder if desired.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ui&lt;/strong&gt; - This folder contains any shared UI components. In your case, it includes a button component that redirects visitors to the NFT purchase page. Placing the button component here makes sense because it is used in multiple features, and code sharing between features is prohibited since they are in the same feature layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;utils&lt;/strong&gt; - This folder contains utility functions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;index.ts&lt;/strong&gt; - This file defines what will be exposed to the rest of the code from all the aforementioned folders.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 3. Widgets and Features
&lt;/h2&gt;

&lt;p&gt;These two layers contain the business logic. I combined them into one step because the boundary between them is not always clear. Formally, the distinction should be as follows:&lt;/p&gt;

&lt;p&gt;Feature: Represents a valuable action, such as user registration or product editing form.&lt;/p&gt;

&lt;p&gt;Widget: Renders components, creating an isolated block that can be placed on pages.&lt;/p&gt;

&lt;p&gt;In practice, there are many boundary situations. Therefore, by default, I recommend treating any element as a Widget and placing all related code in its folder. If a part of a widget is needed in another widget, it is extracted into a separate feature - it is moved to a lower layer to become available for all widgets.&lt;/p&gt;

&lt;p&gt;The sequence of actions is as follows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;After defining the connections with external services and business entities, start creating a widget in a separate folder, placing all the necessary code in it without trying to separate its parts into separate elements outside that folder.&lt;/li&gt;
&lt;li&gt;Ideally, the widget should be self-contained, only interacting with our entities, and freely placed on any page. All the code related to it will be located in one place and guaranteed not to impact other parts of the application.&lt;/li&gt;
&lt;li&gt;If, when adding other widgets, it becomes apparent that they need to use the code from our widget, for example, a form for entering credit card data and processing a payment request, that form is extracted as a separate element in the feature layer. Now different widgets can use it for their purposes, and the widget where it was originally located remains isolated as before.&lt;/li&gt;
&lt;li&gt;In case, for some reason, the functionality of sending a payment request from a card is needed in another feature, it can be moved to the lowest layer - shared. This way, it becomes something like this (assuming the &lt;code&gt;authorizenet&lt;/code&gt; service handles card-related tasks):&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

shared
├── services
│     ├── authorizenet
│     │     ├── config
│     │     ├── utils
│     │     │     ├── chargeCreditCard.ts
│     │     ├── index.ts
│     ├── twilio


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

&lt;/div&gt;

&lt;p&gt;Yes, the general rule is to try to place the code at the highest possible layer, which is within the widgets, and move it to lower layers only when necessary. This approach helps to maintain a clear separation of concerns and isolate the functionality within its respective layer. The goal is to strike a balance between encapsulation and reusability, ensuring that each layer has its specific responsibilities while allowing flexibility when needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4. Pages
&lt;/h2&gt;

&lt;p&gt;This step involves the highest level of architecture. Pages should be kept as "thin" as possible and only handle tasks such as loading data related to the page (e.g., fetching information about a product based on the product slug in the URL) and determining the layout of widgets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Upon initial reading, the FSD architecture may raise more questions than answers. However, in practice, it proves to be quite understandable and straightforward to work with.&lt;/p&gt;

&lt;p&gt;A significant portion of the code is organized into the shared and &lt;code&gt;entities&lt;/code&gt; folders, serving general purposes. The remaining elements are encapsulated within isolated widgets, and as needed, they can be moved to lower layers.&lt;/p&gt;

&lt;p&gt;Code structured in this way is relatively easy to maintain. You can quickly identify "safe" and "risky" parts for modification, as well as understand the impact of changes. When modifying a bank card input form located in the features layer, for example, you can make changes without worrying about unforeseen effects on other features or code located in lower layers. You only need to review the few widgets that use the form. If a utility is placed within a widget's folder, it means it is used exclusively by that widget and nowhere else.&lt;/p&gt;

&lt;p&gt;Compare this convenience to dealing with cluttered &lt;code&gt;components&lt;/code&gt; and &lt;code&gt;lib&lt;/code&gt; folders.&lt;/p&gt;

&lt;p&gt;For my projects, I now always strive to align the code with this architecture, regardless of the application's size.&lt;/p&gt;

</description>
      <category>fsd</category>
      <category>featuresliceddesign</category>
    </item>
    <item>
      <title>State management with Astro and @nanostores</title>
      <dc:creator>Petr Tcoi</dc:creator>
      <pubDate>Fri, 12 May 2023 05:25:24 +0000</pubDate>
      <link>https://forem.com/petrtcoi/state-management-with-astro-and-nanostores-5h17</link>
      <guid>https://forem.com/petrtcoi/state-management-with-astro-and-nanostores-5h17</guid>
      <description>&lt;p&gt;A convenient opportunity arose to try creating a small website using the &lt;code&gt;AstroJS&lt;/code&gt; framework - the radiator store &lt;a href="https://velarshop.ru" rel="noopener noreferrer"&gt;velarshop.ru&lt;/a&gt; (more like an online catalog).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Astro's&lt;/code&gt; work is based on an "islands architecture" approach: each page of the website is considered not as a whole but as a set of &lt;code&gt;islands&lt;/code&gt; responsible for displaying their respective parts of the page. Most of these islands consist of statically generated HTML that is sent to the client immediately. Islands that require JavaScript functionality load it after rendering, adding the necessary interactivity. This approach allows for achieving astonishing speed in application performance.&lt;/p&gt;

&lt;p&gt;For example, the product page has such PageSpeed scores.&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%2F4zrg3rxmu3ectwqs5e4r.jpg" 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%2F4zrg3rxmu3ectwqs5e4r.jpg" alt="Image description" width="800" height="162"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;An additional advantage is that both &lt;code&gt;Vanilla JavaScript&lt;/code&gt; components and components written with &lt;code&gt;React&lt;/code&gt;, &lt;code&gt;Vue&lt;/code&gt;, &lt;code&gt;Solid&lt;/code&gt;, &lt;code&gt;Preact&lt;/code&gt;, or &lt;code&gt;Svelte&lt;/code&gt; can be used as interactive components.&lt;/p&gt;

&lt;p&gt;For my stack, I chose &lt;code&gt;Preact&lt;/code&gt; because it closely resembles &lt;code&gt;React&lt;/code&gt;, which I am familiar with, and it has minimal size.&lt;/p&gt;

&lt;p&gt;It's desirable to keep such components small and place them as far as possible in the application's structure. This way, we achieve atomic "islands" that communicate with each other solely through shared state and, when it comes to page transitions, through &lt;code&gt;localStorage&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Application Structure
&lt;/h2&gt;

&lt;p&gt;The application is quite simple: it consists of a set of pages generated based on the price list and technical catalog of the manufacturer. These pages provide descriptions of products with the ability to add selected items to the shopping cart.&lt;/p&gt;

&lt;p&gt;The state is only needed here to store information about the products added to the cart and the options chosen by the visitor (such as radiator color, casing material, etc.).&lt;/p&gt;

&lt;p&gt;Since &lt;code&gt;Astro&lt;/code&gt; is a set of separate pages, I used the &lt;code&gt;@nanostores/persistent&lt;/code&gt; library to persist the state between page transitions. This library synchronizes the data with &lt;code&gt;localStorage&lt;/code&gt; and retrieves the latest data from it upon each page reload.&lt;/p&gt;

&lt;p&gt;Below is a diagram of a product page, indicating the main blocks that require interactivity, modification, or consume data from the state.&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%2Fqn26236cks7utiv21j4y.jpg" 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%2Fqn26236cks7utiv21j4y.jpg" alt="Image description" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
The blocks that modify the state are marked in red:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Product options: color, connection type, etc.&lt;/li&gt;
&lt;li&gt;Adding/removing products from the shopping cart.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The blocks that consume the state are highlighted in blue:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Product names and prices, which change depending on the selected filters.&lt;/li&gt;
&lt;li&gt;The shopping cart, displays the quantity and total amount of the added products.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Item Options Selection
&lt;/h2&gt;

&lt;p&gt;The fundamental element of the state is the product options. They determine both the price and the item titles.&lt;/p&gt;

&lt;p&gt;For each option, we create a separate folder with the following structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;│
├── features
│     ├── options
│     │     ├── SelectConnection
│     │     ├── SelectGrill
│     │     ├── SelectColor
│     │     │     ├── store
│     │     │     │     ├── color.ts
│     │     │     ├── SelectColor.tsx
│     │     │     ├── index.ts
│     │
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We end up with isolated folders, each containing everything necessary: a JSX component that can be placed anywhere on the website, and a piece of state in the store folder. In the case of color, the store looks like this:&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="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;features&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;SelectColor&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&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;сolors&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;@entities/Сolor&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;persistentAtom&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;@nanostores/persistent&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;computed&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;nanostores&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;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&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;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PUBLIC_LOCAL_STORAGE_VERSION&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;colorId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;persistentAtom&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`velarshop_color_active/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt; &lt;span class="nx"&gt;version&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="dl"&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;color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;colorId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;colorId&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="nx"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;color&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;color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;colorId&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;colorPostfix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;color&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;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;color&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt; &lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&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;colorPricePerSection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;color&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;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;color&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price_section&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;colorId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;colorPostfix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;colorPricePerSection&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Here, I used a version of the store called "version" in case there are updates to the price list or color palette, to ensure that old data from &lt;code&gt;localStorage&lt;/code&gt; doesn't mix with the updated data.&lt;/p&gt;

&lt;p&gt;There is only one variable, &lt;code&gt;colorId&lt;/code&gt;, which represents the user's selection. The other variables are derived from it. Their values are automatically recalculated whenever the user changes the color.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Color&lt;/code&gt; contains all the data about the color. It is selected from an array based on &lt;code&gt;colorId&lt;/code&gt;. This variable is not exported and is used to derive the following two variables.&lt;br&gt;
&lt;code&gt;colorPrefix&lt;/code&gt; is computed based on the &lt;code&gt;color&lt;/code&gt; and is used in the application to display the item title (e.g., &lt;em&gt;VelarP30H white&lt;/em&gt; or &lt;em&gt;VelarP30H black&lt;/em&gt;).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;colorPricePerSection&lt;/code&gt; represents the price of painting one radiator section. It is used in calculating the final cost of the radiator.&lt;/p&gt;

&lt;p&gt;Thus, we have a set of isolated components that can be placed in a convenient location for the user to choose suitable options.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;colorPrefix&lt;/code&gt; is simply added at the end of the displayed item titles.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;colorPricePerSection&lt;/code&gt; is used in a more complex manner and is involved in calculating the final cost of each item.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Calculating the cost of the radiator
&lt;/h2&gt;

&lt;p&gt;We can't simply add the cost of painting one radiator section to the final cost because we also need to know the number of sections.&lt;/p&gt;

&lt;p&gt;For example, for &lt;a href="https://velarshop.ru/model/nostalgia500/" rel="noopener noreferrer"&gt;cast iron radiators&lt;/a&gt;, we display the following table:&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%2Fumr43t4td2m65goln7z2.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%2Fumr43t4td2m65goln7z2.png" alt="Image description" width="800" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In each row, a different number of sections is displayed, ranging from 3 to 15. Consequently, the total cost of painting the radiator varies. Therefore, we cannot simply pass the painting price value from the store. Instead, we will pass a function that calculates the radiator cost based on the radiator data, including the number of sections.&lt;/p&gt;

&lt;p&gt;To achieve this, we create a separate store responsible for calculating the product price based on the available parameters:&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="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;features&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;ItemTotalCost&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&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;computed&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;nanostores&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;colorPricePerSection&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;@features/options/SelectColor&lt;/span&gt;&lt;span class="dl"&gt;'&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getColorCost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;colorPricePerSection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;colorPricePerSection&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;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ModelJson&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;radiator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RadiatorJson&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;colorPricePerSection&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;radiator&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;sections&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getItemTotalCost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="nx"&gt;getColorCost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getConnectionCost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getSomeOtherCost&lt;/span&gt; &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nx"&gt;getColorCost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getConnectionCost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getSomeOtherCost&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;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ModelJson&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;radiator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RadiatorJson&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="nf"&gt;getColorCost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;radiator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;getConnectionCost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;radiator&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="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getItemTotalCost&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we have a function that we can pass to the product table and obtain the total cost for each variant. The cost will automatically update every time the user changes the selected options.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shopping cart
&lt;/h2&gt;

&lt;p&gt;The shopping cart is also placed in a separate folder, which contains the &lt;code&gt;&amp;lt;BuyButton /&amp;gt;&lt;/code&gt; component responsible for adding items to the cart, as well as the store responsible for calculating the total purchase amount.&lt;/p&gt;

&lt;p&gt;The store looks like this:&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="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;features&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;ShoppingCart&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&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;persistentAtom&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;@nanostores/persistent&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;computed&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;nanostores&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ShoppingCart&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;@entities/shopping-cart&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;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&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;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PUBLIC_LOCAL_STORAGE_VERSION&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;storeShoppingCart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;persistentAtom&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ShoppingCart&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`velarshop_shopping_cart/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt; &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;items&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="na"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parse&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;storeCartTotalPrice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;storeShoppingCart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;shoppingCart&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="nx"&gt;shoppingCart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&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;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;item&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;total&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;qnty&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;storeCartTotalQnty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;storeShoppingCart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;shoppingCart&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="nx"&gt;shoppingCart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&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;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;item&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;total&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;qnty&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;storeUniqueItemsQnty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;storeShoppingCart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;shoppingCart&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="nx"&gt;shoppingCart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&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;export&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;storeShoppingCart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;storeCartTotalPrice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;storeCartTotalQnty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;storeUniqueItemsQnty&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, the logic is similar: there is the main variable, &lt;code&gt;storeShoppingCart&lt;/code&gt;, where the added items are stored, and there are derived variables used to display data in the application.&lt;/p&gt;

&lt;p&gt;The only difference is the addition of the &lt;code&gt;encode&lt;/code&gt;/&lt;code&gt;decode&lt;/code&gt; properties when creating &lt;code&gt;storeShoppingCart&lt;/code&gt;. Since it is not a primitive but an array of objects, specifying how to transform the data before saving and retrieving it from local storage is necessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Working with &lt;strong&gt;AstroJS&lt;/strong&gt; has proven to be quite simple and enjoyable. The need to embed JSX components as isolated blocks help maintain the overall architecture of the application.&lt;/p&gt;

&lt;p&gt;When compared to &lt;strong&gt;NextJS&lt;/strong&gt;, at least for small and simple websites, Astro is much easier and more pleasant to work with. If we add the impressive PageSpeed scores to the equation, the choice in favor of this framework becomes even more evident.&lt;/p&gt;

&lt;p&gt;P.S. I haven't had the opportunity to work with the new features of Next13 (with the app folder) yet. Therefore, the comparison with Astro may not be entirely fair.&lt;/p&gt;

</description>
      <category>astro</category>
      <category>preact</category>
    </item>
    <item>
      <title>Performance of the spread operator</title>
      <dc:creator>Petr Tcoi</dc:creator>
      <pubDate>Tue, 04 Apr 2023 19:32:01 +0000</pubDate>
      <link>https://forem.com/petrtcoi/performance-of-the-spread-operator-2bpk</link>
      <guid>https://forem.com/petrtcoi/performance-of-the-spread-operator-2bpk</guid>
      <description>&lt;p&gt;In React, a common way to modify object properties is by using the spread operator. Its syntax is simple and easy to understand.&lt;/p&gt;

&lt;p&gt;While the spread operator is a convenient way to achieve this, it can be less performant than using a traditional loop. This is because the spread operator creates a new array or object every time it is used, which can be expensive when working with large arrays/objects.&lt;/p&gt;

&lt;p&gt;Let's consider an example: we have an input array of objects, and we need to create a new object of key-value pairs from it.&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;faker&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@faker-js/faker&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// generate fakeData&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fakeData&lt;/span&gt; &lt;span class="o"&gt;=&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;let&lt;/span&gt; &lt;span class="nx"&gt;i&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="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&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;fakeData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;faker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;datatype&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uuid&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;faker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cityName&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="cm"&gt;/* fakeData:
[
  { key: '0bc9a57c-7fd1-449a-8d98-6396f722535a', name: 'Abilene' },
  { key: '2ac57365-bc80-45a1-8033-9efd33de4a52', name: 'Aloha' },
  { key: 'a7d64eaa-0202-4c18-ade1-f43b0853c29c', name: 'Johns Creek' },
  { key: '129a89a6-490a-48b1-9394-7d143926e7d0', name: 'Chicopee' },
  { key: '2d606536-7727-496d-bbee-9663b89f40b9', name: 'Covina' }
]
*/&lt;/span&gt;

&lt;span class="c1"&gt;// generate an object with reduce method&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;object&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fakeData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;key&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;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="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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&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="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;

&lt;span class="cm"&gt;/* object:
{
  '83e12032-7558-467e-b840-ead992754df4': 'Jackson',
  '4fe2ce86-b202-4891-8b2f-7fa154b4b448': 'Idaho Falls',
  'de1d95c0-3c25-4b8c-9e1d-a8bc20409d45': 'El Centro',
  'b54dd7d7-b021-4fc5-9de6-633ee4e240bf': 'Fort Pierce',
  'dbee592f-79a0-461a-b477-40ae53f0ff53': 'Palm Springs'
}
*/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you run the code, everything will work very quickly. However, the potential problem here is that on every iteration of the reduce loop, the entire object is fully copied.&lt;/p&gt;

&lt;p&gt;Let's add some code to simply measure the execution time of the function and increase the size of the array:&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="p"&gt;...&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;funcSpread&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;fakeData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;key&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;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="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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&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="p"&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="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;execution time&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;funcSpread&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="nx"&gt;timeEnd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;execution time&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I have the following numbers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For an array length of 5, the execution time was 0.05ms&lt;/li&gt;
&lt;li&gt;For an array length of 1000, the execution time was 115ms&lt;/li&gt;
&lt;li&gt;For an array length of 5000, the execution time was 2.4 seconds!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Such execution times are likely not acceptable for either frontend or backend applications. To speed up the code, we will need to abandon the principles of immutability and simply mutate the existing object, instead of copying it on each iteration.&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="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;funcMutate&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;fakeData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;key&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;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;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;acc&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This function will process an array of 5000 objects in just 2.5ms, which is 1000 times faster!&lt;/p&gt;

&lt;p&gt;A slightly slower but still quite fast solution for this task would be to use the &lt;code&gt;_.set&lt;/code&gt; method from the popular &lt;code&gt;lodash&lt;/code&gt; library:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lodash&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;funcLodash&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;fakeData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;key&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;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;_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;set&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;key&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="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;The execution time was 4.6ms, which is also quite fast.&lt;/p&gt;

&lt;p&gt;Let's consider other ways to solve this problem while still adhering to the principle of immutability.&lt;/p&gt;

&lt;p&gt;Firstly, I tried using &lt;code&gt;Ramda&lt;/code&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;R&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ramda&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;funcRamda&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;fakeData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;key&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;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;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;assoc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&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="nx"&gt;acc&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;p&gt;The execution time was 1.4 seconds, which is still slow, but faster than using the spread operator.&lt;/p&gt;

&lt;p&gt;The next popular library is &lt;code&gt;Immutable.js&lt;/code&gt;. This library works with its data structures, so here we will deviate even further from the purity of the experiment and will be creating a &lt;code&gt;Map&lt;/code&gt; from &lt;code&gt;Immutable.js&lt;/code&gt; instead of an object.&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;immutable&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;funcImmutable&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;fakeData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;key&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;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;acc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&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="p"&gt;},&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;({}))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result was 13 ms, which is very close to our best result. The bottleneck of &lt;code&gt;Immutable.js&lt;/code&gt; is considered to be the conversion of the obtained data into a regular object. However, in this case, it had very little impact on overall performance:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mapData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;funcImmutable&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;object&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mapData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toObject&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same result will be obtained here. Immutable data is very fast!&lt;/p&gt;

&lt;p&gt;The last option will be &lt;code&gt;Immer&lt;/code&gt;. First, let's try a straightforward approach:&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;produce&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;immer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;funcImmer&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;fakeData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;key&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;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;produce&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;draft&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;draft&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;name&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;p&gt;...and get a catastrophic 19 seconds! But if we wrap not each step, but the entire function in &lt;code&gt;produce&lt;/code&gt;, we can achieve significant improvement:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;funcImmer2&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;produce&lt;/span&gt;&lt;span class="p"&gt;({},&lt;/span&gt; &lt;span class="nx"&gt;draft&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;fakeData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;key&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;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;name&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;The result is 8.5 ms. This is still slower than funcMutate or funcLodash, but quite close.&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusions?
&lt;/h1&gt;

&lt;p&gt;It is unlikely that any practical conclusions can be drawn from this mini-experiment. Yes, the spread operator works quite slowly and, in some cases, can be a bottleneck in the work of your application when dealing with large objects or arrays.&lt;/p&gt;

&lt;p&gt;Significant performance improvements can be achieved by abandoning the principle of immutable data. If this option does not suit you, then &lt;code&gt;Immer&lt;/code&gt; or &lt;code&gt;Immutable&lt;/code&gt; can be a good solution.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>immutable</category>
      <category>ramda</category>
      <category>immer</category>
    </item>
  </channel>
</rss>
