<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sanjay Singh</title>
    <description>The latest articles on DEV Community by Sanjay Singh (@sanjay_singh_1).</description>
    <link>https://dev.to/sanjay_singh_1</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3961409%2F9efd98f2-cabb-46b0-817f-1f8cefd2c79b.jpg</url>
      <title>DEV Community: Sanjay Singh</title>
      <link>https://dev.to/sanjay_singh_1</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sanjay_singh_1"/>
    <language>en</language>
    <item>
      <title>How to Use a Custom Domain for Your ZyVOP Publication</title>
      <dc:creator>Sanjay Singh</dc:creator>
      <pubDate>Wed, 21 Oct 2026 10:35:48 +0000</pubDate>
      <link>https://dev.to/sanjay_singh_1/how-to-use-a-custom-domain-for-your-zyvop-publication-2kdp</link>
      <guid>https://dev.to/sanjay_singh_1/how-to-use-a-custom-domain-for-your-zyvop-publication-2kdp</guid>
      <description>&lt;p&gt;When engineers read technical content, the URL in the browser sends an immediate trust signal. An architectural deep-dive hosted on &lt;code&gt;blog.yourbrand.dev/distributed-locks&lt;/code&gt; carries immediate credibility; the same article published on a shared platform path like &lt;code&gt;platform.com/@user/12345&lt;/code&gt; always feels like a guest post on rented land.&lt;/p&gt;

&lt;p&gt;Yet connecting a custom domain to a modern publishing engine often turns into a minefield of edge cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;DNS Protocol Limits&lt;/strong&gt;: RFC 1034 prohibits standard CNAME records on root apex domains, creating routing headaches for writers who want clean root URLs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Certificate Handshake Loops&lt;/strong&gt;: Managing upstream TLS alongside third-party DNS proxies (like Cloudflare orange-cloud) frequently causes verification to stall or certificates to fail renewal.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SEO Cannibalization&lt;/strong&gt;: Cross-posting to community feeds without strict edge-level canonical rewriting splits your domain authority, leaving external aggregators ranking above your primary site.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Link Rot on Plan Changes&lt;/strong&gt;: Most hosted platforms abruptly disconnect custom domains the instant a subscription lapses, returning 404s and permanently damaging hard-earned backlinks.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ZyVOP was architected to eliminate these failure modes. On &lt;strong&gt;Founding Pro&lt;/strong&gt;, you can attach your personal domain or company publication in minutes—complete with automated edge TLS, host-level 308 normalization, dynamic XML sitemaps, and built-in canonical protection.&lt;/p&gt;

&lt;p&gt;This guide walks through the exact mechanics of ZyVOP's custom domain engine and provides step-by-step instructions to configure your DNS correctly on the first attempt.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. System Architecture: Decoupled Identity &amp;amp; Edge Ingress
&lt;/h2&gt;

&lt;p&gt;ZyVOP strictly decouples the human developer from the publication brand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Developer Profile (&lt;/strong&gt;&lt;code&gt;zyvop.com/author/:username&lt;/code&gt;&lt;strong&gt;)&lt;/strong&gt;: Your personal identity on the ZyVOP developer network. Houses your personal bio, avatar, followers, comments, and global bylines.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Publication (&lt;/strong&gt;&lt;code&gt;blog.yourdomain.com&lt;/code&gt; &lt;strong&gt;or&lt;/strong&gt; &lt;code&gt;yourdomain.com&lt;/code&gt;&lt;strong&gt;)&lt;/strong&gt;: Your independent media asset. Features custom typography, dark/light brand logos, custom favicon, static pages (&lt;code&gt;/about&lt;/code&gt;, &lt;code&gt;/projects&lt;/code&gt;), Google AdSense (&lt;code&gt;/ads.txt&lt;/code&gt;), and dedicated feeds.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fpako%3AeNqNVMFum0AQ_ZXpHqJEwnZb9eRDJcc2KVLrWpBYSiCHAcZ45WWX7C6xSeJ_rxZK4iQXc2H38ebtezPAM8tUTmzMCo3VBq5niQQAMHXaAYEsNBkDv7EhDfE8LwhCVVsuCzjrn953Re66MaRDeogTFhLmpGEEU407QRpCeqjJ2ITdw2DwE6Z-nLCpUHW-FqgJ1kpDhBjBCCYV7eEKLe2wSdiR-tRvS52LpVb7Jk7YXbP6u2wRaCE4r9xtaM3FaynJPJEfgnVJyChRW65kH-0NOGtzOminuct75OPVQJdkQ9k2smhr85ywmSqRS-j2CTscuX_jubqX5XwxCxZXMILVPAz822Bx9QJLgXatdBknzEchUsy2YBU8NY-qGmaqhPM70gpmaictL-nifX8-nDCZXger-Us_saWmjHKSGcUJex2la1yPv1P7VNWm9fGRZ0rGCRutu-WQZwpcH2pjVdkTTpCKuKUSKydluuVwXwonNWskljzrGeAT5ScIhipV1jg93a6Gdm-d3LJOBc-wnWvHgbksuDwlbxhFraAxvbkwiuD78OuppgKZ036hdk5lvKWmNxVIY1FauOSyGN2iY8GKNF__d3qC9BILauNiqupPSd2LwLOWdILWRFueiU5ubERdvI10cKmdvbznwIrT7vjrYh4rSZfIczZ-ZnZDpful5LTGWljmdcgKNcdUkHGctZLWx5KLho3ZAKtK0MA0xlLpwaXgcvsHs6jd-0paDxIWUaEIboKEed0MlQe_SDyS5Rl6MNEchQcGpRkY10XmtYdE_Ml5-faj2rPDwWNpMVVCaTZmX3Ybbokd_gGnnqSA%3Ftype%3Dpng" 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%2Fmermaid.ink%2Fimg%2Fpako%3AeNqNVMFum0AQ_ZXpHqJEwnZb9eRDJcc2KVLrWpBYSiCHAcZ45WWX7C6xSeJ_rxZK4iQXc2H38ebtezPAM8tUTmzMCo3VBq5niQQAMHXaAYEsNBkDv7EhDfE8LwhCVVsuCzjrn953Re66MaRDeogTFhLmpGEEU407QRpCeqjJ2ITdw2DwE6Z-nLCpUHW-FqgJ1kpDhBjBCCYV7eEKLe2wSdiR-tRvS52LpVb7Jk7YXbP6u2wRaCE4r9xtaM3FaynJPJEfgnVJyChRW65kH-0NOGtzOminuct75OPVQJdkQ9k2smhr85ywmSqRS-j2CTscuX_jubqX5XwxCxZXMILVPAz822Bx9QJLgXatdBknzEchUsy2YBU8NY-qGmaqhPM70gpmaictL-nifX8-nDCZXger-Us_saWmjHKSGcUJex2la1yPv1P7VNWm9fGRZ0rGCRutu-WQZwpcH2pjVdkTTpCKuKUSKydluuVwXwonNWskljzrGeAT5ScIhipV1jg93a6Gdm-d3LJOBc-wnWvHgbksuDwlbxhFraAxvbkwiuD78OuppgKZ036hdk5lvKWmNxVIY1FauOSyGN2iY8GKNF__d3qC9BILauNiqupPSd2LwLOWdILWRFueiU5ubERdvI10cKmdvbznwIrT7vjrYh4rSZfIczZ-ZnZDpful5LTGWljmdcgKNcdUkHGctZLWx5KLho3ZAKtK0MA0xlLpwaXgcvsHs6jd-0paDxIWUaEIboKEed0MlQe_SDyS5Rl6MNEchQcGpRkY10XmtYdE_Ml5-faj2rPDwWNpMVVCaTZmX3Ybbokd_gGnnqSA%3Ftype%3Dpng" alt="Mermaid Diagram" width="1904" height="749"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Dual-Engine Ingress Strategy
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Custom Subdomains (&lt;/strong&gt;&lt;code&gt;blog.yourcompany.com&lt;/code&gt;&lt;strong&gt;,&lt;/strong&gt; &lt;code&gt;notes.dev&lt;/code&gt;&lt;strong&gt;)&lt;/strong&gt;: Powered by &lt;strong&gt;Cloudflare for SaaS Custom Hostnames&lt;/strong&gt;. A simple &lt;code&gt;CNAME&lt;/code&gt; record routes incoming HTTPS requests directly through ZyVOP's global CDN edge with automated TLS provisioning.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Root / Apex Domains (&lt;/strong&gt;&lt;code&gt;yourcompany.com&lt;/code&gt;&lt;strong&gt;)&lt;/strong&gt;: When apex routing is enabled, traffic resolves via dedicated &lt;code&gt;A&lt;/code&gt; records and cryptographic proxy headers (&lt;code&gt;x-zyvop-vercel-gateway-secret&lt;/code&gt;), or via an &lt;code&gt;ALIAS&lt;/code&gt;/&lt;code&gt;ANAME&lt;/code&gt; root flattening record.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2. Step-by-Step Configuration Guide
&lt;/h2&gt;

&lt;p&gt;Connecting a custom domain to your ZyVOP publication takes four simple steps:&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Open the Publication Routing Hub
&lt;/h3&gt;

&lt;p&gt;Navigate to your ZyVOP settings at &lt;code&gt;/settings?tab=publication&lt;/code&gt; and select the &lt;strong&gt;Domain &amp;amp; Routing&lt;/strong&gt; sub-tab.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/settings?tab=publication
  ├── [General]           Identity &amp;amp; Socials
  ├── [Appearance]        Logos, Favicon &amp;amp; Layout Engine (Magazine / Stacked / Grid)
  ├── [Domain &amp;amp; Routing]  Custom Domains, DNS Records &amp;amp; Canonical Mode  &amp;lt;-- SELECT
  ├── [Pages &amp;amp; Nav]       Static Pages (/about) &amp;amp; 301 Redirect Rules
  ├── [SEO &amp;amp; Export]      Decoupled OpenGraph Cards &amp;amp; Markdown ZIP Export
  └── [Monetization]      AdSense Publisher ID &amp;amp; ads.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you are on the Founding Pro tier, the domain management card will be unlocked and ready for input.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Enter Your Domain
&lt;/h3&gt;

&lt;p&gt;In the &lt;strong&gt;Custom domain&lt;/strong&gt; input, enter the address you want to connect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Recommended Subdomain&lt;/strong&gt;: &lt;code&gt;blog.yourname.com&lt;/code&gt; or &lt;code&gt;dev.yourcompany.com&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WWW Subdomain&lt;/strong&gt;: &lt;code&gt;www.yourbrand.com&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: System domains ending in &lt;code&gt;zyvop.com&lt;/code&gt; or &lt;code&gt;zyvop.dev&lt;/code&gt; are managed internally and cannot be entered.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 3: Configure Your DNS Provider
&lt;/h3&gt;

&lt;p&gt;Once you submit your hostname, ZyVOP calculates the exact DNS requirements for your provider (Cloudflare, Namecheap, AWS Route 53, Porkbun, or Google Cloud DNS).&lt;/p&gt;

&lt;h4&gt;
  
  
  Standard Subdomain Record (CNAME):
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Name / Host&lt;/th&gt;
&lt;th&gt;Target / Value&lt;/th&gt;
&lt;th&gt;TTL&lt;/th&gt;
&lt;th&gt;Proxy Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CNAME&lt;/td&gt;
&lt;td&gt;blog (or full blog.domain.com)&lt;/td&gt;
&lt;td&gt;cname.zyvop.com&lt;/td&gt;
&lt;td&gt;Auto (or 300)&lt;/td&gt;
&lt;td&gt;DNS-Only (Grey Cloud)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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%2Fmermaid.ink%2Fimg%2Fpako%3AeNp1Us1u2zAMfhVOhyIF7BTDtosPAVI7RQO0aRF3ORS5MBLjCJElV6K9pUXffZCdYNm6nQRS3x9FvQnpFIlMBHppyUoqNFYe67UFAMCWnW3rDfljLdl5KKgDDPEg45rTXYOetdQNWoZiUfaIRQmP3nVakYdRblyrtgY9wRUsXcsE375cfiQ_HzrXRPrzYfXwCNco92QVXMBMVfQRHrv5NOLPDO7RYkUK8unaDpSCunQy6bUzmCoFeRvY1VC4GrWF0ca4aoyyprGi7piqR6eTyWCRwZIqHZj8iXvrAlus6dyhWJRH_cX0fgZRFtIJyIgbv0bBsXQ1jIpFmT5Yc7j8R7zcaLmHtch3JPf9K15AWd6txZ-xeqslBWc6Otox-or4KLko09-aKzRaIZPKoHHacgB2f6f639QvLQWGp7sScvKst1oi07DZoJ3VthqYA-PMtGTkNmQwzZ_mqxlcxSlgHkJL6twrzkJddtrFibUir7eaFIwG_qVIRE2-Rq1E9iZ4R3X8uYq22BoWydBZode4MRQiZuss32CtzUFkIsWmMZSGQ2CqE7g22u7vUZZ9feMsJ7AWJVWO4Pt8LRJYuo1jl8AtmY5YS0xg6jWaBALakIaYTyS9SalfY5bPX5uf4v09EZsqd8Z5kYlPP3aaSbz_AhOvH5I%3Ftype%3Dpng" 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%2Fmermaid.ink%2Fimg%2Fpako%3AeNp1Us1u2zAMfhVOhyIF7BTDtosPAVI7RQO0aRF3ORS5MBLjCJElV6K9pUXffZCdYNm6nQRS3x9FvQnpFIlMBHppyUoqNFYe67UFAMCWnW3rDfljLdl5KKgDDPEg45rTXYOetdQNWoZiUfaIRQmP3nVakYdRblyrtgY9wRUsXcsE375cfiQ_HzrXRPrzYfXwCNco92QVXMBMVfQRHrv5NOLPDO7RYkUK8unaDpSCunQy6bUzmCoFeRvY1VC4GrWF0ca4aoyyprGi7piqR6eTyWCRwZIqHZj8iXvrAlus6dyhWJRH_cX0fgZRFtIJyIgbv0bBsXQ1jIpFmT5Yc7j8R7zcaLmHtch3JPf9K15AWd6txZ-xeqslBWc6Otox-or4KLko09-aKzRaIZPKoHHacgB2f6f639QvLQWGp7sScvKst1oi07DZoJ3VthqYA-PMtGTkNmQwzZ_mqxlcxSlgHkJL6twrzkJddtrFibUir7eaFIwG_qVIRE2-Rq1E9iZ4R3X8uYq22BoWydBZode4MRQiZuss32CtzUFkIsWmMZSGQ2CqE7g22u7vUZZ9feMsJ7AWJVWO4Pt8LRJYuo1jl8AtmY5YS0xg6jWaBALakIaYTyS9SalfY5bPX5uf4v09EZsqd8Z5kYlPP3aaSbz_AhOvH5I%3Ftype%3Dpng" alt="Mermaid Diagram" width="1306" height="581"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Crucial Cloudflare Rule&lt;/strong&gt;: If your DNS is managed on Cloudflare, set the CNAME record to &lt;strong&gt;DNS-Only (Grey Cloud)&lt;/strong&gt;. Enabling the orange cloud during provisioning intercepts the HTTP/TLS ownership challenge, causing certificate validation to hang in &lt;code&gt;VERIFYING&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Root / Apex Domain (When Direct Apex is Enabled):
&lt;/h4&gt;

&lt;p&gt;If configuring a root domain (&lt;code&gt;acme.dev&lt;/code&gt;), ZyVOP displays an &lt;strong&gt;A record&lt;/strong&gt; pointing to the designated edge IP, plus an ownership &lt;strong&gt;TXT record&lt;/strong&gt; (e.g. &lt;code&gt;_zyvop-challenge.acme.dev&lt;/code&gt;). If direct apex is disabled on your cluster, configure &lt;code&gt;www.acme.dev&lt;/code&gt; via CNAME and set a 301 forward rule from &lt;code&gt;acme.dev&lt;/code&gt; to &lt;code&gt;www.acme.dev&lt;/code&gt; at your registrar.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Verify and Activate
&lt;/h3&gt;

&lt;p&gt;Click &lt;strong&gt;Check DNS &amp;amp; SSL&lt;/strong&gt;. ZyVOP verifies your CNAME target in real-time, confirms CA issuance, and sets your domain status to &lt;strong&gt;Verified (&lt;/strong&gt;&lt;code&gt;ACTIVE&lt;/code&gt;&lt;strong&gt;)&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Edge Routing and First-Class Protocols
&lt;/h2&gt;

&lt;p&gt;Once your domain is &lt;code&gt;ACTIVE&lt;/code&gt;, ZyVOP's edge proxy automatically serves your entire publication ecosystem directly from your branded URL:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dynamic Technical SEO (&lt;/strong&gt;&lt;code&gt;/sitemap.xml&lt;/code&gt;&lt;strong&gt;)&lt;/strong&gt;: Automatically generates an index of all published articles and static pages with correct &lt;code&gt;&amp;lt;loc&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;lastmod&amp;gt;&lt;/code&gt;, and &lt;code&gt;&amp;lt;changefreq&amp;gt;&lt;/code&gt; tags.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Crawler Directives (&lt;/strong&gt;&lt;code&gt;/robots.txt&lt;/code&gt;&lt;strong&gt;)&lt;/strong&gt;: Emits publication-scoped directives linking to your custom domain's sitemap and honoring your AI Crawler opt-in/opt-out settings.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Syndication Feeds (&lt;/strong&gt;&lt;code&gt;/rss.xml&lt;/code&gt;&lt;strong&gt;)&lt;/strong&gt;: Serves a full RSS 2.0 feed with complete syntax-highlighted code enclosures and publication branding, ready for Feedly, Inoreader, and newsletter parsers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Instant Indexing (&lt;/strong&gt;&lt;code&gt;/&amp;lt;indexnow-key&amp;gt;.txt&lt;/code&gt;&lt;strong&gt;)&lt;/strong&gt;: Serves your unique IndexNow protocol key, notifying Bing, Yandex, and other search engines immediately when you publish a new article.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Brand Favicon (&lt;/strong&gt;&lt;code&gt;/favicon.ico&lt;/code&gt;&lt;strong&gt;)&lt;/strong&gt;: Dynamically redirects to your uploaded 32x32 publication favicon (or gracefully falls back to your profile avatar).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Monetization (&lt;/strong&gt;&lt;code&gt;/ads.txt&lt;/code&gt;&lt;strong&gt;)&lt;/strong&gt;: Outputs validated Google AdSense publisher declarations if monetization is enabled in your settings.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  4. Canonical SEO: Mode A vs. Mode B
&lt;/h2&gt;

&lt;p&gt;Developers often worry about splitting search authority when moving to a custom domain. ZyVOP solves this through an automated, centralized canonical resolution engine (&lt;code&gt;resolveCanonicalUrl&lt;/code&gt;):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fpako%3AeNp9kcFu2zAMhl-F46UXO1mxnQysWNIia4FmC5y2h8bFIEuMLVQSDUlO5jZ590LJBqTANl0EUuT_f6ReUbIiLHBteCtb4SPclpUDAAh93XjRtTBnRRNYpQsmBcx6Y-DTx3OY68aLqNk9HRvSeRw23N2Xtz8nqwpfUjCSbMdf-0B-bIe84xArfII8v9hd390tDkIL8lY4chFKUtqTjDu47ENk-0epNtyMhLQ0UrQ50Tkak1OV-wv09Df0tIDvFLfsn_MbFylRk4Ll4JSW_xxg-m6AU_RRfrHzZL6cSeHYaSnM2Slvamxj7EIxHv-f-73fYSclWaFdgCsdJG_Ii9oQsNsd62ZEalXh4_DwYwHfDNfCwBVtyHBHHtLr6U4wQ5s2qxUWrxhbsumjFa1FbyJmx8yD8DqZhFSzZhdnwmozYIG56DpDeRhCJJvB1Gj3PBdyeYhn7GIGFS6pYYL7mwozKLnmyBlck9lQ1FJkMPFamAyCcCEP5PUas4PJUr8klvPP3S_c7zOsm0s27LHAD9tWR8L9G1vc37M%3Ftype%3Dpng" 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%2Fmermaid.ink%2Fimg%2Fpako%3AeNp9kcFu2zAMhl-F46UXO1mxnQysWNIia4FmC5y2h8bFIEuMLVQSDUlO5jZ590LJBqTANl0EUuT_f6ReUbIiLHBteCtb4SPclpUDAAh93XjRtTBnRRNYpQsmBcx6Y-DTx3OY68aLqNk9HRvSeRw23N2Xtz8nqwpfUjCSbMdf-0B-bIe84xArfII8v9hd390tDkIL8lY4chFKUtqTjDu47ENk-0epNtyMhLQ0UrQ50Tkak1OV-wv09Df0tIDvFLfsn_MbFylRk4Ll4JSW_xxg-m6AU_RRfrHzZL6cSeHYaSnM2Slvamxj7EIxHv-f-73fYSclWaFdgCsdJG_Ii9oQsNsd62ZEalXh4_DwYwHfDNfCwBVtyHBHHtLr6U4wQ5s2qxUWrxhbsumjFa1FbyJmx8yD8DqZhFSzZhdnwmozYIG56DpDeRhCJJvB1Gj3PBdyeYhn7GIGFS6pYYL7mwozKLnmyBlck9lQ1FJkMPFamAyCcCEP5PUas4PJUr8klvPP3S_c7zOsm0s27LHAD9tWR8L9G1vc37M%3Ftype%3Dpng" alt="Mermaid Diagram" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Mode A: Full 301 Permanent Migration
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;How it works&lt;/strong&gt;: Any request to &lt;code&gt;zyvop.com/@user/slug&lt;/code&gt; is immediately redirected via HTTP 301 to &lt;code&gt;https://blog.yourdomain.com/slug&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;When to use&lt;/strong&gt;: When you want all past and future traffic to land exclusively on your independent website. Search engines transfer 100% of accumulated link equity to your new domain.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Mode B: Network-Integrated (Discovery + Canonical Ownership)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;How it works&lt;/strong&gt;: Your post remains readable on the ZyVOP community feed, trending tabs, and topic collections. However, the HTML source contains:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"canonical"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://blog.yourdomain.com/slug"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;When to use&lt;/strong&gt;: When you want the viral reach, comments, and developer discovery of the ZyVOP network, while Google attributes 100% of the SEO search ranking to your personal custom domain.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  5. Multi-Domain Support &amp;amp; Host Normalization
&lt;/h2&gt;

&lt;p&gt;ZyVOP allows you to register secondary custom domains (e.g., holding both &lt;code&gt;www.acme.dev&lt;/code&gt; and &lt;code&gt;blog.acme.dev&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;When multiple domains are connected:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;One domain is marked &lt;strong&gt;Primary&lt;/strong&gt; in the UI.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;If a reader or bot arrives on a non-primary active domain, ZyVOP's edge proxy issues an immediate &lt;strong&gt;HTTP 308 Permanent Redirect&lt;/strong&gt; to the primary host, strictly preserving the original pathname and query parameters:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;www&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;acme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dev&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;design&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="k"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;hackernews&lt;/span&gt;
&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;308&lt;/span&gt; &lt;span class="n"&gt;Permanent&lt;/span&gt; &lt;span class="n"&gt;Redirect&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;blog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;acme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dev&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;design&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="k"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;hackernews&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This prevents duplicate-content penalties across your brand aliases.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. The Downgrade Grace Period: Zero Broken Backlinks
&lt;/h2&gt;

&lt;p&gt;Most developer platforms ruthlessly disable custom domains the exact hour a subscription expires, instantly breaking inbound links, dropping search ranks, and serving generic 404 or certificate error pages.&lt;/p&gt;

&lt;p&gt;ZyVOP protects your SEO through an automated &lt;strong&gt;Downgrade Grace Period (&lt;/strong&gt;&lt;code&gt;downgradeGraceEndsAt&lt;/code&gt;&lt;strong&gt;)&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fpako%3AeNptUsFu2zAM_RVOh2Eb7CVbdih8GNAly1qgwwKn7WXZgZEYh6gsGpKcNg3y74XiNHOA6iKJj498j9JOaTGkChUiRpowVh7rfPN14QAA_n76B3n-HS515A0VMJXWGXYVzLwcg11idz7k_vKoaUaexRQws-hgIo-u8mjIwADG6A38fGrYk-m4h859Guw6oK9g5imQ35ApYIxOHGu0cFfeBHgPE6mRHdyc1KR1IvwX9epiTuj1GshV7ChA25gkwLJ7CB1_3219SalISYY96ciuKjoQjmjnJ8CH0RAmuA0fuwI9wrFAiOKThzS_kkK7DNrz8nwSfdYbk7i6vZ2NhhcFjIYXqX-Njlw8sSAKPG830nzWUg-wjWvxg6IN5M-8vUo5e95rFyK6CH8c5WPL-gFKwoRhZHEqU3Vqx0YVOxXXVKd_Y2iFrY0q6yL36BmXlkLKWYmLU6zZblWhcmwaS3nYhkh1Bj_SvH-jnh_uU3Exg4WaUyUEd9cLlUEpS4mSwRXZDUXWmMGlZ7QZBHQhD-R5pbJDkzk_Jy1fvjVPar_P1LIaixWvCvXucc2R1P4F-nXzsQ%3Ftype%3Dpng" 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%2Fmermaid.ink%2Fimg%2Fpako%3AeNptUsFu2zAM_RVOh2Eb7CVbdih8GNAly1qgwwKn7WXZgZEYh6gsGpKcNg3y74XiNHOA6iKJj498j9JOaTGkChUiRpowVh7rfPN14QAA_n76B3n-HS515A0VMJXWGXYVzLwcg11idz7k_vKoaUaexRQws-hgIo-u8mjIwADG6A38fGrYk-m4h859Guw6oK9g5imQ35ApYIxOHGu0cFfeBHgPE6mRHdyc1KR1IvwX9epiTuj1GshV7ChA25gkwLJ7CB1_3219SalISYY96ciuKjoQjmjnJ8CH0RAmuA0fuwI9wrFAiOKThzS_kkK7DNrz8nwSfdYbk7i6vZ2NhhcFjIYXqX-Njlw8sSAKPG830nzWUg-wjWvxg6IN5M-8vUo5e95rFyK6CH8c5WPL-gFKwoRhZHEqU3Vqx0YVOxXXVKd_Y2iFrY0q6yL36BmXlkLKWYmLU6zZblWhcmwaS3nYhkh1Bj_SvH-jnh_uU3Exg4WaUyUEd9cLlUEpS4mSwRXZDUXWmMGlZ7QZBHQhD-R5pbJDkzk_Jy1fvjVPar_P1LIaixWvCvXucc2R1P4F-nXzsQ%3Ftype%3Dpng" alt="Mermaid Diagram" width="443" height="1007"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;During Grace&lt;/strong&gt;: Your custom domain and canonical headers remain active. No traffic is dropped while you resolve billing or plan transitions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Post-Grace (&lt;/strong&gt;&lt;code&gt;REDIRECTING&lt;/code&gt;&lt;strong&gt;)&lt;/strong&gt;: If not renewed, the domain transitions to a managed redirect state, routing visitors and search spiders cleanly back to your verified author profile (&lt;code&gt;zyvop.com/author/:username&lt;/code&gt;) via HTTP 308 instead of failing abruptly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Instant Restoration&lt;/strong&gt;: Reactivating Pro restores custom domain routing immediately without having to re-provision DNS records.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  7. Custom Domain Pre-Flight Checklist
&lt;/h2&gt;

&lt;p&gt;Before publicizing your new publication, run this quick pre-flight verification:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;DNS CNAME Added&lt;/strong&gt;: Host points to &lt;code&gt;cname.zyvop.com&lt;/code&gt; and is marked &lt;strong&gt;DNS-Only&lt;/strong&gt; (not proxied by intermediate CDNs).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Status Verified&lt;/strong&gt;: Status in &lt;code&gt;/settings?tab=publication&lt;/code&gt; displays a green &lt;strong&gt;Verified&lt;/strong&gt; badge.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Primary Domain Set&lt;/strong&gt;: Marked as &lt;strong&gt;Primary&lt;/strong&gt; if multiple hostnames are connected.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Favicon &amp;amp; Logo Uploaded&lt;/strong&gt;: Appearance tab configured with light/dark SVGs and a crisp favicon.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Technical Feeds Accessible&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;https://yourdomain.com/sitemap.xml&lt;/code&gt; returns valid XML.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;https://yourdomain.com/robots.txt&lt;/code&gt; points to your sitemap.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;https://yourdomain.com/rss.xml&lt;/code&gt; parses cleanly in your feed reader.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Canonical Verified&lt;/strong&gt;: Inspect page source on an article to confirm &lt;code&gt;&amp;lt;link rel="canonical"&amp;gt;&lt;/code&gt; points to your custom domain.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Conclusion: Build on Land You Own
&lt;/h2&gt;

&lt;p&gt;Your custom domain is the single permanent digital asset in your engineering career. Publishing platforms, social networks, and algorithms inevitably change, but incoming backlinks, search engine rank, and reader bookmarks anchored to your personal domain remain yours forever.&lt;/p&gt;

&lt;p&gt;With ZyVOP, you don't have to choose between managing fragile self-hosted infrastructure and being trapped in a closed blogging network. You get the discovery of a global developer feed with the complete authority and independence of your own custom domain.&lt;/p&gt;

&lt;p&gt;Head to &lt;a href="https://dev.to/settings?tab=publication"&gt;&lt;strong&gt;ZyVOP Publication Settings&lt;/strong&gt;&lt;/a&gt;, add your DNS record, and connect your custom domain today.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Published via &lt;a href="https://zyvop.com/how-to-use-a-custom-domain-for-your-zyvop-publication-cki0x?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=syndication" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt; — Write once in Markdown, auto-backup to GitHub, and syndicate to Dev.to, Medium &amp;amp; Hashnode in 1 click.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>customdomains</category>
      <category>devops</category>
      <category>publications</category>
    </item>
    <item>
      <title>Publish from Git to ZyVOP with GitHub Actions</title>
      <dc:creator>Sanjay Singh</dc:creator>
      <pubDate>Mon, 19 Oct 2026 06:12:45 +0000</pubDate>
      <link>https://dev.to/sanjay_singh_1/publish-from-git-to-zyvop-with-github-actions-4o80</link>
      <guid>https://dev.to/sanjay_singh_1/publish-from-git-to-zyvop-with-github-actions-4o80</guid>
      <description>&lt;p&gt;You can keep every ZyVOP article as a Markdown file in Git and let GitHub Actions publish it when you merge to &lt;code&gt;main&lt;/code&gt;. No copy-pasting, and every change goes through a pull request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why publish from Git
&lt;/h2&gt;

&lt;p&gt;Treating posts like code gives you things a web editor can't:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Review and history.&lt;/strong&gt; Edits go through pull requests, and &lt;code&gt;git log&lt;/code&gt; becomes your editorial history.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;One source of truth.&lt;/strong&gt; The Markdown file is the article. ZyVOP can &lt;a href="https://zyvop.com/api-docs" rel="noopener noreferrer"&gt;cross-post it&lt;/a&gt; to Dev.to, Hashnode, Medium, WordPress and Bluesky, with canonical URLs handled for you.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Safe re-runs.&lt;/strong&gt; Give each post a stable &lt;code&gt;slug&lt;/code&gt;, &lt;code&gt;zyvop_id&lt;/code&gt; or &lt;code&gt;canonical_url&lt;/code&gt;, and later pushes update the existing article instead of creating another one.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What you need
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;A ZyVOP account, with any destinations you want (Dev.to, Hashnode and so on) connected in your settings.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A developer token. Generate one in ZyVOP under Settings → Integrations; tokens start with &lt;code&gt;zv_&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A GitHub repository with a &lt;code&gt;posts/&lt;/code&gt; folder.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 1: Add a post with frontmatter
&lt;/h2&gt;

&lt;p&gt;Each post is a Markdown file with YAML frontmatter at the top. A simple repository layout looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;my-blog/
├── .github/
│   └── workflows/
│       └── publish.yml
└── posts/
    └── nodejs-logging-pino-vs-winston.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the post itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Best&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Logging&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Library&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Node.js&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;in&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;2026"&lt;/span&gt;
&lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nodejs-logging-pino-vs-winston&lt;/span&gt;
&lt;span class="na"&gt;subtitle&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;How&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;choose&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;between&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Pino,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Winston&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;rest"&lt;/span&gt;
&lt;span class="na"&gt;excerpt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;A practical comparison of Node.js logging libraries, with code samples.&lt;/span&gt;
&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;nodejs&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;logging&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pino&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;winston&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;backend&lt;/span&gt;
&lt;span class="na"&gt;draft&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="na"&gt;cross_post&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;devto&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;hashnode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;medium&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

Write the article here in plain Markdown, with code blocks, tables and diagrams.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two defaults matter. A post is published unless you say otherwise, and cross-posting is switched on for every connected platform. That is why the example starts with &lt;code&gt;draft: true&lt;/code&gt;. The &lt;code&gt;slug&lt;/code&gt; matters just as much: it is how a later push finds the post it should update.&lt;/p&gt;

&lt;p&gt;These are the frontmatter fields you will use most:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;title&lt;/td&gt;
&lt;td&gt;Headline (required, 3 to 200 characters)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;slug, zyvop_id&lt;/td&gt;
&lt;td&gt;Stable identifiers that let later pushes update this post instead of creating another&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;subtitle&lt;/td&gt;
&lt;td&gt;Short line shown under the title&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;excerpt&lt;/td&gt;
&lt;td&gt;Card preview and search snippet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tags&lt;/td&gt;
&lt;td&gt;Topic tags, as a list or a comma-separated string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;category&lt;/td&gt;
&lt;td&gt;Category slug, such as backend, frontend, devops or ai&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cover_image&lt;/td&gt;
&lt;td&gt;Cover image URL or relative path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;canonical_url&lt;/td&gt;
&lt;td&gt;Original source URL, if the post lives elsewhere first; also identifies the post on later publishes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cross_post&lt;/td&gt;
&lt;td&gt;Per-platform flags: devto, hashnode, medium, bluesky, wordpress&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;draft, status, scheduled_at&lt;/td&gt;
&lt;td&gt;Save as a draft, or set DRAFT, PUBLISHED or SCHEDULED with an ISO 8601 date&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Extra keys such as &lt;code&gt;layout&lt;/code&gt; or &lt;code&gt;featured&lt;/code&gt; are preserved and ignored by ZyVOP, so the same files can also feed a static site built with Astro or Next.js.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Store your token as a secret
&lt;/h2&gt;

&lt;p&gt;In your GitHub repository, open Settings → Secrets and variables → Actions and create a repository secret named &lt;code&gt;ZYVOP_TOKEN&lt;/code&gt; with your developer token as the value. Never commit the token itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Add the workflow
&lt;/h2&gt;

&lt;p&gt;The simplest option is ZyVOP's own action. Save this as &lt;code&gt;.github/workflows/publish.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;posts/**/*.md"&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;publish&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;zyvop/publish-action@v1&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ZYVOP_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.ZYVOP_TOKEN }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The action publishes only the Markdown files changed by a push. Checkout uses &lt;code&gt;fetch-depth: 0&lt;/code&gt; so the base commit is available; when it isn't, or on a manual run (&lt;code&gt;workflow_dispatch&lt;/code&gt;) or a branch's first push, the action processes every post. It reads your token from the &lt;code&gt;ZYVOP_TOKEN&lt;/code&gt; secret, and once you merge a change to &lt;code&gt;main&lt;/code&gt; the run shows up under your repository's Actions tab.&lt;/p&gt;

&lt;h3&gt;
  
  
  Alternative: call the CLI yourself
&lt;/h3&gt;

&lt;p&gt;If you want to own the changed-file logic, run the &lt;code&gt;zyvop&lt;/code&gt; CLI yourself. It needs Node.js 20 or newer. This version, adapted from ZyVOP's CLI documentation, publishes only the files changed since the previous push, and falls back to all posts on a branch's first push or a force push:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;posts/**.md"&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;

&lt;span class="c1"&gt;# Do not let rapid pushes publish the same article concurrently.&lt;/span&gt;
&lt;span class="na"&gt;concurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;zyvop-publish-${{ github.ref }}&lt;/span&gt;
  &lt;span class="na"&gt;cancel-in-progress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;publish&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout repository&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup Node.js&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Detect changed posts and publish&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ZYVOP_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.ZYVOP_TOKEN }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;set -euo pipefail&lt;/span&gt;

          &lt;span class="s"&gt;before="${{ github.event.before }}"&lt;/span&gt;
          &lt;span class="s"&gt;after="${{ github.sha }}"&lt;/span&gt;

          &lt;span class="s"&gt;# A branch's first push has an all-zero before SHA. A force push can&lt;/span&gt;
          &lt;span class="s"&gt;# also reference an unavailable commit, so publish all tracked posts.&lt;/span&gt;
          &lt;span class="s"&gt;if [[ "$before" =~ ^0+$ ]] || ! git cat-file -e "$before^{commit}" 2&amp;gt;/dev/null; then&lt;/span&gt;
            &lt;span class="s"&gt;mapfile -d '' files &amp;lt; &amp;lt;(git ls-files -z -- ':(glob)posts/**/*.md')&lt;/span&gt;
          &lt;span class="s"&gt;else&lt;/span&gt;
            &lt;span class="s"&gt;mapfile -d '' files &amp;lt; &amp;lt;(&lt;/span&gt;
              &lt;span class="s"&gt;git diff --name-only -z --diff-filter=ACMR \&lt;/span&gt;
                &lt;span class="s"&gt;"$before" "$after" -- ':(glob)posts/**/*.md'&lt;/span&gt;
            &lt;span class="s"&gt;)&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;

          &lt;span class="s"&gt;if (( ${#files[@]} == 0 )); then&lt;/span&gt;
            &lt;span class="s"&gt;echo "No Markdown files changed."&lt;/span&gt;
            &lt;span class="s"&gt;exit 0&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;

          &lt;span class="s"&gt;for file in "${files[@]}"; do&lt;/span&gt;
            &lt;span class="s"&gt;if [ -f "$file" ]; then&lt;/span&gt;
              &lt;span class="s"&gt;echo "Deploying $file..."&lt;/span&gt;
              &lt;span class="s"&gt;npx --yes zyvop@1.1.1 publish "$file"&lt;/span&gt;
            &lt;span class="s"&gt;fi&lt;/span&gt;
          &lt;span class="s"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two notes. The workflow only publishes added or modified files, so deleting a post from Git doesn't remove it from ZyVOP. And if you would rather keep your Dev.to, Hashnode, Medium, WordPress or Bluesky credentials in GitHub secrets than in ZyVOP, the CLI has a &lt;code&gt;--local&lt;/code&gt; mode that publishes to those providers directly from the runner. Its documentation warns that Medium's API can't update a published story, so repeated local publishes to Medium can create another one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Check posts on pull requests
&lt;/h2&gt;

&lt;p&gt;The CLI's &lt;code&gt;--dry-run&lt;/code&gt; flag validates a post and previews the exact payload without authentication or publishing, which makes it a cheap check for every pull request. It runs locally, so it can't tell you whether a real publish would create or update the post.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;posts/**'&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;dry-run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Validate every post&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;set -euo pipefail&lt;/span&gt;
          &lt;span class="s"&gt;mapfile -d '' files &amp;lt; &amp;lt;(git ls-files -z -- ':(glob)posts/**/*.md')&lt;/span&gt;
          &lt;span class="s"&gt;for file in "${files[@]}"; do&lt;/span&gt;
            &lt;span class="s"&gt;echo "Checking $file"&lt;/span&gt;
            &lt;span class="s"&gt;npx --yes zyvop@1.1.1 publish "$file" --dry-run&lt;/span&gt;
          &lt;span class="s"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Habits that keep it safe
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Start every post as a draft.&lt;/strong&gt; Keep &lt;code&gt;draft: true&lt;/code&gt; until the pull request that should make it live, and flip it in that PR. A first push or a manual run processes every post, so mark any back catalogue as drafts too.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Give every post a stable identity.&lt;/strong&gt; Set a &lt;code&gt;slug&lt;/code&gt;, &lt;code&gt;zyvop_id&lt;/code&gt; or &lt;code&gt;canonical_url&lt;/code&gt; once and keep it stable, so later pushes update the same article.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Schedule instead of racing.&lt;/strong&gt; Set &lt;code&gt;status: SCHEDULED&lt;/code&gt; and a &lt;code&gt;scheduled_at&lt;/code&gt; timestamp to release a post later.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Mind the rate limit.&lt;/strong&gt; ZyVOP documents 10 requests per minute per token on its REST API, so when you move a large back catalogue, publish it in small batches.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pin the CLI version.&lt;/strong&gt; ZyVOP's own examples pin it (&lt;code&gt;zyvop@1.1.1&lt;/code&gt; at the time of writing), so a new release can't change your pipeline unannounced.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Protect the token.&lt;/strong&gt; Keep it in GitHub secrets only. If it ever leaks, revoke it in ZyVOP and create a new one.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Set&lt;/strong&gt; &lt;code&gt;base_url&lt;/code&gt; &lt;strong&gt;for relative images.&lt;/strong&gt; ZyVOP resolves relative cover and body image paths against it, or against &lt;code&gt;canonical_url&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The workflow doesn't run.&lt;/strong&gt; Check that the branch is &lt;code&gt;main&lt;/code&gt; and that the changed files match the &lt;code&gt;paths&lt;/code&gt; filter.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Every post is republished on each run.&lt;/strong&gt; Check that checkout uses &lt;code&gt;fetch-depth: 0&lt;/code&gt;. When the base commit isn't available, the action processes all tracked posts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A post was created twice.&lt;/strong&gt; Add a stable &lt;code&gt;slug&lt;/code&gt;, &lt;code&gt;zyvop_id&lt;/code&gt; or &lt;code&gt;canonical_url&lt;/code&gt; so ZyVOP can match the file to the existing article.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Authentication fails.&lt;/strong&gt; Confirm the secret is named &lt;code&gt;ZYVOP_TOKEN&lt;/code&gt; and that the token hasn't been revoked.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A platform didn't get the post.&lt;/strong&gt; Connect that destination in ZyVOP and check the &lt;code&gt;cross_post&lt;/code&gt; flags, because ZyVOP only syndicates to connected platforms.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;With one folder, one secret and one workflow file, publishing becomes a side effect of merging. Start with a draft, run the dry-run check on pull requests, and set &lt;code&gt;draft&lt;/code&gt; to false only when the post is ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/zyvop/publish-action" rel="noopener noreferrer"&gt;ZyVOP Publish Action&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/zyvop/zyvop-cli" rel="noopener noreferrer"&gt;ZyVOP CLI&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://zyvop.com/api-docs" rel="noopener noreferrer"&gt;ZyVOP developer API and CLI documentation&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://zyvop.com/agents" rel="noopener noreferrer"&gt;ZyVOP AI agents and MCP server&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Published via &lt;a href="https://zyvop.com/publish-from-git-to-zyvop-with-github-actions-c93ut?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=syndication" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt; — Write once in Markdown, auto-backup to GitHub, and syndicate to Dev.to, Medium &amp;amp; Hashnode in 1 click.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>blogging</category>
      <category>cicd</category>
      <category>devops</category>
      <category>githubactions</category>
    </item>
    <item>
      <title>Docker vs PM2: Which One Does Your Node App Actually Need?</title>
      <dc:creator>Sanjay Singh</dc:creator>
      <pubDate>Sun, 18 Oct 2026 07:01:20 +0000</pubDate>
      <link>https://dev.to/sanjay_singh_1/docker-vs-pm2-which-one-does-your-node-app-actually-need-5d98</link>
      <guid>https://dev.to/sanjay_singh_1/docker-vs-pm2-which-one-does-your-node-app-actually-need-5d98</guid>
      <description>&lt;p&gt;Every few months someone asks the same question: &lt;em&gt;should I use Docker or PM2 to run my Node app?&lt;/em&gt; It sounds like a fair comparison, but it's a bit like asking whether you need a toolbox or a screwdriver. They overlap in a few places, and that overlap is exactly why people get confused.&lt;/p&gt;

&lt;p&gt;The way I make sense of it is to split the job into three layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Supervision:&lt;/strong&gt; keep the process alive, restart it when it dies, use every core.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Packaging:&lt;/strong&gt; make the environment reproducible and keep the app isolated from everything else on the machine.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Orchestration:&lt;/strong&gt; run copies across several machines, roll out new versions safely, replace machines that die.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;PM2 lives in layer one. Docker lives in layer two and borrows a little of layer one through its restart policy. Layer three is where Kubernetes, Swarm and managed container platforms live. Most of the confusion comes from tools reaching into each other's layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What PM2 does
&lt;/h2&gt;

&lt;p&gt;PM2 is a process manager for Node.js. You install it with npm, hand it your app, and it keeps that app running.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; pm2
pm2 start server.js &lt;span class="nt"&gt;--name&lt;/span&gt; api &lt;span class="nt"&gt;-i&lt;/span&gt; max
pm2 startup   &lt;span class="c"&gt;# prints a command; run it once (with sudo) to enable start on boot&lt;/span&gt;
pm2 save
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those commands get you a lot. Your app restarts if it crashes, it runs one worker per CPU core, and it comes back after a server reboot. You also get logs and a live dashboard with &lt;code&gt;pm2 logs&lt;/code&gt; and &lt;code&gt;pm2 monit&lt;/code&gt;. When you deploy new code, &lt;code&gt;pm2 reload api&lt;/code&gt; restarts workers one at a time, so users don't see downtime as long as your app shuts down cleanly. That only works in cluster mode, which is what &lt;code&gt;-i max&lt;/code&gt; turns on.&lt;/p&gt;

&lt;p&gt;In practice you'll want this in a config file that lives in your repo, so the setup is versioned along with your code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ecosystem.config.js&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;apps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./server.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;instances&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;max&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;exec_mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cluster&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;max_memory_restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;400M&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;env_production&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;NODE_ENV&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3000&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;Start it with &lt;code&gt;pm2 start ecosystem.config.js --env production&lt;/code&gt;. If your package.json sets &lt;code&gt;"type": "module"&lt;/code&gt;, name the file &lt;code&gt;ecosystem.config.cjs&lt;/code&gt; instead.&lt;/p&gt;

&lt;p&gt;For a single server and a single app, that's a very pleasant experience. No new concepts, no extra layer. It's just Node, supervised.&lt;/p&gt;

&lt;p&gt;The catch is that PM2 only manages processes. It has no opinion about the machine they run on. Your server needs the right Node version, the right system libraries, the right environment variables. If staging has Node 22 and production has Node 24, PM2 will happily run your app on both and let you find out the hard way.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Docker does
&lt;/h2&gt;

&lt;p&gt;Docker solves a different problem: it packages your app together with everything it needs to run. The Node version, the OS libraries, your dependencies, your build output, all baked into an image. That image runs the same on your laptop, in CI, and on a production server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:24-alpine&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; NODE_ENV=production&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci &lt;span class="nt"&gt;--omit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dev
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; node&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 3000&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "server.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--restart&lt;/span&gt; unless-stopped &lt;span class="nt"&gt;-p&lt;/span&gt; 3000:3000 myapp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two notes on that Dockerfile. Add a &lt;code&gt;.dockerignore&lt;/code&gt; that lists &lt;code&gt;node_modules&lt;/code&gt; and &lt;code&gt;.git&lt;/code&gt;, otherwise &lt;code&gt;COPY . .&lt;/code&gt; copies your local &lt;code&gt;node_modules&lt;/code&gt; into the image on top of the clean install. And it assumes a plain JavaScript app; with TypeScript you'd add a build stage.&lt;/p&gt;

&lt;p&gt;You also get isolation (your app can't trample other things on the box), resource limits, health checks, and a clean way to run a database or Redis next to your app with Docker Compose.&lt;/p&gt;

&lt;p&gt;The price is a learning curve. Images, layers, volumes, networks, registries: there's a whole vocabulary to pick up, and debugging inside a container feels different from debugging on a plain server. Builds also add a step to your workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where they overlap
&lt;/h2&gt;

&lt;p&gt;Both can restart a crashed app, start it on boot, collect logs, and react to runaway memory use. That's the core of the overlap, and it's why people treat them as substitutes. Here's the rest side by side:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;PM2&lt;/th&gt;
&lt;th&gt;Docker&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Main job&lt;/td&gt;
&lt;td&gt;Supervise Node processes&lt;/td&gt;
&lt;td&gt;Package and isolate an app and its environment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Environment consistency&lt;/td&gt;
&lt;td&gt;No, depends on the host&lt;/td&gt;
&lt;td&gt;Yes, the image is the environment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-core usage&lt;/td&gt;
&lt;td&gt;Built in (cluster mode)&lt;/td&gt;
&lt;td&gt;Run several containers, or use cluster in the app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zero-downtime deploys&lt;/td&gt;
&lt;td&gt;pm2 reload&lt;/td&gt;
&lt;td&gt;Needs a reverse proxy or orchestrator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Non-Node apps&lt;/td&gt;
&lt;td&gt;Possible, but not its focus&lt;/td&gt;
&lt;td&gt;Yes, anything&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Learning curve&lt;/td&gt;
&lt;td&gt;Gentle&lt;/td&gt;
&lt;td&gt;Steeper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scaling past one server&lt;/td&gt;
&lt;td&gt;Not its job&lt;/td&gt;
&lt;td&gt;Natural fit with Swarm, Kubernetes, managed container platforms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The cluster mode question
&lt;/h2&gt;

&lt;p&gt;This is where PM2 feels a bit magical. Node runs your code on a single thread, so on an 8-core machine you're using one core unless you do something about it. &lt;code&gt;pm2 start -i max&lt;/code&gt; forks a worker per core and load-balances between them. Done.&lt;/p&gt;

&lt;p&gt;Docker has no equivalent built in. The usual approach is to run several containers behind a reverse proxy like Nginx, or let an orchestrator manage the replicas. In Kubernetes the convention is one process per container, and you scale by adding more of them. It's more moving parts, but it keeps working when you outgrow a single machine, which PM2 can't do.&lt;/p&gt;

&lt;p&gt;Here's the Docker version of the same idea with Compose: two replicas of the app behind Nginx.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.yml&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;NODE_ENV&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wget"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-q"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--spider"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://127.0.0.1:3000/health"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;

  &lt;span class="na"&gt;nginx&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx:alpine&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;80:80"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./nginx.conf:/etc/nginx/conf.d/default.conf:ro&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;api&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;api&lt;/code&gt; service publishes no ports, so the replicas don't fight over one. Nginx reaches them by service name, using a small &lt;code&gt;nginx.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://api:3000&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 health check assumes your app has a &lt;code&gt;/health&lt;/code&gt; route. One gotcha: Nginx looks up the name &lt;code&gt;api&lt;/code&gt; when it starts, so reload it after you recreate the containers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployments and zero downtime
&lt;/h2&gt;

&lt;p&gt;With PM2, a deploy is: pull the code, install, &lt;code&gt;pm2 reload&lt;/code&gt;. With Docker, it's: build an image, push it, pull it on the server, start the new container, switch traffic, stop the old one. By hand on one server, that means a few seconds of downtime. Doing it properly takes a rolling update from an orchestrator, or a proxy that can shift traffic between old and new, which is more work upfront.&lt;/p&gt;

&lt;h2&gt;
  
  
  When things go wrong
&lt;/h2&gt;

&lt;p&gt;The feature lists look similar until something breaks. Here's what each tool does in the situations that actually page people at night:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;PM2&lt;/th&gt;
&lt;th&gt;Docker&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;App crashes&lt;/td&gt;
&lt;td&gt;Restarts it. After too many quick crashes in a row, it gives up and marks the app errored.&lt;/td&gt;
&lt;td&gt;Restarts it per the restart policy, waiting longer after each quick failure.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App hangs but doesn't exit&lt;/td&gt;
&lt;td&gt;Doesn't notice.&lt;/td&gt;
&lt;td&gt;A health check marks the container unhealthy, but plain Docker and Compose won't restart it. An orchestrator will.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory leak&lt;/td&gt;
&lt;td&gt;max_memory_restart restarts the process once it passes the limit.&lt;/td&gt;
&lt;td&gt;A memory limit, if you set one, kills the container when it's exceeded, and the restart policy brings it back.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server reboots&lt;/td&gt;
&lt;td&gt;Comes back if you ran pm2 startup and pm2 save.&lt;/td&gt;
&lt;td&gt;Comes back through the restart policy (unless-stopped or always), as long as the Docker daemon starts on boot.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bad deploy&lt;/td&gt;
&lt;td&gt;Redeploy the old code and reload.&lt;/td&gt;
&lt;td&gt;Start the previous image tag.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The machine dies&lt;/td&gt;
&lt;td&gt;Your app is gone.&lt;/td&gt;
&lt;td&gt;Your app is gone, unless an orchestrator moves it to another machine.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The hung-app row surprises people: a health check only reports status, so something else has to act on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shutdown is your job
&lt;/h2&gt;

&lt;p&gt;Both tools stop your app with a signal, and your code has to listen for it. &lt;code&gt;docker stop&lt;/code&gt; sends &lt;code&gt;SIGTERM&lt;/code&gt; and waits 10 seconds by default before killing the container. PM2 sends &lt;code&gt;SIGINT&lt;/code&gt; and waits only 1.6 seconds by default, which you can change with &lt;code&gt;kill_timeout&lt;/code&gt;. An app that ignores the signal drops whatever requests are in flight.&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;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&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;shutdown&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// stop accepting new connections and let in-flight requests finish&lt;/span&gt;
  &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&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;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="c1"&gt;// give up if that takes too long&lt;/span&gt;
  &lt;span class="nf"&gt;setTimeout&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;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;unref&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SIGTERM&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;shutdown&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Docker&lt;/span&gt;
&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SIGINT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;shutdown&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// PM2, and Ctrl+C&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you use PM2, set &lt;code&gt;kill_timeout&lt;/code&gt; above that 8 seconds, or PM2 will kill the app first. In Docker, you may also want an init process (&lt;code&gt;docker run --init&lt;/code&gt;, or &lt;code&gt;init: true&lt;/code&gt; in Compose) so signals are forwarded properly and stray child processes get cleaned up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Logs and monitoring
&lt;/h2&gt;

&lt;p&gt;PM2 writes each app's output to files under &lt;code&gt;~/.pm2/logs&lt;/code&gt;, split into stdout and error logs. &lt;code&gt;pm2 logs&lt;/code&gt; tails them, &lt;code&gt;pm2 monit&lt;/code&gt; shows CPU and memory per process, and &lt;code&gt;pm2 install pm2-logrotate&lt;/code&gt; keeps the files from eating your disk.&lt;/p&gt;

&lt;p&gt;Docker flips the convention: your app writes to stdout and stderr, and Docker captures it. &lt;code&gt;docker logs -f&lt;/code&gt; follows it, and a logging driver or plugin can ship it somewhere central like CloudWatch or Loki, so your logs stop living on one server's disk. One catch: the default log driver keeps growing its files unless you set &lt;code&gt;max-size&lt;/code&gt; and &lt;code&gt;max-file&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I use Dozzle for this. It's a small web UI that tails every container's logs live, and it only works because everything writes to stdout.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using them together
&lt;/h2&gt;

&lt;p&gt;You can run PM2 inside a container with &lt;code&gt;pm2-runtime&lt;/code&gt;, which exists for exactly that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["pm2-runtime", "start", "ecosystem.config.js", "--env", "production"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's a reasonable move if you want cluster mode on a single host and you like PM2's config file (you'll need to install pm2 in the image, since the base Node image doesn't ship with it). But it's often redundant. Docker's restart policy already handles crashes, and an orchestrator already handles scaling. Put PM2 on top and you have two things deciding when to restart your app, which gets confusing fast.&lt;/p&gt;

&lt;p&gt;If you do combine them, set &lt;code&gt;instances&lt;/code&gt; to a fixed number rather than &lt;code&gt;'max'&lt;/code&gt;. Inside a container, &lt;code&gt;'max'&lt;/code&gt; can count the host's cores instead of the CPU limit you gave the container.&lt;/p&gt;

&lt;h2&gt;
  
  
  So which one should you pick?
&lt;/h2&gt;

&lt;p&gt;Here's how I'd decide.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick PM2&lt;/strong&gt; if you have one or two apps on a single VPS, you control the server, your team is small, and you want to ship today without learning a new toolchain. It's simple and it works, and there's no shame in that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick Docker&lt;/strong&gt; if you run more than one service, need a database or cache alongside your app, deploy through CI/CD, or work on a team where “it works on my machine” has come up. It's also the safer bet if you expect to move to a cloud platform or Kubernetes at some point, since plenty of hosting platforms expect a container anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use both&lt;/strong&gt; if you want PM2's cluster mode and config inside a container on a single host. Just be sure you actually need it.&lt;/p&gt;

&lt;p&gt;I run my own Node apps in Docker, and it's what I'd reach for by default on a new project. My setup is more than the app: a Postgres database, Dozzle for reading logs, Cloudflare Tunnels, and my background workers all run in Docker too. A worker is just another container, often the same image started with a different command. PM2 only manages the Node processes, so all of that would have lived outside it. The container has become the standard unit of deployment across the ecosystem, and the habit transfers everywhere. That said, if PM2 is already running your app happily and nothing is hurting, don't rip it out for the sake of it.&lt;/p&gt;

&lt;p&gt;So it comes down to what you're missing. If you just need a Node process kept alive, PM2 has you covered. If you need your app to behave the same on every machine it touches, that's what Docker is for.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Published via &lt;a href="https://zyvop.com/docker-vs-pm2-which-one-does-your-node-app-actually-need-egnl2?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=syndication" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt; — Write once in Markdown, auto-backup to GitHub, and syndicate to Dev.to, Medium &amp;amp; Hashnode in 1 click.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>deployment</category>
      <category>devops</category>
      <category>docker</category>
      <category>node</category>
    </item>
    <item>
      <title>Zero-Friction Migration: How to Import Your Existing Developer Blog into ZyVOP</title>
      <dc:creator>Sanjay Singh</dc:creator>
      <pubDate>Sun, 18 Oct 2026 05:28:49 +0000</pubDate>
      <link>https://dev.to/sanjay_singh_1/zero-friction-migration-how-to-import-your-existing-developer-blog-into-zyvop-474a</link>
      <guid>https://dev.to/sanjay_singh_1/zero-friction-migration-how-to-import-your-existing-developer-blog-into-zyvop-474a</guid>
      <description>&lt;p&gt;Technical writing is one of the highest-leverage assets in software engineering. Over time, however, developer portfolios become fragmented: early articles on &lt;strong&gt;DEV&lt;/strong&gt;, corporate posts on &lt;strong&gt;Medium&lt;/strong&gt;, a custom domain on &lt;strong&gt;Hashnode&lt;/strong&gt;, or Markdown files in static site generators like &lt;strong&gt;Hugo&lt;/strong&gt;, &lt;strong&gt;Astro&lt;/strong&gt;, or &lt;strong&gt;Ghost&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Switching platforms often leads to broken syntax highlighting, mangled math formulas, orphan image references, duplicate cover photos, and &lt;strong&gt;canonical SEO dilution&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;ZyVOP was built as a sovereign developer publishing engine: &lt;strong&gt;write once, maintain canonical ownership, and distribute across ecosystems&lt;/strong&gt;. But migrations are never one-size-fits-all.&lt;/p&gt;

&lt;p&gt;This guide provides an honest, technical look at how ZyVOP's import engine works, what is natively supported versus partially adapted, and how to migrate your catalog cleanly.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Migration Support Matrix
&lt;/h2&gt;

&lt;p&gt;Not every platform exposes identical APIs or export formats. ZyVOP supports migrations through four distinct ingestion pipelines:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Migration Pathway&lt;/th&gt;
&lt;th&gt;Level of Support&lt;/th&gt;
&lt;th&gt;Key Technical Nuances&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DEV.to&lt;/td&gt;
&lt;td&gt;Native REST API&lt;/td&gt;
&lt;td&gt;First-Class&lt;/td&gt;
&lt;td&gt;Pulls published articles, markdown body, tags, canonical URLs, and original dates.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hashnode&lt;/td&gt;
&lt;td&gt;GraphQL API / Backup JSON&lt;/td&gt;
&lt;td&gt;First-Class&lt;/td&gt;
&lt;td&gt;Supports custom publication domains, backup JSON files, and RSS fallback.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Public RSS / Single URL&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;Ingests via public user feed; image re-hosting is disabled by default to avoid rate limits.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ghost&lt;/td&gt;
&lt;td&gt;Generic RSS / Single URL&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;Full text via RSS content:encoded. Native Ghost JSON archives are not yet parsed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Substack&lt;/td&gt;
&lt;td&gt;Generic RSS / Single URL&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;Imports published posts via RSS. Subscriber lists and paid tiers are not imported.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hugo / Jekyll&lt;/td&gt;
&lt;td&gt;Markdown ZIP Archive&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;Ingests .md and .markdown files with relative image re-hosting. Shortcodes require flattening.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Astro / Next.js&lt;/td&gt;
&lt;td&gt;Markdown ZIP Archive&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;Supports standard Markdown. .mdx files and JSX/Astro components are ignored.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  2. Ingestion Pipelines: How It Works Under the Hood
&lt;/h2&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%2Fmermaid.ink%2Fimg%2Fpako%3AeNptVE1P4zAQ_SuzPqAi0naBPfWwUukH7YpCNgFWIuYwTaaJVceObKelQP_7yi2UtiKXZGy_N2_ejPPGUp0R67CZ1Mu0QOPgvs8VAICtp7nBqoCxysk6oRUknJ23fGzIWghFRVIo4ux5i_BPf_CYcNYfPEI0iO-hG445e4Zm87f_9DvCUOp8ACEaS-YAPbpNOBuhLZTOCK59-r830IY_8d3tHs8XIIrjhLMojmFIlFloXBfaOmhDXE-tw3QObZhQJury9BPvD25TfyAvWj-hDV2nSxio_Ligf4OrhLNYqFwSdI0TqSR4iG4-6SLCDKdCCrdKOJvoVyEl7q_CGfTvJjAU0h1V-zQOPQTNPNNL5UNojOpcezXWGb2T_CQqT7hR3DVpIRYEgxdnMHXaQKNVZtCGVvlBdLpLQirj6qibodEpWStU7tt50YJbbUqU4hU3LT6BrrXkvnPC98yr8YCEs55WjpTb4Y-K-7J5B9pr2549fjcu0FQJZyGZlCpXo4SeXpCBPmV1JUW6FdfYnNvZcsi5M2k7Jb6KiJZGOPJ9JonO27atLqKmnxNvQiO-bPekrrNvaL_zL3baYE7evMsW9NHhFC3BCfRQaSVSlBCvVHrghWfcUG_KeeOs_1EUQa-gdM65anzBHyI_8vfCSTrlbL13tzza87zf0hJCbd079A3OnB9QXFC2jeBsT0toyJJZbPw7vKg7ssGLsM6-QzwXvgf-VVG2P0QsYCWZEkXGOm_MFVT6X0ZGM6ylY8F25RGNwKkk68_MtHJDLIVcsQ5rYlVJatqVdVQGcCWFmk8wjTfxUCsXAGcx5ZrgYcxZAJGeaqcDGJFckBMpBtA1AmUAFpVtWjJixoJNkli8ei3nv6oXtl4HbJr3tNSGddiPZSEcsfV_vtCLhQ%3Ftype%3Dpng" 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%2Fmermaid.ink%2Fimg%2Fpako%3AeNptVE1P4zAQ_SuzPqAi0naBPfWwUukH7YpCNgFWIuYwTaaJVceObKelQP_7yi2UtiKXZGy_N2_ejPPGUp0R67CZ1Mu0QOPgvs8VAICtp7nBqoCxysk6oRUknJ23fGzIWghFRVIo4ux5i_BPf_CYcNYfPEI0iO-hG445e4Zm87f_9DvCUOp8ACEaS-YAPbpNOBuhLZTOCK59-r830IY_8d3tHs8XIIrjhLMojmFIlFloXBfaOmhDXE-tw3QObZhQJury9BPvD25TfyAvWj-hDV2nSxio_Ligf4OrhLNYqFwSdI0TqSR4iG4-6SLCDKdCCrdKOJvoVyEl7q_CGfTvJjAU0h1V-zQOPQTNPNNL5UNojOpcezXWGb2T_CQqT7hR3DVpIRYEgxdnMHXaQKNVZtCGVvlBdLpLQirj6qibodEpWStU7tt50YJbbUqU4hU3LT6BrrXkvnPC98yr8YCEs55WjpTb4Y-K-7J5B9pr2549fjcu0FQJZyGZlCpXo4SeXpCBPmV1JUW6FdfYnNvZcsi5M2k7Jb6KiJZGOPJ9JonO27atLqKmnxNvQiO-bPekrrNvaL_zL3baYE7evMsW9NHhFC3BCfRQaSVSlBCvVHrghWfcUG_KeeOs_1EUQa-gdM65anzBHyI_8vfCSTrlbL13tzza87zf0hJCbd079A3OnB9QXFC2jeBsT0toyJJZbPw7vKg7ssGLsM6-QzwXvgf-VVG2P0QsYCWZEkXGOm_MFVT6X0ZGM6ylY8F25RGNwKkk68_MtHJDLIVcsQ5rYlVJatqVdVQGcCWFmk8wjTfxUCsXAGcx5ZrgYcxZAJGeaqcDGJFckBMpBtA1AmUAFpVtWjJixoJNkli8ei3nv6oXtl4HbJr3tNSGddiPZSEcsfV_vtCLhQ%3Ftype%3Dpng" alt="Mermaid Diagram" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  A. Direct APIs (DEV &amp;amp; Hashnode)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;DEV.to:&lt;/strong&gt; Connects via username or API key. Extracts raw Markdown directly, preserving original syntax fences, tags, and canonical links.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hashnode:&lt;/strong&gt; Queries Hashnode's GraphQL endpoint across either personal subdomains (&lt;code&gt;user.hashnode.dev&lt;/code&gt;) or custom publication apex domains. Also accepts native Hashnode JSON export backups (&lt;code&gt;readHashnodeJson&lt;/code&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  B. Universal RSS / Atom (Ghost, Substack, Medium)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Parses standard RSS 2.0, Atom, and &lt;code&gt;content:encoded&lt;/code&gt; payloads.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Image Re-hosting Nuance:&lt;/strong&gt; For bulk RSS imports, image re-hosting is disabled by default (&lt;code&gt;migrateImages = false&lt;/code&gt;) to prevent timeout failures and remote CDN rate-limiting across large archives. Remote image URLs are preserved as absolute URLs.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  C. Direct Web Article Extractor
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;For standalone articles, ZyVOP executes an outbound fetch with Mozilla Readability, stripping boilerplate noise (&lt;code&gt;.cookie-banner&lt;/code&gt;, &lt;code&gt;.advertisement&lt;/code&gt;, &lt;code&gt;.newsletter-signup&lt;/code&gt;, &lt;code&gt;nav&lt;/code&gt;, &lt;code&gt;footer&lt;/code&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Perceptual Image Deduplication (Sharp):&lt;/strong&gt; Many CMSs duplicate the social &lt;code&gt;og:image&lt;/code&gt; as the first image in the post body. The single-URL extractor creates a 32×32 sRGB luminance fingerprint of the cover image and compares it against body images:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Bounded sRGB luminance and chromatic mean comparison in article-import-images.ts&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;createImageFingerprint&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="nx"&gt;Buffer&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;ImageFingerprint&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;pixels&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;sharp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;limitInputPixels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="nx"&gt;_000_000&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fill&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="nf"&gt;toColourspace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;srgb&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="nf"&gt;removeAlpha&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBuffer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="c1"&gt;// Evaluates luminance array and chromatic delta to strip duplicate hero images&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  D. Static-Site Markdown Archives (ZIP)
&lt;/h3&gt;

&lt;p&gt;When migrating from static site generators like Hugo, Astro, Jekyll, or Obsidian:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Supported Formats:&lt;/strong&gt; The reader accepts &lt;code&gt;.md&lt;/code&gt; and &lt;code&gt;.markdown&lt;/code&gt; files. Files with &lt;code&gt;.mdx&lt;/code&gt; extensions or custom templates (&lt;code&gt;.astro&lt;/code&gt;, &lt;code&gt;.html&lt;/code&gt;) are ignored.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Asset Re-hosting:&lt;/strong&gt; References like &lt;code&gt;./images/diagram.png&lt;/code&gt; or &lt;code&gt;../assets/flowchart.webp&lt;/code&gt; are normalized through &lt;code&gt;resolveArchiveAssetPath&lt;/code&gt;, uploaded to persistent storage, and rewritten in the Markdown body.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Frontmatter Ingestion:&lt;/strong&gt; Maps &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;canonical_url&lt;/code&gt; (or &lt;code&gt;canonicalUrl&lt;/code&gt;, &lt;code&gt;url&lt;/code&gt;), &lt;code&gt;cover_image&lt;/code&gt;, &lt;code&gt;tags&lt;/code&gt;, and &lt;code&gt;published_at&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3. Duplicate Detection: Preventing Content Overwrites
&lt;/h2&gt;

&lt;p&gt;ZyVOP avoids duplicate drafts during repeated syncs or CLI runs. Rather than relying on fragile hashes, the database executes a scoped uniqueness 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;// Scoped duplicate check in markdown-zip-import.service.ts&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;canonicalUrl&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;normalized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canonicalUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;?#&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&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="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&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="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;andWhere&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;(post.canonicalUrl = :canonicalUrl OR post.canonicalUrl = :normalized OR post.canonicalUrl = :slash)&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;canonicalUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;slash&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;normalized&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;else&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="nf"&gt;andWhere&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;LOWER(TRIM(post.title)) = LOWER(TRIM(:title))&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;title&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 an article matches an existing post owned by the author, it is safely skipped rather than duplicated or overwritten.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. How to Migrate: Step-by-Step
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Option 1: Web Dashboard (Zero Installation)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Go to &lt;strong&gt;Dashboard → Migrate&lt;/strong&gt; (&lt;code&gt;/migrate&lt;/code&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Select your source:&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-   **DEV.to:** Enter your username (`devto/yourusername`).

-   **Hashnode:** Enter your publication domain (`blog.yourdomain.com`).

-   **Medium / Ghost / Substack:** Paste your public profile or RSS feed URL (`https://yourblog.com/rss.xml`).

-   **Single URL:** Paste a single article link to test readability extraction.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt; Preview extracted articles, tags, and publication dates before committing them as drafts.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Option 2: Uploading a Static Site ZIP
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;In your Hugo/Astro/Jekyll repository, locate your Markdown directory.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ensure posts use standard Markdown (&lt;code&gt;.md&lt;/code&gt; or &lt;code&gt;.markdown&lt;/code&gt;). Convert &lt;code&gt;.mdx&lt;/code&gt; component imports into standard code fences or markdown blocks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Zip the directory containing your posts and local image assets:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;zip &lt;span class="nt"&gt;-r&lt;/span&gt; blog-migration.zip content/posts/ assets/images/
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Upload via &lt;strong&gt;Dashboard → Import → Upload Markdown ZIP&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Option 3: Terminal Migration via the CLI (&lt;code&gt;zyvop import&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;For developers who prefer terminal workflows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Authenticate&lt;/span&gt;
npx zyvop login

&lt;span class="c"&gt;# Import from DEV&lt;/span&gt;
npx zyvop import devto yourusername

&lt;span class="c"&gt;# Import from Hashnode&lt;/span&gt;
npx zyvop import hashnode blog.yourdomain.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  5. Preflight Audits: The ZyVOP Compatibility Lab
&lt;/h2&gt;

&lt;p&gt;Before publishing or cross-posting imported drafts, inspect them in the &lt;strong&gt;ZyVOP Compatibility Lab&lt;/strong&gt; (&lt;code&gt;/compatibility-lab&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The Compatibility Lab parses your article in-browser without modifying the database and runs preflight checks across destinations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tag Truncation:&lt;/strong&gt; Flags when your post exceeds destination limits (4 tags on DEV, 5 on Hashnode and Medium).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Social Announcement Limits:&lt;/strong&gt; Checks title and excerpt lengths against announcement constraints (180 characters for Bluesky, 320 for Mastodon).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Local Asset Detection:&lt;/strong&gt; Warns if an imported article still references non-public image paths (&lt;code&gt;localhost&lt;/code&gt; or un-migrated relative paths).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Syntax Adaptation:&lt;/strong&gt; Reports how Mermaid diagrams and KaTeX formulas will adapt across each destination.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  6. Post-Migration: Establishing Sovereign GitOps
&lt;/h2&gt;

&lt;p&gt;Once imported into ZyVOP, your articles are no longer locked into any single platform:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sovereign Canonical URL:&lt;/strong&gt; Your ZyVOP publication acts as the primary authoritative source (&lt;code&gt;&amp;lt;link rel="canonical"&amp;gt;&lt;/code&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Multi-Platform Distribution:&lt;/strong&gt; Syndicate updates to DEV, Hashnode, Medium, and WordPress with one click or via &lt;code&gt;npx zyvop publish&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Automated Git Backup:&lt;/strong&gt; Connect your GitHub account to have every post automatically committed to your personal repository via GitHub's Git Database API.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Take sovereign ownership of your technical writing today without losing your hard-earned SEO equity or past article catalog.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Published via &lt;a href="https://zyvop.com/zero-friction-migration-how-to-import-your-existing-developer-blog-into-zyvop-huck5?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=syndication" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt; — Write once in Markdown, auto-backup to GitHub, and syndicate to Dev.to, Medium &amp;amp; Hashnode in 1 click.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>blogging</category>
      <category>developertools</category>
      <category>markdown</category>
      <category>migrations</category>
    </item>
    <item>
      <title>Building Production Subscriptions with Razorpay: Webhooks, Signatures, and Edge Cases</title>
      <dc:creator>Sanjay Singh</dc:creator>
      <pubDate>Fri, 16 Oct 2026 13:00:45 +0000</pubDate>
      <link>https://dev.to/sanjay_singh_1/building-production-subscriptions-with-razorpay-webhooks-signatures-and-edge-cases-59mp</link>
      <guid>https://dev.to/sanjay_singh_1/building-production-subscriptions-with-razorpay-webhooks-signatures-and-edge-cases-59mp</guid>
      <description>&lt;p&gt;If you have integrated Stripe before, you might expect payment gateways to behave consistently: create a customer, attach a payment method, subscribe them to a price ID, and listen for &lt;code&gt;customer.subscription.updated&lt;/code&gt; webhooks.&lt;/p&gt;

&lt;p&gt;When you integrate &lt;strong&gt;Razorpay&lt;/strong&gt; for the Indian market, that mental model breaks almost immediately.&lt;/p&gt;

&lt;p&gt;Between Reserve Bank of India (RBI) e-mandate regulations on recurring card transactions, asynchronous webhook delivery delays, cryptographic signature quirks where parameters are silently inverted between endpoints, and Fastify/NestJS stream parsing bugs, getting Razorpay to run reliably in production requires defensive engineering that the official documentation barely touches.&lt;/p&gt;

&lt;p&gt;In this guide, we break down the exact production architecture we built for ZyVOP: a &lt;strong&gt;dual-mode subscription engine&lt;/strong&gt; in NestJS and Fastify that handles recurring e-mandates, falls back to fixed-term orders when cards fail, enforces constant-time signature verification, and guarantees idempotent webhook processing.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Two Indian Payment Realities: Subscriptions vs. Orders
&lt;/h2&gt;

&lt;p&gt;The single biggest mistake teams make when launching recurring billing in India is assuming every customer can be placed on an automated recurring subscription.&lt;/p&gt;

&lt;p&gt;Under RBI guidelines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Automated recurring debits require a compliant &lt;strong&gt;e-mandate&lt;/strong&gt; registration.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Many Indian debit cards, prepaid cards, corporate credit cards, and UPI apps reject auto-debit mandates during checkout.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Mandates above ₹15,000 require additional factor authentication (AFA) for every single billing cycle.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your checkout &lt;em&gt;only&lt;/em&gt; supports Razorpay Subscriptions (&lt;code&gt;/v1/subscriptions&lt;/code&gt;), you will encounter a 30% to 50% checkout drop-off rate simply because your users' bank instruments cannot register an e-mandate.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Dual-Mode Architecture
&lt;/h3&gt;

&lt;p&gt;To solve this, our backend implements a dual-mode strategy:&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%2Fmermaid.ink%2Fimg%2Fpako%3AeNqdk1FP2zAYRf_KNz9MQ0sp23gh2pg6SkcFtFXTIiGCKsf-mlo4dmQ7LQX63yc7JcuQtoe9OLFj3XNu4jwTpjmSmCyl3rAVNQ5m_VQBAMwtmruU-AswKdiDhXmZG8oRnIaJ0Sm5h07nFM5WyB6eU3JBLRhklTFC5VBKqr5mpnvKtFqKvDLI4T3Yqiy1cci_p2RXc-oxhPi4l1u0MQwqKSGpMsuMKJ3Q6sXPBlJv7lIyGScz6K4_dW1rgw2wDx67EDwO_MX1eDS7uLo9CM8SR11lY-hNJtPxTe9qMTkf9Yejnym5f-sw0tCFuWps4Ywa_gJjw9G8ldB-cU-nha6Ui-H45OToCEoqLEYwHE1rgSkyFKWLwbDSLSprFoeHhw29Hvctw4tN-pd3KZnSJ21Kuq31dOX8eo1r918IftBkNaKvOZ__HRQ6_JGwt-lfhoQbNGK5TaqsKU5L4ctnQkqh8q7ZZ3fXYef-W9BtgcoNeQRt0zAXuaKuMvib6C1bsFDhf3Ghz1849dg0Csgec2KNdb9wTx3CWOXan-T2MWxSWpKthFfpJmMgHpF3ZmgK6DGGdn9O_E-Cyk3QCM3PFYdvoPQGPsKXI-B0a70tiUiBpqCCk_iZuBUW_j_luKSVdCSqV26oETSTaP2epVZuQAshtyQmHVqWEjt2ax0WEfyQQj1cU5aE-UArF0FKEsw1wnyYkgimOtNOR3CBco1OMBpBzwgqI7BU2Y71hUkUIIl48i6fjstHsttFJMvPtNSGxOTdZiUckt0vYURzGg%3Ftype%3Dpng" 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%2Fmermaid.ink%2Fimg%2Fpako%3AeNqdk1FP2zAYRf_KNz9MQ0sp23gh2pg6SkcFtFXTIiGCKsf-mlo4dmQ7LQX63yc7JcuQtoe9OLFj3XNu4jwTpjmSmCyl3rAVNQ5m_VQBAMwtmruU-AswKdiDhXmZG8oRnIaJ0Sm5h07nFM5WyB6eU3JBLRhklTFC5VBKqr5mpnvKtFqKvDLI4T3Yqiy1cci_p2RXc-oxhPi4l1u0MQwqKSGpMsuMKJ3Q6sXPBlJv7lIyGScz6K4_dW1rgw2wDx67EDwO_MX1eDS7uLo9CM8SR11lY-hNJtPxTe9qMTkf9Yejnym5f-sw0tCFuWps4Ywa_gJjw9G8ldB-cU-nha6Ui-H45OToCEoqLEYwHE1rgSkyFKWLwbDSLSprFoeHhw29Hvctw4tN-pd3KZnSJ21Kuq31dOX8eo1r918IftBkNaKvOZ__HRQ6_JGwt-lfhoQbNGK5TaqsKU5L4ctnQkqh8q7ZZ3fXYef-W9BtgcoNeQRt0zAXuaKuMvib6C1bsFDhf3Ghz1849dg0Csgec2KNdb9wTx3CWOXan-T2MWxSWpKthFfpJmMgHpF3ZmgK6DGGdn9O_E-Cyk3QCM3PFYdvoPQGPsKXI-B0a70tiUiBpqCCk_iZuBUW_j_luKSVdCSqV26oETSTaP2epVZuQAshtyQmHVqWEjt2ax0WEfyQQj1cU5aE-UArF0FKEsw1wnyYkgimOtNOR3CBco1OMBpBzwgqI7BU2Y71hUkUIIl48i6fjstHsttFJMvPtNSGxOTdZiUckt0vYURzGg%3Ftype%3Dpng" alt="Mermaid Diagram" width="586" height="935"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Recurring Subscriptions (&lt;/strong&gt;&lt;code&gt;sub_...&lt;/code&gt;&lt;strong&gt;):&lt;/strong&gt; For users with auto-debit cards. The plan automatically renews every month or year.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fixed-Term Orders (&lt;/strong&gt;&lt;code&gt;order_...&lt;/code&gt;&lt;strong&gt;):&lt;/strong&gt; For users paying with UPI QR, NetBanking, or non-mandate debit cards. We generate a one-time order for the exact period amount (e.g., ₹499 or ₹4,999) and set an explicit expiration timestamp (&lt;code&gt;currentPeriodEnd = Date.now() + 30 days&lt;/code&gt;). When the period ends, the user simply renews.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This single design decision eliminated failed payment drop-offs across Indian customers.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The Flipped Signature Verification Trap
&lt;/h2&gt;

&lt;p&gt;To prevent clients from tampering with checkout outcomes, Razorpay returns a cryptographic HMAC-SHA256 signature when checkout completes in the browser. You must verify this signature on your backend before granting access.&lt;/p&gt;

&lt;p&gt;Here is the trap: &lt;strong&gt;The payload format is inverted depending on whether you created an Order or a Subscription.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Order Signature Payload
&lt;/h3&gt;

&lt;p&gt;When verifying a one-time Order: $$\text{payload} = \text{order_id} + \text{"|"} + \text{payment_id}$$&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;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;orderId&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;paymentId&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="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="s1"&gt;hex&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;h3&gt;
  
  
  The Subscription Signature Payload
&lt;/h3&gt;

&lt;p&gt;When verifying a recurring Subscription: $$\text{payload} = \text{payment_id} + \text{"|"} + \text{subscription_id}$$&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;// Notice paymentId comes FIRST!&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;paymentId&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;subscriptionId&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="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="s1"&gt;hex&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;If you use the order format (&lt;code&gt;subscriptionId|paymentId&lt;/code&gt;) to verify a subscription, &lt;strong&gt;the signature will fail every time&lt;/strong&gt;, even though all IDs are valid and the user was successfully charged.&lt;/p&gt;

&lt;h3&gt;
  
  
  Constant-Time Verification
&lt;/h3&gt;

&lt;p&gt;Never compare cryptographic signatures using standard JavaScript equality (&lt;code&gt;expected === signature&lt;/code&gt;). That creates a timing side-channel attack where an attacker can deduce bytes of your secret by measuring microsecond response latency. Always use &lt;code&gt;timingSafeEqual&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;timingSafeEqual&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;node: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;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verifySubscriptionSignature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;paymentId&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;subscriptionId&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;signature&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;keySecret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&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;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;keySecret&lt;/span&gt;&lt;span class="p"&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;paymentId&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;subscriptionId&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="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="s1"&gt;hex&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;expectedBuf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&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;receivedBuf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signature&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&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;expectedBuf&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="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;expectedBuf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;receivedBuf&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="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;timingSafeEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expectedBuf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;receivedBuf&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;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UnauthorizedException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid Razorpay subscription signature&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;
  
  
  3. Fastify &amp;amp; NestJS: Preserving Raw Buffers for Webhooks
&lt;/h2&gt;

&lt;p&gt;Webhooks are the ultimate source of truth for payment status. If a user closes their browser tab right after payment, the client-side checkout callback will never fire. Only the webhook can tell you that the payment succeeded.&lt;/p&gt;

&lt;p&gt;Razorpay signs every webhook payload with the &lt;code&gt;X-Razorpay-Signature&lt;/code&gt; header. The signature is calculated as: $$\text{HMAC-SHA256}(\text{rawRequestBody}, \text{RAZORPAY_WEBHOOK_SECRET})$$&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Naive NestJS/Fastify Implementations Fail
&lt;/h3&gt;

&lt;p&gt;In Fastify and Express, body parsers automatically parse incoming JSON payloads into JavaScript objects before your controller runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Raw&lt;/span&gt; &lt;span class="nx"&gt;Network&lt;/span&gt; &lt;span class="nx"&gt;Stream&lt;/span&gt;  &lt;span class="o"&gt;--&amp;gt;&lt;/span&gt;  &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="o"&gt;--&amp;gt;&lt;/span&gt;  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JS&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your controller attempts to verify the signature by re-serializing the body:&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;// ❌ CRITICAL BUG: This will fail verification randomly!&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&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;raw&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="s1"&gt;hex&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;This fails because &lt;code&gt;JSON.stringify()&lt;/code&gt; does not preserve:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Whitespace or indentation from the original HTTP body.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The original key order of JSON fields.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Escaped Unicode characters or slash formatting.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Even a single missing space alters the SHA-256 hash entirely.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Fix: Fastify Raw Body Preservation
&lt;/h3&gt;

&lt;p&gt;In NestJS with the Fastify adapter (&lt;code&gt;@nestjs/platform-fastify&lt;/code&gt;), you must enable raw body buffering at the application root:&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;// main.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;NestFactory&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;@nestjs/core&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;FastifyAdapter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NestFastifyApplication&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;@nestjs/platform-fastify&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&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;bootstrap&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;app&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;NestFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;NestFastifyApplication&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;AppModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FastifyAdapter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;bufferLogs&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="na"&gt;rawBody&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="c1"&gt;// 👈 Required: Attaches raw Buffer to req.rawBody&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in your billing controller, enforce that the raw buffer exists before passing it to verification:&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;// billing.controller.ts&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;webhooks/razorpay&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;razorpayWebhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;headers&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="kr"&gt;any&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Req&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;FastifyRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&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;rawBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;rawBody&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;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BadRequestException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Raw Razorpay webhook body is unavailable&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;billing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handleRazorpayWebhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in your verification service:&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="nf"&gt;verifyWebhookSignature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signature&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;void&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;secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RAZORPAY_WEBHOOK_SECRET&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;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&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;rawBody&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// 👈 Digests the exact binary stream&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="s1"&gt;hex&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;expectedBuf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&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;receivedBuf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signature&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&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;expectedBuf&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="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;expectedBuf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;receivedBuf&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="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;timingSafeEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expectedBuf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;receivedBuf&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;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UnauthorizedException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid Razorpay webhook signature&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;
  
  
  4. The Client-Server Handshake &amp;amp; Race Conditions
&lt;/h2&gt;

&lt;p&gt;When a payment succeeds, two events happen almost simultaneously:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Client-side:&lt;/strong&gt; The Razorpay modal triggers the &lt;code&gt;handler()&lt;/code&gt; callback with payment IDs and signature. The frontend calls &lt;code&gt;POST /api/v1/billing/razorpay/verify&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Server-side:&lt;/strong&gt; Razorpay's servers send an asynchronous webhook (&lt;code&gt;payment.captured&lt;/code&gt; or &lt;code&gt;subscription.activated&lt;/code&gt;) to &lt;code&gt;POST /api/v1/billing/webhooks/razorpay&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&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%2Fmermaid.ink%2Fimg%2Fpako%3AeNp9kkuP2jAQx7_K1IcVKyVFlXrKIRKPotJnmpQeEFI1cQZi4djp2IFlV3z3yhDYXaHWF2ui_-MX209C2opEIhz96chImircMDYrAwCAnbema0rifpbeMizcZW6RvZKqReNhxtZ4MhWggzHbvSOGQY6Plls8QDH9fH_rGaPc9pZv5PynAkbZ_FY2HQdFZp3fMBU_vtwq8mUWJM91xLsAeVYG4DhNL4QJTGzTavLkYJHNYQgT5ApGna_JeCXRK9s7L5Y4TtN8mSWQ4aEh409iy-qRqrPwigQZMmpNGj48kOxCFOQo6SwIK19m8SuanHzHxkF7zv6tKrgDpzYGfccvjFeYNO0PLoHse_EThtz_93BHrNaHZ0uvi9N0Ok5gUpPcgutKJ1m1JzS7N8SuVi3chdtVO_R9I5rqNfNN657K2tqtu9bD4GX220tedf8voJyk5Qr6IKBdONrBaZv_32Wk0gTOo-_cDTqZSkSiIW5QVSJ5Er6mJjzyitbYaS-i85dfyApLTS5o1tb4GTZKH0QiYmxbTbE7OE9NBGOtzPYryuI0z6zxEaxEQRtLsJivRAS5La23EXwkvaPwhiIYsUIdgUPjYhfuRUSnkkI9BpZ379sHcTxGotxMrLYsEvFmXytP4vgXRI0xew%3Ftype%3Dpng" 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%2Fmermaid.ink%2Fimg%2Fpako%3AeNp9kkuP2jAQx7_K1IcVKyVFlXrKIRKPotJnmpQeEFI1cQZi4djp2IFlV3z3yhDYXaHWF2ui_-MX209C2opEIhz96chImircMDYrAwCAnbema0rifpbeMizcZW6RvZKqReNhxtZ4MhWggzHbvSOGQY6Plls8QDH9fH_rGaPc9pZv5PynAkbZ_FY2HQdFZp3fMBU_vtwq8mUWJM91xLsAeVYG4DhNL4QJTGzTavLkYJHNYQgT5ApGna_JeCXRK9s7L5Y4TtN8mSWQ4aEh409iy-qRqrPwigQZMmpNGj48kOxCFOQo6SwIK19m8SuanHzHxkF7zv6tKrgDpzYGfccvjFeYNO0PLoHse_EThtz_93BHrNaHZ0uvi9N0Ok5gUpPcgutKJ1m1JzS7N8SuVi3chdtVO_R9I5rqNfNN657K2tqtu9bD4GX220tedf8voJyk5Qr6IKBdONrBaZv_32Wk0gTOo-_cDTqZSkSiIW5QVSJ5Er6mJjzyitbYaS-i85dfyApLTS5o1tb4GTZKH0QiYmxbTbE7OE9NBGOtzPYryuI0z6zxEaxEQRtLsJivRAS5La23EXwkvaPwhiIYsUIdgUPjYhfuRUSnkkI9BpZ379sHcTxGotxMrLYsEvFmXytP4vgXRI0xew%3Ftype%3Dpng" alt="Mermaid Diagram" width="1312" height="617"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Race Condition Failure Modes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;If the webhook arrives first and locks the subscription row, the frontend &lt;code&gt;/verify&lt;/code&gt; call might throw a database lock conflict.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If the user has an aggressive ad-blocker or loses Wi-Fi right as the payment succeeds, the frontend &lt;code&gt;/verify&lt;/code&gt; call never happens.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Razorpay retries webhooks on network failure. If your webhook handler isn't idempotent, you might trigger duplicate activation emails or miscount subscriptions.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Solution: Idempotent Webhook Storage with &lt;code&gt;23505&lt;/code&gt; Handling
&lt;/h3&gt;

&lt;p&gt;We maintain a dedicated &lt;code&gt;webhook_events&lt;/code&gt; table in PostgreSQL with a unique constraint on &lt;code&gt;(provider, provider_event_id)&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;// billing.service.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;eventId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-razorpay-event-id&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;eventType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;event&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="c1"&gt;// 1. In-memory / fast-path deduplication&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;duplicate&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhookEvents&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;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BillingProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RAZORPAY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;providerEventId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;eventId&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;duplicate&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;received&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="na"&gt;duplicate&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;// 2. Execute reconciliation&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;providerSubscriptionId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; 
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;subscription&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;id&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; 
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;payment&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;subscription_id&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;providerSubscriptionId&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;eventType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;subscription.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reconcileRazorpaySubscription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;providerSubscriptionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// 3. Atomically persist event ID; catch concurrent duplicate insert race&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;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhookEvents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhookEvents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BillingProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RAZORPAY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;providerEventId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;eventType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;providerSubscriptionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;providerSubscriptionId&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;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// PostgreSQL 23505 = unique_violation. Concurrent worker already handled this.&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="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;23505&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;received&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  5. Preventing Configuration Drift on Startup
&lt;/h2&gt;

&lt;p&gt;One of the nastiest production bugs in payment systems is &lt;strong&gt;currency or amount drift&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Someone edits the price on the marketing page to ₹499/mo.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In the Razorpay Dashboard, someone created a plan for ₹49.90 because they entered &lt;code&gt;499&lt;/code&gt; instead of &lt;code&gt;49900&lt;/code&gt; paise.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Or someone configured the plan as USD instead of INR.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Users will sign up, get charged ₹49.90, and your database will grant them full access while you lose revenue.&lt;/p&gt;

&lt;p&gt;We prevent this by introducing an &lt;strong&gt;assertive configuration guard&lt;/strong&gt; on boot:&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;// razorpay.service.ts&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;assertPlanConfiguration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BillingInterval&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="k"&gt;void&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;planId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;planId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interval&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;validatedPlanIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;planId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;plan&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/plans/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;planId&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;expectedPeriod&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;interval&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;BillingInterval&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MONTH&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;monthly&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;yearly&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;expectedAmount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;interval&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;BillingInterval&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MONTH&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;49&lt;/span&gt;&lt;span class="nx"&gt;_900&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;499&lt;/span&gt;&lt;span class="nx"&gt;_900&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// in paise&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;valid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;plan&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;period&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;expectedPeriod&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;interval&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="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;plan&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="nx"&gt;currency&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;INR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;plan&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="nx"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;plan&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="nx"&gt;unit_amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;expectedAmount&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;valid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ServiceUnavailableException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`Razorpay &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt; plan must be &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;expectedPeriod&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; at ₹&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;expectedAmount&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; INR`&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;validatedPlanIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;planId&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;When a user initiates checkout, the server verifies the plan with Razorpay's API before issuing the subscription ID. If an admin pasted the wrong Plan ID or configured the wrong currency, the transaction fails immediately with an actionable error rather than billing the customer incorrectly.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Prorations, Subunit Math, and Refunds
&lt;/h2&gt;

&lt;p&gt;Like Stripe, Razorpay deals strictly in currency &lt;strong&gt;subunits&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;1 INR = 100 paise&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;₹499.00 must be passed as &lt;code&gt;49900&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;₹4,999.00 must be passed as &lt;code&gt;499900&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Never Use Floating Point for Currency Math
&lt;/h3&gt;

&lt;p&gt;In JavaScript:&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="mf"&gt;0.1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt; &lt;span class="c1"&gt;// false (0.30000000000000004)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When calculating prorated refunds for downgraded subscriptions:&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;// proration.util.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;calculateProratedRefund&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;totalPaidSubunits&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;// 49900&lt;/span&gt;
  &lt;span class="nx"&gt;periodStart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;periodEnd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;cancelDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;totalDurationMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;periodEnd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;periodStart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&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;remainingMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&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="nx"&gt;periodEnd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;cancelDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&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;totalDurationMs&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;remainingMs&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Perform division last, truncate to integer paise&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;refundPaise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;totalPaidSubunits&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;remainingMs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;totalDurationMs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Enforce minimum transaction threshold (Razorpay requires &amp;gt;= 100 paise)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;refundPaise&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;refundPaise&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Issuing the Refund via API
&lt;/h3&gt;

&lt;p&gt;To refund a Razorpay payment, query the payments or invoices linked to the subscription, find the captured payment ID, and issue the partial refund:&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;// razorpay.service.ts&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;refundPayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;paymentId&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;amountPaise&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;notes&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="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="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="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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;status&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="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="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;body&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="kr"&gt;any&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="k"&gt;if &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;amountPaise&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;amountPaise&lt;/span&gt; &lt;span class="o"&gt;&amp;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="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;amountPaise&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;notes&lt;/span&gt;&lt;span class="p"&gt;)&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="nx"&gt;notes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;notes&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/payments/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;paymentId&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;/refund`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  7. Status Mapping &amp;amp; The "Halted" Trap
&lt;/h2&gt;

&lt;p&gt;Razorpay subscription lifecycles do not map 1:1 to generic billing states. In particular, Razorpay has a status called &lt;code&gt;halted&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A subscription moves to &lt;code&gt;halted&lt;/code&gt; when recurring debit attempts fail repeatedly (e.g., customer had insufficient funds or expired card). &lt;strong&gt;A halted subscription is NOT active&lt;/strong&gt;, but naive implementations check:&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;// ❌ WRONG&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;remote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cancelled&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;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isPro&lt;/span&gt; &lt;span class="o"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This would grant free ongoing Pro access to users whose cards are declining!&lt;/p&gt;

&lt;p&gt;Here is the authoritative status mapping:&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;private&lt;/span&gt; &lt;span class="nf"&gt;mapRazorpayStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&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;SubscriptionStatus&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;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&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="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Authenticated = Mandate created, awaiting first cycle&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;s&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;authenticated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;SubscriptionStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ACTIVE&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;s&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cancelled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;SubscriptionStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CANCELLED&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;s&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expired&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;SubscriptionStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;EXPIRED&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Halted / Paused / Pending = Payment failure, revoke paid features immediately&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;s&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paused&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;halted&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;SubscriptionStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SUSPENDED&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;SubscriptionStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;APPROVAL_PENDING&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;
  
  
  8. Summary Checklist for Production
&lt;/h2&gt;

&lt;p&gt;Before taking your Razorpay integration live, run through this checklist:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dual-Mode Checkout:&lt;/strong&gt; Do you support one-time Orders as a fallback for users whose cards reject RBI recurring mandates?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Flipped Argument Verification:&lt;/strong&gt; Are you using &lt;code&gt;${orderId}|${paymentId}&lt;/code&gt; for orders and &lt;code&gt;${paymentId}|${subscriptionId}&lt;/code&gt; for subscriptions?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Timing-Safe Equality:&lt;/strong&gt; Are you comparing HMAC buffers using &lt;code&gt;crypto.timingSafeEqual()&lt;/code&gt; instead of &lt;code&gt;===&lt;/code&gt;?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fastify Raw Body:&lt;/strong&gt; Is &lt;code&gt;{ rawBody: true }&lt;/code&gt; configured in your HTTP adapter so webhooks are verified against the pristine stream?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Idempotent Webhooks:&lt;/strong&gt; Does your webhook handler catch PostgreSQL &lt;code&gt;23505&lt;/code&gt; unique violations to handle concurrent delivery gracefully?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Currency Subunits:&lt;/strong&gt; Are all amounts computed as integers in paise (₹499 = &lt;code&gt;49900&lt;/code&gt;) with no floating-point arithmetic?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Boot Plan Verification:&lt;/strong&gt; Do you assert that remote Razorpay plans match local interval and currency configurations on initialization?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Status Mapping:&lt;/strong&gt; Does your system suspend access when a subscription transitions to &lt;code&gt;halted&lt;/code&gt; or &lt;code&gt;paused&lt;/code&gt;?&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Getting payment plumbing right is unglamorous, but handling these edge cases upfront ensures your billing engine runs autonomously without silent revenue leaks or angry customers.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Published via &lt;a href="https://zyvop.com/building-production-subscriptions-with-razorpay-webhooks-signatures-and-edge-cases-odzkz?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=syndication" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt; — Write once in Markdown, auto-backup to GitHub, and syndicate to Dev.to, Medium &amp;amp; Hashnode in 1 click.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>fastify</category>
      <category>payments</category>
    </item>
    <item>
      <title>How to Publish One Technical Article to Dev.to, Hashnode, Medium, and WordPress (Without Losing Your SEO)</title>
      <dc:creator>Sanjay Singh</dc:creator>
      <pubDate>Fri, 16 Oct 2026 12:56:59 +0000</pubDate>
      <link>https://dev.to/sanjay_singh_1/how-to-publish-one-technical-article-to-devto-hashnode-medium-and-wordpress-without-losing-1mfa</link>
      <guid>https://dev.to/sanjay_singh_1/how-to-publish-one-technical-article-to-devto-hashnode-medium-and-wordpress-without-losing-1mfa</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Publishing across Dev.to, Hashnode, Medium, and WordPress maximizes developer discovery, but manual cross-posting introduces search engine cannibalization, mangled Markdown, stripped Mermaid diagrams, and tedious maintenance. Here is how to automate the entire syndication pipeline—via Web UI or headless GitOps (&lt;code&gt;npx zyvop&lt;/code&gt;)—while anchoring full canonical SEO authority to your sovereign domain.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  The 2-Hour Copy-Paste Tax
&lt;/h3&gt;

&lt;p&gt;You just spent 15 hours on a technical deep dive. You benchmarked memory leaks under high concurrency, profiled heap allocations, built detailed architectural diagrams, and verified code snippets against the latest runtime release.&lt;/p&gt;

&lt;p&gt;Your draft is crisp, authoritative, and finally finished in Markdown.&lt;/p&gt;

&lt;p&gt;Now comes the part every engineer dreads: &lt;strong&gt;the distribution grind&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you publish solely to your personal blog (Next.js, Astro, or Hugo), you hit the cold-start wall: near-zero discoverability without years of domain authority. But if you publish exclusively on walled platforms like Medium or Dev.to, you surrender your audience, brand equity, and content sovereignty.&lt;/p&gt;

&lt;p&gt;The industry-standard solution is &lt;strong&gt;multi-platform syndication&lt;/strong&gt;: publish on your primary home, then syndicate to Dev.to, Hashnode, Medium, and WordPress.&lt;/p&gt;

&lt;p&gt;Yet in practice, doing this manually costs at least two hours per article:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Opening four browser dashboards and copy-pasting Markdown or HTML.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fixing broken code fences and stripped syntax highlighting tags.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Manually re-uploading architecture diagrams because Medium and WordPress don't support Mermaid.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Digging through four distinct settings panels to set canonical links.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Praying you don't discover a typo ten minutes later, requiring you to repeat the cycle across all four platforms.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There is a much better way. Let’s break down the technical pitfalls of multi-platform publishing and explore how to automate the entire flow cleanly with &lt;strong&gt;ZyVOP&lt;/strong&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Four Mechanical Failure Points of Manual Syndication
&lt;/h3&gt;

&lt;p&gt;Before looking at the solution, let’s diagnose why manual syndication breaks:&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%2Fmermaid.ink%2Fimg%2Fpako%3AeNp1ktFu2kAQRX9lOk-tYqciyRNqkWhcSipoKIZELe7D2B7wKusdNF6gNOLfK7OkTSvlcWfv0bk72kcspGTs4tLKrqhIPcySzAEA9NWbwvIiw9S4lWWYcVE5U5B9unqX69ve6zHpQyk7B2eQGFop1Q2cwbWU_CbDHxDHPUg6iwwT3p57CcxAlGuYfkxn0J_ctLl_nAG6WGQ4pKZyUp5Un5TW1dcRpEXFNb2AXS4yHHNpNnWAniyw7bxAXC0yvBctJ8pNE6D7Sfw5vf1yrBiowCUdiM8hQ2X7viAnYR_GwUDF-Zq8Z80QzuMeXLfX7fZky8pm5cLkCMyno-D5JhuFCWsjjiwkUpNxIArf93e3Exhu8r-Vk4ugFjUr48ienjCfjlr_aTXP3SfsMmB_2s7VwpqUav6v6yl_FfJ979XkG2_Ewci4BziDIVMJY_b0nMIIa9aaTIndR_QV1-13KnlJG-sxCpM7UkO55abNLMX5AdXG7rGLMa3XluNm33iuI_hgjXsYU5EezwNxPoIMU14Jw_wmwwimkouXCIZst-xNQRH01ZCNoCHXxA2rWWJ0lKTmV9ulc7X-iYdDhPnqWqwodvHVrjKe8fAbYXv0Dw%3Ftype%3Dpng" 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%2Fmermaid.ink%2Fimg%2Fpako%3AeNp1ktFu2kAQRX9lOk-tYqciyRNqkWhcSipoKIZELe7D2B7wKusdNF6gNOLfK7OkTSvlcWfv0bk72kcspGTs4tLKrqhIPcySzAEA9NWbwvIiw9S4lWWYcVE5U5B9unqX69ve6zHpQyk7B2eQGFop1Q2cwbWU_CbDHxDHPUg6iwwT3p57CcxAlGuYfkxn0J_ctLl_nAG6WGQ4pKZyUp5Un5TW1dcRpEXFNb2AXS4yHHNpNnWAniyw7bxAXC0yvBctJ8pNE6D7Sfw5vf1yrBiowCUdiM8hQ2X7viAnYR_GwUDF-Zq8Z80QzuMeXLfX7fZky8pm5cLkCMyno-D5JhuFCWsjjiwkUpNxIArf93e3Exhu8r-Vk4ugFjUr48ienjCfjlr_aTXP3SfsMmB_2s7VwpqUav6v6yl_FfJ979XkG2_Ewci4BziDIVMJY_b0nMIIa9aaTIndR_QV1-13KnlJG-sxCpM7UkO55abNLMX5AdXG7rGLMa3XluNm33iuI_hgjXsYU5EezwNxPoIMU14Jw_wmwwimkouXCIZst-xNQRH01ZCNoCHXxA2rWWJ0lKTmV9ulc7X-iYdDhPnqWqwodvHVrjKe8fAbYXv0Dw%3Ftype%3Dpng" alt="Mermaid Diagram" width="851" height="374"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  1. SEO Cannibalization &amp;amp; Canonical Disasters
&lt;/h4&gt;

&lt;p&gt;Search engine algorithms penalize or consolidate duplicate content. If Google discovers your article on Dev.to or Medium before your personal blog, their domain authority (DA 80+) will outrank your site unless an explicit &lt;code&gt;rel="canonical"&lt;/code&gt; tag tells Google: &lt;em&gt;"Credit the original author URL as the definitive source."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every platform implements this differently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dev.to:&lt;/strong&gt; Requires &lt;code&gt;canonical_url&lt;/code&gt; in YAML frontmatter or the &lt;code&gt;article&lt;/code&gt; REST payload.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hashnode:&lt;/strong&gt; Requires &lt;code&gt;originalArticleURL&lt;/code&gt; inside its GraphQL &lt;code&gt;PublishPostInput&lt;/code&gt; mutation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Medium:&lt;/strong&gt; Accepts &lt;code&gt;canonicalUrl&lt;/code&gt; only upon initial article creation (Medium's API does not permit updating published posts).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WordPress:&lt;/strong&gt; Lacks a native canonical REST field out of the box, requiring either third-party plugin meta mapping (Yoast/RankMath) or structured HTML attribution.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  2. AST Transformation &amp;amp; Code Fence Collisions
&lt;/h4&gt;

&lt;p&gt;Markdown is not a single standard; it is a family of dialects. If your technical post includes Markdown code snippets with three backticks inside a fenced block, parsers like Dev.to's Forem misinterpret the fences, inject orphaned Liquid tags, and crash with &lt;code&gt;Unknown tag 'endraw'&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. Broken Architecture Diagrams (The Mermaid Problem)
&lt;/h4&gt;

&lt;p&gt;Dev.to and Hashnode render code fences, but Medium and WordPress have zero native Mermaid or LaTeX support. When you copy-paste raw Mermaid syntax into Medium, it displays as an unreadable block of raw plaintext. High-grade syndication must automatically compile diagram code into vector SVGs or standalone high-resolution images.&lt;/p&gt;

&lt;h4&gt;
  
  
  4. The Maintenance Dilemma (Idempotency)
&lt;/h4&gt;

&lt;p&gt;When an author patches a bug in a code example, manual syndication means hunting down URLs and editing four different CMS forms. If you automate via naive scripts, network retries risk spawning duplicate posts across your feeds.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Sovereign Publishing Model with ZyVOP
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;ZyVOP&lt;/strong&gt; was engineered from the ground up as a &lt;strong&gt;Developer Publishing Hub&lt;/strong&gt;. It enforces a single guiding philosophy:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Own your canonical source of truth; treat third-party networks purely as distribution edges.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&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%2Fmermaid.ink%2Fimg%2Fpako%3AeNptk29v2jAQxr_KzS8qqiWLNu1VXiBRaAsTtCmBVpoqTU58ELeOL_MfKKv47pMT2nWCvIrt3z1395z9ykoSyFJm8bdHXeJI8rXh9aMGAODekfZ1geawLh0ZGHhXkQFuYYFlpWXJ1WGvoxpunCxlw7WD4XQSwJ-7-9usXSSwnJzAyOA_7lKvpUbo3aB1P_LzY3yOQtrAdz8JXHilZndw59HjMT7CzYICPsLNF0cwyE6UML4JxJjbSpNAuDa8qe6mx9wMRQBnKKSvT0s9ZIF4ICMyg9bC_DJfdGTHdmbF_f5wOkkh84WStoKGrIPejJtnQVsNCQzyxaH34XQSaDKYwkSv0bqO_gwGLakNQsk1dZNYzg9FB_w9akh6gyZUb2ouBTiC_P4azoCLJ28dhFsAq3AB7H_RrcEpjKRtuCsrsDstZMmdJA1PVNj35mFgd7qsDGnyFq64jm-9607D1-rE_X47ihSy23wBCW9k0tqm0ELvvYVf3qjz49DxTQpN51VG1vXIyLXUXA06heV8eiJohuItm7dobPIqxT4J5n3MuDQqgvFidkrhIXsT2DbxkyWdbJtk8-1NY-CckYVvDSl4-aykfj6ooBYfvIz7_W7uKSy1XEkUXTcHM5fzqYUzUHKD4FBhjc7sWMTqbmAsfWWuwjo8VYEr7pVjUbdzz43khUIbmBVpd8VrqXYsZTFvGoWx3VmHdQQXobYZL_N2fUXaRfDIclwThkfJIphTQY4iGKPaoJMlj2BgJFcRWK5tbNHIFYvaJLn8E2r5-r15Yft9xIr1kBQZlrJP20o6ZPu_zj9w_g%3Ftype%3Dpng" 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%2Fmermaid.ink%2Fimg%2Fpako%3AeNptk29v2jAQxr_KzS8qqiWLNu1VXiBRaAsTtCmBVpoqTU58ELeOL_MfKKv47pMT2nWCvIrt3z1395z9ykoSyFJm8bdHXeJI8rXh9aMGAODekfZ1geawLh0ZGHhXkQFuYYFlpWXJ1WGvoxpunCxlw7WD4XQSwJ-7-9usXSSwnJzAyOA_7lKvpUbo3aB1P_LzY3yOQtrAdz8JXHilZndw59HjMT7CzYICPsLNF0cwyE6UML4JxJjbSpNAuDa8qe6mx9wMRQBnKKSvT0s9ZIF4ICMyg9bC_DJfdGTHdmbF_f5wOkkh84WStoKGrIPejJtnQVsNCQzyxaH34XQSaDKYwkSv0bqO_gwGLakNQsk1dZNYzg9FB_w9akh6gyZUb2ouBTiC_P4azoCLJ28dhFsAq3AB7H_RrcEpjKRtuCsrsDstZMmdJA1PVNj35mFgd7qsDGnyFq64jm-9607D1-rE_X47ihSy23wBCW9k0tqm0ELvvYVf3qjz49DxTQpN51VG1vXIyLXUXA06heV8eiJohuItm7dobPIqxT4J5n3MuDQqgvFidkrhIXsT2DbxkyWdbJtk8-1NY-CckYVvDSl4-aykfj6ooBYfvIz7_W7uKSy1XEkUXTcHM5fzqYUzUHKD4FBhjc7sWMTqbmAsfWWuwjo8VYEr7pVjUbdzz43khUIbmBVpd8VrqXYsZTFvGoWx3VmHdQQXobYZL_N2fUXaRfDIclwThkfJIphTQY4iGKPaoJMlj2BgJFcRWK5tbNHIFYvaJLn8E2r5-r15Yft9xIr1kBQZlrJP20o6ZPu_zj9w_g%3Ftype%3Dpng" alt="Mermaid Diagram" width="1904" height="650"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;ZyVOP automates the entire conversion and publishing lifecycle:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Canonical Anchoring:&lt;/strong&gt; Points all syndicated destinations back to your primary article on your custom domain (&lt;code&gt;https://yourdomain.com/blog/slug&lt;/code&gt;) or your ZyVOP profile.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dynamic Backtick Scaling:&lt;/strong&gt; Detects nested backticks and scales fences (&lt;code&gt;&lt;/code&gt;&lt;br&gt;
&lt;br&gt;
``&lt;code&gt;&lt;/code&gt;) so Dev.to and Forem never break.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Stateless Vector Diagram Compilation:&lt;/strong&gt; Compiles &lt;code&gt;mermaid&lt;/code&gt; code blocks into high-contrast diagram image URLs readable across light and dark themes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Automated UTM Attribution:&lt;/strong&gt; Tags backlinks with structured parameters (&lt;code&gt;?utm_source=devto&amp;amp;utm_medium=crosspost&lt;/code&gt;) to track readership funnels.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Idempotent Updates:&lt;/strong&gt; Recognizes existing articles by slug, updating live copies instead of duplicating them.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  Method 1: The GitOps &amp;amp; CLI Workflow (&lt;code&gt;npx zyvop&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;For developers who live inside Neovim, VS Code, or Obsidian, leaving your terminal to open multiple browser tabs breaks flow. ZyVOP provides a headless CLI that publishes Markdown files directly.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 1: Install or Run Directly via &lt;code&gt;npx&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;No global installation required:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
bash
npx zyvop --help


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

&lt;/div&gt;

&lt;p&gt;Or install globally:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
bash
npm install -g zyvop


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

&lt;/div&gt;
&lt;h4&gt;
  
  
  Step 2: Configure YAML Frontmatter
&lt;/h4&gt;

&lt;p&gt;Add syndication targets and metadata to your Markdown document:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
`markdown
---
title: "Zero-Trust Architecture in Distributed Systems"
subtitle: "A deep dive into mTLS, ephemeral tokens, and SPIFFE/SPIRE"
slug: "zero-trust-architecture-distributed-systems"
canonical_url: "https://alex.dev/blog/zero-trust-architecture"
tags: [security, devops, distributed-systems, architecture]
category: backend
generate_toc: true
cross_post:
  devto: true
  hashnode: true
  medium: true
  wordpress: true
---

## 1. Threat Modeling in Modern Microservices

Here is an architectural overview of mutual TLS handshake verification:



![Mermaid Diagram](https://mermaid.ink/img/pako:eNp1ULFOw0AM_RXjlcuAxJQhUgktVIKFQKcuTuIkFpe7cOe2hKr_jtJ0KAOb7ffs956PWPmaMcXIXzt2FT8KtYH6rQMAyK2w0yTLnkj5QGMKaycqZOH9pbig8MzW-pl_oSVZNmMpFBz2HCDnoNJIRcpwC_kC8o6sZdfyf0KX49eLGw5zKd791bsSXLoqjINyDf3kseAYxTtYRqXSSuy4RoM9h56kxvSI2nE_5a-5oZ1VNPNkQ0GotBwnTuOdrqgXO2KKCQ2D5SSOUbk38GDFfb5SVZz7lXdqYIsFt57hY71FA2--9OrN9Kc9q1RkYBGErIFILiZxSoXmLFLIz-Tl7n74xtPJYNnm3vqAKd4cOlHG0y_woZOe?type=png)

`

When implementing cryptographic handshakes...



````markdown
#### Step 3: Preview with a Dry Run
Verify how ZyVOP parses your frontmatter, resolves relative images, and structures platform payloads without authenticating:

```bash
zyvop publish ./zero-trust.md --dry-run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 4: Authenticate and Publish
&lt;/h4&gt;

&lt;p&gt;Authenticate once with your developer token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ZYVOP_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"zv_live_98a7f..."&lt;/span&gt;
zyvop publish ./zero-trust.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Zero-Trust Local Mode (&lt;code&gt;--local&lt;/code&gt;)
&lt;/h4&gt;

&lt;p&gt;For security-conscious engineers or strict enterprise environments where third-party API credentials cannot be stored on external servers, ZyVOP offers a &lt;code&gt;--local&lt;/code&gt; mode. Credentials are read from your local environment or CI secret manager, sent directly to providers, and never uploaded to ZyVOP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ZYVOP_DEVTO_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"dev_..."&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ZYVOP_HASHNODE_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"hn_..."&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ZYVOP_MEDIUM_API_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"med_..."&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ZYVOP_WORDPRESS_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://wp.alex.dev"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ZYVOP_WORDPRESS_USERNAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"alex"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ZYVOP_WORDPRESS_APP_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"xxxx xxxx xxxx xxxx"&lt;/span&gt;

zyvop publish ./zero-trust.md &lt;span class="nt"&gt;--local&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Automating with GitHub Actions
&lt;/h4&gt;

&lt;p&gt;You can trigger multi-platform distribution on every &lt;code&gt;git push&lt;/code&gt; to &lt;code&gt;main&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Publish Technical Articles&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;posts/**.md'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;syndicate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;22&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Publish to ZyVOP, Dev.to, Hashnode, Medium &amp;amp; WordPress&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ZYVOP_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.ZYVOP_TOKEN }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;npx zyvop publish ./posts/zero-trust.md&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Method 2: The Web Studio &amp;amp; One-Click Publisher
&lt;/h3&gt;

&lt;p&gt;If you prefer a rich visual authoring experience, ZyVOP’s Web Studio provides an author-first interface:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Connect Integrations Once:&lt;/strong&gt; Navigate to &lt;strong&gt;Settings → Integrations&lt;/strong&gt; and connect Dev.to, Hashnode, Medium, and WordPress with your API tokens or Application Passwords.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Distraction-Free Authoring:&lt;/strong&gt; Write in a hybrid Tiptap editor that seamlessly blends Markdown shortcuts, syntax-highlighted code blocks, live KaTeX formulas, and real-time Mermaid previewing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Syndication Drawer:&lt;/strong&gt; Click &lt;strong&gt;Publish&lt;/strong&gt;, select which platforms should receive the post, review canonical preview URLs, and hit confirm.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Real-Time Job Telemetry:&lt;/strong&gt; Monitor queued workers, inspect external platform post URLs, or trigger retries if a third-party platform API encounters a rate limit.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  Platform Syndication Matrix
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Protocol&lt;/th&gt;
&lt;th&gt;Canonical Mechanism&lt;/th&gt;
&lt;th&gt;Diagram Handling&lt;/th&gt;
&lt;th&gt;Idempotent Updates&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dev.to&lt;/td&gt;
&lt;td&gt;REST API&lt;/td&gt;
&lt;td&gt;canonical_url payload field&lt;/td&gt;
&lt;td&gt;Auto-converts to high-res SVG embed&lt;/td&gt;
&lt;td&gt;Full PUT /api/articles/:id updates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hashnode&lt;/td&gt;
&lt;td&gt;GraphQL&lt;/td&gt;
&lt;td&gt;originalArticleURL input&lt;/td&gt;
&lt;td&gt;Auto-converts to high-res SVG embed&lt;/td&gt;
&lt;td&gt;Full updatePost mutation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;REST API v1&lt;/td&gt;
&lt;td&gt;canonicalUrl creation param&lt;/td&gt;
&lt;td&gt;Auto-converts to high-res SVG embed&lt;/td&gt;
&lt;td&gt;Initial creation only (API constraint)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WordPress&lt;/td&gt;
&lt;td&gt;WP REST API&lt;/td&gt;
&lt;td&gt;Head meta + structured HTML backlink&lt;/td&gt;
&lt;td&gt;Auto-converts to high-res SVG embed&lt;/td&gt;
&lt;td&gt;Full PUT /wp-json/wp/v2/posts/:id&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h3&gt;
  
  
  The Result: Sovereign Reach Without the Friction
&lt;/h3&gt;

&lt;p&gt;Technical writing is one of the highest-leverage investments in a software engineer's career. It sharpens your architectural thinking, opens doors to leadership roles, and establishes authority in your domain.&lt;/p&gt;

&lt;p&gt;You should never have to sacrifice distribution to maintain ownership, nor should you spend two hours fighting copy-paste formatting bugs every time you publish.&lt;/p&gt;

&lt;p&gt;By combining &lt;strong&gt;canonical SEO protection&lt;/strong&gt;, &lt;strong&gt;automatic AST transformations&lt;/strong&gt;, and &lt;strong&gt;GitOps workflows&lt;/strong&gt;, ZyVOP gives you the best of both worlds:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Your content lives on your domain&lt;/strong&gt;, building your personal search authority.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Your articles reach tens of thousands of developers&lt;/strong&gt; across Dev.to, Hashnode, Medium, and WordPress on day one.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Your publishing pipeline is version-controlled&lt;/strong&gt;, automated, and painless.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Write once. Own your canonical home. Syndicate everywhere.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Ready to streamline your technical writing workflow? Get started with&lt;/em&gt; &lt;a href="https://zyvop.com" rel="noopener noreferrer"&gt;&lt;em&gt;ZyVOP&lt;/em&gt;&lt;/a&gt; &lt;em&gt;or run&lt;/em&gt; &lt;code&gt;npx zyvop publish --help&lt;/code&gt; &lt;em&gt;in your terminal today.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Published via &lt;a href="https://zyvop.com/how-to-publish-one-technical-article-to-dev-to-hashnode-medium-and-wordpress-without-losing-your-seo-tsvo6?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=syndication" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt; — Write once in Markdown, auto-backup to GitHub, and syndicate to Dev.to, Medium &amp;amp; Hashnode in 1 click.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>blogging</category>
      <category>crossposting</category>
      <category>developertools</category>
      <category>syndication</category>
    </item>
    <item>
      <title>Inside ZyVOP: The Modern Developer Publishing Engine</title>
      <dc:creator>Sanjay Singh</dc:creator>
      <pubDate>Thu, 15 Oct 2026 04:53:15 +0000</pubDate>
      <link>https://dev.to/sanjay_singh_1/inside-zyvop-the-modern-developer-publishing-engine-331f</link>
      <guid>https://dev.to/sanjay_singh_1/inside-zyvop-the-modern-developer-publishing-engine-331f</guid>
      <description>&lt;p&gt;Technical writing is one of the highest-leverage habits an engineer can build. It forces clarity of thought, documents hard-won architecture lessons, and builds career reputation.&lt;/p&gt;

&lt;p&gt;Yet, the modern developer publishing experience has long been caught in a frustrating dilemma:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Self-Hosted Island:&lt;/strong&gt; You build a personal blog with Hugo, Astro, or Next.js. You get 100% control, but you face the cold-start reality: zero discovery and fragmented readership.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Walled Gardens:&lt;/strong&gt; You publish on centralized networks like Dev.to, Medium, or Hashnode. You get immediate audience discovery, but you surrender control, risk algorithm shifts, and battle third-party paywalls.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Cross-Posting Grind:&lt;/strong&gt; You try to do both. You write in Markdown, manually copy-paste into multiple CMS dashboards, re-format code blocks, fix broken image links, and manually set &lt;code&gt;rel="canonical"&lt;/code&gt; tags so search engines don't penalize your original site.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We built &lt;strong&gt;ZyVOP&lt;/strong&gt; to solve this dilemma once and for all: &lt;strong&gt;Write once, own your primary canonical source, and syndicate everywhere.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here is a look under the hood at what ZyVOP offers today.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Developer-First Technical Authoring
&lt;/h2&gt;

&lt;p&gt;Most blogging platforms are built for general prose, treating code snippets as second-class citizens. ZyVOP treats technical formatting as a core requirement:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Rich Hybrid Authoring:&lt;/strong&gt; A distraction-free editing experience powered by Tiptap that seamlessly balances Markdown shortcuts with WYSIWYG convenience.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;First-Class Code Syntax Highlighting:&lt;/strong&gt; Native language detection and syntax highlighting for dozens of programming languages.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Native Mermaid Architecture Diagrams:&lt;/strong&gt; Write diagram code directly in Markdown and have it render crisp, high-resolution architecture diagrams in real-time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;KaTeX Mathematical Notation:&lt;/strong&gt; Built-in LaTeX rendering for equations, formulas, and academic write-ups.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Shareable Draft Previews &amp;amp; Scheduling:&lt;/strong&gt; Generate private, revocable preview links for technical peer reviews before scheduling your publication date.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2. Multi-Platform Syndication with Canonical Protection
&lt;/h2&gt;

&lt;p&gt;Cross-posting without canonical attribution is dangerous: search engines can rank an external copy above your original publication.&lt;/p&gt;

&lt;p&gt;ZyVOP automates distribution across major developer ecosystems while actively safeguarding your SEO authority:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Full-Article Syndication:&lt;/strong&gt; Push full article copies with one click to &lt;strong&gt;Dev.to&lt;/strong&gt;, &lt;strong&gt;Hashnode&lt;/strong&gt;, &lt;strong&gt;Medium&lt;/strong&gt;, and &lt;strong&gt;WordPress&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Social Dispatches:&lt;/strong&gt; Automatically broadcast social dispatches to decentralized networks like &lt;strong&gt;Bluesky&lt;/strong&gt; and &lt;strong&gt;Mastodon&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Automated Canonical Attribution:&lt;/strong&gt; Every syndicated article automatically receives a &lt;code&gt;rel="canonical"&lt;/code&gt; link pointing back to your authoritative primary URL, consolidating search engine ranking signals to your home domain.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Resilient Background Processing:&lt;/strong&gt; Built on an asynchronous queue architecture that handles platform API rate limits, temporary downtimes, and automatic retries gracefully.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3. GitOps &amp;amp; CLI Workflows (&lt;code&gt;npx zyvop&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;Developers shouldn't be forced to leave their code editors just to publish a blog post.&lt;/p&gt;

&lt;p&gt;ZyVOP provides an open-source CLI and Git-native workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Publish or update an article directly from your terminal&lt;/span&gt;
npx zyvop publish ./posts/system-design.md &lt;span class="nt"&gt;--cross-post&lt;/span&gt; devto,hashnode
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Frontmatter-Driven Config:&lt;/strong&gt; Manage tags, canonical URLs, cover images, and cross-posting targets directly in standard YAML frontmatter.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;GitHub Actions Integration:&lt;/strong&gt; Commit Markdown files to your Git repository, push to &lt;code&gt;main&lt;/code&gt;, and let CI/CD automatically syndicate your articles across every platform.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Automated Git Backup:&lt;/strong&gt; Every post published through ZyVOP can automatically create versioned backup commits in your personal GitHub repository, guaranteeing you never lose your work.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  4. Independent Publications &amp;amp; Custom Domains
&lt;/h2&gt;

&lt;p&gt;You shouldn't have to choose between a personal social profile and an independent editorial brand. ZyVOP cleanly separates &lt;strong&gt;Human Identity&lt;/strong&gt; from &lt;strong&gt;Publication Branding&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Custom Domain Support:&lt;/strong&gt; Map your custom domain (&lt;code&gt;blog.yourdomain.com&lt;/code&gt; or &lt;code&gt;you.zyvop.dev&lt;/code&gt;) with automated SSL certification.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Decoupled Branding:&lt;/strong&gt; Customize independent logos, favicons, navigation bars, and static pages (&lt;code&gt;/about&lt;/code&gt;, &lt;code&gt;/projects&lt;/code&gt;) without altering your personal profile.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Flexible Theme Layouts:&lt;/strong&gt; Choose from multiple curated layout styles (Magazine, Stacked, or Grid) optimized for technical readability and dark mode.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Native Redirect Management:&lt;/strong&gt; Manage 301 and 302 HTTP redirects to maintain search equity when migrating legacy paths.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  5. Zero-Lock-In Migration
&lt;/h2&gt;

&lt;p&gt;If you already have a body of work on existing platforms, moving should not take days of manual copy-pasting.&lt;/p&gt;

&lt;p&gt;ZyVOP includes an automated migration pipeline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;1-Click Importer:&lt;/strong&gt; Import published articles directly from Dev.to, Hashnode, Medium, RSS feeds, or standalone URLs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Asset Preservation:&lt;/strong&gt; Cleans up messy HTML, converts raw content into clean Markdown, and preserves original publishing dates and canonical sources.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Full Data Export:&lt;/strong&gt; You own your data. Export your entire library of Markdown and media assets whenever you want.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  6. Audience Ownership &amp;amp; Unified Analytics
&lt;/h2&gt;

&lt;p&gt;When you publish on third-party networks, those networks own your audience. ZyVOP empowers creators to build direct relationships with their readers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Built-In Author Newsletters:&lt;/strong&gt; Double-opt-in email subscription engine that delivers your new articles directly to your readers' inboxes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Exportable Subscriber Lists:&lt;/strong&gt; Complete, portable CSV exports of your audience at any time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Aggregated Performance Metrics:&lt;/strong&gt; View unified engagement metrics—tracking reads, reactions, and cross-platform performance in one consolidated dashboard.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Community Engagement:&lt;/strong&gt; Threaded technical discussions, accepted answers, bookmarks, reading streaks, and achievement badges for active learners.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  7. Built for Modern Web Standards &amp;amp; AI Discovery
&lt;/h2&gt;

&lt;p&gt;Content discoverability is changing rapidly. ZyVOP is architected from the ground up for modern search and answer engines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Edge-Optimized Performance:&lt;/strong&gt; Built with Next.js App Router for sub-second page loads and zero layout shift.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dynamic Open Graph Images:&lt;/strong&gt; Server-rendered social cards tailored with post titles, author branding, and reading times for maximum click-through on social feeds.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Structured Data &amp;amp; Sitemaps:&lt;/strong&gt; Automatic JSON-LD schema generation, comprehensive XML sitemaps, and RSS feeds.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;AI/LLM-Ready Endpoints:&lt;/strong&gt; Optimized &lt;code&gt;llms.txt&lt;/code&gt; and semantic discovery endpoints allowing modern AI answer engines to cite and source your original work accurately.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Summary: A Single Home for Your Writing
&lt;/h2&gt;

&lt;p&gt;ZyVOP was created because technical writers shouldn't have to compromise between &lt;strong&gt;reach&lt;/strong&gt; and &lt;strong&gt;ownership&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;With developer-first authoring, native Git workflows, automated multi-platform distribution, and full canonical control, ZyVOP gives engineers the publishing leverage they need without the friction.&lt;/p&gt;

&lt;p&gt;Ready to test the workflow? Run &lt;code&gt;npx zyvop --help&lt;/code&gt; or explore &lt;a href="https://zyvop.com" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt; today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What problem does ZyVOP aim to solve for developers?
&lt;/h3&gt;

&lt;p&gt;It eliminates the trade‑off between self‑hosting control and the discovery offered by centralized networks by letting you write once, keep a canonical source, and syndicate everywhere.&lt;/p&gt;

&lt;h3&gt;
  
  
  How does ZyVOP handle code snippets and technical formatting?
&lt;/h3&gt;

&lt;p&gt;It provides first‑class syntax highlighting with native language detection, Mermaid diagram rendering, KaTeX math support, and a hybrid Markdown/WYSIWYG editor powered by Tiptap.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which platforms can ZyVOP automatically syndicate full articles to?
&lt;/h3&gt;

&lt;p&gt;With one click it pushes full copies to Dev.to, Hashnode, Medium, and WordPress, and also dispatches social posts to Bluesky and Mastodon.&lt;/p&gt;

&lt;h3&gt;
  
  
  How does ZyVOP protect SEO authority when cross‑posting?
&lt;/h3&gt;

&lt;p&gt;Each syndicated article receives an automatic rel="canonical" link pointing back to the primary URL, consolidating ranking signals to the home domain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I publish or update articles without leaving my code editor?
&lt;/h3&gt;

&lt;p&gt;Yes, the open‑source CLI (npx zyvop) lets you publish Markdown files directly from the terminal, using frontmatter for tags, canonical URLs, and cross‑post targets.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does ZyVOP support custom domains and independent branding?
&lt;/h3&gt;

&lt;p&gt;It allows mapping custom domains with automated SSL, separate logos, favicons, navigation, and multiple theme layouts without affecting personal profiles.&lt;/p&gt;

&lt;h3&gt;
  
  
  How can existing content be moved to ZyVOP?
&lt;/h3&gt;

&lt;p&gt;A 1‑click importer brings articles from Dev.to, Hashnode, Medium, RSS feeds, or URLs, cleans HTML to Markdown, preserves dates and canonical sources, and lets you export the full library anytime.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Published via &lt;a href="https://zyvop.com/inside-zyvop-the-modern-developer-publishing-engine-px1mh?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=syndication" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt; — Write once in Markdown, auto-backup to GitHub, and syndicate to Dev.to, Medium &amp;amp; Hashnode in 1 click.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Are Passkeys a Trap? Fact-Checking a Viral Warning</title>
      <dc:creator>Sanjay Singh</dc:creator>
      <pubDate>Wed, 14 Oct 2026 05:38:37 +0000</pubDate>
      <link>https://dev.to/sanjay_singh_1/are-passkeys-a-trap-fact-checking-a-viral-warning-3nb7</link>
      <guid>https://dev.to/sanjay_singh_1/are-passkeys-a-trap-fact-checking-a-viral-warning-3nb7</guid>
      <description>&lt;p&gt;&lt;a href="https://nonfunctional.substack.com/p/passkeys-are-a-trap-do-not-use-them" rel="noopener noreferrer"&gt;A Substack post&lt;/a&gt; going around says passkeys are a trap and nobody should use them. I read it, then checked its claims against the specs, vendor docs and NIST guidance. Short version: the author spotted a real long-term risk and then oversold it. “Don’t use passkeys” is bad advice for most people. “Don’t trust them blindly” is fair.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the post claims
&lt;/h2&gt;

&lt;p&gt;Passkeys aren’t one thing. Some copy across devices, some don’t. Some sit in a secure chip, some are plain files. Websites can see which type you made, and through attestation they can verify what software made it. So, the author argues, sites could eventually accept only approved software, handing big tech, banks and governments a veto over what you run. The suggested fix: authenticator-app codes (TOTP) or a hardware key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the post is right
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sites can see what kind of passkey you have.&lt;/strong&gt; WebAuthn flags tell a site whether a credential is synced or device-bound. WebAuthn Level 3, which formalizes them, became a W3C Recommendation on August 25, 2026.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sites can block providers.&lt;/strong&gt; Auth vendors sell allow-lists and block-lists by authenticator model, and warn that switching them on can lock out users who are already enrolled. Banks are the target customer.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The people writing the rules aren’t you.&lt;/strong&gt; FIDO’s board includes banks, payment networks and big platforms, and governments have had a seat since 2015. I found no consumer group on the public member list, though I only saw part of it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Device checks already lock people out.&lt;/strong&gt; GrapheneOS users report banking and ID apps refusing to run because of Google’s Play Integrity checks. That isn’t passkeys, but it shows the worry is real.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Moving passkeys has been painful.&lt;/strong&gt; FIDO published a transfer standard (CXP and CXF) as a proposed standard in March 2026. I couldn’t confirm which providers actually ship it.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where it falls apart
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The claim that attestation is coming for everyone.&lt;/strong&gt; Not on the evidence. The default is no attestation, and Apple and Google don’t send any for synced passkeys. Yubico’s own guidance says rejecting credentials without attestation will probably reject Apple, Google and Microsoft passkeys. Those providers hold over 90 percent of passkeys by one industry estimate, so a consumer site demanding attestation would shut out most of its own customers. That is a commercial brake, not a guarantee, but it is a real one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The claim that attestation inspects your whole device.&lt;/strong&gt; In the consumer flows I reviewed, it identifies the make and model of the authenticator. Whether your operating system is approved is checked by other systems, like Play Integrity or Apple’s App Attest. Some platform formats expose more, so the worry isn’t baseless, but I found no mainstream site using passkeys that way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The claim that intelligence agencies are members.&lt;/strong&gt; I couldn’t confirm it. The government members I could identify are standards and cybersecurity bodies such as NIST. That doesn’t prove no one else is involved, but the post doesn’t back the claim up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The nudity scanner.&lt;/strong&gt; The UK plan covers devices used by under-18s. The government describes on-device blocking with no data collection or reporting, and it doesn’t stop adults viewing anything. Nothing I found ties it to passkeys. The post’s wording implies a universal, mandatory scanner. That overstates what the policy says.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TOTP as the safe choice.&lt;/strong&gt; TOTP gives you freedom, but it can be phished: a fake login page grabs your code and replays it in real time. A passkey is bound to the real site’s domain, so a fake site gets nothing. NIST treats that binding as what makes a credential phishing-resistant, and says properly deployed synced passkeys can meet its AAL2 level. The post mentions this only in a footnote, and admits the author would like to recommend passkeys. So the headline says one thing and the author’s own footnote says another.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The claim that you don’t control your passkeys.&lt;/strong&gt; You have choices. 1Password, Bitwarden, Dashlane and Proton Pass all store passkeys. Few people use them, about 1 to 2 percent of new passkeys by one estimate, so platform defaults decide it for everyone else.&lt;/p&gt;

&lt;h2&gt;
  
  
  The risk the post barely touches
&lt;/h2&gt;

&lt;p&gt;You can lose them. A passkey can’t be reset by email like a password. Lose every copy, or the account that syncs them, and you can be locked out. For ordinary users that is a more likely problem than attestation lockouts, which I found no consumer site enforcing.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, trap or not?
&lt;/h2&gt;

&lt;p&gt;Not today. The standard does contain hooks, such as attestation, backup flags and provider allow-lists, that could be used to narrow your choices, and that is worth watching. But the threat is policy, not cryptography, and so far it is speculation. Meanwhile phishing and reused passwords are happening right now, and passkeys are designed to close both holes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I’d actually do
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Turn on passkeys for your email, banking, and Apple, Google or Microsoft account.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Register two: a second device, a security key, or a manager not tied to your phone. Save the recovery codes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Pick a manager that can export, and check whether it supports FIDO’s transfer standard.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Keep a hardware key or TOTP as a backup. TOTP works, but remember it can be phished.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Watch for red flags: a site that rejects your passkey manager, forces its app, or demands a specific device. Tell the site, and look for alternatives.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Published via &lt;a href="https://zyvop.com/are-passkeys-a-trap-fact-checking-a-viral-warning-jporc?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=syndication" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt; — Write once in Markdown, auto-backup to GitHub, and syndicate to Dev.to, Medium &amp;amp; Hashnode in 1 click.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>authentication</category>
      <category>cybersecurity</category>
      <category>factcheck</category>
      <category>passkeys</category>
    </item>
    <item>
      <title>Talorys: A Personal AI Agent in Your Own Cloudflare Account</title>
      <dc:creator>Sanjay Singh</dc:creator>
      <pubDate>Tue, 13 Oct 2026 15:16:46 +0000</pubDate>
      <link>https://dev.to/sanjay_singh_1/talorys-a-personal-ai-agent-in-your-own-cloudflare-account-2dc3</link>
      <guid>https://dev.to/sanjay_singh_1/talorys-a-personal-ai-agent-in-your-own-cloudflare-account-2dc3</guid>
      <description>&lt;p&gt;&lt;em&gt;A closer look at the open-source project&lt;/em&gt; &lt;a href="https://github.com/rociiu/talorys" rel="noopener noreferrer"&gt;&lt;em&gt;rociiu/talorys&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Many AI assistants follow the same pattern. You sign up, hand your conversations to someone else's servers, and trust that your notes, tasks and personal details are handled well. Talorys takes the opposite approach. It is a personal AI agent that runs entirely inside &lt;strong&gt;your own Cloudflare account&lt;/strong&gt;, with no server, database or account operated by its developers.&lt;/p&gt;

&lt;p&gt;The pitch is short: one person, one Cloudflare account, one command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx create-talorys@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What you get
&lt;/h2&gt;

&lt;p&gt;Talorys is a small, focused assistant rather than a sprawling platform. Out of the box it includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Chat&lt;/strong&gt; with streaming responses, Markdown rendering and indicators that show when the agent is using a tool.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Memory&lt;/strong&gt; for durable facts and preferences. You can view, edit and delete everything it remembers, and only the most relevant memories are sent to the model on each turn.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tasks, notes and projects&lt;/strong&gt; with full create, read, update and delete in the UI. You can also manage them from chat, for example by saying "Add a task to review my project tomorrow."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Automations&lt;/strong&gt; such as one-time and recurring reminders, daily task digests and optional AI routines, all delivered to an in-app notification center.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is single-user by design. There is no signup, no teams and no accounts. The installer asks you to set an owner password, and you sign in from any device with it.&lt;/p&gt;

&lt;p&gt;One detail I like: &lt;strong&gt;it works without AI.&lt;/strong&gt; If Workers AI is unavailable or you have used up your free daily quota, tasks, notes, memories and reminders keep working. Only chat pauses until the quota resets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploying in one command
&lt;/h2&gt;

&lt;p&gt;The installer does the tedious parts for you. It checks your Node.js version (20.18 or newer), verifies your Cloudflare login (or opens Cloudflare's authorization page in your browser), and lets you pick an account and an agent name. It then asks for an owner password, which is hashed locally with PBKDF2-SHA256 and stored only as a Cloudflare secret.&lt;/p&gt;

&lt;p&gt;From there it generates a session secret, deploys a private Worker along with its SQLite-backed Durable Object, creates the Pages project, and verifies the live deployment before printing your &lt;code&gt;pages.dev&lt;/code&gt; URL. The verification step checks the frontend, the auth endpoint and storage health without running any AI inference.&lt;/p&gt;

&lt;p&gt;Here is the whole run at a glance, split by where each step happens:&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%2Fmermaid.ink%2Fimg%2Fpako%3AeNptk9FP2zAQxv-Vm582LWG0lGmqJjSgYqCxwehgDw0PjnNtDM5ddnZaAuJ_n5wUWNH8Zsv-vs-_u3tQhgtUYzV3vDKllgC_JhkBAPjtt7NMUX0HRlAHTIN2LK3_4nRAHzJ1_W59sckXousSKm1KSzjL1BlBy408nWTqur8aFw1mmTos0dzCDy5w68bDcHtr8Ol9pq4hTfeAhrNMXaHYeQuHjpti7rQgOF5Y-pzLhz0W4BoJcuGVRwHdhJLF3utgmTa9hr3iTmfJ7BG0MdxQAE0F6AVSANLVq4Q7_avRLFNTDMArQoFae79iKboIpfYlFuDYaOdaWNlQwvnBt8nRMJ0e7w93P24KjnrB3VmmviKh6IDg0XvLBB6NYOhUY6aG7J8GQdBzIwa7dP5ZDal4Bd1EQrNMnayR_0Ns_dWNKCbSn2DtuIVa7DIm-c1yi_KcwAYP05-nNiBMGtG5QzjLb9CEpwKZWKBpYMF1eL9p0UM3HfSudTrYRW96rhfooRaOip1nBy-UUUyW1iDklgpLi03RviZm9NIb8YmzS1wrV0gvetzECrewfwKW5ihIBv8Dcbuvy6Df0m7vsd6avmoFE8ZJOBdLoWdcxz9sFbiEy4vTOAgqURVKpW2hxg8qlFjFmSpwrhsXVNKfXGmxkaaPd-ZM4UhX1rVqrFJd1w5T3_qAVQIHztLtd22m3f6IKSQQO3HBCJcnmUrggnMOnMAxuiUGa3QC-2K1S8Br8qmPgFTSmUztfcwyGNV36vExUfnikB2LGqs3q9IGVI9_AQSwTQ0%3Ftype%3Dpng" 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%2Fmermaid.ink%2Fimg%2Fpako%3AeNptk9FP2zAQxv-Vm582LWG0lGmqJjSgYqCxwehgDw0PjnNtDM5ddnZaAuJ_n5wUWNH8Zsv-vs-_u3tQhgtUYzV3vDKllgC_JhkBAPjtt7NMUX0HRlAHTIN2LK3_4nRAHzJ1_W59sckXousSKm1KSzjL1BlBy408nWTqur8aFw1mmTos0dzCDy5w68bDcHtr8Ol9pq4hTfeAhrNMXaHYeQuHjpti7rQgOF5Y-pzLhz0W4BoJcuGVRwHdhJLF3utgmTa9hr3iTmfJ7BG0MdxQAE0F6AVSANLVq4Q7_avRLFNTDMArQoFae79iKboIpfYlFuDYaOdaWNlQwvnBt8nRMJ0e7w93P24KjnrB3VmmviKh6IDg0XvLBB6NYOhUY6aG7J8GQdBzIwa7dP5ZDal4Bd1EQrNMnayR_0Ns_dWNKCbSn2DtuIVa7DIm-c1yi_KcwAYP05-nNiBMGtG5QzjLb9CEpwKZWKBpYMF1eL9p0UM3HfSudTrYRW96rhfooRaOip1nBy-UUUyW1iDklgpLi03RviZm9NIb8YmzS1wrV0gvetzECrewfwKW5ihIBv8Dcbuvy6Df0m7vsd6avmoFE8ZJOBdLoWdcxz9sFbiEy4vTOAgqURVKpW2hxg8qlFjFmSpwrhsXVNKfXGmxkaaPd-ZM4UhX1rVqrFJd1w5T3_qAVQIHztLtd22m3f6IKSQQO3HBCJcnmUrggnMOnMAxuiUGa3QC-2K1S8Br8qmPgFTSmUztfcwyGNV36vExUfnikB2LGqs3q9IGVI9_AQSwTQ0%3Ftype%3Dpng" alt="Mermaid Diagram" width="346" height="1336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The installer is also forgiving. If it gets interrupted, you run the same command again and it reconciles what already exists, with no duplicate resources and no data deleted. When you want the latest version later, &lt;code&gt;npx create-talorys@latest update&lt;/code&gt; redeploys to the same resources and keeps your password and sessions. There are also &lt;code&gt;status&lt;/code&gt; and &lt;code&gt;doctor&lt;/code&gt; commands, plus a &lt;code&gt;reset-password&lt;/code&gt; command if you get locked out.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works under the hood
&lt;/h2&gt;

&lt;p&gt;The architecture is lean, and it shows how far Cloudflare's platform can go on its own:&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%2Fmermaid.ink%2Fimg%2Fpako%3AeNpdklFP3DAMx7-K56dNpEOTkCadJqQCYqAd2o0e46G5Bzd1exlpUsXpAQO--5QeTNreHMf-_f138oQmtIwL7Fy4N1uKCdZn2gMAnNQaT2K4F44aN1AUx88aL9brVaXxGVa1xlMXprZzFBlW1LN8aeLh8TWTSUDjCAdwSKPdX8H55E2ywWvc7PGrV6Jw3FnD0FjfWt8r8AHGqXHWwM31Mkvd1hpX0e4oMdyGeMdxFroIPkAMU5rn20NvX6GdfeAWrJdE3vACRo4SPLmCevYpQ8ta45pciI9S5tyMnCOB6uwbnE2RGsfwvfnFJv0VKLMAVPV7jdWPpU0895ngdxyFskFRMPAQomVRkEjuJFtKLGouHWPIQFFAUwrDW4uwyFuUkvW9KJiEetb44R_p8rLWuN-CQHk5M78ur4qjj5-LzpFsFUiKTIP1PRxACsGBIees7_8zUS5rjaWjOOwfLvJgfZux5FuIbKYYM0TMltvJsWjcoMKB40C2xcUTpi0P-e-03NHkEqp95idFm3cnuaYLPp3TYN0jLrCgcXRcyKMkHhScOOvvrshU8_k8-KRAY8V9YLi51KjgOjQhBQUX7HacrCEFZbTkFAh5KYSj7VDNIpX9nWf5dDQ-4MuLwqY_DS5EXOC7-61NjC9_AL-d-jI%3Ftype%3Dpng" 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%2Fmermaid.ink%2Fimg%2Fpako%3AeNpdklFP3DAMx7-K56dNpEOTkCadJqQCYqAd2o0e46G5Bzd1exlpUsXpAQO--5QeTNreHMf-_f138oQmtIwL7Fy4N1uKCdZn2gMAnNQaT2K4F44aN1AUx88aL9brVaXxGVa1xlMXprZzFBlW1LN8aeLh8TWTSUDjCAdwSKPdX8H55E2ywWvc7PGrV6Jw3FnD0FjfWt8r8AHGqXHWwM31Mkvd1hpX0e4oMdyGeMdxFroIPkAMU5rn20NvX6GdfeAWrJdE3vACRo4SPLmCevYpQ8ta45pciI9S5tyMnCOB6uwbnE2RGsfwvfnFJv0VKLMAVPV7jdWPpU0895ngdxyFskFRMPAQomVRkEjuJFtKLGouHWPIQFFAUwrDW4uwyFuUkvW9KJiEetb44R_p8rLWuN-CQHk5M78ur4qjj5-LzpFsFUiKTIP1PRxACsGBIees7_8zUS5rjaWjOOwfLvJgfZux5FuIbKYYM0TMltvJsWjcoMKB40C2xcUTpi0P-e-03NHkEqp95idFm3cnuaYLPp3TYN0jLrCgcXRcyKMkHhScOOvvrshU8_k8-KRAY8V9YLi51KjgOjQhBQUX7HacrCEFZbTkFAh5KYSj7VDNIpX9nWf5dDQ-4MuLwqY_DS5EXOC7-61NjC9_AL-d-jI%3Ftype%3Dpng" alt="Mermaid Diagram" width="851" height="734"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A few design choices stand out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The agent has no public URL.&lt;/strong&gt; The Worker is deployed with &lt;code&gt;workers_dev&lt;/code&gt; and preview URLs turned off. Your browser only talks to the Pages site, whose &lt;code&gt;/api&lt;/code&gt; function forwards requests over a service binding.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Auth happens in the Worker, not the frontend.&lt;/strong&gt; Sessions ride in an HttpOnly, SameSite=Strict cookie, the database stores only a keyed hash of each session token, and five failed logins from one IP within 15 minutes lock that client out.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;All state lives in one SQLite-backed Durable Object.&lt;/strong&gt; Conversations, memories, tasks, notes, projects, automations, sessions and usage data share a single store.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Scheduling uses Durable Object alarms.&lt;/strong&gt; Reminders and routines fire without anything having to stay online. Per the architecture doc, each enabled automation holds one pending schedule, and a run missed by less than 24 hours still executes once.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One automation cycles like this, with nothing staying online in between:&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%2Fmermaid.ink%2Fimg%2Fpako%3AeNpNkUFv2zAMhf8Kx7ONYcBOwVDASRosaLcBCbZL1IMi0bFWmQwkamma5r8PttdgR5LvfXqkLujEE86wjXJynU0KjxvDAADNzmBTVHqrQRiI7T6S_7JPH--ECY7EPvABsuvIl0gGn6Cu72C-M7gsaRDDj_1vcgo22tRDGxJlg08TfT6KFxeDD4E9SAupsMHrNF0M0zeDG-oDe0ogCdTmZ_DhQFkNvsFyZ3BTOMMpaCdFoVnf4O_2Zg1JigamwXH_vwMSWV8LxzOoSMzjXsdYMlhg0dBO_RtyOeZd7Qx-H6bBTVcJDNoRBK7t8QiOWCmNqFQYEjlJnvwNcj9BpmL1LyTTi4I4V1IidlQNzLOUBBp6epUpe4MV9pR6GzzOLqgd9cOveWptiYrV1PllUxgOnwdNK6wr24d4xhkO8SLV-ZyV-grmMfDzN-u2Y70S1goMbukgBD_XBivYyF5UKvhK8Q9pcLaCJgUbK8iWc50phRar8ZFteB2yfPp8fMHrtcL9YSFREs7ww6kLSnj9C0MFyvM%3Ftype%3Dpng" 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%2Fmermaid.ink%2Fimg%2Fpako%3AeNpNkUFv2zAMhf8Kx7ONYcBOwVDASRosaLcBCbZL1IMi0bFWmQwkamma5r8PttdgR5LvfXqkLujEE86wjXJynU0KjxvDAADNzmBTVHqrQRiI7T6S_7JPH--ECY7EPvABsuvIl0gGn6Cu72C-M7gsaRDDj_1vcgo22tRDGxJlg08TfT6KFxeDD4E9SAupsMHrNF0M0zeDG-oDe0ogCdTmZ_DhQFkNvsFyZ3BTOMMpaCdFoVnf4O_2Zg1JigamwXH_vwMSWV8LxzOoSMzjXsdYMlhg0dBO_RtyOeZd7Qx-H6bBTVcJDNoRBK7t8QiOWCmNqFQYEjlJnvwNcj9BpmL1LyTTi4I4V1IidlQNzLOUBBp6epUpe4MV9pR6GzzOLqgd9cOveWptiYrV1PllUxgOnwdNK6wr24d4xhkO8SLV-ZyV-grmMfDzN-u2Y70S1goMbukgBD_XBivYyF5UKvhK8Q9pcLaCJgUbK8iWc50phRar8ZFteB2yfPp8fMHrtcL9YSFREs7ww6kLSnj9C0MFyvM%3Ftype%3Dpng" alt="Mermaid Diagram" width="1454" height="267"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the AI side, chat runs on Workers AI using the &lt;code&gt;@cf/zai-org/glm-4.7-flash&lt;/code&gt; model, with streaming and tool calling. Responses stream as Server-Sent Events from end to end, and reasoning is switched off to conserve Workers AI Neurons. Models sit behind a small provider interface, so adding another provider means implementing it in code rather than flipping a setting.&lt;/p&gt;

&lt;p&gt;The agent's tools are tightly scoped, too. According to the security doc, destructive actions such as deleting a task or note need explicit confirmation in your latest message, enforced in code rather than only in the prompt. Scheduled AI runs get read-only tools plus a notify tool, and there are no shell, code-execution or outbound-HTTP tools at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Free-tier friendly, with honest caveats
&lt;/h2&gt;

&lt;p&gt;Talorys is built to fit within the Cloudflare Workers Free plan. It uses only Pages, Workers, SQLite Durable Objects and Workers AI, and it does not provision R2, D1, KV, Vectorize or any paid service.&lt;/p&gt;

&lt;p&gt;The README is refreshingly candid that this is not "unlimited free." Cloudflare sets account-level quotas, including a daily Workers AI allocation, and those can change. When the AI allocation runs out, chat tells you clearly and resumes after the daily reset. If your account is on a paid plan, usage beyond the included amounts is billed by Cloudflare.&lt;/p&gt;

&lt;p&gt;To keep usage predictable, there are adjustable guardrails in &lt;strong&gt;Settings → AI&lt;/strong&gt;: maximum output tokens, maximum context tokens (older history gets summarized), limits on tool calls and reasoning steps per request, and daily caps on AI requests and scheduled AI runs. Simple reminders and task digests never touch the model at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Privacy and data ownership
&lt;/h2&gt;

&lt;p&gt;Talorys includes no telemetry, analytics, tracking or advertising code, and it sends nothing to its developers. Your data sits in your own Cloudflare account.&lt;/p&gt;

&lt;p&gt;One thing to keep in mind is that Cloudflare itself processes your data to provide its services. That includes running Workers AI inference on your chat messages and the relevant memories included in each prompt. "Self-hosted on Cloudflare" is not the same as "never leaves your machine," so it is worth reading Cloudflare's privacy policy and Workers AI terms.&lt;/p&gt;

&lt;p&gt;You also keep an exit. &lt;strong&gt;Settings → Privacy → Download backup&lt;/strong&gt; exports conversations, memories, tasks, notes, projects, settings and automations as a single JSON file, never sessions or credentials. Importing validates the file and merges it in one transaction, so restoring the same backup twice is harmless.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trying it locally
&lt;/h2&gt;

&lt;p&gt;If you would rather poke around before deploying, local development needs only Node.js 20.18 or newer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/rociiu/talorys.git
&lt;span class="nb"&gt;cd &lt;/span&gt;talorys
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This starts the agent Worker, the Pages Function proxy and the Vite UI together, using a deterministic mock AI provider, so no Cloudflare login is required. The repo is a TypeScript monorepo with a Hono-based agent app, a React and Vite frontend, shared Zod schemas, the installer package, and Playwright end-to-end tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who is it for?
&lt;/h2&gt;

&lt;p&gt;Talorys fits people who want a private, low-maintenance assistant without running servers or paying for yet another subscription, and who already have, or are happy to create, a Cloudflare account. It is also a useful reference for anyone curious about building agents on Durable Objects, alarms and Workers AI.&lt;/p&gt;

&lt;p&gt;A few things to weigh before adopting it. It is a small project so far, with 10 commits and 10 stars at the time of writing. It is deliberately single-user, so it will not suit teams. And chat quality depends on the Workers AI model it uses by default (GLM-4.7-flash) rather than a frontier model you pick yourself.&lt;/p&gt;

&lt;p&gt;Still, the core idea is compelling: a personal agent you deploy in minutes, own completely, and can export or tear down whenever you like. If that appeals to you, the repository is at &lt;a href="https://github.com/rociiu/talorys" rel="noopener noreferrer"&gt;github.com/rociiu/talorys&lt;/a&gt; and it is MIT licensed.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Published via &lt;a href="https://zyvop.com/talorys-a-personal-ai-agent-in-your-own-cloudflare-account-ol3ap?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=syndication" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt; — Write once in Markdown, auto-backup to GitHub, and syndicate to Dev.to, Medium &amp;amp; Hashnode in 1 click.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>cloudflare</category>
      <category>durableobjects</category>
      <category>opensource</category>
    </item>
    <item>
      <title>OpenAI's $20 Billion Revenue Gap: What the FT Report Means for the AI Race</title>
      <dc:creator>Sanjay Singh</dc:creator>
      <pubDate>Sun, 11 Oct 2026 06:41:13 +0000</pubDate>
      <link>https://dev.to/sanjay_singh_1/openais-20-billion-revenue-gap-what-the-ft-report-means-for-the-ai-race-4355</link>
      <guid>https://dev.to/sanjay_singh_1/openais-20-billion-revenue-gap-what-the-ft-report-means-for-the-ai-race-4355</guid>
      <description>&lt;p&gt;OpenAI's annualised revenue is about $20 billion lower than the figure that circulated last month, according to a Financial Times report published on 8 October. The company has recently told investors its revenue was approaching $50 billion on an annualised basis at the end of September, against the roughly $70 billion that the FT, Reuters and other outlets reported in late September.&lt;/p&gt;

&lt;p&gt;The gap changes the AI revenue race. At $50 billion, OpenAI would sit below Anthropic's reported $65 billion annualised run rate; the earlier $70 billion figure would have put it ahead. Reuters said it could not independently verify the FT report, and OpenAI did not immediately respond to its request for comment.&lt;/p&gt;

&lt;h2&gt;
  
  
  How a $70 billion figure took hold
&lt;/h2&gt;

&lt;p&gt;The FT traced the higher number to investors rather than to OpenAI. Reuters, summarising the FT report, said the discrepancy came from investors' attempts to compare OpenAI's annualised revenue directly with Anthropic's.&lt;/p&gt;

&lt;p&gt;According to Yahoo Finance's summary of the FT report, investors first tried to work out whether OpenAI's sales had reached $40 billion in August. They then combined that estimate with the 70% revenue growth OpenAI had told them about, which landed them near $70 billion. Outlets including Reuters carried that figure in late September.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why OpenAI's and Anthropic's numbers don't line up
&lt;/h2&gt;

&lt;p&gt;The two companies do not count annualised revenue the same way. Per the FT, as relayed by Reuters, Anthropic includes revenue from sales made through cloud partners such as AWS and Google Cloud, while OpenAI does not.&lt;/p&gt;

&lt;p&gt;Semafor reported in April, citing people familiar with the matter, that when a customer buys $1 of tokens through a cloud partner, OpenAI books its 20-cent cut while Anthropic books the whole dollar. One person familiar with the numbers put the gap at up to $8 billion in annualised revenue at the time. Because the partner percentages vary, Semafor noted normalising the two sets of figures is difficult.&lt;/p&gt;

&lt;p&gt;The metric itself is also blunt. Reuters noted that annualised run rate often just multiplies one month's revenue by 12, and can mislead, even though fast-growing startups lean on it heavily.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it matters now
&lt;/h2&gt;

&lt;p&gt;Both companies are heading towards public markets, where revenue claims face regulatory scrutiny. Reuters noted that listings could give Wall Street a clearer view of their finances, including whether their rapid growth is sustainable. Yahoo Finance reports that OpenAI's IPO is widely expected in early 2027, while Anthropic is rumoured to list before Thanksgiving, with its public S-1 prospectus expected any day.&lt;/p&gt;

&lt;p&gt;TechCrunch framed the revenue question as part of a wider pressure on OpenAI to justify the scale of money behind it. The company raised $122 billion in a single round in March, and its leaked 2025 financials showed about $13 billion in revenue against much larger spending. Its IPO, once rumoured for this year, has been pushed back to early 2027.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to watch
&lt;/h2&gt;

&lt;p&gt;Even at $50 billion, OpenAI's latest figure is more than double the level its CFO, Sarah Friar, cited in January, when she said annualised revenue had passed $20 billion in 2025, up from $6 billion in 2024. The gap also shows how much these figures depend on who is counting, and how.&lt;/p&gt;

&lt;p&gt;Three things will show how it settles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;OpenAI's response.&lt;/strong&gt; OpenAI did not immediately respond to Reuters' request for comment, and Reuters could not verify the FT's account.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Anthropic's S-1.&lt;/strong&gt; A public prospectus would show how it books revenue sold through AWS and Google Cloud.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A like-for-like basis.&lt;/strong&gt; Whether analysts and the press start comparing the two companies on the same gross or net footing.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;p&gt;Figures come from press reports of the FT's account and have not been independently verified.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.933thedrive.com/2026/10/08/openais-annualized-revenue-20-billion-less-than-previously-signaled-ft-reports/" rel="noopener noreferrer"&gt;Reuters: OpenAI's annualized revenue $20 billion less than previously signaled, FT reports&lt;/a&gt; (8 October 2026)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://finance.yahoo.com/technology/article/openais-annualized-revenue-20-billion-lower-than-prior-investor-estimates-174058048.html" rel="noopener noreferrer"&gt;Yahoo Finance: OpenAI's annualized revenue $20 billion lower than prior investor estimates&lt;/a&gt; (8 October 2026)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://techcrunch.com/2026/10/08/openais-revenue-is-reportedly-20-billion-less-than-previously-projected/" rel="noopener noreferrer"&gt;TechCrunch: OpenAI's revenue is reportedly $20 billion less than previously projected&lt;/a&gt; (8 October 2026)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.semafor.com/article/04/10/2026/anthropic-is-gaining-on-openais-revenue-but-hasnt-yet-eclipsed-it" rel="noopener noreferrer"&gt;Semafor: Anthropic is gaining on OpenAI's revenue, but hasn't yet eclipsed it&lt;/a&gt; (10 April 2026)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://finance.yahoo.com/news/openai-cfo-says-annualized-revenue-173519097.html" rel="noopener noreferrer"&gt;Reuters: OpenAI CFO says annualized revenue crosses $20 billion in 2025&lt;/a&gt; (19 January 2026)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.ft.com/content/b66a9858-f8fb-46cb-b506-44bfe26fca2a" rel="noopener noreferrer"&gt;Financial Times original report&lt;/a&gt; (paywalled; summarised here via Reuters and TechCrunch)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Published via &lt;a href="https://zyvop.com/openai-s-20-billion-revenue-gap-what-the-ft-report-means-for-the-ai-race-afhqb?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=syndication" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt; — Write once in Markdown, auto-backup to GitHub, and syndicate to Dev.to, Medium &amp;amp; Hashnode in 1 click.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aibusiness</category>
      <category>aiindustry</category>
      <category>anthropic</category>
      <category>openai</category>
    </item>
    <item>
      <title>NestJS Best Practices for 2026: A Practical Guide (Updated for NestJS 12)</title>
      <dc:creator>Sanjay Singh</dc:creator>
      <pubDate>Sat, 10 Oct 2026 14:44:08 +0000</pubDate>
      <link>https://dev.to/sanjay_singh_1/nestjs-best-practices-for-2026-a-practical-guide-updated-for-nestjs-12-c5b</link>
      <guid>https://dev.to/sanjay_singh_1/nestjs-best-practices-for-2026-a-practical-guide-updated-for-nestjs-12-c5b</guid>
      <description>&lt;p&gt;NestJS 12 came out on August 28, 2026, and it changes more than a normal major release. The core packages are now ESM. New projects get a different test and lint setup. You can validate requests with Zod. And the docs have a whole new section about reliability.&lt;/p&gt;

&lt;p&gt;That means a lot of older "best practices" posts are now only half right.&lt;/p&gt;

&lt;p&gt;So this is my list of what I'd do on a new NestJS project today. Each tip comes with the reason behind it and code you can copy. When something is my opinion and not an official rule, I'll tell you.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A quick note on the code:&lt;/strong&gt; The examples use ESM style, which is what you get when you pick ESM in &lt;code&gt;nest new&lt;/code&gt;. That means imports like &lt;code&gt;./app.module.js&lt;/code&gt; and a top-level &lt;code&gt;await&lt;/code&gt;. If your project is CommonJS, drop the &lt;code&gt;.js&lt;/code&gt; extensions and call &lt;code&gt;bootstrap()&lt;/code&gt; without &lt;code&gt;await&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Versions:&lt;/strong&gt; To run a Nest 12 app you need Node.js 20.19+ or 22.12+. The CLI generators (&lt;code&gt;nest new&lt;/code&gt;, &lt;code&gt;nest generate&lt;/code&gt;, &lt;code&gt;nest upgrade&lt;/code&gt;) need a newer Node: 22.22.3+, 24.15+ or 26+. The easy answer is to use the latest active LTS.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  1. Set up the project on purpose
&lt;/h2&gt;

&lt;p&gt;When you run &lt;code&gt;nest new&lt;/code&gt;, the CLI now asks if you want a CommonJS or an ESM project. The choice changes your defaults:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;ESM project&lt;/th&gt;
&lt;th&gt;CommonJS project&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Test runner&lt;/td&gt;
&lt;td&gt;Vitest&lt;/td&gt;
&lt;td&gt;Jest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linter&lt;/td&gt;
&lt;td&gt;oxlint&lt;/td&gt;
&lt;td&gt;oxlint or ESLint (sources disagree, see the note below)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compiler&lt;/td&gt;
&lt;td&gt;tsc&lt;/td&gt;
&lt;td&gt;tsc&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bundler for monorepos&lt;/td&gt;
&lt;td&gt;Rspack&lt;/td&gt;
&lt;td&gt;Rspack&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Heads up:&lt;/strong&gt; The official sources don't fully agree on the linter. The migration guide says every generated project uses oxlint. The launch post says the CommonJS template keeps ESLint. Open your generated &lt;code&gt;package.json&lt;/code&gt; and see what you actually got.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here's how I'd decide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;New service, nothing legacy:&lt;/strong&gt; pick ESM. You get the modern defaults from day one.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Existing app:&lt;/strong&gt; upgrade the packages and stay on CommonJS until you have a real reason to switch.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Staying on CommonJS is fine because Nest's ESM packages can be loaded from CommonJS through &lt;code&gt;require(esm)&lt;/code&gt;. The upgrade command even leaves your module format alone.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# upgrade the CLI first, globally and locally&lt;/span&gt;
npm i &lt;span class="nt"&gt;-g&lt;/span&gt; @nestjs/cli@latest @nestjs/schematics@latest
npm i &lt;span class="nt"&gt;-D&lt;/span&gt; @nestjs/cli@latest @nestjs/schematics@latest

&lt;span class="c"&gt;# see what would change without touching files&lt;/span&gt;
nest upgrade &lt;span class="nt"&gt;--dry-run&lt;/span&gt;

&lt;span class="c"&gt;# then do it for real&lt;/span&gt;
nest upgrade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;nest upgrade&lt;/code&gt; moves every &lt;code&gt;@nestjs/*&lt;/code&gt; package to v12 at once. That part matters. &lt;strong&gt;Keep all Nest packages on the same major version&lt;/strong&gt;, always. Mixed majors are a classic source of strange errors.&lt;/p&gt;

&lt;p&gt;A few things can bite you during the upgrade:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Jest users:&lt;/strong&gt; Jest can load the ESM-only v12 packages only on Node.js 24.9 or newer. Older versions fail with &lt;code&gt;ERR_REQUIRE_ASYNC_MODULE&lt;/code&gt;. Use Node 24.9+ or move to Vitest.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;AWS Lambda:&lt;/strong&gt; The Node 20, 22 and 24 runtimes turn &lt;code&gt;require(esm)&lt;/code&gt; off by default. A CommonJS Nest 12 app needs &lt;code&gt;NODE_OPTIONS=--experimental-require-module&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;TypeScript jumps to v6:&lt;/strong&gt; &lt;code&gt;nest upgrade&lt;/code&gt; raises &lt;code&gt;typescript&lt;/code&gt; to &lt;code&gt;^6.0.0&lt;/code&gt;, and it bumps Jest to v30 and Joi to v18 as well. The upgrade schematic flags &lt;code&gt;module: commonjs&lt;/code&gt; combined with legacy module resolution, and a missing &lt;code&gt;rootDir&lt;/code&gt; in &lt;code&gt;tsconfig.build.json&lt;/code&gt; (error TS5011). Budget time for new compiler errors.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Logger output changed:&lt;/strong&gt; &lt;code&gt;ConsoleLogger&lt;/code&gt; now treats extra object arguments as structured params by default. If anything parses your logs, set &lt;code&gt;structuredParams: false&lt;/code&gt; to get the old output back.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Lifecycle hook order changed:&lt;/strong&gt; Hooks like &lt;code&gt;onModuleInit&lt;/code&gt; now run by component hierarchy level. If your code depends on the order between providers, test it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;@Optional()&lt;/code&gt; &lt;strong&gt;is no longer inherited:&lt;/strong&gt; A subclass has to declare it again in its own constructor, or Nest throws &lt;code&gt;UnknownDependenciesException&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Removed or replaced:&lt;/strong&gt; the old Terminus health indicator API, &lt;code&gt;subscriptions-transport-ws&lt;/code&gt; in GraphQL (use &lt;code&gt;graphql-ws&lt;/code&gt;), and the old &lt;code&gt;nats&lt;/code&gt; package (now &lt;code&gt;@nats-io/transport-node&lt;/code&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Webpack is deprecated&lt;/strong&gt; in CLI workflows. Rspack takes over for monorepos. &lt;code&gt;tsc&lt;/code&gt; is still the default compiler.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And one rule I like: after upgrading, fix every deprecation warning in your console before you ship. They are cheap to fix now and expensive later.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Organize by feature, not by file type
&lt;/h2&gt;

&lt;p&gt;The folder layout I'd start with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
  main.ts
  setup-app.ts
  app.module.ts
  config/
    env.schema.ts
  auth/
    auth.guard.ts
    public.decorator.ts
  common/
    domain-error.ts
    domain-error.filter.ts
  database/
    ...
  users/
    users.module.ts
    users.controller.ts
    users.service.ts
    users.repository.ts
    db-users.repository.ts
    dto/
      create-user.dto.ts
  orders/
    ...
  payments/
    ...
  health/
    ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything about "users" lives in &lt;code&gt;users/&lt;/code&gt;. You don't hunt through &lt;code&gt;controllers/&lt;/code&gt;, &lt;code&gt;services/&lt;/code&gt; and &lt;code&gt;entities/&lt;/code&gt; folders to change one feature.&lt;/p&gt;

&lt;p&gt;Each feature is a module, and modules only talk through what they export:&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%2Fmermaid.ink%2Fimg%2Fpako%3AeNp1kDFrwzAQhf_K9aYW5KHQyUPBjQnpEApN08XKcLLPtqhsCUlpmgb_9xILUlrIpKen7907dMLaNow5tsYe6p58hLdSjgAAhXOVxMK5tW32hiXuIMseYWHHVneVxCTSI9x2xioydxJ3l_SMbwP7UEmcz8ukv8yLbxKUxBVqxWRiX0lM4h-Vor-VyZ3lbJaqklhSJEWBr2dLhQIH9gPpBvMTxp6H8_c03NLeRBTJeSevSRkOZ6a1Y1zSoM0Rc8zIOcNZOIbIg4Ano8ePNdWb-b60YxQgccOdZdg-SxTwapWNVsCKzSdHXZOAwmsyAgKNIQvsdYtiLtno7_Mu9w_uC6dJoOoW1liPOd4ceh0Zpx-LZJcg%3Ftype%3Dpng" 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%2Fmermaid.ink%2Fimg%2Fpako%3AeNp1kDFrwzAQhf_K9aYW5KHQyUPBjQnpEApN08XKcLLPtqhsCUlpmgb_9xILUlrIpKen7907dMLaNow5tsYe6p58hLdSjgAAhXOVxMK5tW32hiXuIMseYWHHVneVxCTSI9x2xioydxJ3l_SMbwP7UEmcz8ukv8yLbxKUxBVqxWRiX0lM4h-Vor-VyZ3lbJaqklhSJEWBr2dLhQIH9gPpBvMTxp6H8_c03NLeRBTJeSevSRkOZ6a1Y1zSoM0Rc8zIOcNZOIbIg4Ano8ePNdWb-b60YxQgccOdZdg-SxTwapWNVsCKzSdHXZOAwmsyAgKNIQvsdYtiLtno7_Mu9w_uC6dJoOoW1liPOd4ceh0Zpx-LZJcg%3Ftype%3Dpng" alt="Mermaid Diagram" width="628" height="370"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;My rules for modules:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Export as little as possible.&lt;/strong&gt; Export the service other features need. Don't export repositories or internals.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Avoid one giant&lt;/strong&gt; &lt;code&gt;SharedModule&lt;/code&gt;&lt;strong&gt;.&lt;/strong&gt; It slowly becomes a junk drawer that everything imports. Small, named modules age better.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Treat&lt;/strong&gt; &lt;code&gt;forwardRef()&lt;/code&gt; &lt;strong&gt;as a smell.&lt;/strong&gt; If two modules need each other, the usual fix is a third module that holds what they share.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  3. Put app-wide setup in one place
&lt;/h2&gt;

&lt;p&gt;Here's a habit that saves a lot of "works on my machine" bugs. Put your global setup in one function, and call it from both &lt;code&gt;main.ts&lt;/code&gt; and your end-to-end tests.&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/setup-app.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;ValidationPipe&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;INestApplication&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;@nestjs/common&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;helmet&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;helmet&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="cm"&gt;/** App-wide setup. main.ts and the e2e tests both call this. */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;setupApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;INestApplication&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;helmet&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enableCors&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;origin&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;https://app.example.com&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;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useGlobalPipes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ValidationPipe&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;whitelist&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="na"&gt;forbidNonWhitelisted&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="na"&gt;transform&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/main.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;NestFactory&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;@nestjs/core&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;ConfigService&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;@nestjs/config&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;AppModule&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;./app.module.js&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;setupApp&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;./setup-app.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&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;bootstrap&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;app&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;NestFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AppModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;routeConflictPolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;duplicate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;shadow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;warn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;routeResolutionStrategy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;specificity&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="nf"&gt;setupApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enableShutdownHooks&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;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ConfigService&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;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getOrThrow&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;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;PORT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;bootstrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'll explain each line in the sections below. The reason for the shared function is simple: if your tests build the app without your global pipes, they test a different app than the one you ship.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Using Fastify?&lt;/strong&gt; Use &lt;code&gt;@fastify/helmet&lt;/code&gt; instead of &lt;code&gt;helmet&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  4. Keep controllers thin
&lt;/h2&gt;

&lt;p&gt;A controller should do three things: read the input, call a service, return the result. No business rules. No database calls.&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/users/users.controller.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;Body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt;&lt;span class="p"&gt;,&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;Param&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ParseUUIDPipe&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="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@nestjs/common&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;Public&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;../auth/public.decorator.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// defined in the security section&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;CreateUserDto&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;./dto/create-user.dto.js&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;UsersService&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;./users.service.js&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="nd"&gt;Controller&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;users&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;class&lt;/span&gt; &lt;span class="nc"&gt;UsersController&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;usersService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UsersService&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="nd"&gt;Public&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(@&lt;/span&gt;&lt;span class="nd"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CreateUserDto&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usersService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dto&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="nd"&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;:id&lt;/span&gt;&lt;span class="dl"&gt;'&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="nd"&gt;Param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ParseUUIDPipe&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usersService&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;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/users/users.service.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;ConflictException&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NotFoundException&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;@nestjs/common&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;CreateUserDto&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;./dto/create-user.dto.js&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;UsersRepository&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;./users.repository.js&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="nd"&gt;Injectable&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;class&lt;/span&gt; &lt;span class="nc"&gt;UsersService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UsersRepository&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CreateUserDto&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;existing&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&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;existing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ConflictException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Email is already in use&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;errorCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;EMAIL_TAKEN&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="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&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;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findById&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;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;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NotFoundException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;User not found&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;errorCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;USER_NOT_FOUND&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;user&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;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Careful with "check, then insert":&lt;/strong&gt; Two requests can pass the &lt;code&gt;findByEmail&lt;/code&gt; check at the same time. Keep a unique index on the email column and treat that database error as the real source of truth. The check in the service is just for a friendly message.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Know where each piece belongs
&lt;/h3&gt;

&lt;p&gt;Nest gives you five tools that wrap a request. People mix them up all the time. This is how I think about them:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Its job&lt;/th&gt;
&lt;th&gt;Typical use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Middleware&lt;/td&gt;
&lt;td&gt;Low-level request work&lt;/td&gt;
&lt;td&gt;Request IDs, raw body handling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Guard&lt;/td&gt;
&lt;td&gt;"Is this caller allowed?"&lt;/td&gt;
&lt;td&gt;Authentication, roles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pipe&lt;/td&gt;
&lt;td&gt;"Is this input valid? Convert it."&lt;/td&gt;
&lt;td&gt;Validation, ParseUUIDPipe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interceptor&lt;/td&gt;
&lt;td&gt;Wrap the handler&lt;/td&gt;
&lt;td&gt;Timing, response shaping, resilience&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exception filter&lt;/td&gt;
&lt;td&gt;Turn errors into responses&lt;/td&gt;
&lt;td&gt;A consistent error format&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And this is the order they run in:&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%2Fmermaid.ink%2Fimg%2Fpako%3AeNp1kUtrAkEQhP9Kp08JzAYCOXkQ4mONECEoCYEdD-Nurw6Znd70jDEP_O9hR9CAeOz-qqoL-hdLrgh7WDvelRsjEZ7m2gMAPBQap77kxvo1CH1sKUSNS8iyPgwKjTNbVY52Rkjj8mAZJDgsNE62RqpwBMMERikxkpTURpYA1yuqWejmqBsl3bjQ-GxbOvnHaZ8XGue8jQQb4ytHcuR54pOzfFNHklP8JMkeuxgKLftA_xvegkYSYdEIt1kf3gqN468uy7KH2rpIEv5XPTMcy14g-UUyuUjeDp1RYUPSGFth7xfjhpruaxXVZusiqsPm1Yg1K0eh09TsY24a676xh5lpW0dZ-A6RGgUDZ_37zJSLNOfsowKNC1ozwctUo4I5rziygkdynxRtaRQ8iDVOQTA-ZIHE1qjSkYX96brc3bdfuN8rXK2H7Fiwh1e7jY2E-z-W-bnx%3Ftype%3Dpng" 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%2Fmermaid.ink%2Fimg%2Fpako%3AeNp1kUtrAkEQhP9Kp08JzAYCOXkQ4mONECEoCYEdD-Nurw6Znd70jDEP_O9hR9CAeOz-qqoL-hdLrgh7WDvelRsjEZ7m2gMAPBQap77kxvo1CH1sKUSNS8iyPgwKjTNbVY52Rkjj8mAZJDgsNE62RqpwBMMERikxkpTURpYA1yuqWejmqBsl3bjQ-GxbOvnHaZ8XGue8jQQb4ytHcuR54pOzfFNHklP8JMkeuxgKLftA_xvegkYSYdEIt1kf3gqN468uy7KH2rpIEv5XPTMcy14g-UUyuUjeDp1RYUPSGFth7xfjhpruaxXVZusiqsPm1Yg1K0eh09TsY24a676xh5lpW0dZ-A6RGgUDZ_37zJSLNOfsowKNC1ozwctUo4I5rziygkdynxRtaRQ8iDVOQTA-ZIHE1qjSkYX96brc3bdfuN8rXK2H7Fiwh1e7jY2E-z-W-bnx%3Ftype%3Dpng" alt="Mermaid Diagram" width="1796" height="260"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you remember one thing from this picture: guards run before pipes. So a user who isn't allowed in never gets as far as validation.&lt;/p&gt;

&lt;p&gt;The dotted lines show where errors go. An exception thrown by a guard, a pipe, an interceptor or the handler ends up in the exception filters. That's how the &lt;code&gt;UnauthorizedException&lt;/code&gt; from the auth guard later in this post becomes a clean 401 response.&lt;/p&gt;

&lt;h3&gt;
  
  
  A route trap worth knowing
&lt;/h3&gt;

&lt;p&gt;Nest registers routes in the order you declare them. On Express, this can quietly break a route:&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&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;:id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// declared first, so it can swallow /users/me&lt;/span&gt;
&lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;(@&lt;/span&gt;&lt;span class="nd"&gt;Param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="dl"&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&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;me&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;me&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;Version 12 adds two opt-in options to catch this, and I turned both on in &lt;code&gt;main.ts&lt;/code&gt; above. &lt;code&gt;routeConflictPolicy&lt;/code&gt; can warn or throw on shadowed and duplicate routes. &lt;code&gt;routeResolutionStrategy: 'specificity'&lt;/code&gt; picks the most specific route. Both default to the old behavior, so nothing changes until you set them.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Let dependency injection stay boring
&lt;/h2&gt;

&lt;p&gt;Nest providers are &lt;strong&gt;singletons by default&lt;/strong&gt;, and that's what you want. This surprises people coming from other languages. Node.js doesn't handle each request in its own thread, so sharing one instance across requests is normally fine.&lt;/p&gt;

&lt;p&gt;There is one rule: &lt;strong&gt;a singleton must not keep per-request state.&lt;/strong&gt; If a provider stores "the current user" in a field, two overlapping requests will overwrite each other's value. Keep request data in method arguments, or use &lt;code&gt;AsyncLocalStorage&lt;/code&gt; (more on that below).&lt;/p&gt;

&lt;h3&gt;
  
  
  Avoid request scope unless you really need it
&lt;/h3&gt;

&lt;p&gt;You can make a provider request-scoped, so it gets a fresh instance per request. It's tempting. Here's why I avoid it.&lt;/p&gt;

&lt;p&gt;Request scope &lt;strong&gt;bubbles up&lt;/strong&gt;. If your &lt;code&gt;CatsService&lt;/code&gt; is request-scoped, then the &lt;code&gt;CatsController&lt;/code&gt; that uses it becomes request-scoped too. Now Nest creates and throws away that whole chain on every request. The docs say a well-designed app shouldn't pay more than about 5% latency for this, but "well-designed" is doing a lot of work in that sentence.&lt;/p&gt;

&lt;p&gt;Most of the time you only want to read one value that belongs to the current request, like the user, the tenant or the locale. For that, use &lt;code&gt;AsyncLocalStorage&lt;/code&gt;. Your providers stay singletons, and the value is still available anywhere downstream. It works the same in HTTP handlers, message handlers and queue jobs.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Multi-tenant app?&lt;/strong&gt; Nest has "durable providers" for exactly this case. They let you share one DI sub-tree per tenant instead of rebuilding it per request. The docs warn it's not ideal with a very large number of tenants.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Inject by abstraction, not by concrete class
&lt;/h3&gt;

&lt;p&gt;TypeScript interfaces disappear at runtime, so they can't be injection tokens. An abstract class works well instead, because it exists at runtime and still describes the contract:&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/users/users.repository.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&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="nl"&gt;email&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="nl"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;?:&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="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;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UsersRepository&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="nf"&gt;findById&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="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="nx"&gt;User&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="kd"&gt;abstract&lt;/span&gt; &lt;span class="nf"&gt;findByEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&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="nx"&gt;User&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="kd"&gt;abstract&lt;/span&gt; &lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;email&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="nl"&gt;displayName&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="nx"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;DbUsersRepository&lt;/code&gt; is whatever implements that contract with your ORM. Here is the shape to fill in (the bodies are placeholders):&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/users/db-users.repository.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;Injectable&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;@nestjs/common&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="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;UsersRepository&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;./users.repository.js&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="nd"&gt;Injectable&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;class&lt;/span&gt; &lt;span class="nc"&gt;DbUsersRepository&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;UsersRepository&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// inject your ORM client here (Prisma, Drizzle, TypeORM, ...)&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;findById&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="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="nx"&gt;User&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;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Implement with your ORM&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;findByEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&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="nx"&gt;User&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;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Implement with your ORM&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;email&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="nl"&gt;displayName&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="nx"&gt;User&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;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Implement with your ORM&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/users/users.module.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;Module&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;@nestjs/common&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;DbUsersRepository&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;./db-users.repository.js&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;UsersController&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;./users.controller.js&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;UsersRepository&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;./users.repository.js&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;UsersService&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;./users.service.js&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="nd"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;controllers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;UsersController&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;UsersService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;provide&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UsersRepository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;useClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DbUsersRepository&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;UsersService&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;class&lt;/span&gt; &lt;span class="nc"&gt;UsersModule&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your service doesn't know or care which database sits behind &lt;code&gt;UsersRepository&lt;/code&gt;. And in tests you swap it for a fake in one line. We'll use that in the testing section.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Validate every request at the edge
&lt;/h2&gt;

&lt;p&gt;Rule: never trust anything that comes in over the network. Bind the validation pipe globally (we did that in &lt;code&gt;setup-app.ts&lt;/code&gt;) so no endpoint is left unprotected by accident.&lt;/p&gt;

&lt;p&gt;The three options in that pipe do real work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;whitelist: true&lt;/code&gt; strips any property that has no validation decorator.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;forbidNonWhitelisted: true&lt;/code&gt; goes further and rejects the request instead of silently stripping.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;transform: true&lt;/code&gt; turns plain payloads into DTO class instances and converts path and query values to the types you declared.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A DTO 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="c1"&gt;// src/users/dto/create-user.dto.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;IsEmail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;IsOptional&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;IsString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MaxLength&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;class-validator&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;class&lt;/span&gt; &lt;span class="nc"&gt;CreateUserDto&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;IsEmail&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;IsOptional&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;IsString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;MaxLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Four gotchas from the docs
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Every property needs at least one decorator.&lt;/strong&gt; With &lt;code&gt;whitelist: true&lt;/code&gt;, a property with no decorator gets stripped. If your field "disappears," this is usually why.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use concrete classes.&lt;/strong&gt; TypeScript doesn't emit metadata for generics or interfaces, so the pipe can't validate them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Don't use&lt;/strong&gt; &lt;code&gt;import type&lt;/code&gt; &lt;strong&gt;for DTO classes.&lt;/strong&gt; Type-only imports are erased at runtime, and the pipe needs the real class. This rule is about classes. In the schema style below, the DTO is only a type and the schema is the real value, so import the schema normally and mark the type with the inline &lt;code&gt;type&lt;/code&gt; modifier.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Arrays aren't validated by default.&lt;/strong&gt; &lt;code&gt;@Body() dtos: CreateUserDto[]&lt;/code&gt; skips the elements. Wrap the array in a class, or use &lt;code&gt;ParseArrayPipe({ items: CreateUserDto })&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The new option in v12: schema validation
&lt;/h3&gt;

&lt;p&gt;Nest 12 adds a second way to validate, built on the &lt;a href="https://standardschema.dev/" rel="noopener noreferrer"&gt;Standard Schema&lt;/a&gt; spec. That means Zod, Valibot, ArkType and others work out of the box. You pass the schema straight into the parameter decorator:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Zod version:&lt;/strong&gt; These examples use the Zod 4 helpers like &lt;code&gt;z.email()&lt;/code&gt; and &lt;code&gt;z.url()&lt;/code&gt;. On Zod 3, write &lt;code&gt;z.string().email()&lt;/code&gt; and &lt;code&gt;z.string().url()&lt;/code&gt; instead.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/users/dto/create-user.dto.ts&lt;/span&gt;
&lt;span class="c1"&gt;// The schema-first version. Use this file instead of the class version above.&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="s1"&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createUserSchema&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;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;email&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="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;displayName&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="nf"&gt;string&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="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;optional&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;CreateUserDto&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;createUserSchema&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;Now register the pipe. &lt;strong&gt;This step is not optional.&lt;/strong&gt; The &lt;code&gt;schema&lt;/code&gt; option only attaches the schema to the parameter. If you forget the pipe, the schema does nothing and the request goes through unchecked.&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/setup-app.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;StandardSchemaValidationPipe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ValidationPipe&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;@nestjs/common&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Both pipes can be registered together. ValidationPipe checks class-typed&lt;/span&gt;
&lt;span class="c1"&gt;// parameters, and StandardSchemaValidationPipe checks parameters with a schema.&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useGlobalPipes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ValidationPipe&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;whitelist&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="na"&gt;forbidNonWhitelisted&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="na"&gt;transform&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;StandardSchemaValidationPipe&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 a feature uses only schemas, the schema pipe alone is enough. Then use it in the controller:&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/users/users.controller.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;createUserSchema&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;CreateUserDto&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;./dto/create-user.dto.js&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="nd"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(@&lt;/span&gt;&lt;span class="nd"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;createUserSchema&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="nx"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CreateUserDto&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usersService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dto&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 type comes from the schema, so you write the rules once and never keep a class and a type in sync. Coercion also works well for query strings:&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;listUsersQuerySchema&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;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;page&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="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;limit&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="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;search&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="nf"&gt;string&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="nf"&gt;optional&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="nd"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;findAll&lt;/span&gt;&lt;span class="p"&gt;(@&lt;/span&gt;&lt;span class="nd"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;listUsersQuerySchema&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="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;listUsersQuerySchema&lt;/span&gt;&lt;span class="o"&gt;&amp;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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usersService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findAll&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the &lt;code&gt;max(100)&lt;/code&gt; on &lt;code&gt;limit&lt;/code&gt;. &lt;strong&gt;Always cap page sizes.&lt;/strong&gt; An endpoint that lets a client ask for a million rows will eventually get that request.&lt;/p&gt;

&lt;p&gt;One detail to remember: with Zod, &lt;code&gt;z.object()&lt;/code&gt; strips unknown keys (like &lt;code&gt;whitelist&lt;/code&gt;), and &lt;code&gt;z.strictObject()&lt;/code&gt; rejects them (like &lt;code&gt;forbidNonWhitelisted&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  So which one should you pick?
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;ValidationPipe + class-validator&lt;/th&gt;
&lt;th&gt;StandardSchemaValidationPipe&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Where the rules live&lt;/td&gt;
&lt;td&gt;Decorators on a DTO class&lt;/td&gt;
&lt;td&gt;A schema object&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where the type comes from&lt;/td&gt;
&lt;td&gt;The class itself&lt;/td&gt;
&lt;td&gt;Inferred from the schema&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Making variants (create vs update)&lt;/td&gt;
&lt;td&gt;PartialType, PickType, OmitType&lt;/td&gt;
&lt;td&gt;.partial(), .pick(), .omit()&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fits best&lt;/td&gt;
&lt;td&gt;Existing code and class-based DTOs (the Swagger CLI plugin reads classes)&lt;/td&gt;
&lt;td&gt;Teams that already like schema-first, shared schemas&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Choosing schemas doesn't mean giving up OpenAPI. Standard Schema schemas can feed OpenAPI generation too, so check the Swagger chapter for how to set that up.&lt;/p&gt;

&lt;p&gt;The Nest team is clear that this is &lt;strong&gt;not&lt;/strong&gt; a replacement. The docs still suggest class-validator as the default for most projects. According to the validation docs, both pipes can be registered at the same time. &lt;code&gt;ValidationPipe&lt;/code&gt; only validates parameters typed with a class, and &lt;code&gt;StandardSchemaValidationPipe&lt;/code&gt; only validates parameters that declare a schema. So you can adopt schemas one feature at a time. My opinion: pick one style per feature so your team doesn't have to switch mental models in the middle of a file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Don't return your database objects
&lt;/h3&gt;

&lt;p&gt;Whatever you validate on the way in, shape on the way out. Returning raw entities leaks columns like password hashes and internal flags. Use &lt;code&gt;ClassSerializerInterceptor&lt;/code&gt; with class-transformer, or the new &lt;code&gt;StandardSchemaSerializerInterceptor&lt;/code&gt; if you're going schema-first:&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;UseInterceptors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;StandardSchemaSerializerInterceptor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;SerializeOptions&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userResponseSchema&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&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;:id&lt;/span&gt;&lt;span class="dl"&gt;'&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="nd"&gt;Param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ParseUUIDPipe&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usersService&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;id&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;
  
  
  7. Validate your config, too
&lt;/h2&gt;

&lt;p&gt;A missing environment variable should crash your app at startup, not at 3 a.m. when the first request needs it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@nestjs/config&lt;/code&gt; in v12 accepts any Standard Schema object for &lt;code&gt;validationSchema&lt;/code&gt;, so Zod works directly. Keep the schema in its own 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;// src/config/env.schema.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="s1"&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;envSchema&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;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;NODE_ENV&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="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;development&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;production&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;test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;development&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;PORT&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="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;DATABASE_URL&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="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;JWT_SECRET&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="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/app.module.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;Module&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;@nestjs/common&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;ConfigModule&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;@nestjs/config&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;envSchema&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;./config/env.schema.js&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="nd"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;ConfigModule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forRoot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;isGlobal&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="na"&gt;validationSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;envSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="c1"&gt;// ...feature modules&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;class&lt;/span&gt; &lt;span class="nc"&gt;AppModule&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then read values through &lt;code&gt;ConfigService&lt;/code&gt;, not &lt;code&gt;process.env&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getOrThrow&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;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;PORT&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;Three habits I'd keep:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;One place reads the environment.&lt;/strong&gt; Everything else asks &lt;code&gt;ConfigService&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fail early and loudly.&lt;/strong&gt; &lt;code&gt;getOrThrow&lt;/code&gt; over &lt;code&gt;get&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Never commit secrets.&lt;/strong&gt; Real values come from your platform's secret store, not from a &lt;code&gt;.env&lt;/code&gt; file in git.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Still using Joi? It keeps working, but you need Joi v18 or newer, and Joi-specific settings move under &lt;code&gt;validationOptions.libraryOptions&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Make errors useful
&lt;/h2&gt;

&lt;p&gt;Clients need to react to errors without parsing English sentences. Nest 12 helps with a new &lt;code&gt;errorCode&lt;/code&gt; option on every &lt;code&gt;HttpException&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="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BadRequestException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Password is too weak&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;errorCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;WEAK_PASSWORD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code is added to the response body. Your frontend can now do &lt;code&gt;if (error.errorCode === 'WEAK_PASSWORD')&lt;/code&gt; and stop matching message strings. Message text can change. Codes shouldn't.&lt;/p&gt;

&lt;h3&gt;
  
  
  For bigger apps: keep HTTP out of your business code
&lt;/h3&gt;

&lt;p&gt;If you want services that don't know about HTTP at all, throw your own domain errors and map them in one place:&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/common/domain-error.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DomainError&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;code&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;readonly&lt;/span&gt; &lt;span class="nx"&gt;httpStatus&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="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="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;class&lt;/span&gt; &lt;span class="nc"&gt;EmailTakenError&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;DomainError&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Email is already in use&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;EMAIL_TAKEN&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;409&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/common/domain-error.filter.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;ArgumentsHost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Catch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ExceptionFilter&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;@nestjs/common&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;HttpAdapterHost&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;@nestjs/core&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;DomainError&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;./domain-error.js&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="nd"&gt;Catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DomainError&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;class&lt;/span&gt; &lt;span class="nc"&gt;DomainErrorFilter&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;ExceptionFilter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;httpAdapterHost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HttpAdapterHost&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="nx"&gt;DomainError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ArgumentsHost&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;httpAdapter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;httpAdapterHost&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;switchToHttp&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getResponse&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nx"&gt;httpAdapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;statusCode&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;httpStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&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;code&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;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;httpStatus&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// in a module, for example AppModule&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;APP_FILTER&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;@nestjs/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nl"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;provide&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;APP_FILTER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;useClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DomainErrorFilter&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using &lt;code&gt;HttpAdapterHost&lt;/code&gt; instead of &lt;code&gt;response.status().json()&lt;/code&gt; keeps the filter working on both Express and Fastify.&lt;/p&gt;

&lt;p&gt;My rule of thumb: small app, throw Nest's built-in exceptions with an &lt;code&gt;errorCode&lt;/code&gt;. Growing app with lots of business rules, use domain errors and one filter.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. Security basics you shouldn't skip
&lt;/h2&gt;

&lt;p&gt;None of this is exciting. All of it matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security headers and CORS.&lt;/strong&gt; We set both in &lt;code&gt;setup-app.ts&lt;/code&gt;. Always list your allowed origins. Don't leave CORS open "just for now."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rate limiting.&lt;/strong&gt; &lt;code&gt;@nestjs/throttler&lt;/code&gt; is the official answer. In recent versions, &lt;code&gt;ttl&lt;/code&gt; is in milliseconds:&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;APP_GUARD&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;@nestjs/core&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;ThrottlerGuard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ThrottlerModule&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;@nestjs/throttler&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="nd"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ThrottlerModule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forRoot&lt;/span&gt;&lt;span class="p"&gt;([{&lt;/span&gt; &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="p"&gt;}])],&lt;/span&gt;
  &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;provide&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;APP_GUARD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;useClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ThrottlerGuard&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;class&lt;/span&gt; &lt;span class="nc"&gt;AppModule&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;@Throttle()&lt;/code&gt; to set tighter limits on sensitive routes like login, and &lt;code&gt;@SkipThrottle()&lt;/code&gt; for things like health checks.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Two traps.&lt;/strong&gt; Behind a load balancer, every request can look like it comes from the proxy's IP. Fixing that takes two steps. First, tell the HTTP adapter to trust the proxy. Without this, &lt;code&gt;req.ip&lt;/code&gt; and &lt;code&gt;req.ips&lt;/code&gt; never contain the forwarded address, and no throttler override can use it:&lt;/p&gt;


&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/main.ts (Express)&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;NestExpressApplication&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;@nestjs/platform-express&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;app&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;NestFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;NestExpressApplication&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;AppModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;trust proxy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// the number of proxies in front of your app&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Use a hop count or a subnet, not &lt;code&gt;true&lt;/code&gt;. If your app is ever reachable without going through the proxy, clients can spoof &lt;code&gt;X-Forwarded-For&lt;/code&gt;. On Fastify, set the adapter's &lt;code&gt;trustProxy&lt;/code&gt; option instead. This goes in &lt;code&gt;main.ts&lt;/code&gt;, not &lt;code&gt;setupApp()&lt;/code&gt;, because &lt;code&gt;INestApplication&lt;/code&gt; has no &lt;code&gt;set()&lt;/code&gt; and your tests don't need it.&lt;/p&gt;

&lt;p&gt;Second, have the throttler key on the forwarded address. Once &lt;code&gt;trust proxy&lt;/code&gt; is set correctly, plain &lt;code&gt;req.ip&lt;/code&gt; is often enough. If you need explicit control, extend the guard the way the docs do and register it in place of &lt;code&gt;ThrottlerGuard&lt;/code&gt;:&lt;/p&gt;


&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/common/throttler-behind-proxy.guard.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;Injectable&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;@nestjs/common&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;ThrottlerGuard&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;@nestjs/throttler&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="nd"&gt;Injectable&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;class&lt;/span&gt; &lt;span class="nc"&gt;ThrottlerBehindProxyGuard&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;ThrottlerGuard&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;getTracker&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="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="kr"&gt;any&lt;/span&gt;&lt;span class="o"&gt;&amp;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;&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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ips&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ips&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ip&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;// providers: [{ provide: APP_GUARD, useClass: ThrottlerBehindProxyGuard }]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;The other trap is that the default store is in memory, so with several instances each one counts on its own. For a shared limit, plug in a shared store.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Deny by default.&lt;/strong&gt; Instead of remembering to protect every route, protect all of them and mark the open ones. This is the pattern from the official auth docs:&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/auth/public.decorator.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;SetMetadata&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;@nestjs/common&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;IS_PUBLIC_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;isPublic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Public&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;SetMetadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;IS_PUBLIC_KEY&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/auth/auth.guard.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;CanActivate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;ExecutionContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;UnauthorizedException&lt;/span&gt;&lt;span class="p"&gt;,&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;@nestjs/common&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;Reflector&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;@nestjs/core&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;JwtService&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;@nestjs/jwt&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;IS_PUBLIC_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="s1"&gt;./public.decorator.js&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="nd"&gt;Injectable&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;class&lt;/span&gt; &lt;span class="nc"&gt;AuthGuard&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;CanActivate&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;reflector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Reflector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JwtService&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;async&lt;/span&gt; &lt;span class="nf"&gt;canActivate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ExecutionContext&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;boolean&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;isPublic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reflector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getAllAndOverride&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&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;IS_PUBLIC_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getHandler&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getClass&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;isPublic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;switchToHttp&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getRequest&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="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&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;Bearer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UnauthorizedException&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="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verifyAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&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="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UnauthorizedException&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;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;Register it with &lt;code&gt;{ provide: APP_GUARD, useClass: AuthGuard }&lt;/code&gt;. Now a new endpoint is private until someone deliberately writes &lt;code&gt;@Public()&lt;/code&gt;. That's the safe direction to fail in.&lt;/p&gt;

&lt;p&gt;Two notes on this guard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;It assumes &lt;code&gt;JwtModule&lt;/code&gt; is registered with a secret, for example with &lt;code&gt;JwtModule.registerAsync&lt;/code&gt; reading &lt;code&gt;JWT_SECRET&lt;/code&gt; from &lt;code&gt;ConfigService&lt;/code&gt;. Without a secret, &lt;code&gt;verifyAsync&lt;/code&gt; has nothing to check tokens against.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Global guards run in the order you register them. List &lt;code&gt;ThrottlerGuard&lt;/code&gt; before &lt;code&gt;AuthGuard&lt;/code&gt;, so a flood of bad requests gets rate-limited before you spend time verifying tokens.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;A few more:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hash passwords, don't encrypt them.&lt;/strong&gt; Use a slow, salted hash like argon2 or bcrypt. The Nest docs have a chapter on this.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Authorization is not authentication.&lt;/strong&gt; A valid token says who you are. It doesn't say you can touch this record. Check ownership in the service.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Turn on CSRF protection&lt;/strong&gt; if you authenticate with cookies or sessions. Token-in-header APIs don't need it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Keep secrets out of logs.&lt;/strong&gt; Log IDs, not tokens or passwords.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  10. Data access: a few habits that pay off
&lt;/h2&gt;

&lt;p&gt;Nest doesn't force an ORM on you. The docs now have chapters for TypeORM, Prisma, Drizzle, MikroORM, Sequelize and MongoDB. Pick the one your team knows. These habits apply to all of them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hide the database behind a repository&lt;/strong&gt; (like &lt;code&gt;UsersRepository&lt;/code&gt; earlier). Services talk to the contract, not the library.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use migrations.&lt;/strong&gt; Never let the ORM auto-sync your schema in production. If you use TypeORM, make sure &lt;code&gt;synchronize&lt;/code&gt; is off there.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Wrap multi-step writes in a transaction.&lt;/strong&gt; If step two fails, step one should roll back.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Let the database enforce the rules.&lt;/strong&gt; Unique indexes, foreign keys and not-null constraints are your last line of defense, and they never have race conditions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Paginate everything&lt;/strong&gt; that can grow, and cap the page size.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Select only the columns you need.&lt;/strong&gt; It's less data over the wire and fewer accidental leaks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Watch for N+1 queries.&lt;/strong&gt; A loop that runs one query per item looks fine on 10 rows and falls over on 10,000.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  11. Be kind to the things you call
&lt;/h2&gt;

&lt;p&gt;Every app that calls another system will eventually meet one that is slow or down. Without a plan, your request handlers pile up waiting, and retries from every layer make the struggling service even worse.&lt;/p&gt;

&lt;p&gt;The Nest docs now have a Reliability section, and its first chapter covers &lt;code&gt;@nestjs/resilience&lt;/code&gt;. It gives you &lt;strong&gt;timeouts, retries, circuit breakers, bulkheads and fallbacks&lt;/strong&gt; as decorators on your entry points (controllers, resolvers, message handlers and gateways).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i @nestjs/resilience

&lt;span class="c"&gt;# only if you also want idempotency keys on POST routes (see the rules below)&lt;/span&gt;
npm i @nestjs/idempotency
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the idea in small form. Imagine checkout asks a shipping carrier for quotes. Import &lt;code&gt;ResilienceModule&lt;/code&gt; once, in the root module (it's global):&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/app.module.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;Module&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;@nestjs/common&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;ResilienceModule&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;@nestjs/resilience&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="nd"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;ResilienceModule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forRoot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;presets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;carrier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2s&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="na"&gt;circuitBreaker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;failureRateThreshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;minimumCalls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;openDuration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;30s&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="c1"&gt;// ...feature modules&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;class&lt;/span&gt; &lt;span class="nc"&gt;AppModule&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/shipping/shipping.controller.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;CircuitOpenError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Fallback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Resilience&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Timeout&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;@nestjs/resilience&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="nd"&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;quotes&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="nd"&gt;Resilience&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;carrier&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="nd"&gt;Timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.5s&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="nd"&gt;Fallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;flatRateQuotes&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;handleIf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;CircuitOpenError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nf"&gt;getQuotes&lt;/span&gt;&lt;span class="p"&gt;(@&lt;/span&gt;&lt;span class="nd"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;orderId&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Signal&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shippingService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getQuotes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;flatRateQuotes&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shippingService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flatRateQuotes&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;What this does for you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A slow carrier costs the route about 3.2 seconds at most (two 1.5-second attempts plus up to 200 ms of backoff), then it answers &lt;code&gt;504&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Once the carrier looks down, the breaker opens and calls fail fast instead of waiting.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;While the breaker is open, customers see a flat shipping rate instead of an error.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The breaker moves between three states:&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%2Fmermaid.ink%2Fimg%2Fpako%3AeNptzz1Pw0AMBuC_YjyiywBiyoAERVUZEBIVLJTBSZzE4nKO7pxCqfrfUXJC6sBmv3rkjyPW2jCWmIyMH4S6SEOxv94FAID3yw8oiltYeU3c5CzXS_w8ciihJfFTZIhkDKYKvXR9tjNY5IZ8mzW1xhF05PAwRTLRkOmfONtXwhi1YqjJe0hTXTM36R-dB5_Z-aKEDgeOA0mD5RGt52F-s-GWJm_ocvJGUajynGbTarA1DeIPWGJB4-i5SIdkPDi49xI-n6jeLv1agznY4ZY7ZXh93KGDF63U1MGG_Z5NanJwF4W8g0QhFYmjtOiWJVv5mW-5uhm_8XRyWHUr9RqxxIuvXozx9AvOFoaw%3Ftype%3Dpng" 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%2Fmermaid.ink%2Fimg%2Fpako%3AeNptzz1Pw0AMBuC_YjyiywBiyoAERVUZEBIVLJTBSZzE4nKO7pxCqfrfUXJC6sBmv3rkjyPW2jCWmIyMH4S6SEOxv94FAID3yw8oiltYeU3c5CzXS_w8ciihJfFTZIhkDKYKvXR9tjNY5IZ8mzW1xhF05PAwRTLRkOmfONtXwhi1YqjJe0hTXTM36R-dB5_Z-aKEDgeOA0mD5RGt52F-s-GWJm_ocvJGUajynGbTarA1DeIPWGJB4-i5SIdkPDi49xI-n6jeLv1agznY4ZY7ZXh93KGDF63U1MGG_Z5NanJwF4W8g0QhFYmjtOiWJVv5mW-5uhm_8XRyWHUr9RqxxIuvXozx9AvOFoaw%3Ftype%3Dpng" alt="Mermaid Diagram" width="352" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The rules that keep this safe
&lt;/h3&gt;

&lt;p&gt;These come straight from the docs, and they're worth printing out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The decorators only work on entry points.&lt;/strong&gt; That means controllers, resolvers, message handlers and gateways. On a service method they do nothing, and Nest logs a warning at startup. The next section shows what to use inside services.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pass the signal to every I/O call.&lt;/strong&gt; When a timeout fires, the signal aborts. If your code ignores it, the work keeps running in the background.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Retries only apply to safe requests by default.&lt;/strong&gt; On HTTP that's &lt;code&gt;GET&lt;/code&gt;, &lt;code&gt;HEAD&lt;/code&gt; and &lt;code&gt;OPTIONS&lt;/code&gt;. A &lt;code&gt;POST&lt;/code&gt; retry is skipped unless you say &lt;code&gt;@Retry({ idempotent: true })&lt;/code&gt;, which means "running this twice is harmless."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Protect client retries with idempotency keys.&lt;/strong&gt; &lt;code&gt;@Idempotent()&lt;/code&gt; from &lt;code&gt;@nestjs/idempotency&lt;/code&gt; returns the stored response when a client repeats a request with the same &lt;code&gt;Idempotency-Key&lt;/code&gt;. Register &lt;code&gt;IdempotencyModule&lt;/code&gt; before &lt;code&gt;ResilienceModule&lt;/code&gt;, because the first global interceptor runs outermost. Also register a shared idempotency store before you go to production. Without one, the records live in each process's memory and are lost on every deploy, and the docs say production refuses to start.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Retry at one layer only.&lt;/strong&gt; A route that retries 3 times, calling a service that retries 3 times, calling an SDK that retries 3 times, can send 27 requests to something that's already struggling.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;State lives in each process.&lt;/strong&gt; With several instances, every one has its own breakers and bulkheads.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Every breaker needs a timeout.&lt;/strong&gt; A breaker only learns from calls that finish. A call that hangs forever is never counted.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Calls inside services
&lt;/h3&gt;

&lt;p&gt;A background job, or any service that calls another system on its own, isn't an entry point. For those, get a policy object from &lt;code&gt;ResilienceService&lt;/code&gt;. &lt;code&gt;resilience.preset('carrier')&lt;/code&gt; gives you the same &lt;code&gt;carrier&lt;/code&gt; preset, and it shares the breaker the routes use. So a job that finds the carrier failing also protects checkout, and the other way around.&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/shipping/repricing.service.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;Injectable&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;@nestjs/common&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;ResilienceService&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;ResiliencePolicy&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;@nestjs/resilience&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="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Order&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;../orders/order.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// stand-in: your own order type&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;CarrierClient&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;./carrier.client.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// stand-in: your own carrier client&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&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;class&lt;/span&gt; &lt;span class="nc"&gt;RepricingService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ResiliencePolicy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;resilience&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ResilienceService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;carrier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CarrierClient&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;// Create the policy once, not on every call.&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;resilience&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;carrier&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="nf"&gt;getQuotes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Order&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;signal&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;carrier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getQuotes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RepricingService.getQuotes&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;One detail to know: a policy object has no &lt;code&gt;GET&lt;/code&gt; or &lt;code&gt;POST&lt;/code&gt; to look at, so the preset's retry &lt;strong&gt;always&lt;/strong&gt; applies. Only wrap calls that are safe to repeat.&lt;/p&gt;

&lt;h3&gt;
  
  
  More official building blocks worth knowing
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Where to look&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Write to your database and publish a message without losing either&lt;/td&gt;
&lt;td&gt;Transactional outbox chapter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run a scheduled job on only one instance&lt;/td&gt;
&lt;td&gt;Distributed locks (@nestjs/locks)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Send transactional email&lt;/td&gt;
&lt;td&gt;Mail (@nestjs/mail)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Store files on disk or S3-compatible storage&lt;/td&gt;
&lt;td&gt;File storage (@nestjs/storage)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I haven't covered these in depth here. Check each chapter before you commit to one, since several are new.&lt;/p&gt;




&lt;h2&gt;
  
  
  12. Health checks, logs and a clean shutdown
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Health checks
&lt;/h3&gt;

&lt;p&gt;Add a health endpoint with &lt;code&gt;@nestjs/terminus&lt;/code&gt; so your platform knows when to restart or stop sending traffic. In v12, custom indicators use &lt;code&gt;HealthIndicatorService&lt;/code&gt;. The old approach of throwing &lt;code&gt;HealthCheckError&lt;/code&gt; was removed.&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/health/payments.health.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;Injectable&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;@nestjs/common&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;HealthIndicatorService&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;@nestjs/terminus&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;PaymentsClient&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;../payments/payments.client.js&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="nd"&gt;Injectable&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;class&lt;/span&gt; &lt;span class="nc"&gt;PaymentsHealthIndicator&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;healthIndicatorService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HealthIndicatorService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PaymentsClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="nf"&gt;isHealthy&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;healthIndicatorService&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check&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;attempt&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ping&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/health/health.controller.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;Controller&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Get&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;@nestjs/common&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;HealthCheck&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;HealthCheckService&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;@nestjs/terminus&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;SkipThrottle&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;@nestjs/throttler&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;Public&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;../auth/public.decorator.js&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;PaymentsHealthIndicator&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;./payments.health.js&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="nd"&gt;Controller&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;health&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;class&lt;/span&gt; &lt;span class="nc"&gt;HealthController&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;health&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HealthCheckService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PaymentsHealthIndicator&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="nd"&gt;Public&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;SkipThrottle&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;HealthCheck&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nf"&gt;check&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;health&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isHealthy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;payments&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/health/health.module.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;Module&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;@nestjs/common&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;TerminusModule&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;@nestjs/terminus&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;PaymentsModule&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;../payments/payments.module.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// must export PaymentsClient&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;HealthController&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;./health.controller.js&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;PaymentsHealthIndicator&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;./payments.health.js&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="nd"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;TerminusModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PaymentsModule&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;controllers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;HealthController&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;PaymentsHealthIndicator&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;class&lt;/span&gt; &lt;span class="nc"&gt;HealthModule&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that the health route is &lt;code&gt;@Public()&lt;/code&gt; and skips rate limiting. Your load balancer needs to hit it constantly without a token.&lt;/p&gt;

&lt;p&gt;My opinion: split it into two routes. A &lt;strong&gt;liveness&lt;/strong&gt; route that only says "the process is up," and a &lt;strong&gt;readiness&lt;/strong&gt; route that also checks the database and other dependencies. That way a slow third party doesn't make your platform kill a perfectly healthy process.&lt;/p&gt;

&lt;h3&gt;
  
  
  Logs
&lt;/h3&gt;

&lt;p&gt;Plain text logs are hard to search. Switch the built-in logger to JSON:&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;ConsoleLogger&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;@nestjs/common&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;app&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;NestFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AppModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ConsoleLogger&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;json&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In v12 you can also attach structured data to a message, and it stays in one log entry. This is on by default (see the upgrade notes in section 1 if you need the old output back):&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;User signed in&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;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;oauth&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;In JSON mode the extra values sit under a &lt;code&gt;params&lt;/code&gt; key. Turn on &lt;code&gt;flattenParams&lt;/code&gt; if your log tool prefers them at the top level.&lt;/p&gt;

&lt;p&gt;Whatever you log, add a request ID so you can follow one request across many lines. &lt;code&gt;AsyncLocalStorage&lt;/code&gt; is a good home for it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tracing and error monitoring
&lt;/h3&gt;

&lt;p&gt;If you want tracing and error monitoring, Nest 12 promotes &lt;code&gt;@nestjs/observe&lt;/code&gt;. Know what you're opting into: the SDK sends telemetry to NestJS Observe, a hosted dashboard at observe.nestjs.com. You sign up, get an app key and secret (keep them in your secret store), and pick a plan. Features are tiered (for example, log forwarding needs Pro or above), so read the pricing page and the terms before you commit.&lt;/p&gt;

&lt;p&gt;Two defaults are worth knowing before you turn it on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Log forwarding is &lt;strong&gt;off&lt;/strong&gt; by default.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Error source context is &lt;strong&gt;on&lt;/strong&gt; by default. When the SDK captures an error, it attaches a few lines of your application source around each stack frame and sends them to the dashboard. If shipping any source code is not acceptable for your codebase, set &lt;code&gt;sourceContext: false&lt;/code&gt; in &lt;code&gt;createObserveModule()&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's optional. &lt;code&gt;nest new --observe&lt;/code&gt; and &lt;code&gt;nest upgrade --observe&lt;/code&gt; can set it up for you, and OpenTelemetry still works if that's already your setup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shutdown
&lt;/h3&gt;

&lt;p&gt;Call &lt;code&gt;app.enableShutdownHooks()&lt;/code&gt; (we did, in &lt;code&gt;main.ts&lt;/code&gt;) so &lt;code&gt;OnModuleDestroy&lt;/code&gt; and &lt;code&gt;OnApplicationShutdown&lt;/code&gt; run when your platform sends &lt;code&gt;SIGTERM&lt;/code&gt;. Without it, connections and queues may not close cleanly during a deploy.&lt;/p&gt;

&lt;p&gt;New in v12: the Express adapter now drains in-flight requests before the process exits.&lt;/p&gt;




&lt;h2&gt;
  
  
  13. Test the way Nest wants you to
&lt;/h2&gt;

&lt;p&gt;Nest's DI makes testing easy, as long as you actually use it. I split tests into two kinds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Unit tests: the service and a fake
&lt;/h3&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;ConflictException&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;@nestjs/common&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;Test&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;@nestjs/testing&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;beforeEach&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;vi&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;vitest&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;UsersRepository&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;./users.repository.js&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;UsersService&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;./users.service.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;UsersService&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;let&lt;/span&gt; &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UsersService&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;repo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;vi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;findByEmail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;vi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;vi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nf"&gt;beforeEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;vi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resetAllMocks&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;moduleRef&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;Test&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createTestingModule&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;UsersService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;provide&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UsersRepository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;useValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;repo&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;moduleRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;UsersService&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rejects a duplicate email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;findByEmail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mockResolvedValue&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="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="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a@b.co&lt;/span&gt;&lt;span class="dl"&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;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a@b.co&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})).&lt;/span&gt;&lt;span class="nx"&gt;rejects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBeInstanceOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;ConflictException&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;not&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toHaveBeenCalled&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;No database, no network. This runs in milliseconds, so you can have hundreds of them.&lt;/p&gt;

&lt;h3&gt;
  
  
  End-to-end tests: the real app, with the outside world stubbed out
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;AppModule&lt;/code&gt; validates its config at startup (section 7), so the test process needs the variables your schema demands. With Vitest you can set them in the config:&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;// vitest config&lt;/span&gt;
&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;NODE_ENV&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;postgres://test:test@localhost:5432/test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;JWT_SECRET&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test-secret-that-is-at-least-32-characters&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;p&gt;The database needs the same care. Replacing &lt;code&gt;UsersRepository&lt;/code&gt; with a fake doesn't stop an imported database module from trying to connect. Either override its connection provider, as below, or point &lt;code&gt;DATABASE_URL&lt;/code&gt; at a throwaway test database. The second is closer to production, and slower.&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="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;INestApplication&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;@nestjs/common&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;Test&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;@nestjs/testing&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;request&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;supertest&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;afterAll&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;beforeAll&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;it&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;vitest&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;AppModule&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;../src/app.module.js&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;DATABASE_CONNECTION&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;../src/database/database.constants.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// stand-in: use the token your database module exposes&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;setupApp&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;../src/setup-app.js&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;UsersRepository&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;../src/users/users.repository.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST /users&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;let&lt;/span&gt; &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;INestApplication&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;fakeRepo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;findById&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="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;findByEmail&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="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;insert&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="na"&gt;data&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="o"&gt;=&amp;gt;&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;u1&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;data&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nf"&gt;beforeAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;moduleRef&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;Test&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createTestingModule&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;AppModule&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;overrideProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;UsersRepository&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fakeRepo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;overrideProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DATABASE_CONNECTION&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useValue&lt;/span&gt;&lt;span class="p"&gt;({})&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;moduleRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createNestApplication&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;setupApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// same global setup as production&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;afterAll&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;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rejects fields we did not ask for&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="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getHttpServer&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/users&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="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a@b.co&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;isAdmin&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="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&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 test only means something because of &lt;code&gt;setupApp(app)&lt;/code&gt;. Without it, the unknown &lt;code&gt;isAdmin&lt;/code&gt; field would sail through.&lt;/p&gt;

&lt;p&gt;A few notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;@nestjs/testing&lt;/code&gt; is test-runner agnostic. Vitest, Jest, whatever you like.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If you set up Vitest by hand, remember that Nest's DI needs decorator metadata, and not every transformer emits it. Starting from a generated project saves you this headache.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Test the unhappy paths: invalid input, missing auth, duplicate data, a dependency that times out. The happy path usually works already.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  14. Performance without drama
&lt;/h2&gt;

&lt;p&gt;I'll keep this short, because most Nest performance problems aren't framework problems.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Keep request scope rare.&lt;/strong&gt; We covered this in section 5.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Don't block the event loop.&lt;/strong&gt; Heavy CPU work (image processing, big reports, password hashing in loops) should move to a queue or a worker. The docs have a Queues chapter.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cache what's expensive and rarely changes&lt;/strong&gt;, using Nest's caching module, and always decide how it expires.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Measure before switching to Fastify.&lt;/strong&gt; Nest has an official Fastify adapter and a performance chapter. It can help, but profile first so you're fixing the real bottleneck.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Build faster in development&lt;/strong&gt; with the SWC recipe if your compile times are slowing you down.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  The checklist
&lt;/h2&gt;

&lt;p&gt;Copy this into your team's wiki:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;Do this&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Setup&lt;/td&gt;
&lt;td&gt;Pick CJS or ESM on purpose. Keep all @nestjs/* on one major version.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Structure&lt;/td&gt;
&lt;td&gt;Feature modules. Small exports. No giant SharedModule.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App setup&lt;/td&gt;
&lt;td&gt;One setupApp() used by main.ts and tests.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Controllers&lt;/td&gt;
&lt;td&gt;Thin. Input in, service call, result out.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DI&lt;/td&gt;
&lt;td&gt;Singletons by default, with no per-request state. Abstract classes as tokens. AsyncLocalStorage over request scope.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Validation&lt;/td&gt;
&lt;td&gt;Global pipe with whitelist, forbidNonWhitelisted, transform. If you use schemas, register StandardSchemaValidationPipe. Cap page sizes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Config&lt;/td&gt;
&lt;td&gt;Validated at startup. Read through ConfigService.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Errors&lt;/td&gt;
&lt;td&gt;errorCode on exceptions, or domain errors with one filter.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security&lt;/td&gt;
&lt;td&gt;Helmet, explicit CORS, rate limiting (with trust proxy behind a load balancer), deny-by-default auth, hashed passwords.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data&lt;/td&gt;
&lt;td&gt;Repository, migrations, transactions, unique indexes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reliability&lt;/td&gt;
&lt;td&gt;Timeouts (decorators on entry points, policy objects in services), safe retries, a breaker per dependency, idempotency keys with a shared store for POST.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operations&lt;/td&gt;
&lt;td&gt;Health route, JSON logs, shutdown hooks.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testing&lt;/td&gt;
&lt;td&gt;Fast unit tests, a few e2e tests that use setupApp() and a stubbed or throwaway database.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;If you only do three things after reading this, do these:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Put your setup in one &lt;code&gt;setupApp()&lt;/code&gt; and use it in your tests.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Validate input &lt;strong&gt;and&lt;/strong&gt; config, so bad data fails early and loudly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Put a timeout on every call to something you don't control. Use the decorators on entry points and a policy object inside services.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;NestJS 12 is a good moment to clean house. The framework is moving to ESM, the tooling is getting lighter, and the docs now cover production concerns that used to need extra research.&lt;/p&gt;

&lt;p&gt;What would you add to this list? Let me know in the comments.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sources and further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://trilon.io/blog/nestjs-12-is-now-available" rel="noopener noreferrer"&gt;NestJS v12 is Now Available (Trilon)&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://trilon.io/blog/nestjs-12-is-coming" rel="noopener noreferrer"&gt;NestJS v12 is Coming: What's New (Trilon)&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/nestjs/nest/releases/tag/v12.0.0" rel="noopener noreferrer"&gt;NestJS v12.0.0 release notes (GitHub)&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/nestjs/schematics/releases/tag/12.0.0" rel="noopener noreferrer"&gt;@nestjs/schematics 12.0.0 release notes (GitHub)&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.nestjs.com/migration-guide" rel="noopener noreferrer"&gt;Migration guide (NestJS docs)&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.nestjs.com/application/validation" rel="noopener noreferrer"&gt;Validation (NestJS docs)&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.nestjs.com/fundamentals/injection-scopes" rel="noopener noreferrer"&gt;Injection scopes (NestJS docs)&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.nestjs.com/reliability/resilience" rel="noopener noreferrer"&gt;Resilience (NestJS docs)&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.nestjs.com/reliability/idempotency" rel="noopener noreferrer"&gt;Idempotency keys (NestJS docs)&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.nestjs.com/security/rate-limiting" rel="noopener noreferrer"&gt;Rate limiting (NestJS docs)&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.nestjs.com/observability/overview" rel="noopener noreferrer"&gt;NestJS Observe overview (NestJS docs)&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.nestjs.com/observability/sdk" rel="noopener noreferrer"&gt;NestJS Observe SDK (NestJS docs)&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Published via &lt;a href="https://zyvop.com/nestjs-best-practices-for-2026-a-practical-guide-updated-for-nestjs-12-x3fa5?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=syndication" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt; — Write once in Markdown, auto-backup to GitHub, and syndicate to Dev.to, Medium &amp;amp; Hashnode in 1 click.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>backend</category>
      <category>bestpractices</category>
      <category>nestjs</category>
      <category>node</category>
    </item>
    <item>
      <title>OpenAI Is Putting an Invisible Watermark on ChatGPT Text in the EU</title>
      <dc:creator>Sanjay Singh</dc:creator>
      <pubDate>Thu, 08 Oct 2026 02:54:50 +0000</pubDate>
      <link>https://dev.to/sanjay_singh_1/openai-is-putting-an-invisible-watermark-on-chatgpt-text-in-the-eu-3he0</link>
      <guid>https://dev.to/sanjay_singh_1/openai-is-putting-an-invisible-watermark-on-chatgpt-text-in-the-eu-3he0</guid>
      <description>&lt;p&gt;Soon, text written by ChatGPT for people in the EU will carry a mark you can't see.&lt;/p&gt;

&lt;p&gt;OpenAI is adding an invisible watermark to text from ChatGPT and Codex for eligible users across the European Union. The rollout starts over the next few weeks, with no exact date given. It's the first time ChatGPT's text will carry a built-in mark. OpenAI has had a text watermarking method in the works since at least 2024 and held it back. What changed is the law.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why now
&lt;/h2&gt;

&lt;p&gt;The EU AI Act requires providers of generative AI to make their output identifiable by machines. That duty sits in Article 50, which started applying on August 2. Services that were already on the EU market before that date get until December 2 to start marking their output. Newer systems had to comply from launch.&lt;/p&gt;

&lt;p&gt;OpenAI says the watermark is its response to the law. It doesn't point to a specific deadline.&lt;/p&gt;

&lt;p&gt;It isn't the first to do this. Anthropic began marking text from its newer models in early August, and Google already ships SynthID for text. Until now, ChatGPT shipped without one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changes for you
&lt;/h2&gt;

&lt;p&gt;If you use ChatGPT or Codex in the EU, on any plan, eligible text you generate will start carrying the mark. You won't notice anything. The words and formatting look the same.&lt;/p&gt;

&lt;p&gt;If you're outside the EU, ChatGPT stays as it is for now. OpenAI says it isn't making watermarking a global default at launch and wants to learn from the EU rollout first.&lt;/p&gt;

&lt;p&gt;For developers, API customers anywhere can switch watermarking on for select models starting now. It's off by default. OpenAI is also working with cloud partners to offer it through their platforms in the coming weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;OpenAI calls its method textGrain. A language model picks each word from a range of reasonable options. textGrain nudges those choices in a statistical pattern, and a matching detector looks for that pattern later.&lt;/p&gt;

&lt;p&gt;The signal lives in the word choices themselves, not in hidden characters or file metadata. Copying and pasting the text won't remove it. Changing the words can.&lt;/p&gt;

&lt;p&gt;OpenAI also says the watermark doesn't hurt quality. On its latest model, Astra, benchmark scores with and without the mark stayed within a few points of each other, some slightly up and some slightly down. On GPQA Diamond, for example, it scored 94.44% unmarked and 93.94% marked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The limits
&lt;/h2&gt;

&lt;p&gt;OpenAI is fairly direct about where this falls short.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Length matters.&lt;/strong&gt; With the detector tuned to wrongly flag about 1% of unmarked text, it caught the watermark in roughly 80% of 200-token passages and 95% of 400-token passages (about 150 and 300 words). Those were explanatory answers on psychology. On math, where there's less freedom in word choice, detection was much lower.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Editing hurts more.&lt;/strong&gt; On 400-token passages, swapping 10% of the words for synonyms dropped detection from about 92% to 66%. Swapping 25% dropped it to 17%. Translated text is also harder to detect.&lt;/p&gt;

&lt;p&gt;So a missing watermark tells you very little. OpenAI says so itself: no detected watermark doesn't prove a human wrote the text. It could be short, edited, translated, from an older or unsupported model, or from another company's tool.&lt;/p&gt;

&lt;p&gt;A watermark that is present has limits too. It can show that an OpenAI system generated or processed part of a passage. It can't tell you how much a person contributed, who owns the text, who is responsible for it, or whether it's accurate. It doesn't identify the user, and it isn't tied to any account, prompt or conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who can check for it
&lt;/h2&gt;

&lt;p&gt;Not you, at least for now. The detector isn't public. OpenAI opened applications for approved researchers and expert organizations, and will grant access case by case. The tool reports whether it finds an OpenAI watermark, without identifying the user or revealing prompts.&lt;/p&gt;

&lt;p&gt;OpenAI says it will widen access once results can be interpreted responsibly, and plans to release the technology as open source so others can build on it. Images and audio are different. OpenAI's verification tools for those are already public.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why OpenAI waited
&lt;/h2&gt;

&lt;p&gt;In 2024, the Wall Street Journal reported that OpenAI had a text watermark ready but hadn't launched it. OpenAI replied that the method worked well on unedited text but could be beaten by translation or by having another model reword the passage. It also worried about stigmatizing non-native English speakers who use AI as a writing aid. A survey reported at the time found about 30% of ChatGPT users said they'd use it less if it watermarked text.&lt;/p&gt;

&lt;p&gt;The new announcement shows the same caution. The EU-only rollout follows the law, and the detector is locked down because of the reliability gaps above.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to take from this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Users in the EU:&lt;/strong&gt; nothing to do. Just don't read a watermark result, or its absence, as proof of who wrote something.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Teachers and editors:&lt;/strong&gt; you can't run the detector, and OpenAI says a result wouldn't measure how much a person contributed anyway.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Businesses publishing for EU audiences:&lt;/strong&gt; machine-readable provenance for text is now something to plan around.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Developers:&lt;/strong&gt; the opt-in is available today. Decide whether your own transparency duties call for it.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A few things are still open: the exact EU rollout date, the updated technical report and open-source release, and whether OpenAI extends the mark beyond Europe. For now it's a first step, with limits spelled out, from a company that spent years deciding whether to ship it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Published via &lt;a href="https://zyvop.com/openai-is-putting-an-invisible-watermark-on-chatgpt-text-in-the-eu-dlvlv?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=syndication" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt; — Write once in Markdown, auto-backup to GitHub, and syndicate to Dev.to, Medium &amp;amp; Hashnode in 1 click.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>airegulation</category>
      <category>chatgpt</category>
      <category>euaiact</category>
      <category>openai</category>
    </item>
  </channel>
</rss>
